Skip to content

Commit

Permalink
Merge pull request #49 from alexanderjordanbaker/ASSAv1.11
Browse files Browse the repository at this point in the history
Add support for App Store Server API v1.11 and App Store Server Notif…
  • Loading branch information
alexanderjordanbaker committed May 6, 2024
2 parents f371464 + 4d2f5ce commit 72ff5ed
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 7 deletions.
6 changes: 6 additions & 0 deletions Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,14 @@ public enum APIError: Int64 {
///An error that indicates the transaction identifier doesn’t represent a consumable in-app purchase.
///
///[InvalidTransactionNotConsumableError](https://developer.apple.com/documentation/appstoreserverapi/invalidtransactionnotconsumableerror)
@available(*, deprecated)
case invalidTransactionNotConsumable = 4000043

///An error that indicates the transaction identifier represents an unsupported in-app purchase type.
///
///[InvalidTransactionTypeNotSupportedError](https://developer.apple.com/documentation/appstoreserverapi/invalidtransactiontypenotsupportederror)
case invalidTransactionTypeNotSupported = 4000047

///An error that indicates the subscription doesn't qualify for a renewal-date extension due to its subscription state.
///
///[SubscriptionExtensionIneligibleError](https://developer.apple.com/documentation/appstoreserverapi/subscriptionextensionineligibleerror)
Expand Down
21 changes: 19 additions & 2 deletions Sources/AppStoreServerLibrary/Models/ConsumptionRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation
///[ConsumptionRequest](https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest)
public struct ConsumptionRequest: Decodable, Encodable, Hashable {

public init(customerConsented: Bool? = nil, consumptionStatus: ConsumptionStatus? = nil, platform: Platform? = nil, sampleContentProvided: Bool? = nil, deliveryStatus: DeliveryStatus? = nil, appAccountToken: UUID? = nil, accountTenure: AccountTenure? = nil, playTime: PlayTime? = nil, lifetimeDollarsRefunded: LifetimeDollarsRefunded? = nil, lifetimeDollarsPurchased: LifetimeDollarsPurchased? = nil, userStatus: UserStatus? = nil) {
public init(customerConsented: Bool? = nil, consumptionStatus: ConsumptionStatus? = nil, platform: Platform? = nil, sampleContentProvided: Bool? = nil, deliveryStatus: DeliveryStatus? = nil, appAccountToken: UUID? = nil, accountTenure: AccountTenure? = nil, playTime: PlayTime? = nil, lifetimeDollarsRefunded: LifetimeDollarsRefunded? = nil, lifetimeDollarsPurchased: LifetimeDollarsPurchased? = nil, userStatus: UserStatus? = nil, refundPreference: RefundPreference? = nil) {
self.customerConsented = customerConsented
self.consumptionStatus = consumptionStatus
self.platform = platform
Expand All @@ -19,9 +19,10 @@ public struct ConsumptionRequest: Decodable, Encodable, Hashable {
self.lifetimeDollarsRefunded = lifetimeDollarsRefunded
self.lifetimeDollarsPurchased = lifetimeDollarsPurchased
self.userStatus = userStatus
self.refundPreference = refundPreference
}

public init(customerConsented: Bool? = nil, rawConsumptionStatus: Int32? = nil, rawPlatform: Int32? = nil, sampleContentProvided: Bool? = nil, rawDeliveryStatus: Int32? = nil, appAccountToken: UUID? = nil, rawAccountTenure: Int32? = nil, rawPlayTime: Int32? = nil, rawLifetimeDollarsRefunded: Int32? = nil, rawLifetimeDollarsPurchased: Int32? = nil, rawUserStatus: Int32? = nil) {
public init(customerConsented: Bool? = nil, rawConsumptionStatus: Int32? = nil, rawPlatform: Int32? = nil, sampleContentProvided: Bool? = nil, rawDeliveryStatus: Int32? = nil, appAccountToken: UUID? = nil, rawAccountTenure: Int32? = nil, rawPlayTime: Int32? = nil, rawLifetimeDollarsRefunded: Int32? = nil, rawLifetimeDollarsPurchased: Int32? = nil, rawUserStatus: Int32? = nil, rawRefundPreference: Int32? = nil) {
self.customerConsented = customerConsented
self.rawConsumptionStatus = rawConsumptionStatus
self.rawPlatform = rawPlatform
Expand All @@ -33,6 +34,7 @@ public struct ConsumptionRequest: Decodable, Encodable, Hashable {
self.rawLifetimeDollarsRefunded = rawLifetimeDollarsRefunded
self.rawLifetimeDollarsPurchased = rawLifetimeDollarsPurchased
self.rawUserStatus = rawUserStatus
self.rawRefundPreference = rawRefundPreference
}

///A Boolean value that indicates whether the customer consented to provide consumption data to the App Store.
Expand Down Expand Up @@ -169,5 +171,20 @@ public struct ConsumptionRequest: Decodable, Encodable, Hashable {

///See ``userStatus``
public var rawUserStatus: Int32?

///A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund.
///
///[refundPreference](https://developer.apple.com/documentation/appstoreserverapi/refundpreference)
public var refundPreference: RefundPreference? {
get {
return rawRefundPreference.flatMap { RefundPreference(rawValue: $0) }
}
set {
self.rawRefundPreference = newValue.map { $0.rawValue }
}
}

///See ``refundPreference``
public var rawRefundPreference: Int32?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) 2024 Apple Inc. Licensed under MIT License.

///The customer-provided reason for a refund request.
///
///[consumptionRequestReason](https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason)
public enum ConsumptionRequestReason: String, Decodable, Encodable, Hashable {
case unintendedPurchase = "UNINTENDED_PURCHASE"
case fulfillmentIssue = "FULFILLMENT_ISSUE"
case unsatisfiedWithPurchase = "UNSATISFIED_WITH_PURCHASE"
case legal = "LEGAL"
case other = "OTHER"
}
21 changes: 19 additions & 2 deletions Sources/AppStoreServerLibrary/Models/Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@
///[data](https://developer.apple.com/documentation/appstoreservernotifications/data)
public struct Data: Decodable, Encodable, Hashable {

public init(environment: Environment? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, bundleVersion: String? = nil, signedTransactionInfo: String? = nil, signedRenewalInfo: String? = nil, status: Status? = nil) {
public init(environment: Environment? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, bundleVersion: String? = nil, signedTransactionInfo: String? = nil, signedRenewalInfo: String? = nil, status: Status? = nil, consumptionRequestReason: ConsumptionRequestReason? = nil) {
self.environment = environment
self.appAppleId = appAppleId
self.bundleId = bundleId
self.bundleVersion = bundleVersion
self.signedTransactionInfo = signedTransactionInfo
self.signedRenewalInfo = signedRenewalInfo
self.status = status
self.consumptionRequestReason = consumptionRequestReason
}

public init(rawEnvironment: String? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, bundleVersion: String? = nil, signedTransactionInfo: String? = nil, signedRenewalInfo: String? = nil, rawStatus: Int32? = nil) {
public init(rawEnvironment: String? = nil, appAppleId: Int64? = nil, bundleId: String? = nil, bundleVersion: String? = nil, signedTransactionInfo: String? = nil, signedRenewalInfo: String? = nil, rawStatus: Int32? = nil, rawConsumptionRequestReason: String? = nil) {
self.rawEnvironment = rawEnvironment
self.appAppleId = appAppleId
self.bundleId = bundleId
self.bundleVersion = bundleVersion
self.signedTransactionInfo = signedTransactionInfo
self.signedRenewalInfo = signedRenewalInfo
self.rawStatus = rawStatus
self.rawConsumptionRequestReason = rawConsumptionRequestReason
}

///The server environment that the notification applies to, either sandbox or production.
Expand Down Expand Up @@ -79,4 +81,19 @@ public struct Data: Decodable, Encodable, Hashable {

///See ``status``
public var rawStatus: Int32?

///The reason the customer requested the refund.
///
///[consumptionRequestReason](https://developer.apple.com/documentation/appstoreservernotifications/consumptionrequestreason)
public var consumptionRequestReason: ConsumptionRequestReason? {
get {
return rawConsumptionRequestReason.flatMap { ConsumptionRequestReason(rawValue: $0) }
}
set {
self.rawConsumptionRequestReason = newValue.map { $0.rawValue }
}
}

///See ``consumptionRequestReason``
public var rawConsumptionRequestReason: String?
}
11 changes: 11 additions & 0 deletions Sources/AppStoreServerLibrary/Models/RefundPreference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) 2024 Apple Inc. Licensed under MIT License.

///A value that indicates your preferred outcome for the refund request.
///
///[refundPreference](https://developer.apple.com/documentation/appstoreserverapi/refundpreference)
public enum RefundPreference: Int32, Decodable, Encodable, Hashable {
case undeclared = 0
case preferGrant = 1
case preferDecline = 2
case noPreference = 3
}
4 changes: 2 additions & 2 deletions Sources/AppStoreServerLibrary/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ internal func getJsonEncoder() -> JSONEncoder {

private struct RawValueCodingKey: CodingKey {

private static let keysToRawKeys = ["environment": "rawEnvironment", "receiptType": "rawReceiptType", "consumptionStatus": "rawConsumptionStatus", "platform": "rawPlatform", "deliveryStatus": "rawDeliveryStatus", "accountTenure": "rawAccountTenure", "playTime": "rawPlayTime", "lifetimeDollarsRefunded": "rawLifetimeDollarsRefunded", "lifetimeDollarsPurchased": "rawLifetimeDollarsPurchased", "userStatus": "rawUserStatus", "status": "rawStatus", "expirationIntent": "rawExpirationIntent", "priceIncreaseStatus": "rawPriceIncreaseStatus", "offerType": "rawOfferType", "type": "rawType", "inAppOwnershipType": "rawInAppOwnershipType", "revocationReason": "rawRevocationReason", "transactionReason": "rawTransactionReason", "offerDiscountType": "rawOfferDiscountType", "notificationType": "rawNotificationType", "subtype": "rawSubtype", "sendAttemptResult": "rawSendAttemptResult", "autoRenewStatus": "rawAutoRenewStatus"]
private static let rawKeysToKeys = ["rawEnvironment": "environment", "rawReceiptType": "receiptType", "rawConsumptionStatus": "consumptionStatus", "rawPlatform": "platform", "rawDeliveryStatus": "deliveryStatus", "rawAccountTenure": "accountTenure", "rawPlayTime": "playTime", "rawLifetimeDollarsRefunded": "lifetimeDollarsRefunded", "rawLifetimeDollarsPurchased": "lifetimeDollarsPurchased", "rawUserStatus": "userStatus", "rawStatus": "status", "rawExpirationIntent": "expirationIntent", "rawPriceIncreaseStatus": "priceIncreaseStatus", "rawOfferType": "offerType", "rawType": "type", "rawInAppOwnershipType": "inAppOwnershipType", "rawRevocationReason": "revocationReason", "rawTransactionReason": "transactionReason", "rawOfferDiscountType": "offerDiscountType", "rawNotificationType": "notificationType", "rawSubtype": "subtype", "rawSendAttemptResult": "sendAttemptResult", "rawAutoRenewStatus": "autoRenewStatus"]
private static let keysToRawKeys = ["environment": "rawEnvironment", "receiptType": "rawReceiptType", "consumptionStatus": "rawConsumptionStatus", "platform": "rawPlatform", "deliveryStatus": "rawDeliveryStatus", "accountTenure": "rawAccountTenure", "playTime": "rawPlayTime", "lifetimeDollarsRefunded": "rawLifetimeDollarsRefunded", "lifetimeDollarsPurchased": "rawLifetimeDollarsPurchased", "userStatus": "rawUserStatus", "status": "rawStatus", "expirationIntent": "rawExpirationIntent", "priceIncreaseStatus": "rawPriceIncreaseStatus", "offerType": "rawOfferType", "type": "rawType", "inAppOwnershipType": "rawInAppOwnershipType", "revocationReason": "rawRevocationReason", "transactionReason": "rawTransactionReason", "offerDiscountType": "rawOfferDiscountType", "notificationType": "rawNotificationType", "subtype": "rawSubtype", "sendAttemptResult": "rawSendAttemptResult", "autoRenewStatus": "rawAutoRenewStatus", "refundPreference": "rawRefundPeference", "consumptionRequestReason": "rawConsumptionRequestReason"]
private static let rawKeysToKeys = ["rawEnvironment": "environment", "rawReceiptType": "receiptType", "rawConsumptionStatus": "consumptionStatus", "rawPlatform": "platform", "rawDeliveryStatus": "deliveryStatus", "rawAccountTenure": "accountTenure", "rawPlayTime": "playTime", "rawLifetimeDollarsRefunded": "lifetimeDollarsRefunded", "rawLifetimeDollarsPurchased": "lifetimeDollarsPurchased", "rawUserStatus": "userStatus", "rawStatus": "status", "rawExpirationIntent": "expirationIntent", "rawPriceIncreaseStatus": "priceIncreaseStatus", "rawOfferType": "offerType", "rawType": "type", "rawInAppOwnershipType": "inAppOwnershipType", "rawRevocationReason": "revocationReason", "rawTransactionReason": "transactionReason", "rawOfferDiscountType": "offerDiscountType", "rawNotificationType": "notificationType", "rawSubtype": "subtype", "rawSendAttemptResult": "sendAttemptResult", "rawAutoRenewStatus": "autoRenewStatus", "rawRefundPreference": "refundPreference", "rawConsumptionRequestReason": "consumptionRequestReason"]

var stringValue: String
var intValue: Int?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ final class AppStoreServerAPIClientTests: XCTestCase {
XCTAssertEqual(6, decodedJson["lifetimeDollarsRefunded"] as! Int)
XCTAssertEqual(7, decodedJson["lifetimeDollarsPurchased"] as! Int)
XCTAssertEqual(4, decodedJson["userStatus"] as! Int)
XCTAssertEqual(3, decodedJson["refundPreference"] as! Int)
}

let consumptionRequest = ConsumptionRequest(
Expand All @@ -360,7 +361,8 @@ final class AppStoreServerAPIClientTests: XCTestCase {
playTime: PlayTime.oneDayToFourDays,
lifetimeDollarsRefunded: LifetimeDollarsRefunded.oneThousandDollarsToOneThousandNineHundredNinetyNineDollarsAndNinetyNineCents,
lifetimeDollarsPurchased: LifetimeDollarsPurchased.twoThousandDollarsOrGreater,
userStatus: UserStatus.limitedAccess
userStatus: UserStatus.limitedAccess,
refundPreference: RefundPreference.noPreference
)

let response = await client.sendConsumptionData(transactionId: "49571273", consumptionRequest: consumptionRequest)
Expand Down
35 changes: 35 additions & 0 deletions Tests/AppStoreServerLibraryTests/SignedModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,41 @@ final class SignedModelTests: XCTestCase {
XCTAssertEqual("signed_renewal_info_value", notification.data!.signedRenewalInfo)
XCTAssertEqual(Status.active, notification.data!.status)
XCTAssertEqual(1, notification.data!.rawStatus)
XCTAssertNil(notification.data!.consumptionRequestReason)
XCTAssertNil(notification.data!.rawConsumptionRequestReason)
}

public func testConsumptionRequestNotificationDecoding() async throws {
let signedNotification = TestingUtility.createSignedDataFromJson("resources/models/signedConsumptionRequestNotification.json")

let verifiedNotification = await TestingUtility.getSignedDataVerifier().verifyAndDecodeNotification(signedPayload: signedNotification)

guard case .valid(let notification) = verifiedNotification else {
XCTAssertTrue(false)
return
}

XCTAssertEqual(NotificationTypeV2.consumptionRequest, notification.notificationType)
XCTAssertEqual("CONSUMPTION_REQUEST", notification.rawNotificationType)
XCTAssertNil(notification.subtype)
XCTAssertNil(notification.rawSubtype)
XCTAssertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID)
XCTAssertEqual("2.0", notification.version)
XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), notification.signedDate)
XCTAssertNotNil(notification.data)
XCTAssertNil(notification.summary)
XCTAssertNil(notification.externalPurchaseToken)
XCTAssertEqual(Environment.localTesting, notification.data!.environment)
XCTAssertEqual("LocalTesting", notification.data!.rawEnvironment)
XCTAssertEqual(41234, notification.data!.appAppleId)
XCTAssertEqual("com.example", notification.data!.bundleId)
XCTAssertEqual("1.2.3", notification.data!.bundleVersion)
XCTAssertEqual("signed_transaction_info_value", notification.data!.signedTransactionInfo)
XCTAssertEqual("signed_renewal_info_value", notification.data!.signedRenewalInfo)
XCTAssertEqual(Status.active, notification.data!.status)
XCTAssertEqual(1, notification.data!.rawStatus)
XCTAssertEqual(ConsumptionRequestReason.unintendedPurchase, notification.data!.consumptionRequestReason)
XCTAssertEqual("UNINTENDED_PURCHASE", notification.data!.rawConsumptionRequestReason)
}

public func testSummaryNotificationDecoding() async throws {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"notificationType": "CONSUMPTION_REQUEST",
"notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20",
"data": {
"environment": "LocalTesting",
"appAppleId": 41234,
"bundleId": "com.example",
"bundleVersion": "1.2.3",
"signedTransactionInfo": "signed_transaction_info_value",
"signedRenewalInfo": "signed_renewal_info_value",
"status": 1,
"consumptionRequestReason": "UNINTENDED_PURCHASE"
},
"version": "2.0",
"signedDate": 1698148900000
}

0 comments on commit 72ff5ed

Please sign in to comment.