diff --git a/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift b/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift index 2d26cbb..0561252 100644 --- a/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift +++ b/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift @@ -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. @@ -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 { + 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 @@ -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) diff --git a/Sources/AppStoreServerLibrary/Models/AppTransactionInfoResponse.swift b/Sources/AppStoreServerLibrary/Models/AppTransactionInfoResponse.swift new file mode 100644 index 0000000..59aeadc --- /dev/null +++ b/Sources/AppStoreServerLibrary/Models/AppTransactionInfoResponse.swift @@ -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? + } \ No newline at end of file diff --git a/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift b/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift index 2bd9057..3de7c78 100644 --- a/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift +++ b/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift @@ -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) diff --git a/Tests/AppStoreServerLibraryTests/resources/models/appTransactionDoesNotExistError.json b/Tests/AppStoreServerLibraryTests/resources/models/appTransactionDoesNotExistError.json new file mode 100644 index 0000000..a5730ea --- /dev/null +++ b/Tests/AppStoreServerLibraryTests/resources/models/appTransactionDoesNotExistError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4040019, + "errorMessage": "No AppTransaction exists for the customer." +} \ No newline at end of file diff --git a/Tests/AppStoreServerLibraryTests/resources/models/appTransactionInfoResponse.json b/Tests/AppStoreServerLibraryTests/resources/models/appTransactionInfoResponse.json new file mode 100644 index 0000000..2cef034 --- /dev/null +++ b/Tests/AppStoreServerLibraryTests/resources/models/appTransactionInfoResponse.json @@ -0,0 +1,3 @@ +{ + "signedAppTransactionInfo": "signed_app_transaction_info_value" +} \ No newline at end of file diff --git a/Tests/AppStoreServerLibraryTests/resources/models/invalidTransactionIdError.json b/Tests/AppStoreServerLibraryTests/resources/models/invalidTransactionIdError.json new file mode 100644 index 0000000..32fc281 --- /dev/null +++ b/Tests/AppStoreServerLibraryTests/resources/models/invalidTransactionIdError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4000006, + "errorMessage": "Invalid transaction id." +} \ No newline at end of file diff --git a/Tests/AppStoreServerLibraryTests/resources/models/transactionIdNotFoundError.json b/Tests/AppStoreServerLibraryTests/resources/models/transactionIdNotFoundError.json new file mode 100644 index 0000000..f445639 --- /dev/null +++ b/Tests/AppStoreServerLibraryTests/resources/models/transactionIdNotFoundError.json @@ -0,0 +1,4 @@ +{ + "errorCode": 4040010, + "errorMessage": "Transaction id not found." +} \ No newline at end of file