Skip to content

Commit b284240

Browse files

14 files changed

+340
-11
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The Node.js server library for the [App Store Server API](https://developer.appl
99

1010
## Installation
1111

12-
#### Requirements
12+
### Requirements
1313

1414
- Node 16+
1515

@@ -125,12 +125,12 @@ const encodedKey = readFile(filePath) // Specific implementation may vary
125125

126126
const productId = "<product_id>"
127127
const subscriptionOfferId = "<subscription_offer_id>"
128-
const applicationUsername = "<application_username>"
128+
const appAccountToken = "<app_account_token>"
129129
const nonce = "<nonce>"
130130
const timestamp = Date.now()
131131
const signatureCreator = new PromotionalOfferSignatureCreator(encodedKey, keyId, bundleId)
132132

133-
const signature = signatureCreator.createSignature(productId, subscriptionOfferId, applicationUsername, nonce, timestamp)
133+
const signature = signatureCreator.createSignature(productId, subscriptionOfferId, appAccountToken, nonce, timestamp)
134134
console.log(signature)
135135
```
136136

index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export { OrderLookupStatus } from './models/OrderLookupStatus'
5555
export { Platform } from './models/Platform'
5656
export { PlayTime } from './models/PlayTime'
5757
export { PriceIncreaseStatus } from './models/PriceIncreaseStatus'
58+
export { PurchasePlatform } from './models/PurchasePlatform'
5859
export { RefundHistoryResponse } from './models/RefundHistoryResponse'
5960
export { ResponseBodyV2 } from './models/ResponseBodyV2'
6061
export { ResponseBodyV2DecodedPayload } from './models/ResponseBodyV2DecodedPayload'
@@ -71,6 +72,7 @@ export { TransactionReason } from './models/TransactionReason'
7172
export { Type } from './models/Type'
7273
export { UserStatus } from './models/UserStatus'
7374
export { PromotionalOfferSignatureCreator } from './promotional_offer'
75+
export { PromotionalOfferV2SignatureCreator, AdvancedCommerceInAppSignatureCreator, AdvancedCommerceInAppRequest, IntroductoryOfferEligibilitySignatureCreator } from './jws_signature_creator'
7476
export { DecodedSignedData } from './models/DecodedSignedData'
7577
export { AppTransaction } from './models/AppTransaction'
7678

@@ -685,6 +687,13 @@ export enum APIError {
685687
*/
686688
INVALID_TRANSACTION_TYPE_NOT_SUPPORTED = 4000047,
687689

690+
/**
691+
* An error that indicates the endpoint doesn't support an app transaction ID.
692+
*
693+
* {@link https://developer.apple.com/documentation/appstoreserverapi/apptransactionidnotsupportederror AppTransactionIdNotSupportedError}
694+
*/
695+
APP_TRANSACTION_ID_NOT_SUPPORTED_ERROR = 4000048,
696+
688697
/**
689698
* An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state.
690699
*

jws_signature_creator.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import jsonwebtoken = require('jsonwebtoken');
4+
import { randomUUID } from 'crypto';
5+
6+
class BaseSignatureCreator {
7+
private audience: string
8+
private signingKey: string
9+
private keyId: string
10+
private issuerId: string
11+
private bundleId: string
12+
13+
public constructor(audience: string, signingKey: string, keyId: string, issuerId: string, bundleId: string) {
14+
this.audience = audience
15+
this.issuerId = issuerId
16+
this.keyId = keyId
17+
this.bundleId = bundleId
18+
this.signingKey = signingKey
19+
}
20+
21+
protected internalCreateSignature(featureSpecificClaims: { [key: string]: any }) {
22+
var claims = featureSpecificClaims
23+
24+
claims['bid'] = this.bundleId
25+
claims['nonce'] = randomUUID()
26+
27+
return jsonwebtoken.sign(claims, this.signingKey, { algorithm: 'ES256', keyid: this.keyId, issuer: this.issuerId, audience: this.audience})
28+
}
29+
}
30+
31+
export class PromotionalOfferV2SignatureCreator extends BaseSignatureCreator {
32+
/**
33+
* Create a PromotionalOfferV2SignatureCreator
34+
*
35+
* @param signingKey Your private key downloaded from App Store Connect
36+
* @param keyId Your private key ID from App Store Connect
37+
* @param issuerId Your issuer ID from the Keys page in App Store Connect
38+
* @param bundleId Your app's bundle ID
39+
*/
40+
public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) {
41+
super('promotional-offer', signingKey, keyId, issuerId, bundleId)
42+
}
43+
44+
/**
45+
* Create a promotional offer V2 signature.
46+
*
47+
* @param productId The unique identifier of the product
48+
* @param offerIdentifier The promotional offer identifier that you set up in App Store Connect
49+
* @param 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.
50+
* @return The signed JWS.
51+
* {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests}
52+
*/
53+
public createSignature(productId: string, offerIdentifier: string, transactionId: string | undefined = undefined) {
54+
let featureSpecificClaims: { [key: string]: any } = {}
55+
featureSpecificClaims['productId'] = productId
56+
featureSpecificClaims['offerIdentifier'] = offerIdentifier
57+
if (transactionId != null) {
58+
featureSpecificClaims['transactionId'] = transactionId
59+
}
60+
return super.internalCreateSignature(featureSpecificClaims)
61+
}
62+
}
63+
64+
export class IntroductoryOfferEligibilitySignatureCreator extends BaseSignatureCreator {
65+
/**
66+
* Create a IntroductoryOfferEligibilitySignatureCreator
67+
*
68+
* @param signingKey Your private key downloaded from App Store Connect
69+
* @param keyId Your private key ID from App Store Connect
70+
* @param issuerId Your issuer ID from the Keys page in App Store Connect
71+
* @param bundleId Your app's bundle ID
72+
*/
73+
public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) {
74+
super('introductory-offer-eligibility', signingKey, keyId, issuerId, bundleId)
75+
}
76+
77+
/**
78+
* Create an introductory offer eligibility signature.
79+
*
80+
* @param productId The unique identifier of the product
81+
* @param allowIntroductoryOffer A boolean value that determines whether the customer is eligible for an introductory offer
82+
* @param 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.
83+
* @return The signed JWS.
84+
* {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests}
85+
*/
86+
public createSignature(productId: string, allowIntroductoryOffer: boolean, transactionId: string) {
87+
let featureSpecificClaims: { [key: string]: any } = {}
88+
featureSpecificClaims['productId'] = productId
89+
featureSpecificClaims['allowIntroductoryOffer'] = allowIntroductoryOffer
90+
featureSpecificClaims['transactionId'] = transactionId
91+
return super.internalCreateSignature(featureSpecificClaims)
92+
}
93+
}
94+
95+
export interface AdvancedCommerceInAppRequest {}
96+
97+
export class AdvancedCommerceInAppSignatureCreator extends BaseSignatureCreator {
98+
/**
99+
* Create a AdvancedCommerceInAppSignatureCreator
100+
*
101+
* @param signingKey Your private key downloaded from App Store Connect
102+
* @param keyId Your private key ID from App Store Connect
103+
* @param issuerId Your issuer ID from the Keys page in App Store Connect
104+
* @param bundleId Your app's bundle ID
105+
*/
106+
public constructor(signingKey: string, keyId: string, issuerId: string, bundleId: string) {
107+
super('advanced-commerce-api', signingKey, keyId, issuerId, bundleId)
108+
}
109+
110+
/**
111+
* Create an Advanced Commerce in-app signed request.
112+
*
113+
* @param AdvancedCommerceInAppRequest The request to be signed.
114+
* @return The signed JWS.
115+
* {@link https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests Generating JWS to sign App Store requests}
116+
*/
117+
public createSignature(AdvancedCommerceInAppRequest: AdvancedCommerceInAppRequest) {
118+
let jsonRequest = JSON.stringify(AdvancedCommerceInAppRequest)
119+
120+
let base64Request = Buffer.from(jsonRequest, 'utf-8').toString('base64')
121+
122+
let featureSpecificClaims: { [key: string]: string } = {}
123+
featureSpecificClaims['request'] = base64Request
124+
return super.internalCreateSignature(featureSpecificClaims)
125+
}
126+
}

models/AppTransaction.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2023 Apple Inc. Licensed under MIT License.
22

33
import { Environment, EnvironmentValidator } from "./Environment"
4+
import { PurchasePlatform, PurchasePlatformValidator } from "./PurchasePlatform"
45
import { Validator } from "./Validator"
56

67
/**
@@ -86,10 +87,25 @@ export interface AppTransaction {
8687
* {@link https://developer.apple.com/documentation/storekit/apptransaction/4013175-preorderdate preorderDate}
8788
*/
8889
preorderDate?: number
90+
91+
/**
92+
* The unique identifier of the app download transaction.
93+
*
94+
* {@link https://developer.apple.com/documentation/storekit/apptransaction/apptransactionid appTransactionId}
95+
*/
96+
appTransactionId?: string
97+
98+
/**
99+
* The platform on which the customer originally purchased the app.
100+
*
101+
* {@link https://developer.apple.com/documentation/storekit/apptransaction/originalplatform-4mogz originalPlatform}
102+
*/
103+
originalPlatform?: PurchasePlatform | string
89104
}
90105

91106
export class AppTransactionValidator implements Validator<AppTransaction> {
92107
static readonly environmentValidator = new EnvironmentValidator()
108+
static readonly originalPlatformValidator = new PurchasePlatformValidator()
93109
validate(obj: any): obj is AppTransaction {
94110
if ((typeof obj['appAppleId'] !== 'undefined') && !(typeof obj['appAppleId'] === "number")) {
95111
return false
@@ -124,6 +140,12 @@ export class AppTransactionValidator implements Validator<AppTransaction> {
124140
if ((typeof obj['environment'] !== 'undefined') && !(AppTransactionValidator.environmentValidator.validate(obj['environment']))) {
125141
return false
126142
}
143+
if ((typeof obj['appTransactionId'] !== 'undefined') && !(typeof obj['appTransactionId'] === "string" || obj['appTransactionId'] instanceof String)) {
144+
return false
145+
}
146+
if ((typeof obj['originalPlatform'] !== 'undefined') && !(AppTransactionValidator.originalPlatformValidator.validate(obj['originalPlatform']))) {
147+
return false
148+
}
127149
return true
128150
}
129151
}

models/JWSRenewalInfoDecodedPayload.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,28 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData {
140140
*
141141
* {@link https://developer.apple.com/documentation/appstoreserverapi/eligiblewinbackofferids eligibleWinBackOfferIds}
142142
**/
143-
eligibleWinBackOfferIds?: string[];
143+
eligibleWinBackOfferIds?: string[]
144+
145+
/**
146+
* The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction.
147+
*
148+
* {@link https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken appAccountToken}
149+
**/
150+
appAccountToken?: string
151+
152+
/**
153+
* The unique identifier of the app download transaction.
154+
*
155+
* {@link https://developer.apple.com/documentation/appstoreserverapi/appTransactionId appTransactionId}
156+
**/
157+
appTransactionId?: string
158+
159+
/**
160+
* The duration of the offer.
161+
*
162+
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerPeriod offerPeriod}
163+
**/
164+
offerPeriod?: string
144165
}
145166

146167

@@ -213,6 +234,15 @@ export class JWSRenewalInfoDecodedPayloadValidator implements Validator<JWSRenew
213234
}
214235
}
215236
}
237+
if ((typeof obj['appAccountToken'] !== 'undefined') && !(typeof obj['appAccountToken'] === "string" || obj['appAccountToken'] instanceof String)) {
238+
return false
239+
}
240+
if ((typeof obj['appTransactionId'] !== 'undefined') && !(typeof obj['appTransactionId'] === "string" || obj['appTransactionId'] instanceof String)) {
241+
return false
242+
}
243+
if ((typeof obj['offerPeriod'] !== 'undefined') && !(typeof obj['offerPeriod'] === "string" || obj['offerPeriod'] instanceof String)) {
244+
return false
245+
}
216246
return true
217247
}
218248
}

models/JWSTransactionDecodedPayload.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,20 @@ export interface JWSTransactionDecodedPayload extends DecodedSignedData {
198198
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype offerDiscountType}
199199
**/
200200
offerDiscountType?: OfferDiscountType | string
201+
202+
/**
203+
* The unique identifier of the app download transaction.
204+
*
205+
* {@link https://developer.apple.com/documentation/appstoreserverapi/appTransactionId appTransactionId}
206+
**/
207+
appTransactionId?: string
208+
209+
/**
210+
* The duration of the offer.
211+
*
212+
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerPeriod offerPeriod}
213+
**/
214+
offerPeriod?: string
201215
}
202216

203217

@@ -288,6 +302,12 @@ export class JWSTransactionDecodedPayloadValidator implements Validator<JWSTrans
288302
if ((typeof obj['offerDiscountType'] !== 'undefined') && !(JWSTransactionDecodedPayloadValidator.offerDiscountTypeValidator.validate(obj['offerDiscountType']))) {
289303
return false
290304
}
305+
if ((typeof obj['appTransactionId'] !== 'undefined') && !(typeof obj['appTransactionId'] === "string" || obj['appTransactionId'] instanceof String)) {
306+
return false
307+
}
308+
if ((typeof obj['offerPeriod'] !== 'undefined') && !(typeof obj['offerPeriod'] === "string" || obj['offerPeriod'] instanceof String)) {
309+
return false
310+
}
291311
return true
292312
}
293313
}

models/PurchasePlatform.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+
import { StringValidator } from "./Validator";
4+
5+
/**
6+
* Values that represent Apple platforms.
7+
*
8+
* {@link https://developer.apple.com/documentation/storekit/appstore/platform AppStore.Platform}
9+
*/
10+
export enum PurchasePlatform {
11+
IOS = "iOS",
12+
MAC_OS = "macOS",
13+
TV_OS = "tvOS",
14+
VISION_OS = "visionOS"
15+
}
16+
17+
export class PurchasePlatformValidator extends StringValidator {}

promotional_offer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ export class PromotionalOfferSignatureCreator {
2020
* {@link https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers Generating a signature for promotional offers}
2121
* @param productIdentifier The subscription product identifier
2222
* @param subscriptionOfferID The subscription discount identifier
23-
* @param applicationUsername An optional string value that you define; may be an empty string
23+
* @param appAccountToken An optional string value that you define; may be an empty string
2424
* @param nonce A one-time UUID value that your server generates. Generate a new nonce for every signature.
2525
* @param timestamp A timestamp your server generates in UNIX time format, in milliseconds. The timestamp keeps the offer active for 24 hours.
2626
* @return The Base64 encoded signature
2727
*/
28-
public createSignature(productIdentifier: string, subscriptionOfferID: string, applicationUsername: string, nonce: string, timestamp: number): string {
28+
public createSignature(productIdentifier: string, subscriptionOfferID: string, appAccountToken: string, nonce: string, timestamp: number): string {
2929
const payload = this.bundleId + '\u2063' +
3030
this.keyId + '\u2063' +
3131
productIdentifier + '\u2063' +
3232
subscriptionOfferID + '\u2063' +
33-
applicationUsername.toLowerCase() + '\u2063'+
33+
appAccountToken.toLowerCase() + '\u2063'+
3434
nonce.toLowerCase() + '\u2063' +
3535
timestamp;
3636
const sign = createSign('SHA256')

tests/resources/models/appTransaction.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@
99
"originalApplicationVersion": "1.1.2",
1010
"deviceVerification": "device_verification_value",
1111
"deviceVerificationNonce": "48ccfa42-7431-4f22-9908-7e88983e105a",
12-
"preorderDate": 1698148700000
12+
"preorderDate": 1698148700000,
13+
"appTransactionId": "71134",
14+
"originalPlatform": "iOS"
1315
}

tests/resources/models/signedRenewalInfo.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@
1919
"eligibleWinBackOfferIds": [
2020
"eligible1",
2121
"eligible2"
22-
]
22+
],
23+
"appTransactionId": "71134",
24+
"offerPeriod": "P1Y",
25+
"appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138"
2326
}

0 commit comments

Comments
 (0)