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
15 changes: 15 additions & 0 deletions Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ public class AppStoreServerAPIClient {
}
return await makeRequestWithResponseBody(path: "/inApps/" + version.rawValue + "/history/" + transactionId, method: .GET, queryParameters: queryParams, body: request)
}

///Get information about a single transaction for your app.
///- Parameter transactionId: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.
///- Returns: A response that contains signed transaction information for a single transaction.
Expand Down Expand Up @@ -452,6 +453,15 @@ public class AppStoreServerAPIClient {
return await makeRequestWithoutResponseBody(path: "/inApps/v1/messaging/default/" + productId + "/" + locale, method: .DELETE, queryParameters: [:], body: request)
}

///Get a customer's app transaction information for your app.
///- Parameter transactionId: Any originalTransactionId, transactionId or appTransactionId that belongs to the customer for your app.
///- Returns: A response that contains signed app transaction information for a customer.
///[Get App Transaction Info](https://developer.apple.com/documentation/appstoreserverapi/get-app-transaction-info)
public func getAppTransactionInfo(transactionId: String) async -> APIResult<AppTransactionInfoResponse> {
let request: String? = nil
return await makeRequestWithResponseBody(path: "/inApps/v1/transactions/appTransactions/" + transactionId, method: .GET, queryParameters: [:], body: request)
}

internal struct AppStoreServerAPIJWT: JWTPayload, Equatable {
var exp: ExpirationClaim
var iss: IssuerClaim
Expand Down Expand Up @@ -808,6 +818,11 @@ public enum APIError: Int64 {
///[TransactionIdNotFoundError](https://developer.apple.com/documentation/appstoreserverapi/transactionidnotfounderror)
case transactionIdNotFound = 4040010

///An error response that indicates an app transaction doesn't exist for the specified customer.
///
///[AppTransactionDoesNotExistError](https://developer.apple.com/documentation/appstoreserverapi/apptransactiondoesnotexisterror)
case AppTransactionDoesNotExistError = 4040019

///An error that indicates the system can't find the image identifier.
///
///[ImageNotFoundError](https://developer.apple.com/documentation/retentionmessaging/imagenotfounderror)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2025 Apple Inc. Licensed under MIT License.

///A response that contains signed app transaction information for a customer.
///
///[AppTransactionInfoResponse](https://developer.apple.com/documentation/appstoreserverapi/apptransactioninforesponse)
public struct AppTransactionInfoResponse: Decodable, Encodable, Hashable, Sendable {

public init(signedAppTransactionInfo: String? = nil) {
self.signedAppTransactionInfo = signedAppTransactionInfo
}

///A customer’s app transaction information, signed by Apple, in JSON Web Signature (JWS) format.
///
///[JWSAppTransaction](https://developer.apple.com/documentation/appstoreserverapi/jwsapptransaction)
public var signedAppTransactionInfo: String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,68 @@ final class AppStoreServerAPIClientTests: XCTestCase {
}
}

public func testGetAppTransactionInfo() async throws {
let client = try getClientWithBody("resources/models/appTransactionInfoResponse.json") { request, body in
XCTAssertEqual(.GET, request.method)
XCTAssertEqual("https://local-testing-base-url/inApps/v1/transactions/appTransactions/1234", request.url)
XCTAssertNil(request.body)
}

let response = await client.getAppTransactionInfo(transactionId: "1234")

guard case .success(let appTransactionInfoResponse) = response else {
XCTAssertTrue(false)
return
}
XCTAssertEqual("signed_app_transaction_info_value", appTransactionInfoResponse.signedAppTransactionInfo)
TestingUtility.confirmCodableInternallyConsistent(appTransactionInfoResponse)
}

public func testGetAppTransactionInfoInvalidTransactionId() async throws {
let body = TestingUtility.readFile("resources/models/invalidTransactionIdError.json")
let client = try getAppStoreServerAPIClient(body, .badRequest, nil)
let result = await client.getAppTransactionInfo(transactionId: "invalid_id")
guard case .failure(let statusCode, let rawApiError, let apiError, let errorMessage, let causedBy) = result else {
XCTAssertTrue(false)
return
}
XCTAssertEqual(400, statusCode)
XCTAssertEqual(APIError.invalidTransactionId, apiError)
XCTAssertEqual(4000006, rawApiError)
XCTAssertEqual("Invalid transaction id.", errorMessage)
XCTAssertNil(causedBy)
}

public func testGetAppTransactionInfoTransactionIdNotFound() async throws {
let body = TestingUtility.readFile("resources/models/transactionIdNotFoundError.json")
let client = try getAppStoreServerAPIClient(body, .notFound, nil)
let result = await client.getAppTransactionInfo(transactionId: "not_found_id")
guard case .failure(let statusCode, let rawApiError, let apiError, let errorMessage, let causedBy) = result else {
XCTAssertTrue(false)
return
}
XCTAssertEqual(404, statusCode)
XCTAssertEqual(APIError.transactionIdNotFound, apiError)
XCTAssertEqual(4040010, rawApiError)
XCTAssertEqual("Transaction id not found.", errorMessage)
XCTAssertNil(causedBy)
}

public func testGetAppTransactionInfoAppTransactionDoesNotExist() async throws {
let body = TestingUtility.readFile("resources/models/appTransactionDoesNotExistError.json")
let client = try getAppStoreServerAPIClient(body, .notFound, nil)
let result = await client.getAppTransactionInfo(transactionId: "no_app_transaction")
guard case .failure(let statusCode, let rawApiError, let apiError, let errorMessage, let causedBy) = result else {
XCTAssertTrue(false)
return
}
XCTAssertEqual(404, statusCode)
XCTAssertEqual(APIError.AppTransactionDoesNotExistError, apiError)
XCTAssertEqual(4040019, rawApiError)
XCTAssertEqual("No AppTransaction exists for the customer.", errorMessage)
XCTAssertNil(causedBy)
}

public func getClientWithBody(_ path: String, _ requestVerifier: @escaping RequestVerifier) throws -> AppStoreServerAPIClient {
let body = TestingUtility.readFile(path)
return try getAppStoreServerAPIClient(body, requestVerifier)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"errorCode": 4040019,
"errorMessage": "No AppTransaction exists for the customer."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"signedAppTransactionInfo": "signed_app_transaction_info_value"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"errorCode": 4000006,
"errorMessage": "Invalid transaction id."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"errorCode": 4040010,
"errorMessage": "Transaction id not found."
}