diff --git a/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift b/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift index 7d8c125..582d5fe 100644 --- a/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift +++ b/Sources/AppStoreServerLibrary/AppStoreServerAPIClient.swift @@ -274,7 +274,7 @@ public actor AppStoreServerAPIClient: Sendable { 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 { @@ -367,14 +367,25 @@ public actor AppStoreServerAPIClient: Sendable { ///Send consumption information about a consumable in-app purchase to the App Store after your server receives a consumption request notification. /// - ///- Parameter transactionId: The transaction identifier for which you’re providing consumption information. You receive this identifier in the CONSUMPTION_REQUEST notification the App Store sends to your server. + ///- Parameter transactionId: The transaction identifier for which you're providing consumption information. You receive this identifier in the CONSUMPTION_REQUEST notification the App Store sends to your server. ///- Parameter consumptionRequest : The request body containing consumption information. ///- Returns: Success, or information about the failure - ///[Send Consumption Information](https://developer.apple.com/documentation/appstoreserverapi/send_consumption_information) - public func sendConsumptionData(transactionId: String, consumptionRequest: ConsumptionRequest) async -> APIResult { + ///[Send Consumption Information](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1) + @available(*, deprecated, message: "Use sendConsumptionInformation(_:consumptionRequest:) instead") + public func sendConsumptionData(transactionId: String, consumptionRequest: ConsumptionRequestV1) async -> APIResult { return await makeRequestWithoutResponseBody(path: "/inApps/v1/transactions/consumption/" + transactionId, method: .PUT, queryParameters: [:], body: consumptionRequest) } + ///Send consumption information about an In-App Purchase to the App Store after your server receives a consumption request notification. + /// + ///- Parameter transactionId: The transaction identifier for which you're providing consumption information. You receive this identifier in the CONSUMPTION_REQUEST notification the App Store sends to your server's App Store Server Notifications V2 endpoint. + ///- Parameter consumptionRequest: The request body containing consumption information. + ///- Returns: Success, or information about the failure + ///[Send Consumption Information](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information) + public func sendConsumptionInformation(transactionId: String, consumptionRequest: ConsumptionRequest) async -> APIResult { + return await makeRequestWithoutResponseBody(path: "/inApps/v2/transactions/consumption/" + transactionId, method: .PUT, queryParameters: [:], body: consumptionRequest) + } + ///Sets the app account token value for a purchase the customer makes outside your app, or updates its value in an existing transaction. /// ///- Parameter originalTransactionId: The original transaction identifier of the transaction to receive the app account token update. diff --git a/Sources/AppStoreServerLibrary/Models/AppData.swift b/Sources/AppStoreServerLibrary/Models/AppData.swift new file mode 100644 index 0000000..47713ca --- /dev/null +++ b/Sources/AppStoreServerLibrary/Models/AppData.swift @@ -0,0 +1,76 @@ +// Copyright (c) 2025 Apple Inc. Licensed under MIT License. + +import Foundation + +///The object that contains the app metadata and signed app transaction information. +/// +///[appData](https://developer.apple.com/documentation/appstoreservernotifications/appdata) +public struct AppData: Decodable, Encodable, Hashable, Sendable { + + public init(appAppleId: Int64? = nil, bundleId: String? = nil, environment: AppStoreEnvironment? = nil, signedAppTransactionInfo: String? = nil) { + self.appAppleId = appAppleId + self.bundleId = bundleId + self.environment = environment + self.signedAppTransactionInfo = signedAppTransactionInfo + } + + public init(appAppleId: Int64? = nil, bundleId: String? = nil, rawEnvironment: String? = nil, signedAppTransactionInfo: String? = nil) { + self.appAppleId = appAppleId + self.bundleId = bundleId + self.rawEnvironment = rawEnvironment + self.signedAppTransactionInfo = signedAppTransactionInfo + } + + ///The unique identifier of the app that the notification applies to. + /// + ///[appAppleId](https://developer.apple.com/documentation/appstoreservernotifications/appappleid) + public var appAppleId: Int64? + + ///The bundle identifier of the app. + /// + ///[bundleId](https://developer.apple.com/documentation/appstoreservernotifications/bundleid) + public var bundleId: String? + + ///The server environment that the notification applies to, either sandbox or production. + /// + ///[environment](https://developer.apple.com/documentation/appstoreservernotifications/environment) + public var environment: AppStoreEnvironment? { + get { + return rawEnvironment.flatMap { AppStoreEnvironment(rawValue: $0) } + } + set { + self.rawEnvironment = newValue.map { $0.rawValue } + } + } + + ///See ``environment`` + public var rawEnvironment: String? + + ///App transaction information signed by the App Store, in JSON Web Signature (JWS) format. + /// + ///[JWSAppTransaction](https://developer.apple.com/documentation/appstoreservernotifications/jwsapptransaction) + public var signedAppTransactionInfo: String? + + public enum CodingKeys: CodingKey { + case appAppleId + case bundleId + case environment + case signedAppTransactionInfo + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.appAppleId = try container.decodeIfPresent(Int64.self, forKey: .appAppleId) + self.bundleId = try container.decodeIfPresent(String.self, forKey: .bundleId) + self.rawEnvironment = try container.decodeIfPresent(String.self, forKey: .environment) + self.signedAppTransactionInfo = try container.decodeIfPresent(String.self, forKey: .signedAppTransactionInfo) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.appAppleId, forKey: .appAppleId) + try container.encodeIfPresent(self.bundleId, forKey: .bundleId) + try container.encodeIfPresent(self.rawEnvironment, forKey: .environment) + try container.encodeIfPresent(self.signedAppTransactionInfo, forKey: .signedAppTransactionInfo) + } +} diff --git a/Sources/AppStoreServerLibrary/Models/ConsumptionRequest.swift b/Sources/AppStoreServerLibrary/Models/ConsumptionRequest.swift index 6e9e8e9..436928b 100644 --- a/Sources/AppStoreServerLibrary/Models/ConsumptionRequest.swift +++ b/Sources/AppStoreServerLibrary/Models/ConsumptionRequest.swift @@ -1,187 +1,49 @@ -// Copyright (c) 2023 Apple Inc. Licensed under MIT License. +// Copyright (c) 2025 Apple Inc. Licensed under MIT License. import Foundation -///The request body containing consumption information. +///The request body that contains consumption information for an In-App Purchase. /// ///[ConsumptionRequest](https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest) public struct ConsumptionRequest: Decodable, Encodable, Hashable, Sendable { - - 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) { + + public init(customerConsented: Bool, deliveryStatus: DeliveryStatus, sampleContentProvided: Bool, consumptionPercentage: Int32? = nil, refundPreference: RefundPreference? = nil) { self.customerConsented = customerConsented - self.consumptionStatus = consumptionStatus - self.platform = platform self.sampleContentProvided = sampleContentProvided - self.deliveryStatus = deliveryStatus - self.appAccountToken = appAccountToken - self.accountTenure = accountTenure - self.playTime = playTime - self.lifetimeDollarsRefunded = lifetimeDollarsRefunded - self.lifetimeDollarsPurchased = lifetimeDollarsPurchased - self.userStatus = userStatus - self.refundPreference = refundPreference + self.consumptionPercentage = consumptionPercentage + self.rawDeliveryStatus = deliveryStatus.rawValue + self.rawRefundPreference = refundPreference?.rawValue } - - 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) { + + public init(customerConsented: Bool, rawDeliveryStatus: String, sampleContentProvided: Bool, consumptionPercentage: Int32? = nil, rawRefundPreference: String? = nil) { self.customerConsented = customerConsented - self.rawConsumptionStatus = rawConsumptionStatus - self.rawPlatform = rawPlatform - self.sampleContentProvided = sampleContentProvided self.rawDeliveryStatus = rawDeliveryStatus - self.appAccountToken = appAccountToken - self.rawAccountTenure = rawAccountTenure - self.rawPlayTime = rawPlayTime - self.rawLifetimeDollarsRefunded = rawLifetimeDollarsRefunded - self.rawLifetimeDollarsPurchased = rawLifetimeDollarsPurchased - self.rawUserStatus = rawUserStatus + self.sampleContentProvided = sampleContentProvided + self.consumptionPercentage = consumptionPercentage self.rawRefundPreference = rawRefundPreference } - + ///A Boolean value that indicates whether the customer consented to provide consumption data to the App Store. /// ///[customerConsented](https://developer.apple.com/documentation/appstoreserverapi/customerconsented) - public var customerConsented: Bool? - - ///A value that indicates the extent to which the customer consumed the in-app purchase. - /// - ///[consumptionStatus](https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus) - public var consumptionStatus: ConsumptionStatus? { - get { - return rawConsumptionStatus.flatMap { ConsumptionStatus(rawValue: $0) } - } - set { - self.rawConsumptionStatus = newValue.map { $0.rawValue } - } - } - - ///See ``consumptionStatus`` - public var rawConsumptionStatus: Int32? - - ///A value that indicates the platform on which the customer consumed the in-app purchase. - /// - ///[platform](https://developer.apple.com/documentation/appstoreserverapi/platform) - public var platform: Platform? { - get { - return rawPlatform.flatMap { Platform(rawValue: $0) } - } - set { - self.rawPlatform = newValue.map { $0.rawValue } - } - } - - ///See ``platform`` - public var rawPlatform: Int32? - - ///A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. - - ///[sampleContentProvided](https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided) - public var sampleContentProvided: Bool? - - ///A value that indicates whether the app successfully delivered an in-app purchase that works properly. + public var customerConsented: Bool + + ///An integer that indicates the percentage, in milliunits, of the In-App Purchase the customer consumed. /// - ///[deliveryStatus](https://developer.apple.com/documentation/appstoreserverapi/deliverystatus) + ///[consumptionPercentage](https://developer.apple.com/documentation/appstoreserverapi/consumptionpercentage) + public var consumptionPercentage: Int32? + + ///See ``rawDeliveryStatus`` public var deliveryStatus: DeliveryStatus? { - get { - return rawDeliveryStatus.flatMap { DeliveryStatus(rawValue: $0) } - } - set { - self.rawDeliveryStatus = newValue.map { $0.rawValue } - } - } - - ///See ``deliveryStatus`` - public var rawDeliveryStatus: Int32? - - ///The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction. - /// - ///[appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken) - public var appAccountToken: UUID? { - get { - return rawAppAccountToken != "" ? UUID(uuidString: rawAppAccountToken) : nil - } - set { - self.rawAppAccountToken = newValue.map { $0.uuidString } ?? "" - } + DeliveryStatus(rawValue: rawDeliveryStatus) } - - private var rawAppAccountToken: String = "" - ///The age of the customer’s account. - /// - ///[accountTenure](https://developer.apple.com/documentation/appstoreserverapi/accounttenure) - public var accountTenure: AccountTenure? { - get { - return rawAccountTenure.flatMap { AccountTenure(rawValue: $0) } - } - set { - self.rawAccountTenure = newValue.map { $0.rawValue } - } - } - - ///See ``accountTenure`` - public var rawAccountTenure: Int32? - - ///A value that indicates the amount of time that the customer used the app. - /// - ///[ConsumptionRequest](https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest) - public var playTime: PlayTime? { - get { - return rawPlayTime.flatMap { PlayTime(rawValue: $0) } - } - set { - self.rawPlayTime = newValue.map { $0.rawValue } - } - } - - ///See ``playTime`` - public var rawPlayTime: Int32? - - ///A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. - /// - ///[lifetimeDollarsRefunded](https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded) - public var lifetimeDollarsRefunded: LifetimeDollarsRefunded? { - get { - return rawLifetimeDollarsRefunded.flatMap { LifetimeDollarsRefunded(rawValue: $0) } - } - set { - self.rawLifetimeDollarsRefunded = newValue.map { $0.rawValue } - } - } - - ///See ``lifetimeDollarsRefunded`` - public var rawLifetimeDollarsRefunded: Int32? - - ///A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. - /// - ///[lifetimeDollarsPurchased](https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased) - public var lifetimeDollarsPurchased: LifetimeDollarsPurchased? { - get { - return rawLifetimeDollarsPurchased.flatMap { LifetimeDollarsPurchased(rawValue: $0) } - } - set { - self.rawLifetimeDollarsPurchased = newValue.map { $0.rawValue } - } - } - - ///See ``lifetimeDollarsPurchased`` - public var rawLifetimeDollarsPurchased: Int32? - - ///The status of the customer’s account. + ///A value that indicates whether the app successfully delivered an In-App Purchase that works properly. /// - ///[userStatus](https://developer.apple.com/documentation/appstoreserverapi/userstatus) - public var userStatus: UserStatus? { - get { - return rawUserStatus.flatMap { UserStatus(rawValue: $0) } - } - set { - self.rawUserStatus = newValue.map { $0.rawValue } - } - } - - ///See ``userStatus`` - public var rawUserStatus: Int32? + ///[deliveryStatus](https://developer.apple.com/documentation/appstoreserverapi/deliverystatus) + public var rawDeliveryStatus: String - ///A value that indicates your preference, based on your operational logic, as to whether Apple should grant the refund. + ///A value that indicates your preferred outcome for the refund request. /// ///[refundPreference](https://developer.apple.com/documentation/appstoreserverapi/refundpreference) public var refundPreference: RefundPreference? { @@ -192,54 +54,38 @@ public struct ConsumptionRequest: Decodable, Encodable, Hashable, Sendable { self.rawRefundPreference = newValue.map { $0.rawValue } } } - + ///See ``refundPreference`` - public var rawRefundPreference: Int32? - + public var rawRefundPreference: String? + + ///A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. + /// + ///[sampleContentProvided](https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided) + public var sampleContentProvided: Bool + public enum CodingKeys: CodingKey { case customerConsented - case consumptionStatus - case platform - case sampleContentProvided + case consumptionPercentage case deliveryStatus - case appAccountToken - case accountTenure - case playTime - case lifetimeDollarsRefunded - case lifetimeDollarsPurchased - case userStatus case refundPreference + case sampleContentProvided } - + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.customerConsented = try container.decodeIfPresent(Bool.self, forKey: .customerConsented) - self.rawConsumptionStatus = try container.decodeIfPresent(Int32.self, forKey: .consumptionStatus) - self.rawPlatform = try container.decodeIfPresent(Int32.self, forKey: .platform) - self.sampleContentProvided = try container.decodeIfPresent(Bool.self, forKey: .sampleContentProvided) - self.rawDeliveryStatus = try container.decodeIfPresent(Int32.self, forKey: .deliveryStatus) - self.rawAppAccountToken = try container.decode(String.self, forKey: .appAccountToken) - self.rawAccountTenure = try container.decodeIfPresent(Int32.self, forKey: .accountTenure) - self.rawPlayTime = try container.decodeIfPresent(Int32.self, forKey: .playTime) - self.rawLifetimeDollarsRefunded = try container.decodeIfPresent(Int32.self, forKey: .lifetimeDollarsRefunded) - self.rawLifetimeDollarsPurchased = try container.decodeIfPresent(Int32.self, forKey: .lifetimeDollarsPurchased) - self.rawUserStatus = try container.decodeIfPresent(Int32.self, forKey: .userStatus) - self.rawRefundPreference = try container.decodeIfPresent(Int32.self, forKey: .refundPreference) + self.customerConsented = try container.decode(Bool.self, forKey: .customerConsented) + self.consumptionPercentage = try container.decodeIfPresent(Int32.self, forKey: .consumptionPercentage) + self.rawDeliveryStatus = try container.decode(String.self, forKey: .deliveryStatus) + self.rawRefundPreference = try container.decodeIfPresent(String.self, forKey: .refundPreference) + self.sampleContentProvided = try container.decode(Bool.self, forKey: .sampleContentProvided) } - + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(self.customerConsented, forKey: .customerConsented) - try container.encodeIfPresent(self.rawConsumptionStatus, forKey: .consumptionStatus) - try container.encodeIfPresent(self.rawPlatform, forKey: .platform) - try container.encodeIfPresent(self.sampleContentProvided, forKey: .sampleContentProvided) - try container.encodeIfPresent(self.rawDeliveryStatus, forKey: .deliveryStatus) - try container.encode(self.rawAppAccountToken, forKey: .appAccountToken) - try container.encodeIfPresent(self.rawAccountTenure, forKey: .accountTenure) - try container.encodeIfPresent(self.rawPlayTime, forKey: .playTime) - try container.encodeIfPresent(self.rawLifetimeDollarsRefunded, forKey: .lifetimeDollarsRefunded) - try container.encodeIfPresent(self.rawLifetimeDollarsPurchased, forKey: .lifetimeDollarsPurchased) - try container.encodeIfPresent(self.rawUserStatus, forKey: .userStatus) + try container.encode(self.customerConsented, forKey: .customerConsented) + try container.encodeIfPresent(self.consumptionPercentage, forKey: .consumptionPercentage) + try container.encode(self.rawDeliveryStatus, forKey: .deliveryStatus) try container.encodeIfPresent(self.rawRefundPreference, forKey: .refundPreference) + try container.encode(self.sampleContentProvided, forKey: .sampleContentProvided) } } diff --git a/Sources/AppStoreServerLibrary/Models/ConsumptionRequestV1.swift b/Sources/AppStoreServerLibrary/Models/ConsumptionRequestV1.swift new file mode 100644 index 0000000..6b57a5d --- /dev/null +++ b/Sources/AppStoreServerLibrary/Models/ConsumptionRequestV1.swift @@ -0,0 +1,246 @@ +// Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +import Foundation + +///The request body containing consumption information. +/// +///[ConsumptionRequestV1](https://developer.apple.com/documentation/appstoreserverapi/consumptionrequestv1) +@available(*, deprecated, renamed: "ConsumptionRequest") +public struct ConsumptionRequestV1: Decodable, Encodable, Hashable, Sendable { + + public init(customerConsented: Bool? = nil, consumptionStatus: ConsumptionStatus? = nil, platform: Platform? = nil, sampleContentProvided: Bool? = nil, deliveryStatus: DeliveryStatusV1? = nil, appAccountToken: UUID? = nil, accountTenure: AccountTenure? = nil, playTime: PlayTime? = nil, lifetimeDollarsRefunded: LifetimeDollarsRefunded? = nil, lifetimeDollarsPurchased: LifetimeDollarsPurchased? = nil, userStatus: UserStatus? = nil, refundPreference: RefundPreferenceV1? = nil) { + self.customerConsented = customerConsented + self.consumptionStatus = consumptionStatus + self.platform = platform + self.sampleContentProvided = sampleContentProvided + self.deliveryStatus = deliveryStatus + self.appAccountToken = appAccountToken + self.accountTenure = accountTenure + self.playTime = playTime + 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, rawRefundPreference: Int32? = nil) { + self.customerConsented = customerConsented + self.rawConsumptionStatus = rawConsumptionStatus + self.rawPlatform = rawPlatform + self.sampleContentProvided = sampleContentProvided + self.rawDeliveryStatus = rawDeliveryStatus + self.appAccountToken = appAccountToken + self.rawAccountTenure = rawAccountTenure + self.rawPlayTime = rawPlayTime + 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. + /// + ///[customerConsented](https://developer.apple.com/documentation/appstoreserverapi/customerconsented) + public var customerConsented: Bool? + + ///A value that indicates the extent to which the customer consumed the in-app purchase. + /// + ///[consumptionStatus](https://developer.apple.com/documentation/appstoreserverapi/consumptionstatus) + public var consumptionStatus: ConsumptionStatus? { + get { + return rawConsumptionStatus.flatMap { ConsumptionStatus(rawValue: $0) } + } + set { + self.rawConsumptionStatus = newValue.map { $0.rawValue } + } + } + + ///See ``consumptionStatus`` + public var rawConsumptionStatus: Int32? + + ///A value that indicates the platform on which the customer consumed the in-app purchase. + /// + ///[platform](https://developer.apple.com/documentation/appstoreserverapi/platform) + public var platform: Platform? { + get { + return rawPlatform.flatMap { Platform(rawValue: $0) } + } + set { + self.rawPlatform = newValue.map { $0.rawValue } + } + } + + ///See ``platform`` + public var rawPlatform: Int32? + + ///A Boolean value that indicates whether you provided, prior to its purchase, a free sample or trial of the content, or information about its functionality. + + ///[sampleContentProvided](https://developer.apple.com/documentation/appstoreserverapi/samplecontentprovided) + public var sampleContentProvided: Bool? + + ///A value that indicates whether the app successfully delivered an in-app purchase that works properly. + /// + ///[deliveryStatus](https://developer.apple.com/documentation/appstoreserverapi/deliverystatus) + public var deliveryStatus: DeliveryStatusV1? { + get { + return rawDeliveryStatus.flatMap { DeliveryStatusV1(rawValue: $0) } + } + set { + self.rawDeliveryStatus = newValue.map { $0.rawValue } + } + } + + ///See ``deliveryStatus`` + public var rawDeliveryStatus: Int32? + + ///The UUID that an app optionally generates to map a customer’s in-app purchase with its resulting App Store transaction. + /// + ///[appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken) + public var appAccountToken: UUID? { + get { + return rawAppAccountToken != "" ? UUID(uuidString: rawAppAccountToken) : nil + } + set { + self.rawAppAccountToken = newValue.map { $0.uuidString } ?? "" + } + } + + private var rawAppAccountToken: String = "" + + ///The age of the customer’s account. + /// + ///[accountTenure](https://developer.apple.com/documentation/appstoreserverapi/accounttenure) + public var accountTenure: AccountTenure? { + get { + return rawAccountTenure.flatMap { AccountTenure(rawValue: $0) } + } + set { + self.rawAccountTenure = newValue.map { $0.rawValue } + } + } + + ///See ``accountTenure`` + public var rawAccountTenure: Int32? + + ///A value that indicates the amount of time that the customer used the app. + /// + ///[ConsumptionRequest](https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest) + public var playTime: PlayTime? { + get { + return rawPlayTime.flatMap { PlayTime(rawValue: $0) } + } + set { + self.rawPlayTime = newValue.map { $0.rawValue } + } + } + + ///See ``playTime`` + public var rawPlayTime: Int32? + + ///A value that indicates the total amount, in USD, of refunds the customer has received, in your app, across all platforms. + /// + ///[lifetimeDollarsRefunded](https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarsrefunded) + public var lifetimeDollarsRefunded: LifetimeDollarsRefunded? { + get { + return rawLifetimeDollarsRefunded.flatMap { LifetimeDollarsRefunded(rawValue: $0) } + } + set { + self.rawLifetimeDollarsRefunded = newValue.map { $0.rawValue } + } + } + + ///See ``lifetimeDollarsRefunded`` + public var rawLifetimeDollarsRefunded: Int32? + + ///A value that indicates the total amount, in USD, of in-app purchases the customer has made in your app, across all platforms. + /// + ///[lifetimeDollarsPurchased](https://developer.apple.com/documentation/appstoreserverapi/lifetimedollarspurchased) + public var lifetimeDollarsPurchased: LifetimeDollarsPurchased? { + get { + return rawLifetimeDollarsPurchased.flatMap { LifetimeDollarsPurchased(rawValue: $0) } + } + set { + self.rawLifetimeDollarsPurchased = newValue.map { $0.rawValue } + } + } + + ///See ``lifetimeDollarsPurchased`` + public var rawLifetimeDollarsPurchased: Int32? + + ///The status of the customer’s account. + /// + ///[userStatus](https://developer.apple.com/documentation/appstoreserverapi/userstatus) + public var userStatus: UserStatus? { + get { + return rawUserStatus.flatMap { UserStatus(rawValue: $0) } + } + set { + self.rawUserStatus = newValue.map { $0.rawValue } + } + } + + ///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: RefundPreferenceV1? { + get { + return rawRefundPreference.flatMap { RefundPreferenceV1(rawValue: $0) } + } + set { + self.rawRefundPreference = newValue.map { $0.rawValue } + } + } + + ///See ``refundPreference`` + public var rawRefundPreference: Int32? + + public enum CodingKeys: CodingKey { + case customerConsented + case consumptionStatus + case platform + case sampleContentProvided + case deliveryStatus + case appAccountToken + case accountTenure + case playTime + case lifetimeDollarsRefunded + case lifetimeDollarsPurchased + case userStatus + case refundPreference + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.customerConsented = try container.decodeIfPresent(Bool.self, forKey: .customerConsented) + self.rawConsumptionStatus = try container.decodeIfPresent(Int32.self, forKey: .consumptionStatus) + self.rawPlatform = try container.decodeIfPresent(Int32.self, forKey: .platform) + self.sampleContentProvided = try container.decodeIfPresent(Bool.self, forKey: .sampleContentProvided) + self.rawDeliveryStatus = try container.decodeIfPresent(Int32.self, forKey: .deliveryStatus) + self.rawAppAccountToken = try container.decode(String.self, forKey: .appAccountToken) + self.rawAccountTenure = try container.decodeIfPresent(Int32.self, forKey: .accountTenure) + self.rawPlayTime = try container.decodeIfPresent(Int32.self, forKey: .playTime) + self.rawLifetimeDollarsRefunded = try container.decodeIfPresent(Int32.self, forKey: .lifetimeDollarsRefunded) + self.rawLifetimeDollarsPurchased = try container.decodeIfPresent(Int32.self, forKey: .lifetimeDollarsPurchased) + self.rawUserStatus = try container.decodeIfPresent(Int32.self, forKey: .userStatus) + self.rawRefundPreference = try container.decodeIfPresent(Int32.self, forKey: .refundPreference) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.customerConsented, forKey: .customerConsented) + try container.encodeIfPresent(self.rawConsumptionStatus, forKey: .consumptionStatus) + try container.encodeIfPresent(self.rawPlatform, forKey: .platform) + try container.encodeIfPresent(self.sampleContentProvided, forKey: .sampleContentProvided) + try container.encodeIfPresent(self.rawDeliveryStatus, forKey: .deliveryStatus) + try container.encode(self.rawAppAccountToken, forKey: .appAccountToken) + try container.encodeIfPresent(self.rawAccountTenure, forKey: .accountTenure) + try container.encodeIfPresent(self.rawPlayTime, forKey: .playTime) + try container.encodeIfPresent(self.rawLifetimeDollarsRefunded, forKey: .lifetimeDollarsRefunded) + try container.encodeIfPresent(self.rawLifetimeDollarsPurchased, forKey: .lifetimeDollarsPurchased) + try container.encodeIfPresent(self.rawUserStatus, forKey: .userStatus) + try container.encodeIfPresent(self.rawRefundPreference, forKey: .refundPreference) + } +} diff --git a/Sources/AppStoreServerLibrary/Models/DeliveryStatus.swift b/Sources/AppStoreServerLibrary/Models/DeliveryStatus.swift index 5ccee17..9f86be5 100644 --- a/Sources/AppStoreServerLibrary/Models/DeliveryStatus.swift +++ b/Sources/AppStoreServerLibrary/Models/DeliveryStatus.swift @@ -1,13 +1,12 @@ -// Copyright (c) 2023 Apple Inc. Licensed under MIT License. +// Copyright (c) 2025 Apple Inc. Licensed under MIT License. -///A value that indicates whether the app successfully delivered an in-app purchase that works properly. +///A value that indicates whether the app successfully delivered an In-App Purchase that works properly. /// ///[deliveryStatus](https://developer.apple.com/documentation/appstoreserverapi/deliverystatus) -public enum DeliveryStatus: Int32, Decodable, Encodable, Hashable, Sendable { - case deliveredAndWorkingProperly = 0 - case didNotDeliverDueToQualityIssue = 1 - case deliveredWrongItem = 2 - case didNotDeliverDueToServerOutage = 3 - case didNotDeliverDueToIngameCurrencyChange = 4 - case didNotDeliverForOtherReason = 5 +public enum DeliveryStatus: String, Decodable, Encodable, Hashable, Sendable { + case delivered = "DELIVERED" + case undeliveredQualityIssue = "UNDELIVERED_QUALITY_ISSUE" + case undeliveredWrongItem = "UNDELIVERED_WRONG_ITEM" + case undeliveredServerOutage = "UNDELIVERED_SERVER_OUTAGE" + case undeliveredOther = "UNDELIVERED_OTHER" } diff --git a/Sources/AppStoreServerLibrary/Models/DeliveryStatusV1.swift b/Sources/AppStoreServerLibrary/Models/DeliveryStatusV1.swift new file mode 100644 index 0000000..653bfe9 --- /dev/null +++ b/Sources/AppStoreServerLibrary/Models/DeliveryStatusV1.swift @@ -0,0 +1,14 @@ +// Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +///A value that indicates whether the app successfully delivered an in-app purchase that works properly. +/// +///[DeliveryStatusV1](https://developer.apple.com/documentation/appstoreserverapi/deliverystatusv1) +@available(*, deprecated, renamed: "DeliveryStatus") +public enum DeliveryStatusV1: Int32, Decodable, Encodable, Hashable, Sendable { + case deliveredAndWorkingProperly = 0 + case didNotDeliverDueToQualityIssue = 1 + case deliveredWrongItem = 2 + case didNotDeliverDueToServerOutage = 3 + case didNotDeliverDueToIngameCurrencyChange = 4 + case didNotDeliverForOtherReason = 5 +} diff --git a/Sources/AppStoreServerLibrary/Models/JWSTransactionDecodedPayload.swift b/Sources/AppStoreServerLibrary/Models/JWSTransactionDecodedPayload.swift index aba02ca..32c70ec 100644 --- a/Sources/AppStoreServerLibrary/Models/JWSTransactionDecodedPayload.swift +++ b/Sources/AppStoreServerLibrary/Models/JWSTransactionDecodedPayload.swift @@ -6,7 +6,7 @@ import Foundation ///[JWSTransactionDecodedPayload](https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload) public struct JWSTransactionDecodedPayload: DecodedSignedData, Decodable, Encodable, Hashable, Sendable { - public init(originalTransactionId: String? = nil, transactionId: String? = nil, webOrderLineItemId: String? = nil, bundleId: String? = nil, productId: String? = nil, subscriptionGroupIdentifier: String? = nil, purchaseDate: Date? = nil, originalPurchaseDate: Date? = nil, expiresDate: Date? = nil, quantity: Int32? = nil, type: ProductType? = nil, appAccountToken: UUID? = nil, inAppOwnershipType: InAppOwnershipType? = nil, signedDate: Date? = nil, revocationReason: RevocationReason? = nil, revocationDate: Date? = nil, isUpgraded: Bool? = nil, offerType: OfferType? = nil, offerIdentifier: String? = nil, environment: AppStoreEnvironment? = nil, storefront: String? = nil, storefrontId: String? = nil, transactionReason: TransactionReason? = nil, currency: String? = nil, price: Int64? = nil, offerDiscountType: OfferDiscountType? = nil, appTransactionId: String? = nil, offerPeriod: String? = nil) { + public init(originalTransactionId: String? = nil, transactionId: String? = nil, webOrderLineItemId: String? = nil, bundleId: String? = nil, productId: String? = nil, subscriptionGroupIdentifier: String? = nil, purchaseDate: Date? = nil, originalPurchaseDate: Date? = nil, expiresDate: Date? = nil, quantity: Int32? = nil, type: ProductType? = nil, appAccountToken: UUID? = nil, inAppOwnershipType: InAppOwnershipType? = nil, signedDate: Date? = nil, revocationReason: RevocationReason? = nil, revocationDate: Date? = nil, isUpgraded: Bool? = nil, offerType: OfferType? = nil, offerIdentifier: String? = nil, environment: AppStoreEnvironment? = nil, storefront: String? = nil, storefrontId: String? = nil, transactionReason: TransactionReason? = nil, currency: String? = nil, price: Int64? = nil, offerDiscountType: OfferDiscountType? = nil, appTransactionId: String? = nil, offerPeriod: String? = nil, revocationType: RevocationType? = nil, revocationPercentage: Int32? = nil) { self.originalTransactionId = originalTransactionId self.transactionId = transactionId self.webOrderLineItemId = webOrderLineItemId @@ -35,9 +35,11 @@ public struct JWSTransactionDecodedPayload: DecodedSignedData, Decodable, Encoda self.offerDiscountType = offerDiscountType self.appTransactionId = appTransactionId self.offerPeriod = offerPeriod + self.revocationType = revocationType + self.revocationPercentage = revocationPercentage } - public init(originalTransactionId: String? = nil, transactionId: String? = nil, webOrderLineItemId: String? = nil, bundleId: String? = nil, productId: String? = nil, subscriptionGroupIdentifier: String? = nil, purchaseDate: Date? = nil, originalPurchaseDate: Date? = nil, expiresDate: Date? = nil, quantity: Int32? = nil, rawType: String? = nil, appAccountToken: UUID? = nil, rawInAppOwnershipType: String? = nil, signedDate: Date? = nil, rawRevocationReason: Int32? = nil, revocationDate: Date? = nil, isUpgraded: Bool? = nil, rawOfferType: Int32? = nil, offerIdentifier: String? = nil, rawEnvironment: String? = nil, storefront: String? = nil, storefrontId: String? = nil, rawTransactionReason: String? = nil, currency: String? = nil, price: Int64? = nil, rawOfferDiscountType: String? = nil, appTransactionId: String? = nil, offerPeriod: String? = nil) { + public init(originalTransactionId: String? = nil, transactionId: String? = nil, webOrderLineItemId: String? = nil, bundleId: String? = nil, productId: String? = nil, subscriptionGroupIdentifier: String? = nil, purchaseDate: Date? = nil, originalPurchaseDate: Date? = nil, expiresDate: Date? = nil, quantity: Int32? = nil, rawType: String? = nil, appAccountToken: UUID? = nil, rawInAppOwnershipType: String? = nil, signedDate: Date? = nil, rawRevocationReason: Int32? = nil, revocationDate: Date? = nil, isUpgraded: Bool? = nil, rawOfferType: Int32? = nil, offerIdentifier: String? = nil, rawEnvironment: String? = nil, storefront: String? = nil, storefrontId: String? = nil, rawTransactionReason: String? = nil, currency: String? = nil, price: Int64? = nil, rawOfferDiscountType: String? = nil, appTransactionId: String? = nil, offerPeriod: String? = nil, rawRevocationType: String? = nil, revocationPercentage: Int32? = nil) { self.originalTransactionId = originalTransactionId self.transactionId = transactionId self.webOrderLineItemId = webOrderLineItemId @@ -66,6 +68,8 @@ public struct JWSTransactionDecodedPayload: DecodedSignedData, Decodable, Encoda self.rawOfferDiscountType = rawOfferDiscountType self.appTransactionId = appTransactionId self.offerPeriod = offerPeriod + self.rawRevocationType = rawRevocationType + self.revocationPercentage = revocationPercentage } ///The original transaction identifier of a purchase. @@ -281,6 +285,26 @@ public struct JWSTransactionDecodedPayload: DecodedSignedData, Decodable, Encoda /// ///[offerPeriod](https://developer.apple.com/documentation/appstoreserverapi/offerPeriod) public var offerPeriod: String? + + ///The type of the refund or revocation that applies to the transaction. + /// + ///[revocationType](https://developer.apple.com/documentation/appstoreservernotifications/revocationtype) + public var revocationType: RevocationType? { + get { + return rawRevocationType.flatMap { RevocationType(rawValue: $0) } + } + set { + self.rawRevocationType = newValue.map { $0.rawValue } + } + } + + ///See ``revocationType`` + public var rawRevocationType: String? + + ///The percentage, in milliunits, of the transaction that the App Store has refunded or revoked. + /// + ///[revocationPercentage](https://developer.apple.com/documentation/appstoreservernotifications/revocationpercentage) + public var revocationPercentage: Int32? public enum CodingKeys: CodingKey { case originalTransactionId @@ -311,6 +335,8 @@ public struct JWSTransactionDecodedPayload: DecodedSignedData, Decodable, Encoda case offerDiscountType case appTransactionId case offerPeriod + case revocationType + case revocationPercentage } public init(from decoder: any Decoder) throws { @@ -343,6 +369,8 @@ public struct JWSTransactionDecodedPayload: DecodedSignedData, Decodable, Encoda self.rawOfferDiscountType = try container.decodeIfPresent(String.self, forKey: .offerDiscountType) self.appTransactionId = try container.decodeIfPresent(String.self, forKey: .appTransactionId) self.offerPeriod = try container.decodeIfPresent(String.self, forKey: .offerPeriod) + self.rawRevocationType = try container.decodeIfPresent(String.self, forKey: .revocationType) + self.revocationPercentage = try container.decodeIfPresent(Int32.self, forKey: .revocationPercentage) } public func encode(to encoder: any Encoder) throws { @@ -375,5 +403,7 @@ public struct JWSTransactionDecodedPayload: DecodedSignedData, Decodable, Encoda try container.encodeIfPresent(self.rawOfferDiscountType, forKey: .offerDiscountType) try container.encodeIfPresent(self.appTransactionId, forKey: .appTransactionId) try container.encodeIfPresent(self.offerPeriod, forKey: .offerPeriod) + try container.encodeIfPresent(self.rawRevocationType, forKey: .revocationType) + try container.encodeIfPresent(self.revocationPercentage, forKey: .revocationPercentage) } } diff --git a/Sources/AppStoreServerLibrary/Models/NotificationTypeV2.swift b/Sources/AppStoreServerLibrary/Models/NotificationTypeV2.swift index 3a5e786..17d205f 100644 --- a/Sources/AppStoreServerLibrary/Models/NotificationTypeV2.swift +++ b/Sources/AppStoreServerLibrary/Models/NotificationTypeV2.swift @@ -23,4 +23,5 @@ public enum NotificationTypeV2: String, Decodable, Encodable, Hashable, Sendable case refundReversed = "REFUND_REVERSED" case externalPurchaseToken = "EXTERNAL_PURCHASE_TOKEN" case oneTimeCharge = "ONE_TIME_CHARGE" + case rescindConsent = "RESCIND_CONSENT" } diff --git a/Sources/AppStoreServerLibrary/Models/RefundPreference.swift b/Sources/AppStoreServerLibrary/Models/RefundPreference.swift index e5d86de..2ad6a87 100644 --- a/Sources/AppStoreServerLibrary/Models/RefundPreference.swift +++ b/Sources/AppStoreServerLibrary/Models/RefundPreference.swift @@ -1,11 +1,10 @@ -// Copyright (c) 2024 Apple Inc. Licensed under MIT License. +// Copyright (c) 2025 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, Sendable { - case undeclared = 0 - case preferGrant = 1 - case preferDecline = 2 - case noPreference = 3 +public enum RefundPreference: String, Decodable, Encodable, Hashable, Sendable { + case decline = "DECLINE" + case grantFull = "GRANT_FULL" + case grantProrated = "GRANT_PRORATED" } diff --git a/Sources/AppStoreServerLibrary/Models/RefundPreferenceV1.swift b/Sources/AppStoreServerLibrary/Models/RefundPreferenceV1.swift new file mode 100644 index 0000000..9bd5dde --- /dev/null +++ b/Sources/AppStoreServerLibrary/Models/RefundPreferenceV1.swift @@ -0,0 +1,12 @@ +// Copyright (c) 2024 Apple Inc. Licensed under MIT License. + +///A value that indicates your preferred outcome for the refund request. +/// +///[RefundPreferenceV1](https://developer.apple.com/documentation/appstoreserverapi/refundpreferencev1) +@available(*, deprecated, renamed: "RefundPreference") +public enum RefundPreferenceV1: Int32, Decodable, Encodable, Hashable, Sendable { + case undeclared = 0 + case preferGrant = 1 + case preferDecline = 2 + case noPreference = 3 +} diff --git a/Sources/AppStoreServerLibrary/Models/ResponseBodyV2DecodedPayload.swift b/Sources/AppStoreServerLibrary/Models/ResponseBodyV2DecodedPayload.swift index 4e89f2e..608a8c9 100644 --- a/Sources/AppStoreServerLibrary/Models/ResponseBodyV2DecodedPayload.swift +++ b/Sources/AppStoreServerLibrary/Models/ResponseBodyV2DecodedPayload.swift @@ -6,7 +6,7 @@ import Foundation ///[responseBodyV2DecodedPayload](https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload) public struct ResponseBodyV2DecodedPayload: DecodedSignedData, Decodable, Encodable, Hashable, Sendable { - public init(notificationType: NotificationTypeV2? = nil, subtype: Subtype? = nil, notificationUUID: String? = nil, data: NotificationData? = nil, version: String? = nil, signedDate: Date? = nil, summary: Summary? = nil, externalPurchaseToken: ExternalPurchaseToken? = nil) { + public init(notificationType: NotificationTypeV2? = nil, subtype: Subtype? = nil, notificationUUID: String? = nil, data: NotificationData? = nil, version: String? = nil, signedDate: Date? = nil, summary: Summary? = nil, externalPurchaseToken: ExternalPurchaseToken? = nil, appData: AppData? = nil) { self.notificationType = notificationType self.subtype = subtype self.notificationUUID = notificationUUID @@ -15,9 +15,10 @@ public struct ResponseBodyV2DecodedPayload: DecodedSignedData, Decodable, Encoda self.signedDate = signedDate self.summary = summary self.externalPurchaseToken = externalPurchaseToken + self.appData = appData } - - public init(rawNotificationType: String? = nil, rawSubtype: String? = nil, notificationUUID: String? = nil, data: NotificationData? = nil, version: String? = nil, signedDate: Date? = nil, summary: Summary? = nil, externalPurchaseToken: ExternalPurchaseToken? = nil) { + + public init(rawNotificationType: String? = nil, rawSubtype: String? = nil, notificationUUID: String? = nil, data: NotificationData? = nil, version: String? = nil, signedDate: Date? = nil, summary: Summary? = nil, externalPurchaseToken: ExternalPurchaseToken? = nil, appData: AppData? = nil) { self.rawNotificationType = rawNotificationType self.rawSubtype = rawSubtype self.notificationUUID = notificationUUID @@ -26,6 +27,7 @@ public struct ResponseBodyV2DecodedPayload: DecodedSignedData, Decodable, Encoda self.signedDate = signedDate self.summary = summary self.externalPurchaseToken = externalPurchaseToken + self.appData = appData } ///The in-app purchase event for which the App Store sends this version 2 notification. @@ -94,6 +96,11 @@ public struct ResponseBodyV2DecodedPayload: DecodedSignedData, Decodable, Encoda /// ///[externalPurchaseToken](https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken) public var externalPurchaseToken: ExternalPurchaseToken? + + ///This field appears when the notificationType is RESCIND_CONSENT. It contains the app metadata and signed app transaction information. + /// + ///[appData](https://developer.apple.com/documentation/appstoreservernotifications/appdata) + public var appData: AppData? enum CodingKeys: CodingKey { case notificationType @@ -104,6 +111,7 @@ public struct ResponseBodyV2DecodedPayload: DecodedSignedData, Decodable, Encoda case signedDate case summary case externalPurchaseToken + case appData } public init(from decoder: any Decoder) throws { @@ -116,6 +124,7 @@ public struct ResponseBodyV2DecodedPayload: DecodedSignedData, Decodable, Encoda self.signedDate = try container.decodeIfPresent(Date.self, forKey: .signedDate) self.summary = try container.decodeIfPresent(Summary.self, forKey: .summary) self.externalPurchaseToken = try container.decodeIfPresent(ExternalPurchaseToken.self, forKey: .externalPurchaseToken) + self.appData = try container.decodeIfPresent(AppData.self, forKey: .appData) } public func encode(to encoder: any Encoder) throws { @@ -128,5 +137,6 @@ public struct ResponseBodyV2DecodedPayload: DecodedSignedData, Decodable, Encoda try container.encodeIfPresent(self.signedDate, forKey: .signedDate) try container.encodeIfPresent(self.summary, forKey: .summary) try container.encodeIfPresent(self.externalPurchaseToken, forKey: .externalPurchaseToken) + try container.encodeIfPresent(self.appData, forKey: .appData) } } diff --git a/Sources/AppStoreServerLibrary/Models/RevocationType.swift b/Sources/AppStoreServerLibrary/Models/RevocationType.swift new file mode 100644 index 0000000..6ce7901 --- /dev/null +++ b/Sources/AppStoreServerLibrary/Models/RevocationType.swift @@ -0,0 +1,10 @@ +// Copyright (c) 2025 Apple Inc. Licensed under MIT License. + +///The type of the refund or revocation that applies to the transaction. +/// +///[revocationType](https://developer.apple.com/documentation/appstoreservernotifications/revocationtype) +public enum RevocationType: String, Decodable, Encodable, Hashable, Sendable { + case refundFull = "REFUND_FULL" + case refundProrated = "REFUND_PRORATED" + case familyRevoke = "FAMILY_REVOKE" +} diff --git a/Sources/AppStoreServerLibrary/SignedDataVerifier.swift b/Sources/AppStoreServerLibrary/SignedDataVerifier.swift index 5dedc2c..977d9b5 100644 --- a/Sources/AppStoreServerLibrary/SignedDataVerifier.swift +++ b/Sources/AppStoreServerLibrary/SignedDataVerifier.swift @@ -102,6 +102,10 @@ public struct SignedDataVerifier: Sendable { } else { environment = .production } + } else if let appData = notification.appData { + appAppleId = appData.appAppleId + bundleId = appData.bundleId + environment = appData.environment } else { appAppleId = nil bundleId = nil diff --git a/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift b/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift index 5cfe1ca..fcefd2b 100644 --- a/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift +++ b/Tests/AppStoreServerLibraryTests/AppStoreServerAPIClientTests.swift @@ -433,23 +433,23 @@ final class AppStoreServerAPIClientTests: XCTestCase { XCTAssertEqual(4, decodedJson["userStatus"] as! Int) XCTAssertEqual(3, decodedJson["refundPreference"] as! Int) } - - let consumptionRequest = ConsumptionRequest( + + let consumptionRequest = ConsumptionRequestV1( customerConsented: true, consumptionStatus: ConsumptionStatus.notConsumed, platform: Platform.nonApple, sampleContentProvided: false, - deliveryStatus: DeliveryStatus.didNotDeliverDueToServerOutage, + deliveryStatus: DeliveryStatusV1.didNotDeliverDueToServerOutage, appAccountToken: UUID(uuidString: "7389a31a-fb6d-4569-a2a6-db7d85d84813"), accountTenure: AccountTenure.thirtyDaysToNinetyDays, playTime: PlayTime.oneDayToFourDays, lifetimeDollarsRefunded: LifetimeDollarsRefunded.oneThousandDollarsToOneThousandNineHundredNinetyNineDollarsAndNinetyNineCents, lifetimeDollarsPurchased: LifetimeDollarsPurchased.twoThousandDollarsOrGreater, userStatus: UserStatus.limitedAccess, - refundPreference: RefundPreference.noPreference + refundPreference: RefundPreferenceV1.noPreference ) TestingUtility.confirmCodableInternallyConsistent(consumptionRequest) - + let response = await client.sendConsumptionData(transactionId: "49571273", consumptionRequest: consumptionRequest) guard case .success(_) = response else { XCTAssertTrue(false) @@ -475,13 +475,13 @@ final class AppStoreServerAPIClientTests: XCTestCase { XCTAssertEqual(7, decodedJson["lifetimeDollarsPurchased"] as! Int) XCTAssertEqual(4, decodedJson["userStatus"] as! Int) } - - let consumptionRequest = ConsumptionRequest( + + let consumptionRequest = ConsumptionRequestV1( customerConsented: true, consumptionStatus: ConsumptionStatus.notConsumed, platform: Platform.nonApple, sampleContentProvided: false, - deliveryStatus: DeliveryStatus.didNotDeliverDueToServerOutage, + deliveryStatus: DeliveryStatusV1.didNotDeliverDueToServerOutage, appAccountToken: nil, accountTenure: AccountTenure.thirtyDaysToNinetyDays, playTime: PlayTime.oneDayToFourDays, @@ -489,15 +489,72 @@ final class AppStoreServerAPIClientTests: XCTestCase { lifetimeDollarsPurchased: LifetimeDollarsPurchased.twoThousandDollarsOrGreater, userStatus: UserStatus.limitedAccess ) - + TestingUtility.confirmCodableInternallyConsistent(consumptionRequest) - + let response = await client.sendConsumptionData(transactionId: "49571273", consumptionRequest: consumptionRequest) guard case .success(_) = response else { XCTAssertTrue(false) return } } + + public func testSendConsumptionInformation() async throws { + let client = try await getAppStoreServerAPIClient("") { request, body in + XCTAssertEqual(.PUT, request.method) + XCTAssertEqual("https://local-testing-base-url/inApps/v2/transactions/consumption/49571273", request.url) + XCTAssertEqual(["application/json"], request.headers["Content-Type"]) + let decodedJson = try! JSONSerialization.jsonObject(with: body!) as! [String: Any] + XCTAssertEqual(true, decodedJson["customerConsented"] as! Bool) + XCTAssertEqual(false, decodedJson["sampleContentProvided"] as! Bool) + XCTAssertEqual("DELIVERED", decodedJson["deliveryStatus"] as! String) + XCTAssertEqual(50000, decodedJson["consumptionPercentage"] as! Int) + XCTAssertEqual("GRANT_FULL", decodedJson["refundPreference"] as! String) + } + + let consumptionRequest = ConsumptionRequest( + customerConsented: true, + deliveryStatus: DeliveryStatus.delivered, + sampleContentProvided: false, + consumptionPercentage: 50000, + refundPreference: RefundPreference.grantFull + ) + TestingUtility.confirmCodableInternallyConsistent(consumptionRequest) + + let response = await client.sendConsumptionInformation(transactionId: "49571273", consumptionRequest: consumptionRequest) + guard case .success(_) = response else { + XCTAssertTrue(false) + return + } + } + + public func testSendConsumptionInformationWithMinimalFields() async throws { + let client = try await getAppStoreServerAPIClient("") { request, body in + XCTAssertEqual(.PUT, request.method) + XCTAssertEqual("https://local-testing-base-url/inApps/v2/transactions/consumption/49571273", request.url) + XCTAssertEqual(["application/json"], request.headers["Content-Type"]) + let decodedJson = try! JSONSerialization.jsonObject(with: body!) as! [String: Any] + XCTAssertEqual(true, decodedJson["customerConsented"] as! Bool) + XCTAssertEqual(false, decodedJson["sampleContentProvided"] as! Bool) + XCTAssertEqual("UNDELIVERED_QUALITY_ISSUE", decodedJson["deliveryStatus"] as! String) + XCTAssertNil(decodedJson["consumptionPercentage"]) + XCTAssertNil(decodedJson["refundPreference"]) + } + + let consumptionRequest = ConsumptionRequest( + customerConsented: true, + deliveryStatus: DeliveryStatus.undeliveredQualityIssue, + sampleContentProvided: false + ) + TestingUtility.confirmCodableInternallyConsistent(consumptionRequest) + + let response = await client.sendConsumptionInformation(transactionId: "49571273", consumptionRequest: consumptionRequest) + guard case .success(_) = response else { + XCTAssertTrue(false) + return + } + } + public func testHeaders() async throws { let client = try await getClientWithBody("resources/models/transactionInfoResponse.json") { request, body in diff --git a/Tests/AppStoreServerLibraryTests/SignedModelTests.swift b/Tests/AppStoreServerLibraryTests/SignedModelTests.swift index dc8d383..ef5b6bb 100644 --- a/Tests/AppStoreServerLibraryTests/SignedModelTests.swift +++ b/Tests/AppStoreServerLibraryTests/SignedModelTests.swift @@ -173,7 +173,7 @@ final class SignedModelTests: XCTestCase { let signedTransaction = TestingUtility.createSignedDataFromJson("resources/models/signedTransaction.json") let verifiedTransaction = await TestingUtility.getSignedDataVerifier().verifyAndDecodeTransaction(signedTransaction: signedTransaction) - + guard case .valid(let transaction) = verifiedTransaction else { XCTAssertTrue(false) return @@ -216,7 +216,58 @@ final class SignedModelTests: XCTestCase { XCTAssertEqual("P1Y", transaction.offerPeriod) TestingUtility.confirmCodableInternallyConsistent(transaction) } - + + public func testTransactionWithRevocationDecoding() async throws { + let signedTransaction = TestingUtility.createSignedDataFromJson("resources/models/signedTransactionWithRevocation.json") + + let verifiedTransaction = await TestingUtility.getSignedDataVerifier().verifyAndDecodeTransaction(signedTransaction: signedTransaction) + + guard case .valid(let transaction) = verifiedTransaction else { + XCTAssertTrue(false) + return + } + + XCTAssertEqual("12345", transaction.originalTransactionId) + XCTAssertEqual("23456", transaction.transactionId) + XCTAssertEqual("34343", transaction.webOrderLineItemId) + XCTAssertEqual("com.example", transaction.bundleId) + XCTAssertEqual("com.example.product", transaction.productId) + XCTAssertEqual("55555", transaction.subscriptionGroupIdentifier) + XCTAssertEqual(Date(timeIntervalSince1970: 1698148800), transaction.originalPurchaseDate) + XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), transaction.purchaseDate) + XCTAssertEqual(Date(timeIntervalSince1970: 1698148950), transaction.revocationDate) + XCTAssertEqual(Date(timeIntervalSince1970: 1698149000), transaction.expiresDate) + XCTAssertEqual(1, transaction.quantity) + XCTAssertEqual(ProductType.autoRenewableSubscription, transaction.type) + XCTAssertEqual("Auto-Renewable Subscription", transaction.rawType) + XCTAssertEqual(UUID(uuidString: "7e3fb20b-4cdb-47cc-936d-99d65f608138"), transaction.appAccountToken) + XCTAssertEqual(InAppOwnershipType.purchased, transaction.inAppOwnershipType) + XCTAssertEqual("PURCHASED", transaction.rawInAppOwnershipType) + XCTAssertEqual(Date(timeIntervalSince1970: 1698148900), transaction.signedDate) + XCTAssertEqual(RevocationReason.refundedDueToIssue, transaction.revocationReason) + XCTAssertEqual(1, transaction.rawRevocationReason) + XCTAssertEqual("abc.123", transaction.offerIdentifier) + XCTAssertEqual(true, transaction.isUpgraded) + XCTAssertEqual(OfferType.introductoryOffer, transaction.offerType) + XCTAssertEqual(1, transaction.rawOfferType) + XCTAssertEqual("USA", transaction.storefront) + XCTAssertEqual("143441", transaction.storefrontId) + XCTAssertEqual(TransactionReason.purchase, transaction.transactionReason) + XCTAssertEqual("PURCHASE", transaction.rawTransactionReason) + XCTAssertEqual(AppStoreEnvironment.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) + XCTAssertEqual("71134", transaction.appTransactionId) + XCTAssertEqual("P1Y", transaction.offerPeriod) + XCTAssertEqual(RevocationType.refundProrated, transaction.revocationType) + XCTAssertEqual("REFUND_PRORATED", transaction.rawRevocationType) + XCTAssertEqual(50000, transaction.revocationPercentage) + TestingUtility.confirmCodableInternallyConsistent(transaction) + } + public func testRenewalInfoDecoding() async throws { let signedRenewalInfo = TestingUtility.createSignedDataFromJson("resources/models/signedRenewalInfo.json") @@ -306,6 +357,48 @@ final class SignedModelTests: XCTestCase { TestingUtility.confirmCodableInternallyConsistent(request) } + public func testRescindConsentNotificationDecoding() async throws { + let signedNotification = TestingUtility.createSignedDataFromJson("resources/models/signedRescindConsentNotification.json") + + let verifiedNotification = await TestingUtility.getSignedDataVerifier().verifyAndDecodeNotification(signedPayload: signedNotification) + + guard case .valid(let notification) = verifiedNotification else { + XCTAssertTrue(false) + return + } + + XCTAssertEqual(NotificationTypeV2.rescindConsent, notification.notificationType) + XCTAssertEqual("RESCIND_CONSENT", 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) + XCTAssertNil(notification.data) + XCTAssertNil(notification.summary) + XCTAssertNil(notification.externalPurchaseToken) + XCTAssertNotNil(notification.appData) + XCTAssertEqual(AppStoreEnvironment.localTesting, notification.appData!.environment) + XCTAssertEqual("LocalTesting", notification.appData!.rawEnvironment) + XCTAssertEqual(41234, notification.appData!.appAppleId) + XCTAssertEqual("com.example", notification.appData!.bundleId) + XCTAssertEqual("signed_app_transaction_info_value", notification.appData!.signedAppTransactionInfo) + TestingUtility.confirmCodableInternallyConsistent(notification) + } + + public func testAppData() throws { + let json = TestingUtility.readFile("resources/models/appData.json") + let jsonDecoder = getJsonDecoder() + + let appData = try jsonDecoder.decode(AppData.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(987654321, appData.appAppleId) + XCTAssertEqual("com.example", appData.bundleId) + XCTAssertEqual(AppStoreEnvironment.sandbox, appData.environment) + XCTAssertEqual("Sandbox", appData.rawEnvironment) + XCTAssertEqual("signed-app-transaction-info", appData.signedAppTransactionInfo) + } + // Xcode-generated dates are not well formed, therefore we only compare to ms precision private func compareXcodeDates(_ first: Date, _ second: Date?) { XCTAssertEqual(floor((first.timeIntervalSince1970 * 1000)), floor(((second?.timeIntervalSince1970 ?? 0.0) * 1000))) diff --git a/Tests/AppStoreServerLibraryTests/resources/models/appData.json b/Tests/AppStoreServerLibraryTests/resources/models/appData.json new file mode 100644 index 0000000..14b93ac --- /dev/null +++ b/Tests/AppStoreServerLibraryTests/resources/models/appData.json @@ -0,0 +1,6 @@ +{ + "appAppleId": 987654321, + "bundleId": "com.example", + "environment": "Sandbox", + "signedAppTransactionInfo": "signed-app-transaction-info" +} \ No newline at end of file diff --git a/Tests/AppStoreServerLibraryTests/resources/models/signedRescindConsentNotification.json b/Tests/AppStoreServerLibraryTests/resources/models/signedRescindConsentNotification.json new file mode 100644 index 0000000..0624e93 --- /dev/null +++ b/Tests/AppStoreServerLibraryTests/resources/models/signedRescindConsentNotification.json @@ -0,0 +1,12 @@ +{ + "notificationType": "RESCIND_CONSENT", + "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", + "appData": { + "appAppleId": 41234, + "bundleId": "com.example", + "environment": "LocalTesting", + "signedAppTransactionInfo": "signed_app_transaction_info_value" + }, + "version": "2.0", + "signedDate": 1698148900000 +} \ No newline at end of file diff --git a/Tests/AppStoreServerLibraryTests/resources/models/signedTransactionWithRevocation.json b/Tests/AppStoreServerLibraryTests/resources/models/signedTransactionWithRevocation.json new file mode 100644 index 0000000..3886b2e --- /dev/null +++ b/Tests/AppStoreServerLibraryTests/resources/models/signedTransactionWithRevocation.json @@ -0,0 +1,32 @@ +{ + "originalTransactionId": "12345", + "transactionId": "23456", + "webOrderLineItemId": "34343", + "bundleId": "com.example", + "productId": "com.example.product", + "subscriptionGroupIdentifier": "55555", + "purchaseDate": 1698148900000, + "originalPurchaseDate": 1698148800000, + "expiresDate": 1698149000000, + "quantity": 1, + "type": "Auto-Renewable Subscription", + "appAccountToken": "7e3fb20b-4cdb-47cc-936d-99d65f608138", + "inAppOwnershipType": "PURCHASED", + "signedDate": 1698148900000, + "revocationReason": 1, + "revocationDate": 1698148950000, + "isUpgraded": true, + "offerType": 1, + "offerIdentifier": "abc.123", + "environment": "LocalTesting", + "storefront": "USA", + "storefrontId": "143441", + "transactionReason": "PURCHASE", + "price": 10990, + "currency": "USD", + "offerDiscountType": "PAY_AS_YOU_GO", + "appTransactionId": "71134", + "offerPeriod": "P1Y", + "revocationType": "REFUND_PRORATED", + "revocationPercentage": 50000 +} \ No newline at end of file