The dreaded SKErrorDomain Code=2 (Payment cancelled) in 2/3 of all attempts
Bug Report
About 2/3 of attempts to purchase a renewable subscription in production are failing with error 2, payment cancelled. This is hard to reproduce in TestFlight/Sandbox. I realize this is both a common symptom and that the details I provide (below) are sketchy. We can't contact the users and ask them "What did you do?" I am collecting log information.
To Reproduce
These are the steps I am taking in code
1. SwiftyStoreKit.completeTransactions(atomically: true)
- Never reports anything
2. SwiftyStoreKit.retrieveProductsInfo(productIDs)
- Always returns all 3 of our IAPs
- Always returns before results of step 3 come in.
3. SwiftyStoreKit.verifyReceipt(using:validator, forceRefresh: false)
- Always returns not-purchased for all 3 of our IAPs
- This call is made concurrently with step 2 but always finishes after.
- Should I serialize by calling 3 in the completion handler of 2?
All three of these calls return (i.e. completion handlers run) before purchase attempt is made.
- To purchase, called 4. SwiftyStoreKit.purchaseProduct(productID) - Returns (via completion handler) Error Domain=SKErrorDomain Code=2 (Payment cancelled) Expected behavior Expect purchase to succeed more than 1/3 of the time.
Platform Information
- OS: dozens of different devices and OS versions
- Purchase Type: [auto-renewable subscription
- Environment: production
- SwiftyStoreKit version: 0.16.1
Thank you so much for this bug report! This is one of the most solid explanations I've seen of the strange behavior yet. Could you attempt to serialize the call to step 3 using step 2's completion handler and report back?
I assume some users may change their minds before completing subscription. @andrewzboard have you confirmed that there's a definite issue with this library preventing a user from subscribing and users are not just changing their minds?
Sam, I did try serializing the call, although it's a pretty sketchy thing, experimenting on live releases and real customers. Didn't seem to make any difference.
Josh, you are pretty much on target. What I've found is that if a customer is presented with an Apple ID sign-in dialog, and impatiently taps Cancel, then later on, when the purchase is attempted, the error in question is returned. So not only is the user-facing message ("Could not connect to store") misleading, so is the developer-facing one ("Payment canceled").
Now, this brings up another long-standing mystery: why do the Apple ID dialogs pop up so often? My code can't detect when this is happening, so I have no stats, but have personally seen this happen up to four times before the actual purchase is attempted. Bizz84 and others have (correctly) pointed out that in most cases, the user would have just downloaded the app from the store, and so must have recently signed in. So one would think that would persist for the amount of time (a few minutes) it takes to go through my onboarding process.
To make it more frustrating, we can't duplicate the error with alpha or beta testers on TestFlight builds. But our theory has evolved from "we're doing something wrong" to "the users are either not signing in, or are dismayed to find that they have to make a purchase -- albeit with a 7-day free period -- before going on to use the app"