Skip to content

Commit 59c8a80

Browse files
authored
Merge pull request #10 from AmoreComputer/feat/license-details
Expose license issue date and customer name
2 parents 447a744 + f612ab5 commit 59c8a80

4 files changed

Lines changed: 96 additions & 2 deletions

File tree

‎Sources/AmoreLicensing/Models/Customer.swift‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ public struct Customer: Hashable, Codable, Sendable {
55
/// The email address the license was issued to, or `nil` if the issuing
66
/// token carries a customer without an email.
77
public var email: String?
8+
/// The customer's name, or `nil` if the issuing token carries a customer
9+
/// without a name.
10+
public var name: String?
811
}

‎Sources/AmoreLicensing/Models/License.swift‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public struct License: Identifiable, Hashable, Codable, Sendable {
1616
/// The customer this license was issued to, or `nil` if the license has no
1717
/// associated customer. Use `customer?.email` to show "Licensed to <email>".
1818
public var customer: Customer?
19+
/// When the license was issued, or `nil` if the token predates this claim.
20+
/// For purchased licenses this is the purchase time. Useful for showing
21+
/// license details in settings, e.g. to help customers find their email
22+
/// receipt. The server carries this claim on every token it mints for the
23+
/// license, so the value is stable across validations.
24+
public var issuedAt: Date?
1925

2026
/// The product name this license is for.
2127
@available(*, deprecated, renamed: "product.name", message: "Use `product.name` instead.")
@@ -31,7 +37,8 @@ extension License {
3137
expiresAt: payload.exp,
3238
entitlements: payload.entitlements,
3339
subscriptionState: payload.subscriptionState,
34-
customer: payload.customer
40+
customer: payload.customer,
41+
issuedAt: payload.issuedAt
3542
)
3643
}
3744

‎Sources/AmoreLicensing/Payload/LicensePayload.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ struct LicensePayload: Codable, Sendable {
1010
var entitlements: Set<License.Entitlement> = []
1111
var subscriptionState: SubscriptionState?
1212
var customer: Customer?
13+
var issuedAt: Date?
1314

1415
enum CodingKeys: String, CodingKey {
1516
case exp
@@ -21,5 +22,6 @@ struct LicensePayload: Codable, Sendable {
2122
case entitlements
2223
case subscriptionState = "subscription_state"
2324
case customer
25+
case issuedAt = "issued_at"
2426
}
2527
}

‎Tests/AmoreLicensingTests/LicensePayloadDecodeTests.swift‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,34 @@ struct LicensePayloadDecodeTests {
9999
"nonce": "n-1",
100100
"product": { "name": "Pro", "identifier": "pro" },
101101
"entitlements": [],
102-
"customer": { "email": "buyer@example.com" }
102+
"customer": { "email": "buyer@example.com", "name": "Amy Worrall" }
103103
}
104104
"""
105105
let payload = try decodePayload(json)
106106
let license = License(from: payload)
107107
#expect(license.customer?.email == "buyer@example.com")
108+
#expect(license.customer?.name == "Amy Worrall")
109+
}
110+
111+
// A name-only customer (license issued without an email) must decode with
112+
// a nil email and carry the name.
113+
@Test func customerDecodesWithNameOnly() throws {
114+
let json = """
115+
{
116+
"exp": 1800000000,
117+
"iat": 1779000000,
118+
"hardware_id": "hw-1",
119+
"license_id": "C7B53B0E-2C18-4F1D-8C9B-2B7B6A8B6A7E",
120+
"nonce": "n-1",
121+
"product": { "name": "Pro", "identifier": "pro" },
122+
"entitlements": [],
123+
"customer": { "name": "Amy Worrall" }
124+
}
125+
"""
126+
let payload = try decodePayload(json)
127+
let license = License(from: payload)
128+
#expect(license.customer?.name == "Amy Worrall")
129+
#expect(license.customer?.email == nil)
108130
}
109131

110132
// An older token (or any license issued without an email) has no `customer`
@@ -126,6 +148,66 @@ struct LicensePayloadDecodeTests {
126148
#expect(license.customer == nil)
127149
}
128150

151+
// The full claim set a current server mints: customer name and email plus
152+
// the issue date, all on one token.
153+
@Test func decodesFullCurrentTokenShape() throws {
154+
let json = """
155+
{
156+
"exp": 1800000000,
157+
"iat": 1779000000,
158+
"hardware_id": "hw-1",
159+
"license_id": "C7B53B0E-2C18-4F1D-8C9B-2B7B6A8B6A7E",
160+
"nonce": "n-1",
161+
"product": { "name": "Pro", "identifier": "pro" },
162+
"entitlements": [],
163+
"customer": { "email": "buyer@example.com", "name": "Amy Worrall" },
164+
"issued_at": 1778000000
165+
}
166+
"""
167+
let payload = try decodePayload(json)
168+
let license = License(from: payload)
169+
#expect(license.customer?.email == "buyer@example.com")
170+
#expect(license.customer?.name == "Amy Worrall")
171+
#expect(license.issuedAt == Date(timeIntervalSince1970: 1_778_000_000))
172+
}
173+
174+
@Test func licenseFromPayloadCarriesIssueDate() throws {
175+
let json = """
176+
{
177+
"exp": 1800000000,
178+
"iat": 1779000000,
179+
"hardware_id": "hw-1",
180+
"license_id": "C7B53B0E-2C18-4F1D-8C9B-2B7B6A8B6A7E",
181+
"nonce": "n-1",
182+
"product": { "name": "Pro", "identifier": "pro" },
183+
"entitlements": [],
184+
"issued_at": 1778000000
185+
}
186+
"""
187+
let payload = try decodePayload(json)
188+
let license = License(from: payload)
189+
#expect(license.issuedAt == Date(timeIntervalSince1970: 1_778_000_000))
190+
}
191+
192+
// An older token has no `issued_at` claim; the optional must decode to
193+
// nil, not error.
194+
@Test func licenseHasNilIssueDateWhenClaimAbsent() throws {
195+
let json = """
196+
{
197+
"exp": 1800000000,
198+
"iat": 1779000000,
199+
"hardware_id": "hw-1",
200+
"license_id": "C7B53B0E-2C18-4F1D-8C9B-2B7B6A8B6A7E",
201+
"nonce": "n-1",
202+
"product": { "name": "Pro", "identifier": "pro" },
203+
"entitlements": []
204+
}
205+
"""
206+
let payload = try decodePayload(json)
207+
let license = License(from: payload)
208+
#expect(license.issuedAt == nil)
209+
}
210+
129211
// A `customer` object may be present without an `email` (partial or
130212
// forward-compatible token). It must decode with a nil email rather than
131213
// throw and reject the entire license.

0 commit comments

Comments
 (0)