Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscription expiration reason #676

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions Sources/SwiftyStoreKit/InAppReceipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {

Expand Down Expand Up @@ -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)
}
}

Expand Down
36 changes: 33 additions & 3 deletions Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions SwiftyStoreKit-iOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -341,10 +343,10 @@ extension ViewController {
func alertForVerifySubscriptions(_ result: VerifySubscriptionResult, productIds: Set<String>) -> 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:
Expand Down
6 changes: 4 additions & 2 deletions SwiftyStoreKit-macOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
38 changes: 27 additions & 11 deletions Tests/SwiftyStoreKitTests/InAppReceiptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down