Skip to content
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
21 changes: 21 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { SignedDataVerifier, VerificationException, VerificationStatus } from '.
export { ReceiptUtility } from './receipt_utility'
export { AccountTenure } from "./models/AccountTenure"
export { AlternateProduct } from './models/AlternateProduct'
export { AppTransactionInfoResponse } from './models/AppTransactionInfoResponse';
export { AutoRenewStatus } from './models/AutoRenewStatus'
export { CheckTestNotificationResponse } from './models/CheckTestNotificationResponse'
export { ConsumptionRequest } from './models/ConsumptionRequest'
Expand Down Expand Up @@ -100,6 +101,7 @@ export { AppTransaction } from './models/AppTransaction'
export { ExternalPurchaseToken } from './models/ExternalPurchaseToken'

import jsonwebtoken = require('jsonwebtoken');
import { AppTransactionInfoResponse, AppTransactionInfoResponseValidator } from './models/AppTransactionInfoResponse';
import { NotificationHistoryRequest } from './models/NotificationHistoryRequest';
import { NotificationHistoryResponse, NotificationHistoryResponseValidator } from './models/NotificationHistoryResponse';
import { URLSearchParams } from 'url';
Expand Down Expand Up @@ -508,6 +510,18 @@ export class AppStoreServerAPIClient {
await this.makeRequest("/inApps/v1/messaging/default/" + productId + "/" + locale, "DELETE", {}, null, null, undefined);
}

/**
* Get a customer's app transaction information for your app.
*
* @param transactionId Any originalTransactionId, transactionId or appTransactionId that belongs to the customer for your app.
* @return A response that contains signed app transaction information for a customer.
* @throws APIException If a response was returned indicating the request could not be processed
* {@link https://developer.apple.com/documentation/appstoreserverapi/get-app-transaction-info Get App Transaction Info}
*/
public async getAppTransactionInfo(transactionId: string): Promise<AppTransactionInfoResponse> {
return await this.makeRequest("/inApps/v1/transactions/appTransactions/" + transactionId, "GET", {}, null, new AppTransactionInfoResponseValidator(), undefined);
}

private createBearerToken(): string {
const payload = {
bid: this.bundleId
Expand Down Expand Up @@ -1023,6 +1037,13 @@ export enum APIError {
*/
MESSAGE_NOT_FOUND = 4040015,

/**
* An error response that indicates an app transaction doesn’t exist for the specified customer.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/apptransactiondoesnotexisterror AppTransactionDoesNotExistError}
*/
APP_TRANSACTION_DOES_NOT_EXIST_ERROR = 4040019,

/**
* An error that indicates the image identifier already exists.
*
Expand Down
26 changes: 26 additions & 0 deletions models/AppTransactionInfoResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

import { Validator } from "./Validator"

/**
* A response that contains signed app transaction information for a customer.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/apptransactioninforesponse AppTransactionInfoResponse}
*/
export interface AppTransactionInfoResponse {
/**
* A customer’s app transaction information, signed by Apple, in JSON Web Signature (JWS) format.
*
* {@link https://developer.apple.com/documentation/appstoreserverapi/jwsapptransaction JWSAppTransaction}
**/
signedAppTransactionInfo?: string
}

export class AppTransactionInfoResponseValidator implements Validator<AppTransactionInfoResponse> {
validate(obj: any): obj is AppTransactionInfoResponse {
if ((typeof obj['signedAppTransactionInfo'] !== 'undefined') && !(typeof obj['signedAppTransactionInfo'] === "string" || obj['signedAppTransactionInfo'] instanceof String)) {
return false
}
return true
}
}
4 changes: 4 additions & 0 deletions tests/resources/models/appTransactionDoesNotExistError.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"errorCode": 4040019,
"errorMessage": "No AppTransaction exists for the customer."
}
3 changes: 3 additions & 0 deletions tests/resources/models/appTransactionInfoResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"signedAppTransactionInfo": "signed_app_transaction_info_value"
}
4 changes: 4 additions & 0 deletions tests/resources/models/invalidTransactionIdError.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"errorCode": 4000006,
"errorMessage": "Invalid transaction id."
}
4 changes: 4 additions & 0 deletions tests/resources/models/transactionIdNotFoundError.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"errorCode": 4040010,
"errorMessage": "Transaction id not found."
}
71 changes: 71 additions & 0 deletions tests/unit-tests/api_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,4 +801,75 @@ describe('The api client ', () => {

await client.deleteDefaultMessage("com.example.product", "en-US")
})

it('calls getAppTransactionInfo', async () => {
const client = getClientWithBody("tests/resources/models/appTransactionInfoResponse.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => {
expect("GET").toBe(method)
expect("/inApps/v1/transactions/appTransactions/1234").toBe(path)
expect(parsedQueryParameters.entries.length).toBe(0)
expect(requestBody).toBeUndefined()
});

const appTransactionInfoResponse = await client.getAppTransactionInfo("1234");

expect(appTransactionInfoResponse).toBeTruthy()
expect("signed_app_transaction_info_value").toBe(appTransactionInfoResponse.signedAppTransactionInfo)
})

it('calls getAppTransactionInfo but receives invalid transaction id error', async () => {
const client = getClientWithBody("tests/resources/models/invalidTransactionIdError.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => {
expect("GET").toBe(method)
expect("/inApps/v1/transactions/appTransactions/invalid_id").toBe(path)
expect(parsedQueryParameters.entries.length).toBe(0)
expect(requestBody).toBeUndefined()
}, 400);

try {
const appTransactionInfoResponse = await client.getAppTransactionInfo("invalid_id");
fail('this test call is expected to throw')
} catch (e) {
let error = e as APIException
expect(error.httpStatusCode).toBe(400)
expect(error.apiError).toBe(APIError.INVALID_TRANSACTION_ID)
expect(error.errorMessage).toBe("Invalid transaction id.")
}
})

it('calls getAppTransactionInfo but receives app transaction does not exist error', async () => {
const client = getClientWithBody("tests/resources/models/appTransactionDoesNotExistError.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => {
expect("GET").toBe(method)
expect("/inApps/v1/transactions/appTransactions/5678").toBe(path)
expect(parsedQueryParameters.entries.length).toBe(0)
expect(requestBody).toBeUndefined()
}, 404);

try {
const appTransactionInfoResponse = await client.getAppTransactionInfo("5678");
fail('this test call is expected to throw')
} catch (e) {
let error = e as APIException
expect(error.httpStatusCode).toBe(404)
expect(error.apiError).toBe(APIError.APP_TRANSACTION_DOES_NOT_EXIST_ERROR)
expect(error.errorMessage).toBe("No AppTransaction exists for the customer.")
}
})

it('calls getAppTransactionInfo but receives transaction id not found error', async () => {
const client = getClientWithBody("tests/resources/models/transactionIdNotFoundError.json", (path: string, parsedQueryParameters: URLSearchParams, method: string, requestBody: string | Buffer | undefined, headers: { [key: string]: string; }) => {
expect("GET").toBe(method)
expect("/inApps/v1/transactions/appTransactions/9999").toBe(path)
expect(parsedQueryParameters.entries.length).toBe(0)
expect(requestBody).toBeUndefined()
}, 404);

try {
const appTransactionInfoResponse = await client.getAppTransactionInfo("9999");
fail('this test call is expected to throw')
} catch (e) {
let error = e as APIException
expect(error.httpStatusCode).toBe(404)
expect(error.apiError).toBe(APIError.TRANSACTION_ID_NOT_FOUND)
expect(error.errorMessage).toBe("Transaction id not found.")
}
})
})