Skip to content

Commit eee7609

Browse files
Merge pull request #146 from alexanderjordanbaker/AppStoreServerAPI112
Add support for App Store Server API v1.12 and App Store Server Notif…
2 parents eb340d8 + ba6b87f commit eee7609

File tree

7 files changed

+102
-9
lines changed

7 files changed

+102
-9
lines changed

index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,12 @@ export class AppStoreServerAPIClient {
288288
*
289289
* @param transactionId The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.
290290
* @param revision A token you provide to get the next set of up to 20 transactions. All responses include a revision token. Note: For requests that use the revision token, include the same query parameters from the initial request. Use the revision token from the previous HistoryResponse.
291+
* @param version The version of the Get Transaction History endpoint to use. V2 is recommended.
291292
* @return A response that contains the customer’s transaction history for an app.
292293
* @throws APIException If a response was returned indicating the request could not be processed
293294
* {@link https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history Get Transaction History}
294295
*/
295-
public async getTransactionHistory(transactionId: string, revision: string | null, transactionHistoryRequest: TransactionHistoryRequest): Promise<HistoryResponse> {
296+
public async getTransactionHistory(transactionId: string, revision: string | null, transactionHistoryRequest: TransactionHistoryRequest, version: GetTransactionHistoryVersion = GetTransactionHistoryVersion.V1): Promise<HistoryResponse> {
296297
const queryParameters: { [key: string]: string[]} = {}
297298
if (revision != null) {
298299
queryParameters["revision"] = [revision];
@@ -321,7 +322,7 @@ export class AppStoreServerAPIClient {
321322
if (transactionHistoryRequest.revoked !== undefined) {
322323
queryParameters["revoked"] = [transactionHistoryRequest.revoked.toString()];
323324
}
324-
return await this.makeRequest("/inApps/v1/history/" + transactionId, "GET", queryParameters, null, new HistoryResponseValidator());
325+
return await this.makeRequest("/inApps/" + version + "/history/" + transactionId, "GET", queryParameters, null, new HistoryResponseValidator());
325326
}
326327

327328
/**
@@ -794,4 +795,12 @@ export enum APIError {
794795
* {@link https://developer.apple.com/documentation/appstoreserverapi/generalinternalretryableerror GeneralInternalRetryableError}
795796
*/
796797
GENERAL_INTERNAL_RETRYABLE = 5000001,
797-
}
798+
}
799+
800+
export enum GetTransactionHistoryVersion {
801+
/**
802+
* @deprecated
803+
*/
804+
V1 = "v1",
805+
V2 = "v2",
806+
}

models/JWSRenewalInfoDecodedPayload.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AutoRenewStatus, AutoRenewStatusValidator } from "./AutoRenewStatus"
44
import { DecodedSignedData } from "./DecodedSignedData"
55
import { Environment, EnvironmentValidator } from "./Environment"
66
import { ExpirationIntent, ExpirationIntentValidator } from "./ExpirationIntent"
7+
import { OfferDiscountType, OfferDiscountTypeValidator } from "./OfferDiscountType"
78
import { OfferType, OfferTypeValidator } from "./OfferType"
89
import { PriceIncreaseStatus, PriceIncreaseStatusValidator } from "./PriceIncreaseStatus"
910
import { Validator } from "./Validator"
@@ -112,6 +113,27 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData {
112113
* {@link https://developer.apple.com/documentation/appstoreserverapi/renewaldate renewalDate}
113114
**/
114115
renewalDate?: number
116+
117+
/**
118+
* The currency code for the renewalPrice of the subscription.
119+
*
120+
* {@link https://developer.apple.com/documentation/appstoreserverapi/currency currency}
121+
**/
122+
currency?: string
123+
124+
/**
125+
* The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period.
126+
*
127+
* {@link https://developer.apple.com/documentation/appstoreserverapi/renewalprice renewalPrice}
128+
**/
129+
renewalPrice?: number
130+
131+
/**
132+
* The payment mode of the discount offer.
133+
*
134+
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype offerDiscountType}
135+
**/
136+
offerDiscountType?: OfferDiscountType | string
115137
}
116138

117139

@@ -121,6 +143,7 @@ export class JWSRenewalInfoDecodedPayloadValidator implements Validator<JWSRenew
121143
static readonly priceIncreaseStatusValidator = new PriceIncreaseStatusValidator()
122144
static readonly autoRenewStatusValidator = new AutoRenewStatusValidator()
123145
static readonly expirationIntentValidator = new ExpirationIntentValidator()
146+
static readonly offerDiscountTypeValidator = new OfferDiscountTypeValidator()
124147
validate(obj: any): obj is JWSRenewalInfoDecodedPayload {
125148
if ((typeof obj['expirationIntent'] !== 'undefined') && !(JWSRenewalInfoDecodedPayloadValidator.expirationIntentValidator.validate(obj['expirationIntent']))) {
126149
return false
@@ -164,6 +187,15 @@ export class JWSRenewalInfoDecodedPayloadValidator implements Validator<JWSRenew
164187
if ((typeof obj['renewalDate'] !== 'undefined') && !(typeof obj['renewalDate'] === 'number')) {
165188
return false
166189
}
190+
if ((typeof obj['currency'] !== 'undefined') && !(typeof obj['currency'] === "string" || obj['currency'] instanceof String)) {
191+
return false
192+
}
193+
if ((typeof obj['renewalPrice'] !== 'undefined') && !(typeof obj['renewalPrice'] === "number")) {
194+
return false
195+
}
196+
if ((typeof obj['offerDiscountType'] !== 'undefined') && !(JWSRenewalInfoDecodedPayloadValidator.offerDiscountTypeValidator.validate(obj['offerDiscountType']))) {
197+
return false
198+
}
167199
return true
168200
}
169201
}

models/NotificationTypeV2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export enum NotificationTypeV2 {
2626
RENEWAL_EXTENSION = "RENEWAL_EXTENSION",
2727
REFUND_REVERSED = "REFUND_REVERSED",
2828
EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN",
29+
ONE_TIME_CHARGE = "ONE_TIME_CHARGE",
2930
}
3031

3132
export class NotificationTypeV2Validator extends StringValidator {}

tests/resources/models/signedRenewalInfo.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
"signedDate": 1698148800000,
1313
"environment": "LocalTesting",
1414
"recentSubscriptionStartDate": 1698148800000,
15-
"renewalDate": 1698148850000
15+
"renewalDate": 1698148850000,
16+
"renewalPrice": 9990,
17+
"currency": "USD",
18+
"offerDiscountType": "PAY_AS_YOU_GO"
1619
}

tests/resources/models/signedTransaction.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@
2121
"environment":"LocalTesting",
2222
"transactionReason":"PURCHASE",
2323
"storefront":"USA",
24-
"storefrontId":"143441"
24+
"storefrontId":"143441",
25+
"price": 10990,
26+
"currency": "USD",
27+
"offerDiscountType": "PAY_AS_YOU_GO"
2528
}

tests/unit-tests/api_client.test.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { UserStatus } from "../../models/UserStatus";
1616
import { readFile } from "../util"
1717
import { InAppOwnershipType } from "../../models/InAppOwnershipType";
1818
import { RefundPreference } from "../../models/RefundPreference";
19-
import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index";
19+
import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, GetTransactionHistoryVersion, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index";
2020
import { Response } from "node-fetch";
2121

2222
import jsonwebtoken = require('jsonwebtoken');
@@ -288,7 +288,7 @@ describe('The api client ', () => {
288288
expect(expectedNotificationHistory).toStrictEqual(notificationHistoryResponse.notificationHistory)
289289
})
290290

291-
it('calls getTransactionHistory', async () => {
291+
it('calls getTransactionHistory V1', async () => {
292292
const client = getClientWithBody("tests/resources/models/transactionHistoryResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => {
293293
expect("GET").toBe(method)
294294
expect("/inApps/v1/history/1234").toBe(path)
@@ -315,7 +315,7 @@ describe('The api client ', () => {
315315
subscriptionGroupIdentifiers: ["sub_group_id", "sub_group_id_2"]
316316
}
317317

318-
const historyResponse = await client.getTransactionHistory("1234", "revision_input", request);
318+
const historyResponse = await client.getTransactionHistory("1234", "revision_input", request, GetTransactionHistoryVersion.V1);
319319

320320
expect(historyResponse).toBeTruthy()
321321
expect("revision_output").toBe(historyResponse.revision)
@@ -326,6 +326,44 @@ describe('The api client ', () => {
326326
expect(["signed_transaction_value", "signed_transaction_value2"]).toStrictEqual(historyResponse.signedTransactions)
327327
})
328328

329+
it('calls getTransactionHistory V2', async () => {
330+
const client = getClientWithBody("tests/resources/models/transactionHistoryResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => {
331+
expect("GET").toBe(method)
332+
expect("/inApps/v2/history/1234").toBe(path)
333+
expect("revision_input").toBe(parsedQueryParameters.get("revision"))
334+
expect("123455").toBe(parsedQueryParameters.get("startDate"))
335+
expect("123456").toBe(parsedQueryParameters.get("endDate"))
336+
expect(["com.example.1", "com.example.2"]).toStrictEqual(parsedQueryParameters.getAll("productId"))
337+
expect(["CONSUMABLE", "AUTO_RENEWABLE"]).toStrictEqual(parsedQueryParameters.getAll("productType"))
338+
expect("ASCENDING").toBe(parsedQueryParameters.get("sort"))
339+
expect(["sub_group_id", "sub_group_id_2"]).toStrictEqual(parsedQueryParameters.getAll("subscriptionGroupIdentifier"))
340+
expect("FAMILY_SHARED").toBe(parsedQueryParameters.get("inAppOwnershipType"))
341+
expect("false").toBe(parsedQueryParameters.get("revoked"))
342+
expect(stringBody).toBeUndefined()
343+
});
344+
345+
const request: TransactionHistoryRequest = {
346+
sort: Order.ASCENDING,
347+
productTypes: [ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],
348+
endDate: 123456,
349+
startDate: 123455,
350+
revoked: false,
351+
inAppOwnershipType: InAppOwnershipType.FAMILY_SHARED,
352+
productIds: ["com.example.1", "com.example.2"],
353+
subscriptionGroupIdentifiers: ["sub_group_id", "sub_group_id_2"]
354+
}
355+
356+
const historyResponse = await client.getTransactionHistory("1234", "revision_input", request, GetTransactionHistoryVersion.V2);
357+
358+
expect(historyResponse).toBeTruthy()
359+
expect("revision_output").toBe(historyResponse.revision)
360+
expect(historyResponse.hasMore).toBe(true)
361+
expect("com.example").toBe(historyResponse.bundleId)
362+
expect(323232).toBe(historyResponse.appAppleId)
363+
expect(Environment.LOCAL_TESTING).toBe(historyResponse.environment)
364+
expect(["signed_transaction_value", "signed_transaction_value2"]).toStrictEqual(historyResponse.signedTransactions)
365+
})
366+
329367
it('calls getTransactionInfo', async () => {
330368
const client = getClientWithBody("tests/resources/models/transactionInfoResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => {
331369
expect("GET").toBe(method)
@@ -481,7 +519,7 @@ describe('The api client ', () => {
481519
subscriptionGroupIdentifiers: ["sub_group_id", "sub_group_id_2"]
482520
}
483521

484-
const historyResponse = await client.getTransactionHistory("1234", "revision_input", request);
522+
const historyResponse = await client.getTransactionHistory("1234", "revision_input", request, GetTransactionHistoryVersion.V2);
485523
expect(historyResponse.environment).toBe("LocalTestingxxx")
486524
})
487525

tests/unit-tests/transaction_decoding.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { RevocationReason } from "../../models/RevocationReason";
1414
import { TransactionReason } from "../../models/TransactionReason";
1515
import { Type } from "../../models/Type";
1616
import { ConsumptionRequestReason } from "../../models/ConsumptionRequestReason";
17+
import { OfferDiscountType } from "../../models/OfferDiscountType";
1718

1819

1920
describe('Testing decoding of signed data', () => {
@@ -53,6 +54,9 @@ describe('Testing decoding of signed data', () => {
5354
expect(Environment.LOCAL_TESTING).toBe(renewalInfo.environment)
5455
expect(1698148800000).toBe(renewalInfo.recentSubscriptionStartDate)
5556
expect(1698148850000).toBe(renewalInfo.renewalDate)
57+
expect(9990).toBe(renewalInfo.renewalPrice)
58+
expect("USD").toBe(renewalInfo.currency)
59+
expect(OfferDiscountType.PAY_AS_YOU_GO).toBe(renewalInfo.offerDiscountType)
5660
})
5761
it('should decode a transaction info', async () => {
5862
const signedTransaction = createSignedDataFromJson("tests/resources/models/signedTransaction.json")
@@ -82,6 +86,9 @@ describe('Testing decoding of signed data', () => {
8286
expect("143441").toBe(transaction.storefrontId)
8387
expect(TransactionReason.PURCHASE).toBe(transaction.transactionReason)
8488
expect(Environment.LOCAL_TESTING).toBe(transaction.environment)
89+
expect(10990).toBe(transaction.price)
90+
expect("USD").toBe(transaction.currency)
91+
expect(OfferDiscountType.PAY_AS_YOU_GO).toBe(transaction.offerDiscountType)
8592
})
8693
it('should decode a signed notification', async () => {
8794
const signedNotification = createSignedDataFromJson("tests/resources/models/signedNotification.json")

0 commit comments

Comments
 (0)