Swiftystorekit: Verify subscription for multiple/all products?

Created on 3 May 2017  路  13Comments  路  Source: bizz84/SwiftyStoreKit

Platform

  • [ ] iOS

In app purchase type

  • [ ] Auto-Renewable Subscription

Environment

  • [ ] Sandbox
  • [ ] Production

Version

0.8.6

Related issues

Report

Issue summary

From your readme you call:
SwiftyStoreKit.verifyReceipt(...) { ... SwiftyStoreKit.verifySubscription(... productId: ...} ... }
What to do if we don't know this productId and have multiple subscriptions which may vary?

What did you expect to happen

Get any valid subscription from the request result without of binding to concrete Id?

What happened instead

The app don't have to know productId but the result contains info about this subscription. Why must I know productId to check if just ANY subscription from list is valid?

receipt-validation help wanted needs analysis enhancement

Most helpful comment

verifySubscription is a convenience method to check the state of a subscription for a given productId from the receipt.

Would you like something like this instead?

class func verifySubscriptions(
        type: SubscriptionType,
        productIds: Set<String>,
        inReceipt receipt: ReceiptInfo,
        validUntil date: Date = Date()
        ) -> VerifySubscriptionResult

or this?

class func verifyAllSubscriptions(
        type: SubscriptionType,
        inReceipt receipt: ReceiptInfo,
        validUntil date: Date = Date()
        ) -> VerifySubscriptionResult

At the moment this is not possible, but this work will be considered alongside #192, #190 which also require some change to the subscription verification code.

For the time being, you could call verifySubscription on all known productIds:

let productIds = [] // all your product ids
let verifySubscriptionResults = productIds.map { SwiftyStoreKit.verifySubscription(..., productId: $0, ...) }

Alternatively, you could parse the receipt manually.

All 13 comments

verifySubscription is a convenience method to check the state of a subscription for a given productId from the receipt.

Would you like something like this instead?

class func verifySubscriptions(
        type: SubscriptionType,
        productIds: Set<String>,
        inReceipt receipt: ReceiptInfo,
        validUntil date: Date = Date()
        ) -> VerifySubscriptionResult

or this?

class func verifyAllSubscriptions(
        type: SubscriptionType,
        inReceipt receipt: ReceiptInfo,
        validUntil date: Date = Date()
        ) -> VerifySubscriptionResult

At the moment this is not possible, but this work will be considered alongside #192, #190 which also require some change to the subscription verification code.

For the time being, you could call verifySubscription on all known productIds:

let productIds = [] // all your product ids
let verifySubscriptionResults = productIds.map { SwiftyStoreKit.verifySubscription(..., productId: $0, ...) }

Alternatively, you could parse the receipt manually.

Yes. I need this one:

class func verifyAllSubscriptions(
        type: SubscriptionType,
        inReceipt receipt: ReceiptInfo,
        validUntil date: Date = Date()
        ) -> VerifySubscriptionResult

My current solution (I use autorenewable subscriptions only):

SwiftyStoreKit.verifyReceipt(using: appleValidator, password: shared_secret) { result in
...
                    if let receiptInfos = receipt["latest_receipt_info"] as? [ReceiptInfo] {
                        if let receiptIDs = receiptInfos.map({ $0["product_id"] }) as? [String] {
                            let distinctReceiptIDs = Set(receiptIDs)
                            for subscriptionID in distinctReceiptIDs {
                                 ...
                            }
...

In short in my case productID's may be unpredictably changed. So I parse ReceiptInfo and get all the possible IDs to use in the existing function verifySubscription(...).

The only problem I have (if it is a problem) is the following:

        SwiftyStoreKit.verifyReceipt(using: appleValidator, password: shared_secret) { result in
        ...
       let purchaseResult = SwiftyStoreKit.verifySubscription(
      ...

I don't call restorePurchases but the code above sometimes works like this method - it finds a valid subscription even if the app is installed for the first.

@gerchicov-bp you say this:

in my case productID's may be unpredictably changed

Why is this the case? As the developer you should have control to all registered IAPs in iTunes Connect right?

Regarding this:

it finds a valid subscription even if the app is installed for the first.

I believe that when the app is deleted and installed and the user is logged in with Apple ID, the receipt from the previous app installation is reloaded.
This makes sense as you could have purchased a subscription and you still want to access it in the receipt even if you reinstall the app.

Also, please take a look at release 0.9.0, as I have improved verifySubscription().

I'm not sure how to best implement a verifyAllSubscriptions method. Should it parse all receipt items to get the set of all product IDs, filter out only the subscriptions (note that the receipt also contains items that are not subscriptions), and return a set of VerifySubscriptionResult items?

This seems a bit convoluted. If you have access to the subscription product ids this is much more easily done by reusing the existing verifySubscription method.

@bizz84
As the developer you should have control to all registered IAPs in iTunes Connect right?
In my case these purchase ids come from my server (it is not a server for subscriptions - it just stores some settings). And I can't predict which which set of ids it will send next time. Why? It is because of my customer only.

It seems you should imlement verifyAllSubscriptions but with type param (autorenewable/nonrenewing). The result can be a set of VerifySubscriptionResult or even array because you could sort the items for example by expiration date (for example if the subscriptions open the same features then the item with max expiration date "overlaps" other items and so it could be placed in the beginning).

If you haven't a possibility to add verifyAllSubscriptions()/verifyAllItems() then what about to add a method which returns all distinct purchase ids from receipt for a given purchase type? My code above is for autorenewable subscriptions only.

Ok, I'll have a think and see what I can do.

I come to this issue too now , I have two subscriptions:
monthly and yearly.
In case the user was subscribed with monthly and then with yearly, I should get the latest subscription in order to check if he is subscribed so that he could have access to the paid content.

I think the best way is to filter the receipt subscriptions by expirydate and get the newest one and compare it to the current date

@sam961
I've already thought about it. Filter is not appropriate, only sort. The reason is the following. Let look for example at Microsoft Office 365. There are 3 subscriptions which represent 3 various products - so you can't just take "the latest one". So it may comply your case and my one but not all the cases.

@gerchicov-bp @sam961 in your specific case, would it make sense to have the monthly and yearly subscriptions in the same subscription group? From Apple Docs:

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.

Subscriptions within a subscription group are mutually exclusive, meaning that users are only able to subscribe to one option within a group at a time. If you want users to be able to purchase more than one subscription at a time, you can put these in-app purchases in different subscription groups.

@sam961 @gerchicov-bp Apologies, haven't had more time to look into this.

The quickest and most flexible solution for now is to parse the receipt manually.

Since version 0.9.0, you can create a strong-typed ReceiptItem value from each receipt item.

Then, you can compare all your items with the receipt date and check if there is at least one non-expired one.

I've included some sample code from how I ended up implementing this. I'm not 100% sure if this is correct, but it seems to work for my mixture of non-consumables and autorenewing subscriptions.

typealias SubscriptionPurchaseSuccessClosure = () -> ()
typealias SubscriptionPurchaseFailureClosure = (_ errorMessage: String) -> ()

fileprivate typealias SubscriptionVerifyPurchaseClosure = (_ subscription: Subscription, _ result: SubscriptionVerifyResult) -> ()
fileprivate typealias SubscriptionVerifyRestoreResult = (subscription: Subscription?, verificationResult: SubscriptionVerifyResult)
fileprivate typealias SubscriptionVerifyRestoredPurchaseClosure = (SubscriptionVerifyRestoreResult) -> ()

/// Attempts to restore any past subscription purchases on this account
///
/// - Parameters:
///   - success: A closure to call if a subscription was restored successfully
///   - failure: A closure to call if no subscriptions were found or there was an error restoring it
func restoreSubscription(success: @escaping SubscriptionPurchaseSuccessClosure, failure: @escaping SubscriptionPurchaseFailureClosure) {
    // Note: this method just replays a history of any previous purchases, but does not
    // actually tell you if they are still active subscriptions. You have to manually
    // check that against a reduced set of the results
    SwiftyStoreKit.restorePurchases(atomically: true) { results in
        for purchase in results.restoredPurchases where purchase.needsFinishTransaction {
            // Deliver content from server if necessary, then:
            SwiftyStoreKit.finishTransaction(purchase.transaction)
        }

        if results.restoredPurchases.count > 0 {
            // Check if any prior purchases are still valid/active
            self._verify(restoredPurchases: results.restoredPurchases, completion: { [weak self] verifyRestoredResult in
                guard let strongSelf = self else { return }
                strongSelf._handle(verificationResult: verifyRestoredResult.verificationResult, for: verifyRestoredResult.subscription, success: success, failure: failure)
            })

            CBLog(namespace: self.namespace, "Restored prior purchases successfully; need to check if any are still active")
            return
        }

        if results.restoreFailedPurchases.count > 0 {
            failure(SubscriptionLocalizations.UnableToRestoreSubscription)

            CBLog(namespace: self.namespace, "Restore failed")
            return
        }

        failure(SubscriptionLocalizations.RestoreNoPreviousSubscription)
        CBLog(namespace: self.namespace, "No previous purchases to restore")
    }
}

// MARK: - Verifying Purchases

/// Note: this method accepts a replay / history of any previous purchases, and then
/// we have to enumerate a reduced set of them and call verify on each in order to
/// see if any of them are still active.
func _verify(restoredPurchases: [Purchase], completion: @escaping SubscriptionVerifyRestoredPurchaseClosure) {
    let identifiersSet = Set(restoredPurchases.map({ $0.productId }))

    // We store the last known verification state here
    var validSubscription: Subscription? = nil
    var lastState: SubscriptionVerifyResult? = nil

    for identifier in identifiersSet {
        guard let subscription = Subscription.from(identifier: identifier) else { return }

        self.dispatchGroup.enter()
        _verifyPurchase(of: subscription, completion: { validatedSubscription, verificationResult in
            lastState = verificationResult

            switch verificationResult {
            case .purchased:
                // Note: we use the validated subscription here to avoid any race conditions
                validSubscription = validatedSubscription
                self.dispatchGroup.leaveAll()
                break
            case .notPurchased, .expired, .error(_):
                self.dispatchGroup.leave()
            }
        })
    }

    // Notify whether or not we found a valid/active subscription
    dispatchGroup.notify {
        completion(SubscriptionVerifyRestoreResult(validSubscription, lastState ?? .notPurchased))
    }
}

func _handle(verificationResult: SubscriptionVerifyResult, for subscription: Subscription?, success: @escaping SubscriptionPurchaseSuccessClosure, failure: @escaping SubscriptionPurchaseFailureClosure) {
    switch verificationResult {
    case .purchased:
        if let subscription = subscription {
            self._storePurchase(of: subscription)
            self.hasActiveSubscription = true
            success()
        } else {
            failure(SubscriptionLocalizations.ErrorVerifyingAsPurchased)
            self.hasActiveSubscription = false
        }
    case .notPurchased:
        self.hasActiveSubscription = false
        failure(SubscriptionLocalizations.ErrorVerifyingAsPurchased)
    case .expired:
        self.hasActiveSubscription = false
        failure(SubscriptionLocalizations.PurchaseHasExpired)
    case .error(let error):
        self.hasActiveSubscription = false
        failure(String(format: SubscriptionLocalizations.ErrorVerifyingPurchaseFormat, error.localizedDescription))
    }
}

func _verifyPurchase(of subscription: Subscription, completion: @escaping SubscriptionVerifyPurchaseClosure) {
    if subscription.isAutoRenewingSubscription {
        _verifyAutoRenewingPurchase(of: subscription, completion: completion)
    } else if subscription.isNonConsumable {
        _verifyForeverPurchase(of: subscription, completion: completion)
    }
}

I'd also like to see an implementation of this in the library.

Currently, I'm using this change: https://github.com/felix-dumit/SwiftyStoreKit/commit/3c75f8ff5b790c8ea5c5751a32a0e4938ed5b27d

It's useful to handle subscriptions in the same family since they're mutually exclusive and it's nice to handle them all together as subscribed or not.

I guess it's related to #269 too.

@felix-dumit I implemented a new verifySubscriptions method based on your suggested change-set.

See #333. Available on pod version 0.12.0.

@gerchicov-bp this new method solves this:

  • [x] Verify subscription for multiple products at once

It does not solve this:

  • [ ] Verify subscription for all products at once

Is there a valid use case for this or can we close this issue?

@bizz84 Thanks for implementing it, looks good!

Was this page helpful?
0 / 5 - 0 ratings