Skip to content

Add support for App Store Server API v1.12 and App Store Server Notif… #146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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