From c98e90b518eef4e0728126115ef4973e33c110c9 Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Thu, 19 May 2022 22:58:00 +0300 Subject: [PATCH 1/4] Fix demo compilation error after b3f2601196a87569d463283c3a582ddcc7fccf0b - Merge pull request #652 from azouts/master Communicate deferred transactions to the app --- SwiftyStoreKit-iOS-Demo/ViewController.swift | 2 ++ SwiftyStoreKit-macOS-Demo/ViewController.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/SwiftyStoreKit-iOS-Demo/ViewController.swift b/SwiftyStoreKit-iOS-Demo/ViewController.swift index fa640325..a690403a 100644 --- a/SwiftyStoreKit-iOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-iOS-Demo/ViewController.swift @@ -279,6 +279,8 @@ extension ViewController { case .success(let purchase): print("Purchase Success: \(purchase.productId)") return nil + case .deferred(purchase: _): + return alertWithTitle("Purchase deferred", message: "The purchase deferred") case .error(let error): print("Purchase Failed: \(error)") switch error.code { diff --git a/SwiftyStoreKit-macOS-Demo/ViewController.swift b/SwiftyStoreKit-macOS-Demo/ViewController.swift index 071c6d9f..d0eaf729 100644 --- a/SwiftyStoreKit-macOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-macOS-Demo/ViewController.swift @@ -193,6 +193,8 @@ extension ViewController { case .success(let purchase): print("Purchase Success: \(purchase.productId)") return alertWithTitle("Thank You", message: "Purchase completed") + case .deferred(purchase: _): + return alertWithTitle("Purchase deferred", message: "The purchase deferred") case .error(let error): print("Purchase Failed: \(error)") switch error.code { From fbd95f1b00019673a4a5f94c083fdfb4122019bf Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Fri, 20 May 2022 00:21:35 +0300 Subject: [PATCH 2/4] Add expirationIntent to the ReceiptItem --- Sources/SwiftyStoreKit/InAppReceipt.swift | 3 +++ Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Sources/SwiftyStoreKit/InAppReceipt.swift b/Sources/SwiftyStoreKit/InAppReceipt.swift index b78df1b8..969082e0 100644 --- a/Sources/SwiftyStoreKit/InAppReceipt.swift +++ b/Sources/SwiftyStoreKit/InAppReceipt.swift @@ -58,6 +58,9 @@ extension ReceiptItem { self.originalPurchaseDate = originalPurchaseDate self.webOrderLineItemId = receiptInfo["web_order_line_item_id"] as? String self.subscriptionExpirationDate = ReceiptItem.parseDate(from: receiptInfo, key: "expires_date_ms") + if let expirationIntent = receiptInfo["expiration_intent"] as? String { + self.expirationIntent = Int(expirationIntent) + } self.cancellationDate = ReceiptItem.parseDate(from: receiptInfo, key: "cancellation_date_ms") if let isTrialPeriod = receiptInfo["is_trial_period"] as? String { self.isTrialPeriod = Bool(isTrialPeriod) ?? false diff --git a/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift b/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift index 53a9ae3b..55273bb0 100644 --- a/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift +++ b/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift @@ -209,6 +209,9 @@ public struct ReceiptItem: Purchased, Codable { /// The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT. This key is **only** present for **auto-renewable** subscription receipts. public var subscriptionExpirationDate: Date? + /// For an expired subscription, the reason for the subscription expiration. + public var expirationIntent: Int? + /// For a transaction that was canceled by Apple customer support, the time and date of the cancellation. /// /// Treat a canceled receipt the same as if no purchase had ever been made. @@ -316,6 +319,9 @@ public enum ReceiptInfoField: String { case original_purchase_date /// The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT. This key is only present for auto-renewable subscription receipts. case expires_date + ///For an expired subscription, the reason for the subscription expiration. + case expiration_intent + /// For a transaction that was canceled by Apple customer support, the time and date of the cancellation. Treat a canceled receipt the same as if no purchase had ever been made. case cancellation_date #if os(iOS) || os(tvOS) From 89048cea4084c16aaca278e6f45fb364a5da5d80 Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Mon, 23 May 2022 19:56:29 +0300 Subject: [PATCH 3/4] Fix expirationIntent parsing. Add new PendingRenewalInfo struct --- Sources/SwiftyStoreKit/InAppReceipt.swift | 58 ++++++++++++++++--- .../SwiftyStoreKit/SwiftyStoreKit+Types.swift | 36 ++++++++++-- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftyStoreKit/InAppReceipt.swift b/Sources/SwiftyStoreKit/InAppReceipt.swift index 969082e0..6ad29e68 100644 --- a/Sources/SwiftyStoreKit/InAppReceipt.swift +++ b/Sources/SwiftyStoreKit/InAppReceipt.swift @@ -58,9 +58,6 @@ extension ReceiptItem { self.originalPurchaseDate = originalPurchaseDate self.webOrderLineItemId = receiptInfo["web_order_line_item_id"] as? String self.subscriptionExpirationDate = ReceiptItem.parseDate(from: receiptInfo, key: "expires_date_ms") - if let expirationIntent = receiptInfo["expiration_intent"] as? String { - self.expirationIntent = Int(expirationIntent) - } self.cancellationDate = ReceiptItem.parseDate(from: receiptInfo, key: "cancellation_date_ms") if let isTrialPeriod = receiptInfo["is_trial_period"] as? String { self.isTrialPeriod = Bool(isTrialPeriod) ?? false @@ -75,8 +72,7 @@ extension ReceiptItem { self.isUpgraded = receiptInfo["is_upgraded"] as? Bool ?? false } - private static func parseDate(from receiptInfo: ReceiptInfo, key: String) -> Date? { - + fileprivate static func parseDate(from receiptInfo: ReceiptInfo, key: String) -> Date? { guard let requestDateString = receiptInfo[key] as? String, let requestDateMs = Double(requestDateString) else { @@ -86,6 +82,48 @@ extension ReceiptItem { } } +extension PendingRenewalInfo { + + public init?(receiptInfo: ReceiptInfo) { + guard + let productId = receiptInfo["auto_renew_product_id"] as? String ?? receiptInfo["product_id"] as? String, + let originalTransactionId = receiptInfo["original_transaction_id"] as? String + else { + print("could not parse receipt item: \(receiptInfo). Skipping...") + return nil + } + self.productId = productId + if let statusString = receiptInfo["auto_renew_status"] as? String { + status = Int(statusString) + } else { + status = nil + } + if let expirationIntent = receiptInfo["expiration_intent"] as? String { + self.expirationIntent = Int(expirationIntent) + } else { + self.expirationIntent = nil + } + self.gracePeriodExpiresDate = ReceiptItem.parseDate(from: receiptInfo, key: "grace_period_expires_date_ms") + if let billingRetryString = receiptInfo["is_in_billing_retry_period"] as? String, + let billingRetryInt = Int(billingRetryString) { + self.isInBillingRetryPeriod = billingRetryInt == 1 + } else { + self.isInBillingRetryPeriod = nil + } + self.transactionId = originalTransactionId + if let priceConsentStatusString = receiptInfo["price_consent_status"] as? String { + self.priceConsentStatus = Int(priceConsentStatusString) + } else { + self.priceConsentStatus = nil + } + if let priceIncreaseStatusString = receiptInfo["price_increase_status"] as? String { + self.priceIncreaseStatus = Int(priceIncreaseStatusString) + } else { + self.priceIncreaseStatus = nil + } + } +} + // MARK: - receipt mangement internal class InAppReceipt { @@ -166,10 +204,16 @@ internal class InAppReceipt { } let sortedReceiptItems = sortedExpiryDatesAndItems.map { $0.1 } + let renewalInfo = receipt["pending_renewal_info"] as? [ReceiptInfo] + #if swift(>=4.1) + let renewal = renewalInfo?.compactMap { PendingRenewalInfo(receiptInfo: $0) } + #else + let renewal = renewalInfo?.flatMap { PendingRenewalInfo(receiptInfo: $0) } + #endif if firstExpiryDateItemPair.0 > receiptDate { - return .purchased(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems) + return .purchased(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems, renewalInfo: renewal) } else { - return .expired(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems) + return .expired(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems, renewalInfo: renewal) } } diff --git a/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift b/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift index 55273bb0..1e0da4db 100644 --- a/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift +++ b/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift @@ -171,8 +171,8 @@ public enum VerifyPurchaseResult { /// Verify subscription result public enum VerifySubscriptionResult { - case purchased(expiryDate: Date, items: [ReceiptItem]) - case expired(expiryDate: Date, items: [ReceiptItem]) + case purchased(expiryDate: Date, items: [ReceiptItem], renewalInfo: [PendingRenewalInfo]?) + case expired(expiryDate: Date, items: [ReceiptItem], renewalInfo: [PendingRenewalInfo]?) case notPurchased } @@ -209,10 +209,7 @@ public struct ReceiptItem: Purchased, Codable { /// The expiration date for the subscription, expressed as the number of milliseconds since January 1, 1970, 00:00:00 GMT. This key is **only** present for **auto-renewable** subscription receipts. public var subscriptionExpirationDate: Date? - /// For an expired subscription, the reason for the subscription expiration. - public var expirationIntent: Int? - - /// For a transaction that was canceled by Apple customer support, the time and date of the cancellation. + /// For a transaction that was canceled by Apple customer support, the time and date of the cancellation. /// /// Treat a canceled receipt the same as if no purchase had ever been made. public var cancellationDate: Date? @@ -242,6 +239,33 @@ public struct ReceiptItem: Purchased, Codable { } } +public struct PendingRenewalInfo: Codable { + /// The value for this key corresponds to the `productIdentifier` property of the product that the customer’s subscription renews. + /// The unique identifier of the product purchased. You provide this value when creating the product in App Store Connect, and it corresponds to the `productIdentifier` property of the `SKPayment` object stored in the transaction's payment property. + public let productId: String + + /// The current renewal status for the **auto-renewable** subscription. See `auto_renew_status` for more information. + public let status: Int? + + /// The reason a subscription expired. This field is present **only** for a receipt that contains an **expired auto-renewable** subscription. + public let expirationIntent: Int? + + /// The time at which the grace period for subscription renewals expires + public let gracePeriodExpiresDate: Date? + + /// A flag that indicates Apple is attempting to renew an **expired subscription** automatically. This key is **only** present if an **auto-renewable** subscription is in the billing retry state + public let isInBillingRetryPeriod: Bool? + + /// The transaction identifier of the original purchase. + public let transactionId: String + + /// The price consent status for an auto-renewable subscription price increase that requires customer consent. This field is present only if the App Store requested customer consent for a price increase that requires customer consent. + public let priceConsentStatus: Int? + + /// The status that indicates if an **auto-renewable** subscription is subject to a price increase. + public let priceIncreaseStatus: Int? +} + /// Error when managing receipt public enum ReceiptError: Swift.Error { /// No receipt data From 8c35f9415e53f3b0a494513a32089492df03dc64 Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Tue, 24 May 2022 14:58:38 +0300 Subject: [PATCH 4/4] Fix demos and tests after adding PendingRenewalInfo struct (89048ce) --- SwiftyStoreKit-iOS-Demo/ViewController.swift | 4 +- .../ViewController.swift | 4 +- .../InAppReceiptTests.swift | 38 +++++++++++++------ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/SwiftyStoreKit-iOS-Demo/ViewController.swift b/SwiftyStoreKit-iOS-Demo/ViewController.swift index a690403a..9c8c88d4 100644 --- a/SwiftyStoreKit-iOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-iOS-Demo/ViewController.swift @@ -343,10 +343,10 @@ extension ViewController { func alertForVerifySubscriptions(_ result: VerifySubscriptionResult, productIds: Set) -> UIAlertController { switch result { - case .purchased(let expiryDate, let items): + case .purchased(let expiryDate, let items, _): print("\(productIds) is valid until \(expiryDate)\n\(items)\n") return alertWithTitle("Product is purchased", message: "Product is valid until \(expiryDate)") - case .expired(let expiryDate, let items): + case .expired(let expiryDate, let items, _): print("\(productIds) is expired since \(expiryDate)\n\(items)\n") return alertWithTitle("Product expired", message: "Product is expired since \(expiryDate)") case .notPurchased: diff --git a/SwiftyStoreKit-macOS-Demo/ViewController.swift b/SwiftyStoreKit-macOS-Demo/ViewController.swift index d0eaf729..141c5221 100644 --- a/SwiftyStoreKit-macOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-macOS-Demo/ViewController.swift @@ -249,10 +249,10 @@ extension ViewController { func alertForVerifySubscription(_ result: VerifySubscriptionResult) -> NSAlert { switch result { - case .purchased(let expiryDate): + case .purchased(let expiryDate, _, _): print("Product is valid until \(expiryDate)") return alertWithTitle("Product is purchased", message: "Product is valid until \(expiryDate)") - case .expired(let expiryDate): + case .expired(let expiryDate, _, _): print("Product is expired since \(expiryDate)") return alertWithTitle("Product expired", message: "Product is expired since \(expiryDate)") case .notPurchased: diff --git a/Tests/SwiftyStoreKitTests/InAppReceiptTests.swift b/Tests/SwiftyStoreKitTests/InAppReceiptTests.swift index ca6d8051..dc0b9643 100644 --- a/Tests/SwiftyStoreKitTests/InAppReceiptTests.swift +++ b/Tests/SwiftyStoreKitTests/InAppReceiptTests.swift @@ -82,15 +82,31 @@ extension ReceiptItem: Equatable { } } +extension PendingRenewalInfo: Equatable { + public static func == (lhs: PendingRenewalInfo, rhs: PendingRenewalInfo) -> Bool { + return + lhs.productId == rhs.productId && + lhs.status == rhs.status && + lhs.expirationIntent == rhs.expirationIntent && + lhs.gracePeriodExpiresDate == rhs.gracePeriodExpiresDate && + lhs.isInBillingRetryPeriod == rhs.isInBillingRetryPeriod && + lhs.transactionId == rhs.transactionId && + lhs.priceConsentStatus == rhs.priceConsentStatus && + lhs.priceIncreaseStatus == rhs.priceIncreaseStatus + } +} + extension VerifySubscriptionResult: Equatable { public static func == (lhs: VerifySubscriptionResult, rhs: VerifySubscriptionResult) -> Bool { switch (lhs, rhs) { case (.notPurchased, .notPurchased): return true - case (.purchased(let lhsExpiryDate, let lhsReceiptItem), .purchased(let rhsExpiryDate, let rhsReceiptItem)): - return lhsExpiryDate == rhsExpiryDate && lhsReceiptItem == rhsReceiptItem - case (.expired(let lhsExpiryDate, let lhsReceiptItem), .expired(let rhsExpiryDate, let rhsReceiptItem)): - return lhsExpiryDate == rhsExpiryDate && lhsReceiptItem == rhsReceiptItem + case (.purchased(let lhsExpiryDate, let lhsReceiptItem, let lhsPendingInfo), + .purchased(let rhsExpiryDate, let rhsReceiptItem, let rhsPendingInfo)): + return lhsExpiryDate == rhsExpiryDate && lhsReceiptItem == rhsReceiptItem && lhsPendingInfo == rhsPendingInfo + case (.expired(let lhsExpiryDate, let lhsReceiptItem, let lhsPendingInfo), + .expired(let rhsExpiryDate, let rhsReceiptItem, let rhsPendingInfo)): + return lhsExpiryDate == rhsExpiryDate && lhsReceiptItem == rhsReceiptItem && lhsPendingInfo == rhsPendingInfo default: return false } } @@ -169,7 +185,7 @@ class InAppReceiptTests: XCTestCase { let verifySubscriptionResult = SwiftyStoreKit.verifySubscription(ofType: .autoRenewable, productId: productId, inReceipt: receipt) - let expectedSubscriptionResult = VerifySubscriptionResult.expired(expiryDate: expirationDate, items: [item]) + let expectedSubscriptionResult = VerifySubscriptionResult.expired(expiryDate: expirationDate, items: [item], renewalInfo: nil) XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult) } @@ -185,7 +201,7 @@ class InAppReceiptTests: XCTestCase { let verifySubscriptionResult = SwiftyStoreKit.verifySubscription(ofType: .autoRenewable, productId: productId, inReceipt: receipt) - let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: expirationDate, items: [item]) + let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: expirationDate, items: [item], renewalInfo: nil) XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult) } @@ -232,7 +248,7 @@ class InAppReceiptTests: XCTestCase { let verifySubscriptionResult = SwiftyStoreKit.verifySubscription(ofType: .nonRenewing(validDuration: duration), productId: productId, inReceipt: receipt) - let expectedSubscriptionResult = VerifySubscriptionResult.expired(expiryDate: expirationDate, items: [item]) + let expectedSubscriptionResult = VerifySubscriptionResult.expired(expiryDate: expirationDate, items: [item], renewalInfo: nil) XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult) } @@ -250,7 +266,7 @@ class InAppReceiptTests: XCTestCase { let verifySubscriptionResult = SwiftyStoreKit.verifySubscription(ofType: .nonRenewing(validDuration: duration), productId: productId, inReceipt: receipt) - let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: expirationDate, items: [item]) + let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: expirationDate, items: [item], renewalInfo: nil) XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult) } @@ -298,7 +314,7 @@ class InAppReceiptTests: XCTestCase { let verifySubscriptionResult = SwiftyStoreKit.verifySubscription(ofType: .autoRenewable, productId: productId, inReceipt: receipt) - let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: newerExpirationDate, items: [newerItem, olderItem]) + let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: newerExpirationDate, items: [newerItem, olderItem], renewalInfo: nil) XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult) } @@ -328,7 +344,7 @@ class InAppReceiptTests: XCTestCase { let verifySubscriptionResult = SwiftyStoreKit.verifySubscription(ofType: .autoRenewable, productId: productId, inReceipt: receipt) - let expectedSubscriptionResult = VerifySubscriptionResult.expired(expiryDate: newerExpirationDate, items: [newerItem, olderItem]) + let expectedSubscriptionResult = VerifySubscriptionResult.expired(expiryDate: newerExpirationDate, items: [newerItem, olderItem], renewalInfo: nil) XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult) } @@ -369,7 +385,7 @@ class InAppReceiptTests: XCTestCase { let verifySubscriptionResult = SwiftyStoreKit.verifySubscriptions(ofType: .autoRenewable, productIds: productIds, inReceipt: receipt) - let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: newerExpirationDate, items: [newerItem, olderItem]) + let expectedSubscriptionResult = VerifySubscriptionResult.purchased(expiryDate: newerExpirationDate, items: [newerItem, olderItem], renewalInfo: nil) XCTAssertEqual(verifySubscriptionResult, expectedSubscriptionResult) }