Swiftystorekit: PaymentQueueController's shouldAddStorePayment method Implementation

Created on 3 Jul 2017  Β·  44Comments  Β·  Source: bizz84/SwiftyStoreKit

Platform

  • iOS

Version

β„Ή swift-4.0 branch

Report

Issue summary

β„Ή As of iOS 11, we have a new optional paymentQueue method that SwiftyStoreKit's PaymentQueueController can implement.

What did you expect to happen

β„Ή As far as I can tell, PaymentQueueController is an internal class (not accessible from outside) therefore there's no way to implement this in our app.

What happened instead

β„Ή I simply added this code inside SwiftyStoreKit/PaymentQueueController.swift:

swift @available(iOS 11.0, *) func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool { return true }

Question

Is there a better way to do this? What I did above works as long as I don't update the pods: then I will have to do it again (for each $ pod update).

purchase flows help wanted iOS & iPadOS enhancement

Most helpful comment

You must create a brand new new payment.

I'm not so sure about that. https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html states:

To defer a transaction:

  1. Save the payment to use when the app is ready. The payment already contains information about the product. Do not create a new SKPayment with the same product.

All 44 comments

@zntfdr thanks for reporting this.

Note that this method is optional:

@available(iOS 11.0, *)
optional public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool

From Apple Docs:

This delegate method is called when the user has started an in-app purchase in the App Store, and is continuing the transaction in your app. Specifically, if your app is already installed, the method is called automatically.
If your app is not yet installed when the user starts the in-app purchase in the App Store, the user gets a notification when the app installation is complete. This method is called when the user taps the notification. Otherwise, if the user opens the app manually, this method is called only if the app is opened soon after the purchase was started.

We need to verify that if this method is implemented and returns true, then this delegate method in the payment queue is called:

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])

If this is the case, SwiftyStoreKit will then handle this by calling the completion block of completeTransactions().

Questions

  • Should SwiftyStoreKit implement this method to always return true?
  • Should the resulting purchase be handled by completeTransactions()?
  • How can we test that this works correctly when the purchase is initiated in the App Store? Seems like this would be hard to test with the Sandbox.
  • Do we want to distinguish if a payment was started within the app or from the App Store? Potentially this could be done by adding a Purchase.isAppStoreInitiated value:
public struct Purchase {
    public let productId: String
    public let quantity: Int
    public let transaction: PaymentTransaction
    public let originalTransaction: PaymentTransaction?
    public let needsFinishTransaction: Bool
    public let isAppStoreInitiated: Bool
}

Hi Andrea,
thanks for the reply.

You're correct: it just calls the method you mentioned.

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])

Regarding the other questions:

  • Should SwiftyStoreKit implement this method to always return true?
    Absolutely not: the user might try to buy something that is not available to him already (for example because he doesn't have an account yet, or he hasn't reached the right game level etc).
  • Should the resulting purchase be handled by completeTransactions()?
    I believe this is the correct way (and it does this already automatically).
  • How can we test that this works correctly when the purchase is initiated in the App Store? Seems like this would be hard to test with the Sandbox.
    We simply open the following link in our device:
    itms-services://?action=purchaseIntent&bundleId=com.example.app&productIdentifier=product_name.
    I've already tested it and it works as expected.
    You can find more information on the WWDC17 session What's New in StoreKit.
    (slide number 165 shows the link above)
  • Do we want to distinguish if a payment was started within the app or from the App Store?
    I haven't checked but I believe it's not possible to know where the the purchase comes from.
    On the same matter: can we detect whether the user has actually purchased or redeemed any of our product? If the answer is no, then I believe the answer is no for this as well.
    Either way, I don't see an actual benefit in doing this.

Should SwiftyStoreKit implement this method to always return true?
Absolutely not: the user might try to buy something that is not available to him already (for example because he doesn't have an account yet, or he hasn't reached the right game level etc).

Sounds like the user should be able to register a closure to be called by SwiftyStoreKit when shouldAddStorePayment is called. Something like this would work:

var shouldAddStorePaymentHandler: (_ payment: SKPayment, _ product: SKProduct) -> Bool

I'd like to open this for contributions from the community. This is an outline that may be useful to get things started:

Summary of requirements

  1. Add a closure to SwiftyStoreKit. Sample implementation:
class SwiftyStoreKit {
     typealias ShouldAddStorePaymentHandler = (_ payment: SKPayment, _ product: SKProduct) -> Bool
     class var shouldAddStorePaymentHandler: ShouldAddStorePaymentHandler? {
        didSet {
            sharedInstance.paymentQueueController.shouldAddStorePaymentHandler = shouldAddStorePaymentHandler
        }
     }
}
  1. Implement shouldAddStorePayment method:
public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
    return shouldAddStorePaymentHandler?(payment, product) ?? false
}
  1. Unit tests
  2. Verify app compiles and runs on tvOS, macOS where this is not available
  3. Update README and CHANGELOG
  4. Pull request to swift-4.0

Was the swift-4.0 branch merged or transferred somewhere else? It appears to have been deleted from this repo and the SwiftyStoreKit pod build is currently failing on Xcode 9 / Swift 4.

I deleted it because the project now compiles fine on Xcode 9 beta 3. You can point your pod to develop until I publish a new pod version.

Hey, whats the expected time for this feature to be ready?

Also a concern I have after watching the WWDC video:

I think App Store IAP is looking if SKPaymentTransactionObserver is implemented in AppDelegate. If it is, its showing the IAPs that can be bought from the store, if not I think its hiding those IAPs. Of course I have no way of testing or confirming this at the moment but something to keep on mind.

http://prntscr.com/fxnfbx

Also a concern I have after watching the WWDC video:
I think App Store IAP is looking if SKPaymentTransactionObserver is implemented in AppDelegate.

Don't mind that, it's just for simplicity and demo's sake.
I'm already using this feature as I've described above, with the AppDelegate not being any kind of StoreKit delegate. πŸ˜‰

Hey, whats the expected time for this feature to be ready?

This is an open source project! πŸ˜€ If you'd like, please feel free to implement it and send a PR 😊

Thanks for the answer.

I'd love to contribute but I don't understand Apple's crappy StoreKit frameworks to implement them in anyway, thats why I'm using this great library πŸ˜„

@zntfdr @bizz84 Any updates on this?

Looks like no willing contributors are to be seen on this one. ;)

I’ll try to implement it this week.

@bizz84 Awesome! I would help but haven't really worked with StoreKit on a low level in a while. Especially because of this package. Thanks for this amazing package!!

This has now been implemented and is available on version 0.10.6.

I have not tested it on device - please let me know if it works ok for you.

Once this is confirmed, I will close the issue.

Great, thanks!
I will test it this evening: looking at the code everything seems all right :)

Follow up: works for me! πŸ’― πŸ’― Thanks πŸ€—

Awesome!

@bizz84 Is documentation going to be updated for this?

@fishcharlie You're right, I forgot! Will do tomorrow.

Done.

@bizz84 So is there a best practice for where to implement SwiftyStoreKit.shouldAddStorePaymentHandler? And how exactly does it handle it when someone goes to that link in Safari or tries to purchase from app store?

Hi @fishcharlie , please refer to the dedicated WWDC session to know everything about the new handler.

In short, the method is called whenever the user tries to buy an in-app purchase from the App Store (possible only from iOS 11).

If we return true to the method, then the purchase is handled exactly as if it was initialized inside the app (your storekit delegate methods will be called etc etc).

AFAIK the link in safari is used for testing purposes only (to imitate the in-app purchase from the App Store).

I hope this answer clears things up :)

@zntfdr And with this latest version of SwiftyStoreKit and that video will I be able to implement it all so users can subscribe to my auto-renewal subscription from the app store?

Yup πŸ˜„ .

The implementation requires very little effort from your side. Just update your pod and add something like
swift SwiftyStoreKit.shouldAddStorePaymentHandler = { (_ payment: SKPayment, _ product: SKProduct) in // check if user can purchase the product return true // or false if user shall not purchase the product yet }
in your codebase.

Cheers!

@zntfdr Ok, I'm just going to have to figure out how to actually handle unlocking everything and the process after going in my app.

If you already process in-app purchases inside your app, there's nothing else that you must do:
adding the 4 lines in my previous reply is all I had to do on my project (already tested).

Best of luck πŸ’ͺ πŸ’ͺ

@zntfdr But it's all linked to a button. The SwiftyStoreKit.purchaseProduct is all in a button tap. So I'm not sure how that gets linked up.

Charlie, the purchaseProduct is automatically called for you. The new method shouldAddStorePaymentHandler is just asking you the authorizathion to do so. Please do a couple of tests and you'll see πŸ‘

@zntfdr Is there a best practice for where to set SwiftyStoreKit.shouldAddStorePaymentHandler? Maybe in the didFinishLaunchingWithOptions? How did you handle that?

Hi Charlie!
Yes, this is something that you want to address/initialize ASAP in your app (a la completeTransactions).
Therefore, like you suggested, I'd implement it in the AppDelegate's didFinishLaunchingWithOptions method 😊

@zntfdr Awesome. Thanks so much. Trying it now. After looking into it more it seems easier than I thought it was. I'm honestly shocked at how easy it looks... kinda crazy

Haha, sure!
People at Apple are working hard to make things easy for us πŸ˜… (and thanks to @bizz84 for the easy implementation in SwiftyStoreKit πŸ’― πŸ’― )

Standing on the shoulders of giants... 🍏

Thanks for the addition of the new feature. In fact, I saw this message in the transcript

Well, what if the user is in the middle of onboarding? Or if they're creating an account? Or if they've already unlocked the item they're trying to buy? Well, in this case, you can hold onto the payment and return false. Then, when the user is done onboarding or whatever they needed to do, simply add the payment to the payment queue as you would with a normal in-app purchase.

To my understanding, I check whether Onboarding is done, and return false if the user is going through it. Do I need to add another code piece to keep the SKPayment object here and submit later?

@harryworld:
If you return true, a new acquisition/payment will be triggered automatically, no further actions are necessary from your code/side.

@zntfdr Thanks for quick reply, I know the implication for returning true. My question is about returning false, which is true in cases like Onboarding.

@harryworld:
I offer my apologies, I completely misunderstood! πŸ˜†

Yes, you can keep track of the user wish and trigger a new payment later (suggestion: show something to the user before launching a new purchase, maybe a prompt like "_do you still want to purchase ..??_").

Note: all this "keep track and launch new purchase later" has to be done by your code/app.

Cheers!

So my question is what kind of API should I use to reuse that SKPayment?

Yes, as described in the documentation, you must reuse the same SKPayment.

You must create a brand new new payment.

I'm not so sure about that. https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html states:

To defer a transaction:

  1. Save the payment to use when the app is ready. The payment already contains information about the product. Do not create a new SKPayment with the same product.

I stand corrected! Thanks (I've updated my previous comment)

πŸ‘‹ apologies in advance for my newbie questions, but I'm having a hard time figuring out how to accomplish this. Please see this code sample with inline questions:

// unable to get this link to work in safari, it just takes me to google?
// itms-services://?action=purchaseIntent&bundleId=com.example.app&productIdentifier=product_name

// where I am so far in my implementation:
import StoreKit
import SwiftyStoreKit

class AppDelegate: SKPaymentTransactionObserver {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // I think this is correct and working properly, but who knows! :)
    SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
            for purchase in purchases {
                if purchase.transaction.transactionState == .purchased || purchase.transaction.transactionState == .restored {
                    if purchase.needsFinishTransaction {
                        SwiftyStoreKit.finishTransaction(purchase.transaction)
                    }
                    handlePurchase(purchase.productId)
                }
            }
        }

    // if all IAPs would be supported (I only have one, anyway) you can just return true here?
    SwiftyStoreKit.shouldAddStorePaymentHandler = { payment, product in
            return true
    }
    }

    // ...and here? do I need both this and the above?
    func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
        return true
    }

    // do I need to worry about completing transactions here?
    // do I need to verify the receipt here? (I try via a shared function, but not sure if I have the receipt)
    // ... or can I see an auto renewing subscription status here, like the expiration date?
    // ...or can I just pass to my handlePurchase function if there's a local receipt?
    // is transaction.payment.productIdentifier == purchase.productId?
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            if transaction.transactionState == .purchased || transaction.transactionState == .restored {
                handlePurchase(transaction.payment.productIdentifier)
            }
        }
    }
}

// for normal IAPs done from within the app, and for completeTransactions, I do something like this:
func handlePurchase(_ productId: String) {
    let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: sharedSecret)
    SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
        if case .success(let receipt) = result {
            let purchaseResult = SwiftyStoreKit.verifySubscription(ofType: .autoRenewable, productId: productId, inReceipt: receipt)
            switch purchaseResult {
            case .purchased(let expiryDate, _):
                PurchaseHandler().setPaidExpiring(expiryDate)
            case .expired(let expiryDate, _):
                showError("Subscription expired \(expiryDate)", message: "Please renew your membership")
            case .notPurchased:
                showError("Receipt validation error. Please contact support", message: "Error code: notPurchased")
            }
        } else if case .error(let error) = result {
            showError("Receipt validation error. Please contact support", message: "\(error)")
        } else {
            showError("Receipt validation error. Please contact support", message: "No details available")
        }
    }
}

Apologies again for the series of questions, but I haven't been able to find any full examples and I'm terribly confused about this. Any help would be appreciated!

Hi,

When using the existing implementation I received this rejection from Apple review...

"We noticed that your app did not fully meet the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.

When the user initiates an in-app purchase on the App Store, they are taken into your app to continue the transaction.

However, information about the subscription must be displayed to the user prior to the purchase:
β€’ Title of publication or service
β€’ Length of subscription (time period and content or services provided during each subscription period)
Price of subscription, and price per unit if appropriate

Your app must include links to your privacy policy and Terms of Use, and users should be able to access these links at any time.

Next Steps

To resolve this issue, please revise your app to include the missing information prior to initiating any auto-renewing subscription purchases within your app."

Anyone been through this part of the process? It seems like they are asking me to show the same view controller I do before the user initiates a subscription in-app, which seems a bit daft if jumping in from an App Store request?

I think that to implement this I would need SwiftyStoreKit to call a function in my code instead of completing purchase, but I may be wrong? Am I misinterpreting their message?

Hi

It seems like they are asking me to show the same view controller I do before the user initiates a subscription in-app,l

This is correct. I also got rejected with the same message and this is what I had to do to get approved 😊

Sent from my iPhone

On 6 Jul 2019, at 22:52, Solublepeter notifications@github.com wrote:

Hi,

When using the existing implementation I received this rejection from Apple review...

"We noticed that your app did not fully meet the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.

When the user initiates an in-app purchase on the App Store, they are taken into your app to continue the transaction.

However, information about the subscription must be displayed to the user prior to the purchase:
β€’ Title of publication or service
β€’ Length of subscription (time period and content or services provided during each subscription period)
Price of subscription, and price per unit if appropriate

Your app must include links to your privacy policy and Terms of Use, and users should be able to access these links at any time.

Next Steps

To resolve this issue, please revise your app to include the missing information prior to initiating any auto-renewing subscription purchases within your app."

Anyone been through this part of the process? It seems like they are asking me to show the same view controller I do before the user initiates a subscription in-app, which seems a bit daft if jumping in from an App Store request?

I think that to implement this I would need SwiftyStoreKit to call a function in my code instead of completing purchase, but I may be wrong? Am I misinterpreting their message?

β€”
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

Thanks, that’s really helpful, can I ask how you approached it?

By which I mean- did you add your own handler for the system call, or modify your copy of SwiftStoreKit?

When I got this rejection, I didn't bothered doing it in the proper way.

This is my code:

SwiftyStoreKit.shouldAddStorePaymentHandler = { [weak self] _, _ in
  self?.showUpgrade()
  return false
}

In short, when the app receives a prompt to upgrade started from the App Store, I show the upgrade screen as if it was the user opening it from within the app.

Previously this was required as Apple wanted you to show all the subscription legalese stuff.
Now we don't need to show all of that any more, I'm assuming Apple wants your app to showcase the upgrade benefits from within the app before the user upgrades.

Hi
It seems like they are asking me to show the same view controller I do before the user initiates a subscription in-app,l
This is correct. I also got rejected with the same message and this is what I had to do to get approved 😊
[…](#227

--Can u explain brief. same view controller means which view subscription page or payment page?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

X901 picture X901  Β·  5Comments

Sam-Spencer picture Sam-Spencer  Β·  5Comments

jlaws picture jlaws  Β·  6Comments

astrokin picture astrokin  Β·  4Comments

cwinUser picture cwinUser  Β·  3Comments