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
17 changes: 15 additions & 2 deletions Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,21 @@ public class AppStoreServerAPIClient {
let request: String? = nil
return await makeRequestWithResponseBody(path: "/inApps/v1/notifications/test/" + testNotificationToken, method: .GET, queryParameters: [:], body: request)
}

///See `getTransactionHistory(transactionId: String, revision: String?, transactionHistoryRequest: TransactionHistoryRequest, version: GetTransactionHistoryVersion)`
@available(*, deprecated)
public func getTransactionHistory(transactionId: String, revision: String?, transactionHistoryRequest: TransactionHistoryRequest) async -> APIResult<HistoryResponse> {
return await self.getTransactionHistory(transactionId: transactionId, revision: revision, transactionHistoryRequest: transactionHistoryRequest, version: .v1)
}

///Get a customer’s in-app purchase transaction history for your app.
///
///- Parameter transactionId: The identifier of a transaction that belongs to the customer, and which may be an original transaction identifier.
///- Parameter 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.
///- Parameter version: The version of the Get Transaction History endpoint to use. V2 is recommended.
///- Returns: A response that contains the customer’s transaction history for an app, or information about the failure
///[Get Transaction History](https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history)
public func getTransactionHistory(transactionId: String, revision: String?, transactionHistoryRequest: TransactionHistoryRequest) async -> APIResult<HistoryResponse> {
public func getTransactionHistory(transactionId: String, revision: String?, transactionHistoryRequest: TransactionHistoryRequest, version: GetTransactionHistoryVersion) async -> APIResult<HistoryResponse> {
let request: String? = nil
var queryParams: [String: [String]] = [:]
if let innerRevision = revision {
Expand Down Expand Up @@ -266,7 +273,7 @@ public class AppStoreServerAPIClient {
if let innerRevoked = transactionHistoryRequest.revoked {
queryParams["revoked"] = [String(innerRevoked)]
}
return await makeRequestWithResponseBody(path: "/inApps/v1/history/" + transactionId, method: .GET, queryParameters: queryParams, body: request)
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.
Expand Down Expand Up @@ -621,3 +628,9 @@ public enum APIError: Int64 {
///[GeneralInternalRetryableError](https://developer.apple.com/documentation/appstoreserverapi/generalinternalretryableerror)
case generalInternalRetryable = 5000001
}

public enum GetTransactionHistoryVersion: String {
@available(*, deprecated)
case v1 = "v1"
case v2 = "v2"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
///[JWSRenewalInfoDecodedPayload](https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload)
public struct JWSRenewalInfoDecodedPayload: DecodedSignedData, Decodable, Encodable, Hashable {

public init(expirationIntent: ExpirationIntent? = nil, originalTransactionId: String? = nil, autoRenewProductId: String? = nil, productId: String? = nil, autoRenewStatus: AutoRenewStatus? = nil, isInBillingRetryPeriod: Bool? = nil, priceIncreaseStatus: PriceIncreaseStatus? = nil, gracePeriodExpiresDate: Date? = nil, offerType: OfferType? = nil, offerIdentifier: String? = nil, signedDate: Date? = nil, environment: Environment? = nil, recentSubscriptionStartDate: Date? = nil, renewalDate: Date? = nil) {
public init(expirationIntent: ExpirationIntent? = nil, originalTransactionId: String? = nil, autoRenewProductId: String? = nil, productId: String? = nil, autoRenewStatus: AutoRenewStatus? = nil, isInBillingRetryPeriod: Bool? = nil, priceIncreaseStatus: PriceIncreaseStatus? = nil, gracePeriodExpiresDate: Date? = nil, offerType: OfferType? = nil, offerIdentifier: String? = nil, signedDate: Date? = nil, environment: Environment? = nil, recentSubscriptionStartDate: Date? = nil, renewalDate: Date? = nil, currency: String? = nil, renewalPrice: Int64? = nil, offerDiscountType: OfferDiscountType? = nil) {
self.expirationIntent = expirationIntent
self.originalTransactionId = originalTransactionId
self.autoRenewProductId = autoRenewProductId
Expand All @@ -21,9 +21,12 @@ public struct JWSRenewalInfoDecodedPayload: DecodedSignedData, Decodable, Encoda
self.environment = environment
self.recentSubscriptionStartDate = recentSubscriptionStartDate
self.renewalDate = renewalDate
self.currency = currency
self.renewalPrice = renewalPrice
self.offerDiscountType = offerDiscountType
}

public init(rawExpirationIntent: Int32? = nil, originalTransactionId: String? = nil, autoRenewProductId: String? = nil, productId: String? = nil, rawAutoRenewStatus: Int32? = nil, isInBillingRetryPeriod: Bool? = nil, rawPriceIncreaseStatus: Int32? = nil, gracePeriodExpiresDate: Date? = nil, rawOfferType: Int32? = nil, offerIdentifier: String? = nil, signedDate: Date? = nil, rawEnvironment: String? = nil, recentSubscriptionStartDate: Date? = nil, renewalDate: Date? = nil) {
public init(rawExpirationIntent: Int32? = nil, originalTransactionId: String? = nil, autoRenewProductId: String? = nil, productId: String? = nil, rawAutoRenewStatus: Int32? = nil, isInBillingRetryPeriod: Bool? = nil, rawPriceIncreaseStatus: Int32? = nil, gracePeriodExpiresDate: Date? = nil, rawOfferType: Int32? = nil, offerIdentifier: String? = nil, signedDate: Date? = nil, rawEnvironment: String? = nil, recentSubscriptionStartDate: Date? = nil, renewalDate: Date? = nil, currency: String? = nil, renewalPrice: Int64? = nil, offerDiscountType: OfferDiscountType? = nil) {
self.rawExpirationIntent = rawExpirationIntent
self.originalTransactionId = originalTransactionId
self.autoRenewProductId = autoRenewProductId
Expand All @@ -38,6 +41,9 @@ public struct JWSRenewalInfoDecodedPayload: DecodedSignedData, Decodable, Encoda
self.rawEnvironment = rawEnvironment
self.recentSubscriptionStartDate = recentSubscriptionStartDate
self.renewalDate = renewalDate
self.currency = currency
self.renewalPrice = renewalPrice
self.offerDiscountType = offerDiscountType
}

///The reason the subscription expired.
Expand Down Expand Up @@ -159,6 +165,31 @@ public struct JWSRenewalInfoDecodedPayload: DecodedSignedData, Decodable, Encoda
///
///[renewalDate](https://developer.apple.com/documentation/appstoreserverapi/renewaldate)
public var renewalDate: Date?

///The currency code for the renewalPrice of the subscription.
///
///[currency](https://developer.apple.com/documentation/appstoreserverapi/currency)
public var currency: String?

///The renewal price, in milliunits, of the auto-renewable subscription that renews at the next billing period.
///
///[renewalPrice](https://developer.apple.com/documentation/appstoreserverapi/renewalprice)
public var renewalPrice: Int64?

///The payment mode of the discount offer.
///
///[offerDiscountType](https://developer.apple.com/documentation/appstoreserverapi/offerdiscounttype)
public var offerDiscountType: OfferDiscountType? {
get {
return rawOfferDiscountType.flatMap { OfferDiscountType(rawValue: $0) }
}
set {
self.rawOfferDiscountType = newValue.map { $0.rawValue }
}
}

///See ``offerDiscountType``
public var rawOfferDiscountType: String?

public enum CodingKeys: CodingKey {
case expirationIntent
Expand All @@ -175,6 +206,9 @@ public struct JWSRenewalInfoDecodedPayload: DecodedSignedData, Decodable, Encoda
case environment
case recentSubscriptionStartDate
case renewalDate
case currency
case renewalPrice
case offerDiscountType
}

public init(from decoder: any Decoder) throws {
Expand All @@ -193,6 +227,9 @@ public struct JWSRenewalInfoDecodedPayload: DecodedSignedData, Decodable, Encoda
self.rawEnvironment = try container.decodeIfPresent(String.self, forKey: .environment)
self.recentSubscriptionStartDate = try container.decodeIfPresent(Date.self, forKey: .recentSubscriptionStartDate)
self.renewalDate = try container.decodeIfPresent(Date.self, forKey: .renewalDate)
self.currency = try container.decodeIfPresent(String.self, forKey: .currency)
self.renewalPrice = try container.decodeIfPresent(Int64.self, forKey: .renewalPrice)
self.rawOfferDiscountType = try container.decodeIfPresent(String.self, forKey: .offerDiscountType)
}

public func encode(to encoder: any Encoder) throws {
Expand All @@ -211,5 +248,8 @@ public struct JWSRenewalInfoDecodedPayload: DecodedSignedData, Decodable, Encoda
try container.encodeIfPresent(self.rawEnvironment, forKey: .environment)
try container.encodeIfPresent(self.recentSubscriptionStartDate, forKey: .recentSubscriptionStartDate)
try container.encodeIfPresent(self.renewalDate, forKey: .renewalDate)
try container.encodeIfPresent(self.currency, forKey: .currency)
try container.encodeIfPresent(self.renewalPrice, forKey: .renewalPrice)
try container.encodeIfPresent(self.rawOfferDiscountType, forKey: .offerDiscountType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ public enum NotificationTypeV2: String, Decodable, Encodable, Hashable {
case renewalExtension = "RENEWAL_EXTENSION"
case refundReversed = "REFUND_REVERSED"
case externalPurchaseToken = "EXTERNAL_PURCHASE_TOKEN"
case oneTimeCharge = "ONE_TIME_CHARGE"
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ final class AppStoreServerAPIClientTests: XCTestCase {
TestingUtility.confirmCodableInternallyConsistent(notificationHistoryResponse)
}

public func testGetTransactionHistory() async throws {
public func testGetTransactionHistoryV1() async throws {
let client = try getClientWithBody("resources/models/transactionHistoryResponse.json") { request, body in
XCTAssertEqual(.GET, request.method)
let url = request.url
Expand Down Expand Up @@ -294,6 +294,54 @@ final class AppStoreServerAPIClientTests: XCTestCase {
TestingUtility.confirmCodableInternallyConsistent(historyResponse)
}

public func testGetTransactionHistoryV2() async throws {
let client = try getClientWithBody("resources/models/transactionHistoryResponse.json") { request, body in
XCTAssertEqual(.GET, request.method)
let url = request.url
let urlComponents = URLComponents(string: url)
XCTAssertEqual("/inApps/v2/history/1234", urlComponents?.path)
let params: [String: [String]] = (urlComponents?.queryItems?.reduce(into: [String:[String]](), { dict, item in
dict[item.name, default: []].append(item.value!)
}))!
XCTAssertEqual(["revision_input"], params["revision"])
XCTAssertEqual(["123455"], params["startDate"])
XCTAssertEqual(["123456"], params["endDate"])
XCTAssertEqual(["com.example.1", "com.example.2"], params["productId"])
XCTAssertEqual(["CONSUMABLE", "AUTO_RENEWABLE"], params["productType"])
XCTAssertEqual(["ASCENDING"], params["sort"])
XCTAssertEqual(["sub_group_id", "sub_group_id_2"], params["subscriptionGroupIdentifier"])
XCTAssertEqual(["FAMILY_SHARED"], params["inAppOwnershipType"])
XCTAssertEqual(["false"], params["revoked"])
XCTAssertNil(request.body)
}

let request = TransactionHistoryRequest(
startDate: Date(timeIntervalSince1970: 123.455),
endDate: Date(timeIntervalSince1970: 123.456),
productIds: ["com.example.1", "com.example.2"],
productTypes: [.consumable, .autoRenewable],
sort: TransactionHistoryRequest.Order.ascending,
subscriptionGroupIdentifiers: ["sub_group_id", "sub_group_id_2"],
inAppOwnershipType: InAppOwnershipType.familyShared,
revoked: false
)

let response = await client.getTransactionHistory(transactionId: "1234", revision: "revision_input", transactionHistoryRequest: request, version: .v2)

guard case .success(let historyResponse) = response else {
XCTAssertTrue(false)
return
}
XCTAssertEqual("revision_output", historyResponse.revision)
XCTAssertEqual(true, historyResponse.hasMore)
XCTAssertEqual("com.example", historyResponse.bundleId)
XCTAssertEqual(323232, historyResponse.appAppleId)
XCTAssertEqual(Environment.localTesting, historyResponse.environment)
XCTAssertEqual("LocalTesting", historyResponse.rawEnvironment)
XCTAssertEqual(["signed_transaction_value", "signed_transaction_value2"], historyResponse.signedTransactions)
TestingUtility.confirmCodableInternallyConsistent(historyResponse)
}

public func testGetTransactionInfo() async throws {
let client = try getClientWithBody("resources/models/transactionInfoResponse.json") { request, body in
XCTAssertEqual(.GET, request.method)
Expand Down Expand Up @@ -519,7 +567,7 @@ final class AppStoreServerAPIClientTests: XCTestCase {
revoked: false
)

let response = await client.getTransactionHistory(transactionId: "1234", revision: "revision_input", transactionHistoryRequest: request)
let response = await client.getTransactionHistory(transactionId: "1234", revision: "revision_input", transactionHistoryRequest: request, version: .v2)

guard case .success(let historyResponse) = response else {
XCTAssertTrue(false)
Expand Down
8 changes: 8 additions & 0 deletions Tests/AppStoreServerLibraryTests/SignedModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual("PURCHASE", transaction.rawTransactionReason)
XCTAssertEqual(Environment.localTesting, transaction.environment)
XCTAssertEqual("LocalTesting", transaction.rawEnvironment)
XCTAssertEqual(10990, transaction.price)
XCTAssertEqual("USD", transaction.currency)
XCTAssertEqual(OfferDiscountType.payAsYouGo, transaction.offerDiscountType)
XCTAssertEqual("PAY_AS_YOU_GO", transaction.rawOfferDiscountType)
TestingUtility.confirmCodableInternallyConsistent(transaction)
}

Expand Down Expand Up @@ -240,6 +244,10 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual("LocalTesting", renewalInfo.rawEnvironment)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148800), renewalInfo.recentSubscriptionStartDate)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148850), renewalInfo.renewalDate)
XCTAssertEqual(9990, renewalInfo.renewalPrice)
XCTAssertEqual("USD", renewalInfo.currency)
XCTAssertEqual(OfferDiscountType.payAsYouGo, renewalInfo.offerDiscountType)
XCTAssertEqual("PAY_AS_YOU_GO", renewalInfo.rawOfferDiscountType)
TestingUtility.confirmCodableInternallyConsistent(renewalInfo)
}

Expand Down
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"
}
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"
}