Skip to content

Commit 3baa349

Browse files

15 files changed

+485
-22
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,14 @@ let encodedKey = try! String(contentsOfFile: "/path/to/key/SubscriptionKey_ABCDE
139139

140140
let productId = "<product_id>"
141141
let subscriptionOfferId = "<subscription_offer_id>"
142-
let applicationUsername = "<application_username>"
142+
let appAccountToken = "<app_account_token>"
143143

144144
// try! used for example purposes only
145145
let signatureCreator = try! PromotionalOfferSignatureCreator(privateKey: encodedKey, keyId: keyId, bundleId: bundleId)
146146

147147
let nonce = UUID()
148148
let timestamp = Int64(Date().timeIntervalSince1970) * 1000
149-
let signature = signatureCreator.createSignature(productIdentifier: productIdentifier, subscriptionOfferID: subscriptionOfferID, applicationUsername: applicationUsername, nonce: nonce, timestamp: timestamp)
149+
let signature = signatureCreator.createSignature(productIdentifier: productIdentifier, subscriptionOfferID: subscriptionOfferID, appAccountToken: appAccountToken, nonce: nonce, timestamp: timestamp)
150150
print(signature)
151151
```
152152

Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,6 @@ public class AppStoreServerAPIClient {
332332
var bid: String
333333
var aud: AudienceClaim
334334
var iat: IssuedAtClaim
335-
336335
func verify(using algorithm: some JWTAlgorithm) async throws {
337336
fatalError("Do not attempt to locally verify a JWT")
338337
}
@@ -548,6 +547,11 @@ public enum APIError: Int64 {
548547
///[InvalidTransactionTypeNotSupportedError](https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror)
549548
case invalidTransactionTypeNotSupported = 4000047
550549

550+
///An error that indicates the endpoint doesn't support an app transaction ID.
551+
///
552+
///[AppTransactionIdNotSupportedError](https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror)
553+
case appTransactionIdNotSupported = 4000048
554+
551555
///An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state.
552556
///
553557
///[SubscriptionExtensionIneligibleError](https://developer.apple.com/documentation/appstoreserverapi/subscriptionextensionineligibleerror)
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import Foundation
4+
import JWTKit
5+
import Crypto
6+
7+
fileprivate protocol BasePayload: Codable {
8+
var nonce: String { get }
9+
var iss: IssuerClaim { get }
10+
var bid: String { get }
11+
var aud: AudienceClaim { get }
12+
var iat: IssuedAtClaim { get }
13+
}
14+
15+
fileprivate class BasePayloadObject: BasePayload {
16+
let nonce: String
17+
let iss: IssuerClaim
18+
let bid: String
19+
let aud: AudienceClaim
20+
let iat: IssuedAtClaim
21+
init(nonce: String, iss: IssuerClaim, bid: String, aud: AudienceClaim, iat: IssuedAtClaim) {
22+
self.nonce = nonce
23+
self.iss = iss
24+
self.bid = bid
25+
self.aud = aud
26+
self.iat = iat
27+
}
28+
}
29+
30+
fileprivate final class PromotionalOfferV2Payload: BasePayload, JWTPayload {
31+
32+
let nonce: String
33+
let iss: IssuerClaim
34+
let bid: String
35+
let aud: AudienceClaim
36+
let iat: IssuedAtClaim
37+
let productId: String
38+
let offerIdentifier: String
39+
let transactionId: String?
40+
41+
init(basePayload: BasePayload, productId: String, offerIdentifier: String, transactionId: String? = nil) {
42+
self.productId = productId
43+
self.offerIdentifier = offerIdentifier
44+
self.transactionId = transactionId
45+
self.nonce = basePayload.nonce
46+
self.iss = basePayload.iss
47+
self.bid = basePayload.bid
48+
self.aud = basePayload.aud
49+
self.iat = basePayload.iat
50+
}
51+
52+
required init(from decoder: any Decoder) throws {
53+
fatalError("Do not attempt to decode a JWS locally")
54+
}
55+
56+
func verify(using algorithm: some JWTKit.JWTAlgorithm) async throws {
57+
fatalError("Do not attempt to locally verify a JWS")
58+
}
59+
}
60+
61+
fileprivate final class IntroductoryOfferEligibilityPayload: BasePayload, JWTPayload {
62+
let nonce: String
63+
let iss: IssuerClaim
64+
let bid: String
65+
let aud: AudienceClaim
66+
let iat: IssuedAtClaim
67+
let productId: String
68+
let allowIntroductoryOffer: Bool
69+
let transactionId: String
70+
71+
init(basePayload: BasePayload, productId: String, allowIntroductoryOffer: Bool, transactionId: String) {
72+
self.productId = productId
73+
self.allowIntroductoryOffer = allowIntroductoryOffer
74+
self.transactionId = transactionId
75+
self.nonce = basePayload.nonce
76+
self.iss = basePayload.iss
77+
self.bid = basePayload.bid
78+
self.aud = basePayload.aud
79+
self.iat = basePayload.iat
80+
}
81+
82+
required init(from decoder: any Decoder) throws {
83+
fatalError("Do not attempt to decode a JWS locally")
84+
}
85+
86+
func verify(using algorithm: some JWTKit.JWTAlgorithm) async throws {
87+
fatalError("Do not attempt to locally verify a JWS")
88+
}
89+
}
90+
91+
fileprivate final class AdvancedCommerceInAppPayload: BasePayload, JWTPayload {
92+
let nonce: String
93+
let iss: IssuerClaim
94+
let bid: String
95+
let aud: AudienceClaim
96+
let iat: IssuedAtClaim
97+
let request: String
98+
99+
init(basePayload: BasePayload, request: String) {
100+
self.request = request
101+
self.nonce = basePayload.nonce
102+
self.iss = basePayload.iss
103+
self.bid = basePayload.bid
104+
self.aud = basePayload.aud
105+
self.iat = basePayload.iat
106+
}
107+
108+
required init(from decoder: any Decoder) throws {
109+
fatalError("Do not attempt to decode a JWS locally")
110+
}
111+
112+
func verify(using algorithm: some JWTKit.JWTAlgorithm) async throws {
113+
fatalError("Do not attempt to locally verify a JWS")
114+
}
115+
}
116+
117+
public class JWSSignatureCreator {
118+
119+
private let audience: String
120+
private let signingKey: P256.Signing.PrivateKey
121+
private let keyId: String
122+
private let issuerId: String
123+
private let bundleId: String
124+
125+
init(audience: String, signingKey: String, keyId: String, issuerId: String, bundleId: String) throws {
126+
self.audience = audience
127+
self.signingKey = try P256.Signing.PrivateKey(pemRepresentation: signingKey)
128+
self.keyId = keyId
129+
self.issuerId = issuerId
130+
self.bundleId = bundleId
131+
}
132+
133+
fileprivate func getBasePayload() -> BasePayload {
134+
return BasePayloadObject(
135+
nonce: UUID().uuidString,
136+
iss: .init(value: self.issuerId),
137+
bid: self.bundleId,
138+
aud: .init(value: self.audience),
139+
iat: .init(value: Date())
140+
)
141+
}
142+
143+
fileprivate func createSignature(payload: JWTPayload) async throws -> String {
144+
let keys = JWTKeyCollection()
145+
try await keys.add(ecdsa: ECDSA.PrivateKey<P256>(backing: signingKey))
146+
return try await keys.sign(payload, header: ["typ": "JWT", "kid": .string(self.keyId)])
147+
}
148+
}
149+
150+
public class PromotionalOfferV2SignatureCreator: JWSSignatureCreator {
151+
///Create a PromotionalOfferV2SignatureCreator
152+
///
153+
///- Parameter signingKey: Your private key downloaded from App Store Connect
154+
///- Parameter issuerId: Your issuer ID from the Keys page in App Store Connect
155+
///- Parameter bundleId: Your app’s bundle ID
156+
///- Parameter environment: The environment to target
157+
public init(signingKey: String, keyId: String, issuerId: String, bundleId: String) throws {
158+
try super.init(audience: "promotional-offer", signingKey: signingKey, keyId: keyId, issuerId: issuerId, bundleId: bundleId)
159+
}
160+
161+
///Create a promotional offer V2 signature.
162+
///
163+
///- Parameter productId: The unique identifier of the product
164+
///- Parameter offerIdentifier: The promotional offer identifier that you set up in App Store Connect
165+
///- Parameter transactionId: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. This field is optional, but recommended.
166+
///- Returns: The signed JWS.
167+
///[Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
168+
public func createSignature(productId: String, offerIdentifier: String, transactionId: String? = nil) async throws -> String {
169+
let baseClaims = super.getBasePayload()
170+
let claims = PromotionalOfferV2Payload(basePayload: baseClaims, productId: productId, offerIdentifier: offerIdentifier, transactionId: transactionId)
171+
return try await super.createSignature(payload: claims)
172+
}
173+
}
174+
175+
public class IntroductoryOfferEligibilitySignatureCreator: JWSSignatureCreator {
176+
///Create a IntroductoryOfferEligibilitySignatureCreator
177+
///
178+
///- Parameter signingKey: Your private key downloaded from App Store Connect
179+
///- Parameter issuerId: Your issuer ID from the Keys page in App Store Connect
180+
///- Parameter bundleId: Your app’s bundle ID
181+
///- Parameter environment: The environment to target
182+
public init(signingKey: String, keyId: String, issuerId: String, bundleId: String) throws {
183+
try super.init(audience: "introductory-offer-eligibility", signingKey: signingKey, keyId: keyId, issuerId: issuerId, bundleId: bundleId)
184+
}
185+
186+
///Create an introductory offer eligibility signature.
187+
///
188+
///- Parameter productId: The unique identifier of the product
189+
///- Parameter allowIntroductoryOffer: A boolean value that determines whether the customer is eligible for an introductory offer
190+
///- Parameter transactionId: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app.
191+
///- Returns: The signed JWS.
192+
///[Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
193+
public func createSignature(productId: String, allowIntroductoryOffer: Bool, transactionId: String) async throws -> String {
194+
let baseClaims = super.getBasePayload()
195+
let claims = IntroductoryOfferEligibilityPayload(basePayload: baseClaims, productId: productId, allowIntroductoryOffer: allowIntroductoryOffer, transactionId: transactionId)
196+
return try await super.createSignature(payload: claims)
197+
}
198+
}
199+
200+
public protocol AdvancedCommerceInAppRequest: Encodable {
201+
202+
}
203+
204+
public class AdvancedCommerceInAppSignatureCreator: JWSSignatureCreator {
205+
///Create a AdvancedCommerceInAppSignatureCreator
206+
///
207+
///- Parameter signingKey: Your private key downloaded from App Store Connect
208+
///- Parameter issuerId: Your issuer ID from the Keys page in App Store Connect
209+
///- Parameter bundleId: Your app’s bundle ID
210+
///- Parameter environment: The environment to target
211+
public init(signingKey: String, keyId: String, issuerId: String, bundleId: String) throws {
212+
try super.init(audience: "advanced-commerce-api", signingKey: signingKey, keyId: keyId, issuerId: issuerId, bundleId: bundleId)
213+
}
214+
215+
///Create an Advanced Commerce in-app signed request.
216+
///
217+
///- Parameter advancedCommerceInAppRequest: The request to be signed.
218+
///- Returns: The signed JWS.
219+
///[Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
220+
public func createSignature(advancedCommerceInAppRequest: AdvancedCommerceInAppRequest) async throws -> String {
221+
let jsonEncoder = getJsonEncoder()
222+
let body = try jsonEncoder.encode(advancedCommerceInAppRequest)
223+
224+
let base64EncodedBody = body.base64EncodedString()
225+
let baseClaims = super.getBasePayload()
226+
let claims = AdvancedCommerceInAppPayload(basePayload: baseClaims, request: base64EncodedBody)
227+
return try await super.createSignature(payload: claims)
228+
}
229+
}

Sources/AppStoreServerLibrary/Models/AppTransaction.swift

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Foundation
88
///[AppTransaction](https://developer.apple.com/documentation/storekit/apptransaction)
99
public struct AppTransaction: DecodedSignedData, Decodable, Encodable, Hashable, Sendable {
1010

11-
public init(receiptType: AppStoreEnvironment? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, applicationVersion: String? = nil, versionExternalIdentifier: Int64? = nil, receiptCreationDate: Date? = nil, originalPurchaseDate: Date? = nil, originalApplicationVersion: String? = nil, deviceVerification: String? = nil, deviceVerificationNonce: UUID? = nil, preorderDate: Date? = nil) {
11+
public init(receiptType: AppStoreEnvironment? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, applicationVersion: String? = nil, versionExternalIdentifier: Int64? = nil, receiptCreationDate: Date? = nil, originalPurchaseDate: Date? = nil, originalApplicationVersion: String? = nil, deviceVerification: String? = nil, deviceVerificationNonce: UUID? = nil, preorderDate: Date? = nil, appTransactionId: String? = nil, originalPlatform: PurchasePlatform? = nil) {
1212
self.receiptType = receiptType
1313
self.appAppleId = appAppleId
1414
self.bundleId = bundleId
@@ -20,9 +20,11 @@ public struct AppTransaction: DecodedSignedData, Decodable, Encodable, Hashable,
2020
self.deviceVerification = deviceVerification
2121
self.deviceVerificationNonce = deviceVerificationNonce
2222
self.preorderDate = preorderDate
23+
self.appTransactionId = appTransactionId
24+
self.originalPlatform = originalPlatform
2325
}
2426

25-
public init(rawReceiptType: String? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, applicationVersion: String? = nil, versionExternalIdentifier: Int64? = nil, receiptCreationDate: Date? = nil, originalPurchaseDate: Date? = nil, originalApplicationVersion: String? = nil, deviceVerification: String? = nil, deviceVerificationNonce: UUID? = nil, preorderDate: Date? = nil) {
27+
public init(rawReceiptType: String? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, applicationVersion: String? = nil, versionExternalIdentifier: Int64? = nil, receiptCreationDate: Date? = nil, originalPurchaseDate: Date? = nil, originalApplicationVersion: String? = nil, deviceVerification: String? = nil, deviceVerificationNonce: UUID? = nil, preorderDate: Date? = nil, appTransactionId: String? = nil, rawOriginalPlatform: String? = nil) {
2628
self.rawReceiptType = rawReceiptType
2729
self.appAppleId = appAppleId
2830
self.bundleId = bundleId
@@ -34,6 +36,8 @@ public struct AppTransaction: DecodedSignedData, Decodable, Encodable, Hashable,
3436
self.deviceVerification = deviceVerification
3537
self.deviceVerificationNonce = deviceVerificationNonce
3638
self.preorderDate = preorderDate
39+
self.appTransactionId = appTransactionId
40+
self.rawOriginalPlatform = rawOriginalPlatform
3741
}
3842

3943
///The server environment that signs the app transaction.
@@ -108,6 +112,26 @@ public struct AppTransaction: DecodedSignedData, Decodable, Encodable, Hashable,
108112
public var signedDate: Date? {
109113
receiptCreationDate
110114
}
115+
116+
///The unique identifier of the app download transaction.
117+
///
118+
///[appTransactionId](https://developer.apple.com/documentation/storekit/apptransaction/apptransactionid)
119+
public var appTransactionId: String?
120+
121+
///The platform on which the customer originally purchased the app.
122+
///
123+
///[originalPlatform](https://developer.apple.com/documentation/storekit/apptransaction/originalplatform-4mogz)
124+
public var originalPlatform: PurchasePlatform? {
125+
get {
126+
return rawOriginalPlatform.flatMap { PurchasePlatform(rawValue: $0) }
127+
}
128+
set {
129+
self.rawOriginalPlatform = newValue.map { $0.rawValue }
130+
}
131+
}
132+
133+
///See ``originalPlatform``
134+
public var rawOriginalPlatform: String?
111135

112136

113137
public enum CodingKeys: CodingKey {
@@ -122,6 +146,8 @@ public struct AppTransaction: DecodedSignedData, Decodable, Encodable, Hashable,
122146
case deviceVerification
123147
case deviceVerificationNonce
124148
case preorderDate
149+
case appTransactionId
150+
case originalPlatform
125151
}
126152

127153
public init(from decoder: any Decoder) throws {
@@ -137,6 +163,8 @@ public struct AppTransaction: DecodedSignedData, Decodable, Encodable, Hashable,
137163
self.deviceVerification = try container.decodeIfPresent(String.self, forKey: .deviceVerification)
138164
self.deviceVerificationNonce = try container.decodeIfPresent(UUID.self, forKey: .deviceVerificationNonce)
139165
self.preorderDate = try container.decodeIfPresent(Date.self, forKey: .preorderDate)
166+
self.appTransactionId = try container.decodeIfPresent(String.self, forKey: .appTransactionId)
167+
self.rawOriginalPlatform = try container.decodeIfPresent(String.self, forKey: .originalPlatform)
140168
}
141169

142170
public func encode(to encoder: any Encoder) throws {
@@ -152,5 +180,7 @@ public struct AppTransaction: DecodedSignedData, Decodable, Encodable, Hashable,
152180
try container.encodeIfPresent(self.deviceVerification, forKey: .deviceVerification)
153181
try container.encodeIfPresent(self.deviceVerificationNonce, forKey: .deviceVerificationNonce)
154182
try container.encodeIfPresent(self.preorderDate, forKey: .preorderDate)
183+
try container.encodeIfPresent(self.appTransactionId, forKey: .appTransactionId)
184+
try container.encodeIfPresent(self.rawOriginalPlatform, forKey: .originalPlatform)
155185
}
156186
}

0 commit comments

Comments
 (0)