Users charged even though we get an error back

Hi,


Our app users are complaining they are being charged, even though we're getting back SKErrorPaymentCancelled reponse, so we assume the transaction failed and not giving them premium features.


Any reason why would that happen ? calling later restoreCompletedTransactions will almost always fix it.

Replies

Is this the so-called "StoreKit flow" where users get kicked over to the App Store app to do additional verification after they authenticate to StoreKit in your app? (credit card expired, need to enter two-factor code, etc.) In that case you will see two updated transactions, the first failed and the second successful, when the user switches back to your app.

Where can I read about that "StoreKit flow" ? I looked at the documentation, couldn't find a place the scneraio you describe is mentioned.


Plus, Any way to know to ingnore the first cancel ?

No unfortunately I don't know of any documentation and more importantly, I don't know of any way to actually test that scenario. It did happen to me once when purchasing my IAP on the live app with a real account and real money. I was a little surprised to see my "purchase failed" UI immediately followed by the "purchase succeeded" once I got back to the app, but luckily my code did happen to work (enabled the feature after the second transaction) in that situation. I say luckily because as you've noted, it doesn't seem to be mentioned in the docs or IAP testing suggested procedures.

For those interested in the StoreKit flow -

The “store kit flow”, is a process taken at the time of an attempt to purchase an "in app purchase" item such that the store kit determines that there is a problem with the user’s storekit account - for example, when the credit card information has expired. When this issue is detected, the user is given the option to be taken to the App Store app to update their account information.


It used to be that the StoreKit would alert the user and ask them to fix the issue in the App Store app, when the issue was detected. Beginning with iOS 7, this check won’t bother the user until an actual charge is attempted - either when an app is requested for download or when a user attempts an in app purchase.


When such a purchase is attempted (application makes the addPayment call and the user credentials are checked), and the store detects the account problem the StoreKit flow process occurs. The process begins with an initial purchase transaction result as a failure. It’s not clear that the user credit info or whatever issued caused the problem, will be fixed.


The transaction failure notification is delayed since the application is moved to the background as the App Store app is moved to the foreground. iOS stores the transaction failure to be passed to the app when it moves back to the foregound.


With the App Store application in the foreground, and the user updates their information then is presented with the option to continue with the purchase attempt - which many agree to. In this case, a successful purchase transaction is now sent to the app.


When the app moves to the foreground, the queued transaction results are passed to the updatedTransactions delegate method. For many apps, this means seeing first the queued SKPaymentTransactionStateFailed result, followed by a second SKPaymentTransactionStatePurchased result.


The proper way to handle this sequence of events is to make sure to call finishTransaction for the SKPaymentTransactionStateFailed state, then process the SKPaymentTransactionStatePurchased state as you normally would in the app. This might mean validating the receipt or downloading hosted content. Make sure that there are no other actions to be taken before the application responds to the successful transaction with the finishTransaction result.


Some observations to consider - don't assume that one call to addPayment will result in either a successful or failed transaction result. An app should never call removeTransactionObserver following a transaction result. Of course it's a best practice to make the addTransactionObserver call only once in the didFinishLaunchingWithOptions delegate method.


In some cases, the user may not relaunch the app until much later, when the app starts up as a new launch. In this case, it becomes especially important that the app make the addTransactionObserver call early on, so as to process the queued transaction result.


rich kubota - rkubota@apple.com

developer technical support CoreOS/Hardware/MFI

NOTE: this is needed not only for credit card expirations but also for 'Parental permission required' IAPs.

Rich wrote his 'best practice as follows:

"Some observations to consider - don't assume that one call to addPayment will result in either a successful or failed transaction result. An app should never call removeTransactionObserver following a transaction result. Of course it's a best practice to make the addTransactionObserver call only once in the didFinishLaunchingWithOptions delegate method.....In some cases, the user may not relaunch the app until much later, when the app starts up as a new launch. In this case, it becomes especially important that the app make the addTransactionObserver call early on, so as to process the queued transaction result."

Again, here is another approach that does not require permanent installation of IAP code in memory:

1) after a failed transaction wait a few seconds before terminating the IAP purchase code and calling removeTransactionObserver. This gives StoreKit a chance to send two transactions in rapid succsession

2) give the user a chance to "Get approved purchases". If they pick this then execute an addTransactionObserver and add a few second time-out if no transaction is found.

There are some complications which I've observed when the removeTransactionObserver call is used. First, lets review what the StoreKit guide says about the observers


"The transaction queue plays a central role in letting your app communicate with the App Store through the Store Kit framework. You add work to the queue that the App Store needs to act on, such as a payment request that needs to be processed. When the transaction’s state changes—for example, when a payment request succeeds—Store Kit calls the app’s transaction queue observer. Using an observer this way means your app doesn’t constantly poll the status of its active transactions. In addition to using the transaction queue for payment requests, your app also uses the transaction queue to download Apple-hosted content and to find out that subscriptions have been renewed.


Register a transaction queue observer when your app is launched, as shown in Listing 4-1. Make sure that the observer is ready to handle a transaction at any time, not just after you add a transaction to the queue. For example, consider the case of a user buying something in your app right before going into a tunnel. Your app isn’t able to deliver the purchased content because there’s no network connection. The next time your app is launched, Store Kit calls your transaction queue observer again and delivers the purchased content at that time. Similarly, if your app fails to mark a transaction as finished, Store Kit calls the observer every time your app is launched until the transaction is properly finished."


With an active observer, the delegate method will be called to process pending transactions. Removing the observer doesn't stop a transaction, but it does prevent the app from being notified that a transaction has taken place. The app could activate the transactionObserver at any time, but there's not timing criteria for knowing when StoreKit is going to respond. For this reason, StoreKit engineering finds it a best practice to only remove the transaction observer when the application is going away. I refer to

Tech Note 2387 - “Best Practices for in app purchases”

<https://developer.apple.com/library/ios/technotes/tn2387/_index.html#//apple_ref/doc/uid/DTS40014795>


There was previous mention as to an "endless loop" issue <https://forums.developer.apple.com/thread/6497>. I guessing that the problem is something like seeing the updatedTransactions delegate method called endlessly. It's understandable that calling removeTransactionObserver will stop the repeated calls to updatedTransactions, as the observer mechanism is now unregistered. However, this does remove the issue that StoreKit sees the need to notify the app of incomplete purchases. This is a bug report issue that needs to be investigated.


This raises the question - how to present this as a bug report. For now, I see this as an issue that requires more specific attention via a DTS incident. To investigate such issue, it's important to be able to replicate the problem. I digress on this matter.


That there is a need to call removeTransactionObserver - is something I'm happy to discuss further in this posting or others.


rich kubota - rkubota@apple.com

developer technical support CoreOS/Hardware/MFI

Regarding removeTransactionObserver, see my post elsewhere...

Hi Guys,


Have a conclusion here?


Some of our game player meet the same issue, the payment transaction failed on SKErrorPaymentCancelled, but it get charged with receipt.


How to figure out the cause or how to solve it?



Thanks very much

Perhaps the question is whether your code handles the situation where the device is sent a transaction that is 'SKErrorPaymentCancelled" and is then followed, sometimes immediately, sometimes a minute or so later, with a successful transaction. (This can happen for purchases that need parental permission or for purchases in which the current credit card has expired.) Your code should be able to handle that by giving the user the option of checking whether there are hanging transactions (e.g. 'tap here to check for approved purchases' - for purchases that need parental consent or if the user has just updated their credit card information) or by adding a transaction observer each time it restarts (Apple says this is 'best practice'). If your code responds to a failed transaction by shutting down its ability to receive a second successful transaction then the problem is in the code.

Is there a way to test that one's StoreKit integration can handle this scenario correctly? In other words, is it possible to fake this flow where the user is redirected to the App Store, corrects their payment info, T&Cs etc, continues with the purchase attempt resulting in a SKPaymentTransactionStateFailed followed by a second SKPaymentTransactionStatePurchased?

We have the same problem, too. 😕

Players are sending us even Apple Invoices screenshot from their email. And we sending user back to Apple Support for refund. 😟

The “store kit flow”, is a process taken at the time of an attempt to purchase an "in app purchase" item such that the store kit determines that there is a problem with the user’s storekit account - for example, when the credit card information has expired. When this issue is detected, the user is given the option to be taken to the App Store app to update their account information.


In this case we will have two different tranctions? First fails and second completes? Or it is only one transaction, why i have applicationUsername field empty in this case, i actualy fill it on initialization of transation?

You worte: "In this case we will have two different tranctions? First fails and second completes? Or it is only one transaction, why i have applicationUsername field empty in this case, i actualy fill it on initialization of transation?"


1) "two different transactions?" - YES

2) "First fails and second completes?" - YES if the user goes through with the updated credit card info and the purchase

3) "why i have applicationUsername..." - the applicationUsername is something you provide to the Apple server, for Apple's use only. Apple does not give it back to you.

Hi Rich,


I have a question about receiving SKPaymentTransactionStateFailed followed by SKPaymentTransactionStatePurchased.


As recommended, when receiving SKPaymentTransactionStateFailed, we finish the transaction. We also display an error to the user because the transaction failed and they should know there was an error (and we have no way to know that this "failed" will be followed by a "success").


However, the transaction then switches to SKPaymentTransactionStatePurchased and is purchased. But in the meantime, the user has seen as "Failed" alert error and will attempt to buy the item again, since they believe the first purchase didn't go through.


How are we supposed to handle the following:

- display a proper error message if there was an actual error

- not display an error message if the transaction fails and is then followed by a "StatePurchased" notification


?

For the most part, StoreKit implements it's own UI to inform the user as to what is happening. When the app sees the SKPaymentTransactionStateFailed result in the updatedTransactions delegate method, the user has already been alerted to a reason for the transaction "failure". In the case of the "Interrupted Purchase Flow" where the application can see first the SKPaymentTransactionStateFailed state, followed by the SKPaymentTransactionStatePurchased state, it's best if the application does not present any user interface. Instead, I recommend that the application use the NSLog method to log the result to the device console log.


Another confusing situation occurs when the application offers multiple auto-renewing subscription options in a group. There is no problem with the first attempt to purchase the auto-renewing subscription. However, if the user attempts to purchase a downgrade option, they are presented with the alert to the effect that the downgrade will take effect after the current "active" subscription ends. In this case, the application receives the SKPaymentTransactionStateFailed state at the updatedTransactions delegate method. In this case, the attempt to downgrade the auto-renewing subscription failed and SKPaymentTransactionStateFailed is the result. In some cases, this state also coincides with an error message regarding a failure to connect to the iTunes Store. Such failure should be sent to the console log. It would be confusing if presented to the user.


To access the console log, advise the user to connect their device to a macOS system with macOS 10.12 or newer present. Have them open the Console App, select the device, in the Devices section, then have them run the application and replicate the problem. When the problem happens, ask the user to save the log contents and send them to you for review.


rich kubota - rkubota@apple.com

developer technical support CoreOS/Hardware/MFI