diff --git a/Sources/SwiftyStoreKit/InAppReceipt.swift b/Sources/SwiftyStoreKit/InAppReceipt.swift index b78df1b8..6ad29e68 100644 --- a/Sources/SwiftyStoreKit/InAppReceipt.swift +++ b/Sources/SwiftyStoreKit/InAppReceipt.swift @@ -72,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 { @@ -83,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 { @@ -163,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 53a9ae3b..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,7 +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 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? @@ -239,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 @@ -316,6 +343,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) diff --git a/SwiftyStoreKit-iOS-Demo/ViewController.swift b/SwiftyStoreKit-iOS-Demo/ViewController.swift index fa640325..9c8c88d4 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 { @@ -341,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 071c6d9f..141c5221 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 { @@ -247,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) }