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

Verify subscription for multiple/all products? #194

Open
4 tasks
gerchicov-bp opened this issue May 3, 2017 · 13 comments
Open
4 tasks

Verify subscription for multiple/all products? #194

gerchicov-bp opened this issue May 3, 2017 · 13 comments
Labels
area: receipt-validation validating receipts for customer or purchase verification help wanted status: needs analysis community analysis is needed type: enhancement

Comments

@gerchicov-bp
Copy link

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.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?

@bizz84
Copy link
Owner

bizz84 commented May 8, 2017

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.

@bizz84 bizz84 changed the title Verify subscription without id? Verify subscription for multiple/all products? May 8, 2017
@gerchicov-bp
Copy link
Author

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.

@bizz84
Copy link
Owner

bizz84 commented May 11, 2017

@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.

@gerchicov-bp
Copy link
Author

gerchicov-bp commented May 11, 2017

@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.

@bizz84
Copy link
Owner

bizz84 commented May 11, 2017

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

@sam961
Copy link

sam961 commented May 11, 2017

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

@gerchicov-bp
Copy link
Author

@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.

@bizz84 bizz84 added the status: needs analysis community analysis is needed label May 15, 2017
@bizz84
Copy link
Owner

bizz84 commented May 15, 2017

@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.

@bizz84 bizz84 added the area: receipt-validation validating receipts for customer or purchase verification label May 18, 2017
@bizz84
Copy link
Owner

bizz84 commented May 29, 2017

@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.

@iwasrobbed
Copy link

iwasrobbed commented Aug 5, 2017

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)
    }
}

@felix-dumit
Copy link

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

Currently, I'm using this change: felix-dumit@3c75f8f

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.

@bizz84
Copy link
Owner

bizz84 commented Dec 30, 2017

@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:

  • 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?

@felix-dumit
Copy link

@bizz84 Thanks for implementing it, looks good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: receipt-validation validating receipts for customer or purchase verification help wanted status: needs analysis community analysis is needed type: enhancement
Projects
None yet
Development

No branches or pull requests

5 participants