Skip to content

Commit

Permalink
Merge pull request #146 from alexanderjordanbaker/AppStoreServerAPI112
Browse files Browse the repository at this point in the history
Add support for App Store Server API v1.12 and App Store Server Notif…
  • Loading branch information
alexanderjordanbaker authored Jun 11, 2024
2 parents eb340d8 + ba6b87f commit eee7609
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 9 deletions.
15 changes: 12 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,12 @@ export class AppStoreServerAPIClient {
*
* @param transactionId The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.
* @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.
* @param version The version of the Get Transaction History endpoint to use. V2 is recommended.
* @return A response that contains the customer’s transaction history for an app.
* @throws APIException If a response was returned indicating the request could not be processed
* {@link https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history Get Transaction History}
*/
public async getTransactionHistory(transactionId: string, revision: string | null, transactionHistoryRequest: TransactionHistoryRequest): Promise<HistoryResponse> {
public async getTransactionHistory(transactionId: string, revision: string | null, transactionHistoryRequest: TransactionHistoryRequest, version: GetTransactionHistoryVersion = GetTransactionHistoryVersion.V1): Promise<HistoryResponse> {
const queryParameters: { [key: string]: string[]} = {}
if (revision != null) {
queryParameters["revision"] = [revision];
Expand Down Expand Up @@ -321,7 +322,7 @@ export class AppStoreServerAPIClient {
if (transactionHistoryRequest.revoked !== undefined) {
queryParameters["revoked"] = [transactionHistoryRequest.revoked.toString()];
}
return await this.makeRequest("/inApps/v1/history/" + transactionId, "GET", queryParameters, null, new HistoryResponseValidator());
return await this.makeRequest("/inApps/" + version + "/history/" + transactionId, "GET", queryParameters, null, new HistoryResponseValidator());
}

/**
Expand Down Expand Up @@ -794,4 +795,12 @@ export enum APIError {
* {@link https://developer.apple.com/documentation/appstoreserverapi/generalinternalretryableerror GeneralInternalRetryableError}
*/
GENERAL_INTERNAL_RETRYABLE = 5000001,
}
}

export enum GetTransactionHistoryVersion {
/**
* @deprecated
*/
V1 = "v1",
V2 = "v2",
}
32 changes: 32 additions & 0 deletions models/JWSRenewalInfoDecodedPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AutoRenewStatus, AutoRenewStatusValidator } from "./AutoRenewStatus"
import { DecodedSignedData } from "./DecodedSignedData"
import { Environment, EnvironmentValidator } from "./Environment"
import { ExpirationIntent, ExpirationIntentValidator } from "./ExpirationIntent"
import { OfferDiscountType, OfferDiscountTypeValidator } from "./OfferDiscountType"
import { OfferType, OfferTypeValidator } from "./OfferType"
import { PriceIncreaseStatus, PriceIncreaseStatusValidator } from "./PriceIncreaseStatus"
import { Validator } from "./Validator"
Expand Down Expand Up @@ -112,6 +113,27 @@ export interface JWSRenewalInfoDecodedPayload extends DecodedSignedData {
* {@link https://developer.apple.com/documentation/appstoreserverapi/renewaldate renewalDate}
**/
renewalDate?: number

/**
* The currency code for the renewalPrice of the subscription.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/currency currency}
**/
currency?: string

/**
* The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/renewalprice renewalPrice}
**/
renewalPrice?: number

/**
* The payment mode of the discount offer.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype offerDiscountType}
**/
offerDiscountType?: OfferDiscountType | string
}


Expand All @@ -121,6 +143,7 @@ export class JWSRenewalInfoDecodedPayloadValidator implements Validator<JWSRenew
static readonly priceIncreaseStatusValidator = new PriceIncreaseStatusValidator()
static readonly autoRenewStatusValidator = new AutoRenewStatusValidator()
static readonly expirationIntentValidator = new ExpirationIntentValidator()
static readonly offerDiscountTypeValidator = new OfferDiscountTypeValidator()
validate(obj: any): obj is JWSRenewalInfoDecodedPayload {
if ((typeof obj['expirationIntent'] !== 'undefined') && !(JWSRenewalInfoDecodedPayloadValidator.expirationIntentValidator.validate(obj['expirationIntent']))) {
return false
Expand Down Expand Up @@ -164,6 +187,15 @@ export class JWSRenewalInfoDecodedPayloadValidator implements Validator<JWSRenew
if ((typeof obj['renewalDate'] !== 'undefined') && !(typeof obj['renewalDate'] === 'number')) {
return false
}
if ((typeof obj['currency'] !== 'undefined') && !(typeof obj['currency'] === "string" || obj['currency'] instanceof String)) {
return false
}
if ((typeof obj['renewalPrice'] !== 'undefined') && !(typeof obj['renewalPrice'] === "number")) {
return false
}
if ((typeof obj['offerDiscountType'] !== 'undefined') && !(JWSRenewalInfoDecodedPayloadValidator.offerDiscountTypeValidator.validate(obj['offerDiscountType']))) {
return false
}
return true
}
}
1 change: 1 addition & 0 deletions models/NotificationTypeV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum NotificationTypeV2 {
RENEWAL_EXTENSION = "RENEWAL_EXTENSION",
REFUND_REVERSED = "REFUND_REVERSED",
EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN",
ONE_TIME_CHARGE = "ONE_TIME_CHARGE",
}

export class NotificationTypeV2Validator extends StringValidator {}
5 changes: 4 additions & 1 deletion tests/resources/models/signedRenewalInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
"signedDate": 1698148800000,
"environment": "LocalTesting",
"recentSubscriptionStartDate": 1698148800000,
"renewalDate": 1698148850000
"renewalDate": 1698148850000,
"renewalPrice": 9990,
"currency": "USD",
"offerDiscountType": "PAY_AS_YOU_GO"
}
5 changes: 4 additions & 1 deletion tests/resources/models/signedTransaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@
"environment":"LocalTesting",
"transactionReason":"PURCHASE",
"storefront":"USA",
"storefrontId":"143441"
"storefrontId":"143441",
"price": 10990,
"currency": "USD",
"offerDiscountType": "PAY_AS_YOU_GO"
}
46 changes: 42 additions & 4 deletions tests/unit-tests/api_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { UserStatus } from "../../models/UserStatus";
import { readFile } from "../util"
import { InAppOwnershipType } from "../../models/InAppOwnershipType";
import { RefundPreference } from "../../models/RefundPreference";
import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index";
import { APIError, APIException, AppStoreServerAPIClient, ExtendReasonCode, ExtendRenewalDateRequest, GetTransactionHistoryVersion, MassExtendRenewalDateRequest, NotificationHistoryRequest, NotificationHistoryResponseItem, Order, OrderLookupStatus, ProductType, SendAttemptResult, TransactionHistoryRequest } from "../../index";
import { Response } from "node-fetch";

import jsonwebtoken = require('jsonwebtoken');
Expand Down Expand Up @@ -288,7 +288,7 @@ describe('The api client ', () => {
expect(expectedNotificationHistory).toStrictEqual(notificationHistoryResponse.notificationHistory)
})

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

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

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

it('calls getTransactionHistory V2', async () => {
const client = getClientWithBody("tests/resources/models/transactionHistoryResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => {
expect("GET").toBe(method)
expect("/inApps/v2/history/1234").toBe(path)
expect("revision_input").toBe(parsedQueryParameters.get("revision"))
expect("123455").toBe(parsedQueryParameters.get("startDate"))
expect("123456").toBe(parsedQueryParameters.get("endDate"))
expect(["com.example.1", "com.example.2"]).toStrictEqual(parsedQueryParameters.getAll("productId"))
expect(["CONSUMABLE", "AUTO_RENEWABLE"]).toStrictEqual(parsedQueryParameters.getAll("productType"))
expect("ASCENDING").toBe(parsedQueryParameters.get("sort"))
expect(["sub_group_id", "sub_group_id_2"]).toStrictEqual(parsedQueryParameters.getAll("subscriptionGroupIdentifier"))
expect("FAMILY_SHARED").toBe(parsedQueryParameters.get("inAppOwnershipType"))
expect("false").toBe(parsedQueryParameters.get("revoked"))
expect(stringBody).toBeUndefined()
});

const request: TransactionHistoryRequest = {
sort: Order.ASCENDING,
productTypes: [ProductType.CONSUMABLE, ProductType.AUTO_RENEWABLE],
endDate: 123456,
startDate: 123455,
revoked: false,
inAppOwnershipType: InAppOwnershipType.FAMILY_SHARED,
productIds: ["com.example.1", "com.example.2"],
subscriptionGroupIdentifiers: ["sub_group_id", "sub_group_id_2"]
}

const historyResponse = await client.getTransactionHistory("1234", "revision_input", request, GetTransactionHistoryVersion.V2);

expect(historyResponse).toBeTruthy()
expect("revision_output").toBe(historyResponse.revision)
expect(historyResponse.hasMore).toBe(true)
expect("com.example").toBe(historyResponse.bundleId)
expect(323232).toBe(historyResponse.appAppleId)
expect(Environment.LOCAL_TESTING).toBe(historyResponse.environment)
expect(["signed_transaction_value", "signed_transaction_value2"]).toStrictEqual(historyResponse.signedTransactions)
})

it('calls getTransactionInfo', async () => {
const client = getClientWithBody("tests/resources/models/transactionInfoResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, stringBody: string | undefined, headers: { [key: string]: string; }) => {
expect("GET").toBe(method)
Expand Down Expand Up @@ -481,7 +519,7 @@ describe('The api client ', () => {
subscriptionGroupIdentifiers: ["sub_group_id", "sub_group_id_2"]
}

const historyResponse = await client.getTransactionHistory("1234", "revision_input", request);
const historyResponse = await client.getTransactionHistory("1234", "revision_input", request, GetTransactionHistoryVersion.V2);
expect(historyResponse.environment).toBe("LocalTestingxxx")
})

Expand Down
7 changes: 7 additions & 0 deletions tests/unit-tests/transaction_decoding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { RevocationReason } from "../../models/RevocationReason";
import { TransactionReason } from "../../models/TransactionReason";
import { Type } from "../../models/Type";
import { ConsumptionRequestReason } from "../../models/ConsumptionRequestReason";
import { OfferDiscountType } from "../../models/OfferDiscountType";


describe('Testing decoding of signed data', () => {
Expand Down Expand Up @@ -53,6 +54,9 @@ describe('Testing decoding of signed data', () => {
expect(Environment.LOCAL_TESTING).toBe(renewalInfo.environment)
expect(1698148800000).toBe(renewalInfo.recentSubscriptionStartDate)
expect(1698148850000).toBe(renewalInfo.renewalDate)
expect(9990).toBe(renewalInfo.renewalPrice)
expect("USD").toBe(renewalInfo.currency)
expect(OfferDiscountType.PAY_AS_YOU_GO).toBe(renewalInfo.offerDiscountType)
})
it('should decode a transaction info', async () => {
const signedTransaction = createSignedDataFromJson("tests/resources/models/signedTransaction.json")
Expand Down Expand Up @@ -82,6 +86,9 @@ describe('Testing decoding of signed data', () => {
expect("143441").toBe(transaction.storefrontId)
expect(TransactionReason.PURCHASE).toBe(transaction.transactionReason)
expect(Environment.LOCAL_TESTING).toBe(transaction.environment)
expect(10990).toBe(transaction.price)
expect("USD").toBe(transaction.currency)
expect(OfferDiscountType.PAY_AS_YOU_GO).toBe(transaction.offerDiscountType)
})
it('should decode a signed notification', async () => {
const signedNotification = createSignedDataFromJson("tests/resources/models/signedNotification.json")
Expand Down

0 comments on commit eee7609

Please sign in to comment.