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
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
}