Closed jonathankau closed 5 years ago
By default, the plugin doesn't auto finish transactions except if you set store.autoFinishTransactions
to true.
See the note about it: https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#random-tips
Are you sure you've not been using the autoFinishTransactions
option while in dev and forgot to disable it?
Sorry, I do not have much time to dig in more, so just checking this first could save some pain ;-)
@j3k0 Thanks for the quick response!
We actually have never touched the autoFinishTransactions
option. Looking at the implementation here https://github.com/j3k0/cordova-plugin-purchase/blob/master/src/ios/InAppPurchase.m#L459, the code seems to finish failed transactions when auto finish is off. Since auto finish is off by default, this seems to be the default behavior, right?
// Finish failed transactions, when autoFinish is off
if (!g_autoFinishEnabled) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self transactionFinished:transaction];
}
If the default behavior is to finish failed transactions, then that means that a transaction that sees SKPaymentTransactionStateFailed
in the case described will be finished, right?
Diving a little deeper into this post on the Apple forums (https://forums.developer.apple.com/thread/6431):
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.
Perhaps the issue is instead with the state machine that the plugin relies on? This seems to indicate that finishing failed transactions is still the right approach, but we need to be able to handle future SKPaymentTransactionStatePurchased
responses that may come in later.
If you look down to line 503 in the same function, failed transactions are always finished:
else if (g_autoFinishEnabled && canFinish) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self transactionFinished:transaction];
}
The if (!g_autoFinishTransaction)
in the code you mentioned (the case SKPaymentTransactionStateFailed:
) is here to prevents double finish (it could be refactored to be clearer I admit). So my question about you using this or not was irrelevant.
So, indeed SKPaymentTransactionStateFailed
will finish the transaction in all cases. However, I wonder if the SKPaymentTransactionStatePurchased
call that is subsequent contains the exact same transaction identifier or a new one. In the first case, I tried to dig for what kind of problem this could create but can't find anything: as far as I understand the code, the call to finish
doesn't trigger any kind of state changes (it's not even bubbled-up to the JS side). But it's easy to make sure of that; if you can reproduce the issue on a device that's connected to a mac, make sure you added store.verbosity = store.DEBUG
and run from XCode, all minor state changes will appear. You can copy-paste the logs here and I'll take a look.
I can try to guide you if you're willing to dig this up, I am pretty busy for a few weeks because I'm between a lot of deliveries and the start of new projects, but I'll try to respond as quickly as possible.
Note, realizing that JS side autoFinishTransactions
and Obj-C side's g_autoFinishTransactions
didn't have the same default at startup (but this was invisible as JS side will change Obj-C side at initialization). Anyway, to be consistent, I fixed this with fa40179.
However, I wonder if the SKPaymentTransactionStatePurchased call that is subsequent contains the exact same transaction identifier or a new one.
Great question. I'm not completely certain, but my guess from reading the Apple forums is that it uses a new transaction identifier. Off the top of your head, do you know how the plugin would handle might handle a subsequent SKPaymentTransactionStatePurchased
call? We log every product state change in production, and all we're seeing is that product gets cancelled
then it goes back to valid
. We never seem to get an additional approved
callback from the plugin.
All SKPaymentTransactionStatePurchased
calls will bubble up to the storekitPurchased
method that handles emitting approved
. https://github.com/j3k0/cordova-plugin-purchase/blob/fa40179236e32146e9026688ab1e7bd34f8768f2/src/js/platforms/ios-adapter.js#L323
Only 2 things can prevent approved
to be triggered.
store.register
)product.transactions
array, if the transaction is already in it we return from the function).I tried to replicate the behavior, but some of the past transactions/receipts on this iPhone seems to have made the logging confusing. I added some comments as to what I think happened though. Do you know if it's possible to clear out Apple's cache for debugging purposes? Will see if I can try again on a clean iPhone.
Oct 3 14:28:59 InAppPurchase[objc]: About to do IAP
Oct 3 14:28:59 InAppPurchase[objc]: Transaction updated: com.pennyapp.subscription_4
Oct 3 14:28:59 InAppPurchase[objc]: Purchasing...
Oct 3 14:28:59 InAppPurchase[objc]: State: PaymentTransactionStatePurchasing
Oct 3 14:29:25 InAppPurchase[objc]: Transaction updated: com.pennyapp.subscription_4
Oct 3 14:29:25 InAppPurchase[objc]: Error ERR_PAYMENT_CANCELLED - Cannot connect to iTunes Store
Oct 3 14:29:25 InAppPurchase[objc]: State: PaymentTransactionStateFailed
// At the same time that the transaction fails, Apple brings you out of your app and
// into iTunes to update your billing info. If you update your billing info successfully,
// then a confirming modal pops up, but you don't get sent back to your app automatically.
// Returned to the app, but nothing had changed/triggered.
// This seems to suggest that we don't even receive the subsequent state update to
// `PaymentTransactionStatePurchased`.
// Closed and reopened the app.
// These all seemed to happen upon our initial `store.refresh` at app open. I believe
// these are due to past subscription transactions on this device.
Oct 3 14:31:58 InAppPurchase[objc]: Transaction updated: com.pennyapp.subscription_4
Oct 3 14:31:58 InAppPurchase[objc]: State: PaymentTransactionStatePurchased
Oct 3 14:32:00 InAppPurchase[objc]: Transaction <A> finished.
Oct 3 14:32:06 InAppPurchase[objc]: Request to refresh app receipt
Oct 3 14:32:06 InAppPurchase[objc]: Starting receipt refresh request...
Oct 3 14:32:06 InAppPurchase[objc]: Receipt refresh request started
Oct 3 14:32:08 InAppPurchase[objc]: Transaction updated: com.pennyapp.subscription_5
Oct 3 14:32:08 InAppPurchase[objc]: State: PaymentTransactionStateRestored
Oct 3 14:32:08 InAppPurchase[objc]: Transaction updated: com.pennyapp.subscription_4
Oct 3 14:32:08 InAppPurchase[objc]: State: PaymentTransactionStateRestored
Oct 3 14:32:10 InAppPurchase[objc]: Transaction <B> finished.
Oct 3 14:32:10 InAppPurchase[objc]: Transaction <C> finished.
// Tried to subscribe again/restore the purchase, got the classic
// "You've already bought this..." modal.
Oct 3 14:32:18 InAppPurchase[objc]: About to do IAP
Oct 3 14:32:18 InAppPurchase[objc]: Transaction updated: com.pennyapp.subscription_4
Oct 3 14:32:18 InAppPurchase[objc]: Purchasing...
Oct 3 14:32:18 InAppPurchase[objc]: State: PaymentTransactionStatePurchasing
Oct 3 14:32:19 InAppPurchase[objc]: Transaction updated: com.pennyapp.subscription_4
Oct 3 14:32:19 InAppPurchase[objc]: State: PaymentTransactionStatePurchased
Oct 3 14:32:20 InAppPurchase[objc]: Transaction <D> finished.
// ^This behavior actually doesn't even happen with our users who encounter the same issue.
// In their cases, we always immediately get a 'Cancelled' event from the plugin along with the
// "You've already bought this..." modal.
// I believe the discrepancy is because this iPhone already had a prior receipt, so the new
// purchase updated the old one.
Oct 3 14:32:43 THREAD WARNING: ['InAppPurchase'] took '20.123047' ms. Plugin should use a background thread.
Oct 3 14:32:43 InAppPurchase[objc]: Getting products data
For the very purpose of cleaning up the transactions on a device you can set store. autoFinishTransactions = true;
at initialization (before the initial refresh) then launch the app. Wait a few minutes. Exit and uninstall the app. Then log out from iTunes in your app settings, create a new Test User in iTunesConnect...
Then you can start the app again (don't forget to disable autoFinishTransactions), login with the Test User from your app (compiled for debug, not release).
From the logs above, seems like the Obj-C side doesn't receive any event. I wonder if it's possible that the transaction is reported before the transaction observer is added.
deviceready
).Hello, would you like to try with the branch linked bellow? It implemented the above-mentionned fixes.
https://github.com/j3k0/cordova-plugin-purchase/tree/fix/ios-early-observer
It's roughly tested, seems alright.
@j3k0 Thanks for that. Unfortunately, I haven't figured out a way to reproduce this behavior while in debug. This is because when you test IAP's in sandbox mode, Apple doesn't actually check whether the sandbox user has a valid payment method attached.
It looks like it's possible to build an IPA for release and then install it via iTunes, so I might try that approach to test out the early observer branch you wrote. I'll also try and grab logs of this edge case for an iPhone that hasn't had any IAP's with our apps before.
@j3k0 Still haven't been able to figure out how to properly test a new iOS release build pointing at Apple's production IAP system. I'm not even sure if it's possible. Any ideas on that front?
The reason why I need to test using Apple's production IAP system is because reproducing the issue starts with removing all of your payment methods from your Apple ID account. In Sandbox, Apple just automatically approves all IAP purchases.
At the very least, after investigating some more, I believe you're on the right track with the early observer solution. First, I just want to walk you through some screenshots so that we're on the same page.
1) To start out, I remove my credit card from my Apple account via iTunes. After initiating a purchase, you get a modal to Confirm. One you confirm, this modal pops up saying you need to update your payment info.
2) After tapping on Continue, you get sent to the App Store to update your billing info.
3) Once you confirm your billing info, you get brought back to the homepage of the App Store. There, a new modal shows up to confirm your Apple account password.
4) After confirming your password, you get a "Purchase Successful" modal on the App Store page. Returning to your app does not trigger anything.
I believe you're on the right track with the early observer because of this issue I found - https://github.com/infinitered/ProMotion-iap/issues/17. It references Apple documentation that says the following:
StoreKit automatically notifies your observer when the content of the payment queue changes upon resuming or while running your app. Adding your app's observer at launch ensures that it will persist during all launches of your app, thus allowing your app to receive all the payment queue notifications.
When the app resumes, it has no observers as the above observer was deallocated when your app was sent to the background. As a result, your app would not get notified about the transaction in the queue.
My guess is that the observer is deallocated upon getting sent to the App Store. Thus, when you complete the transaction on the main page of the App Store, the app itself has no way of receiving the SKPaymentTransactionStatePurchased
event. Does that make any sense?
I think we've got the same issue with some auto-renewing subscriptions failing, even though the payment goes through to Apple we get no notification on the server (most of the time is all fine). Our customers say there was no error and the money is taken, but when they look the subscription status shows as Pending (that never changes). We have no log at all our side.
Any help much appreciated - and whatever I can do as well just let me know.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
This needs to be tested. I'd like to be sure this is fixed now.
No more reports for about 6 months, let's close.
It appears that when a user is prompted to update their App Store payment information before purchasing, a transaction will move to
SKPaymentTransactionStateFailed
before receivingSKPaymentTransactionStatePurchased
once their information is updated.The default behavior of this plugin is to automatically call
finishTransaction
as soon as it seesSKPaymentTransactionStateFailed
(https://github.com/j3k0/cordova-plugin-purchase/blob/master/src/ios/InAppPurchase.m#L458). I believe this is causing the plugin to treat these types of purchases as "Cancelled" even though they actually go through to Apple.I'm not too familiar with iOS native code development, but my guess is that the plugin should no longer automatically finish transactions that receive
SKPaymentTransactionStateFailed
. Instead, the plugin needs to be able to enter a waiting state in case it hitsSKPaymentTransactionStatePurchased
.Steps to Reproduce:
We have seen many of our users hit this flow and have verified with several people that had payment issues. The common thread was that they had to update their payment information. Here's a couple quotes from our users:
This causes the purchase to go through to Apple, but we never receive a receipt and the plugin indicates that the purchase was canceled.
Relevant links: