Skip to content

Commit

Permalink
Merge pull request #333 from bizz84/verify-subscriptions
Browse files Browse the repository at this point in the history
Add verifySubscriptions method to check all subscriptions in a group at once
  • Loading branch information
bizz84 authored Dec 31, 2017
2 parents 861f40f + bcdf9fd commit 28285c7
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 70 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

All notable changes to this project will be documented in this file.

## [0.12.0](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.12.0) Add `verifySubscriptions` method for subscription groups

* Add `verifySubscriptions` method to check all subscriptions in a group at once ([#333](https://github.com/bizz84/SwiftyStoreKit/pull/333), related issue: [#194](https://github.com/bizz84/SwiftyStoreKit/issues/194))
* Rename `verifySubscription(type:productId:inReceipt:validUntil:)` to `verifySubscription(ofType:productId:inReceipt:validUntil:)` ([#333](https://github.com/bizz84/SwiftyStoreKit/pull/333))
* Add video tutorials section in README ([#328](https://github.com/bizz84/SwiftyStoreKit/pull/328), [#330](https://github.com/bizz84/SwiftyStoreKit/pull/330), see [#326](https://github.com/bizz84/SwiftyStoreKit/issues/326))
* Update iOS Demo App ([#327](https://github.com/bizz84/SwiftyStoreKit/pull/327), see [#147](https://github.com/bizz84/SwiftyStoreKit/issues/147))

Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
let productId = "com.musevisions.SwiftyStoreKit.Subscription"
// Verify the purchase of a Subscription
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable, // or .nonRenewing (see below)
ofType: .autoRenewable, // or .nonRenewing (see below)
productId: productId,
inReceipt: receipt)

Expand All @@ -416,7 +416,7 @@ SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
#### Auto-Renewable
```swift
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable,
ofType: .autoRenewable,
productId: "com.musevisions.SwiftyStoreKit.Subscription",
inReceipt: receipt)
```
Expand All @@ -425,7 +425,7 @@ let purchaseResult = SwiftyStoreKit.verifySubscription(
```swift
// validDuration: time interval in seconds
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .nonRenewing(validDuration: 3600 * 24 * 30),
ofType: .nonRenewing(validDuration: 3600 * 24 * 30),
productId: "com.musevisions.SwiftyStoreKit.Subscription",
inReceipt: receipt)
```
Expand Down Expand Up @@ -454,7 +454,7 @@ SwiftyStoreKit.purchaseProduct(productId, atomically: true) { result in

if case .success(let receipt) = result {
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable,
ofType: .autoRenewable,
productId: productId,
inReceipt: receipt)

Expand All @@ -477,6 +477,36 @@ SwiftyStoreKit.purchaseProduct(productId, atomically: true) { result in
}
```

### Subscription Groups

From [Apple Docs - Offering Subscriptions](https://developer.apple.com/app-store/subscriptions/):

> A subscription group is a set of in-app purchases that you can create to provide users with a range of content offerings, service levels, or durations to best meet their needs. Users can only buy one subscription within a subscription group at a time. If users would want to buy more that one type of subscription — for example, to subscribe to more than one channel in a streaming app — you can put these in-app purchases in different subscription groups.
You can verify all subscriptions within the same group with the `verifySubscriptions` method:

```swift
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let productIds = Set([ "com.musevisions.SwiftyStoreKit.Weekly",
"com.musevisions.SwiftyStoreKit.Monthly",
"com.musevisions.SwiftyStoreKit.Yearly" ])
let purchaseResult = SwiftyStoreKit.verifySubscriptions(productIds: productIds, inReceipt: receipt)
switch purchaseResult {
case .purchased(let expiryDate, let items):
print("\(productIds) are valid until \(expiryDate)\n\(items)\n")
case .expired(let expiryDate, let items):
print("\(productIds) are expired since \(expiryDate)\n\(items)\n")
case .notPurchased:
print("The user has never purchased \(productIds)")
}
case .error(let error):
print("Receipt verification failed: \(error)")
}
}
```

## Notes
The framework provides a simple block based API with robust error handling on top of the existing StoreKit framework. It does **NOT** persist in app purchases data locally. It is up to clients to do this with a storage solution of choice (i.e. NSUserDefaults, CoreData, Keychain).
Expand Down
48 changes: 30 additions & 18 deletions SwiftyStoreKit-iOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class ViewController: UIViewController {
purchase(autoRenewableSubscription, atomically: autoRenewableIsAtomic)
}
@IBAction func autoRenewableVerifyPurchase() {
verifyPurchase(autoRenewableSubscription)
verifySubscriptions([.autoRenewableWeekly, .autoRenewableMonthly, .autoRenewableYearly])
}

func getInfo(_ purchase: RegisteredPurchase) {
Expand Down Expand Up @@ -172,7 +172,7 @@ class ViewController: UIViewController {
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
SwiftyStoreKit.verifyReceipt(using: appleValidator, completion: completion)
}

func verifyPurchase(_ purchase: RegisteredPurchase) {

NetworkActivityIndicatorManager.networkOperationStarted()
Expand All @@ -187,25 +187,20 @@ class ViewController: UIViewController {
switch purchase {
case .autoRenewableWeekly, .autoRenewableMonthly, .autoRenewableYearly:
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable,
ofType: .autoRenewable,
productId: productId,
inReceipt: receipt,
validUntil: Date()
)
self.showAlert(self.alertForVerifySubscription(purchaseResult, productId: productId))
inReceipt: receipt)
self.showAlert(self.alertForVerifySubscriptions(purchaseResult, productIds: [productId]))
case .nonRenewingPurchase:
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .nonRenewing(validDuration: 60),
ofType: .nonRenewing(validDuration: 60),
productId: productId,
inReceipt: receipt,
validUntil: Date()
)
self.showAlert(self.alertForVerifySubscription(purchaseResult, productId: productId))
inReceipt: receipt)
self.showAlert(self.alertForVerifySubscriptions(purchaseResult, productIds: [productId]))
default:
let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: productId,
inReceipt: receipt
)
inReceipt: receipt)
self.showAlert(self.alertForVerifyPurchase(purchaseResult, productId: productId))
}

Expand All @@ -214,6 +209,23 @@ class ViewController: UIViewController {
}
}
}

func verifySubscriptions(_ purchases: Set<RegisteredPurchase>) {

NetworkActivityIndicatorManager.networkOperationStarted()
verifyReceipt { result in
NetworkActivityIndicatorManager.networkOperationFinished()

switch result {
case .success(let receipt):
let productIds = Set(purchases.map { self.appBundleId + "." + $0.rawValue })
let purchaseResult = SwiftyStoreKit.verifySubscriptions(productIds: productIds, inReceipt: receipt)
self.showAlert(self.alertForVerifySubscriptions(purchaseResult, productIds: productIds))
case .error:
self.showAlert(self.alertForVerifyReceipt(result))
}
}
}

#if os(iOS)
override var preferredStatusBarStyle: UIStatusBarStyle {
Expand Down Expand Up @@ -315,17 +327,17 @@ extension ViewController {
}
}

func alertForVerifySubscription(_ result: VerifySubscriptionResult, productId: String) -> UIAlertController {
func alertForVerifySubscriptions(_ result: VerifySubscriptionResult, productIds: Set<String>) -> UIAlertController {

switch result {
case .purchased(let expiryDate, let items):
print("\(productId) is valid until \(expiryDate)\n\(items)\n")
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):
print("\(productId) is expired since \(expiryDate)\n\(items)\n")
print("\(productIds) is expired since \(expiryDate)\n\(items)\n")
return alertWithTitle("Product expired", message: "Product is expired since \(expiryDate)")
case .notPurchased:
print("\(productId) has never been purchased")
print("\(productIds) has never been purchased")
return alertWithTitle("Not purchased", message: "This product has never been purchased")
}
}
Expand Down
13 changes: 5 additions & 8 deletions SwiftyStoreKit-macOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,20 @@ class ViewController: NSViewController {
switch purchase {
case .autoRenewablePurchase:
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable,
ofType: .autoRenewable,
productId: productId,
inReceipt: receipt
)
inReceipt: receipt)
self.showAlert(self.alertForVerifySubscription(purchaseResult))
case .nonRenewingPurchase:
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .nonRenewing(validDuration: 60),
ofType: .nonRenewing(validDuration: 60),
productId: productId,
inReceipt: receipt
)
inReceipt: receipt)
self.showAlert(self.alertForVerifySubscription(purchaseResult))
default:
let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: productId,
inReceipt: receipt
)
inReceipt: receipt)
self.showAlert(self.alertForVerifyPurchase(purchaseResult))
}

Expand Down
41 changes: 23 additions & 18 deletions SwiftyStoreKit/InAppReceipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ internal class InAppReceipt {

// Get receipts info for the product
let receipts = getInAppReceipts(receipt: receipt)
let filteredReceiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
let filteredReceiptsInfo = filterReceiptsInfo(receipts: receipts, withProductIds: [productId])
let nonCancelledReceiptsInfo = filteredReceiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }

let receiptItems = nonCancelledReceiptsInfo.flatMap { ReceiptItem(receiptInfo: $0) }
Expand All @@ -105,24 +105,27 @@ internal class InAppReceipt {
}

/**
* Verify the purchase of a subscription (auto-renewable, free or non-renewing) in a receipt. This method extracts all transactions mathing the given productId and sorts them by date in descending order, then compares the first transaction expiry date against the validUntil value.
* - parameter type: .autoRenewable or .nonRenewing(duration)
* - Parameter productId: the product id of the purchase to verify
* - Parameter inReceipt: the receipt to use for looking up the subscription
* - Parameter validUntil: date to check against the expiry date of the subscription. If nil, no verification
* - Parameter validDuration: the duration of the subscription. Only required for non-renewable subscription.
* - return: either NotPurchased or Purchased / Expired with the expiry date found in the receipt
* Verify the validity of a set of subscriptions in a receipt.
*
* This method extracts all transactions matching the given productIds and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date, to determine its validity.
* - Note: You can use this method to check the validity of (mutually exclusive) subscriptions in a subscription group.
* - Remark: The type parameter determines how the expiration dates are calculated for all subscriptions. Make sure all productIds match the specified subscription type to avoid incorrect results.
* - Parameter type: .autoRenewable or .nonRenewing.
* - Parameter productIds: The product ids of the subscriptions to verify.
* - Parameter receipt: The receipt to use for looking up the subscriptions
* - Parameter validUntil: Date to check against the expiry date of the subscriptions. This is only used if a date is not found in the receipt.
* - return: Either .notPurchased or .purchased / .expired with the expiry date found in the receipt.
*/
class func verifySubscription(
type: SubscriptionType,
productId: String,
class func verifySubscriptions(
ofType type: SubscriptionType,
productIds: Set<String>,
inReceipt receipt: ReceiptInfo,
validUntil date: Date = Date()
) -> VerifySubscriptionResult {

// The values of the latest_receipt and latest_receipt_info keys are useful when checking whether an auto-renewable subscription is currently active. By providing any transaction receipt for the subscription and checking these values, you can get information about the currently-active subscription period. If the receipt being validated is for the latest renewal, the value for latest_receipt is the same as receipt-data (in the request) and the value for latest_receipt_info is the same as receipt.
let (receipts, duration) = getReceiptsAndDuration(for: type, inReceipt: receipt)
let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductIds: productIds)
let nonCancelledReceiptsInfo = receiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }
if nonCancelledReceiptsInfo.count == 0 {
return .notPurchased
Expand Down Expand Up @@ -198,19 +201,21 @@ internal class InAppReceipt {
* - Parameter receipts: the receipts array to grab info from
* - Parameter productId: the product id
*/
private class func filterReceiptsInfo(receipts: [ReceiptInfo]?, withProductId productId: String) -> [ReceiptInfo] {
private class func filterReceiptsInfo(receipts: [ReceiptInfo]?, withProductIds productIds: Set<String>) -> [ReceiptInfo] {

guard let receipts = receipts else {
return []
}

// Filter receipts with matching product id
let receiptsMatchingProductId = receipts
// Filter receipts with matching product ids
let receiptsMatchingProductIds = receipts
.filter { (receipt) -> Bool in
let product_id = receipt["product_id"] as? String
return product_id == productId
if let productId = receipt["product_id"] as? String {
return productIds.contains(productId)
}
return false
}

return receiptsMatchingProductId
return receiptsMatchingProductIds
}
}
35 changes: 27 additions & 8 deletions SwiftyStoreKit/SwiftyStoreKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,34 @@ extension SwiftyStoreKit {
}

/**
* Verify the purchase of a subscription (auto-renewable, free or non-renewing) in a receipt. This method extracts all transactions mathing the given productId and sorts them by date in descending order, then compares the first transaction expiry date against the validUntil value.
* - Parameter type: autoRenewable or nonRenewing
* - Parameter productId: the product id of the purchase to verify
* - Parameter inReceipt: the receipt to use for looking up the subscription
* - Parameter validUntil: date to check against the expiry date of the subscription. If nil, no verification
* - return: either .notPurchased or .purchased / .expired with the expiry date found in the receipt
* Verify the validity of a subscription (auto-renewable, free or non-renewing) in a receipt.
*
* This method extracts all transactions matching the given productId and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date to determine its validity.
* - Parameter type: .autoRenewable or .nonRenewing.
* - Parameter productId: The product id of the subscription to verify.
* - Parameter receipt: The receipt to use for looking up the subscription.
* - Parameter validUntil: Date to check against the expiry date of the subscription. This is only used if a date is not found in the receipt.
* - return: Either .notPurchased or .purchased / .expired with the expiry date found in the receipt.
*/
public class func verifySubscription(ofType type: SubscriptionType, productId: String, inReceipt receipt: ReceiptInfo, validUntil date: Date = Date()) -> VerifySubscriptionResult {

return InAppReceipt.verifySubscriptions(ofType: type, productIds: [productId], inReceipt: receipt, validUntil: date)
}

/**
* Verify the validity of a set of subscriptions in a receipt.
*
* This method extracts all transactions matching the given productIds and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date, to determine its validity.
* - Note: You can use this method to check the validity of (mutually exclusive) subscriptions in a subscription group.
* - Remark: The type parameter determines how the expiration dates are calculated for all subscriptions. Make sure all productIds match the specified subscription type to avoid incorrect results.
* - Parameter type: .autoRenewable or .nonRenewing.
* - Parameter productIds: The product ids of the subscriptions to verify.
* - Parameter receipt: The receipt to use for looking up the subscriptions
* - Parameter validUntil: Date to check against the expiry date of the subscriptions. This is only used if a date is not found in the receipt.
* - return: Either .notPurchased or .purchased / .expired with the expiry date found in the receipt.
*/
public class func verifySubscription(type: SubscriptionType, productId: String, inReceipt receipt: ReceiptInfo, validUntil date: Date = Date()) -> VerifySubscriptionResult {
public class func verifySubscriptions(ofType type: SubscriptionType = .autoRenewable, productIds: Set<String>, inReceipt receipt: ReceiptInfo, validUntil date: Date = Date()) -> VerifySubscriptionResult {

return InAppReceipt.verifySubscription(type: type, productId: productId, inReceipt: receipt, validUntil: date)
return InAppReceipt.verifySubscriptions(ofType: type, productIds: productIds, inReceipt: receipt, validUntil: date)
}
}
Loading

0 comments on commit 28285c7

Please sign in to comment.