Handle cancelled IAP transactions - iphone

I'm using StoreKit for in-app purchases. I'm finding that the API is quirky in its behavior when the user presses the "Cancel" button.
For example, if I push Cancel on the "Confirm Your In App Purchase" screen, I get a SKPaymentTransactionStateFailed transaction with error.code == SKErrorPaymentCancelled as I'd expect.
But if I push Buy and then press Cancel, I get a Failed transaction with error.code == 0. The error.localizedDescription is "Cannot connect to iTunes Store" which is clearly a lie.
It's tempting to treat all Failed transactions as ignorable cancellations, but I can also clearly see that if the device is offline in airplane mode, I get a Failed transaction with no alert popup; I should really notify the user to explain the problem in that case.
I note that MKStoreKit assumes all failures are cancellations. MKStoreManager's failedTransaction method is never called; MKStoreObserver always calls transactionCanceled for all Failed transactions. The MKStoreManager.h comments recommend no error message for transactionCanceled, which makes sense, but then who will notify the user about Failed non-cancelled transactions?
What's the best practice for handling these failures? Should I swallow errors? Always show an error, even if it's redundant?

We have a pretty substantial user base buying stuff over mobile connections and only show alerts for
code != SKErrorPaymentCancelled && code != SKErrorPaymentNotAllowed
Apparently, it's the best you can do. I've also seen the weird behavior on cancellation that you mention, which is a framework bug as far as I can tell.

The previous answer is pretty close. MKStoreKit can automatically show error messages for valid error conditions like parental control turned on and such that.
Despite that, to handle purchase cancellations, I've also provided a delegate (starting from v3.5) called transactionCanceled in MKStoreKitDelegate.
Handle that and stop any activity spinners or progress hud on the view controller that makes the purchase call...

I just wanted to add that errors due to no internet connection should mostly be caught prior to any transaction using Apple's Reachability class IMO. This way you don't need to rely on Apple's API for a straight forward and common kind of error.

I think it's your responsibility and decision to where show the alert for cancelled transaction or not. But you should definitely finish it, otherwise it'll drop to Failed all the time. So should be something like this:
if (transaction.error.code == SKErrorPaymentCancelled) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
} else {
[self notifyError:transaction.error];
}
UPDATE:
Due to The Business of iPhone App Development: Making and Marketing Apps that Succeed we should finish transaction after any update to Failed state. Would be interesting to know if there are situations when we shouldn't.

Still have a one issue...
After clicking on Buy button it will show another Alert view and ask about Account information.
if i did cancel there then it will goes into case SKErrorUnknown:
then
i cant show message like this
"Your purchase could not be completed. Please check your network settings and try again later."
- (void) failedTransaction: (SKPaymentTransaction *)transaction
{
switch (transaction.error.code) {
case SKErrorUnknown:
NSLog(#"SKErrorUnknown");
break;
case SKErrorClientInvalid:
NSLog(#"SKErrorClientInvalid");
break;
case SKErrorPaymentCancelled:
NSLog(#"SKErrorPaymentCancelled");
break;
case SKErrorPaymentInvalid:
NSLog(#"SKErrorPaymentInvalid");
break;
case SKErrorPaymentNotAllowed:
NSLog(#"SKErrorPaymentNotAllowed");
break;
default:
NSLog(#"No Match Found for error");
break;
}
NSLog(#"transaction.error.code %#",[transaction.error description]);
if (transaction.error.code == SKErrorPaymentCancelled) {
[[MKStoreManager sharedManager] transactionCanceled:transaction];
} else {
[[MKStoreManager sharedManager] failedTransaction:transaction];
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

Related

How to customize the error handling when user choose not to purchase an item in iOS InApp Purchase?

For example, when user asked to login during InApp purchase, they can click on the "Cancel" button, then the app will shoe
Can't connect to the iTunes Store
Is it possible to use our own callback instead of this standard message?
I believe you don't get the alert with "Can't connect to the iTunes Store
" , I suspect you are showing alert view with the error in callback.
I have checked on the iOS 5 and 4.3. if you are NOT seeing this 4.3 and up I would not worry about that.
you can check if user cancelled the transaction with following code
Try following code inside restoreCompletedTransactionsFailedWithError
if (error.code == SKErrorPaymentCancelled || error.code == SKErrorPaymentNotAllowed){
NSLog(#"User Cancelled");
}
I know it looks little confusing but works great, it works even user canceled on Restore or New Purchase.
The system does not display any alert when a purchase is canceled, it's up to your application. See Step #10 in the documentation.
Basically, it goes like this :
The method paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions of your transactionObserver is called whenever a transaction begins, succeeds, or fails. If it fails, the -[SKPaymentTransaction error] will contain a regular NSError. You can then check its code and act accordingly.
The known codes are : (from <StoreKit/SKError.h>)
// error codes for the SKErrorDomain
enum {
SKErrorUnknown,
SKErrorClientInvalid, // client is not allowed to issue the request, etc.
SKErrorPaymentCancelled, // user cancelled the request, etc.
SKErrorPaymentInvalid, // purchase identifier was invalid, etc.
SKErrorPaymentNotAllowed // this device is not allowed to make the payment
};
You probably want to check at least for SKErrorPaymentCancelled and SKErrorPaymentNotAllowed.
Another interesting point is that the NSError contains a localizedDescription which you can display in an UIAlertView. That's where the "Can't connect to the iTunes Store" text probably comes.
(Also, possibly related : the Sandbox store used to test In-App purchase is flakey, which may explain the error.)
While I was testing I would not get that message when canceling the login, but only messages generated by my code:
Are you sure you are not triggering the message within the SKPaymentQueue Callback method paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions for transaction state SKPaymentTransactionStateFailed?
In case of canceled transaction, the transaction.error.code is set to SKErrorPaymentCancelled. In such cases one can omit any error pop ups or show their own.

iPhone inApp Purchase Queue won't clear out

I have InApp purchasing setup in my app. I am having some weird behavior though. Each time I start up the app I call
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
to setup the initial observer. However this immediately triggers
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
with a full array of every transaction. I have tried just calling
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
on each of these transaction then restarting the app again but paymentQueue is still trigger as soon as I call addTransactionObserver. My main goal right now is just to flush the transaction queue and start clean. I don't know how I got into this state, nor how to get out of it.
Make sure you have implemented this method:
-(void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions;
From SKPaymentTransactionObserver:PaymentQueue.. call:
SKPaymentQueue.default().finishTransaction(transaction)
Note that you can not call for all types. Calling finishTransaction for .purchasing will crash with error. So a for through all transactions is not a complete solution.
When you Call finishTransaction for .purchasing:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot finish a purchasing transaction'
Put this somewhere to flush the queue (possibly your load method, but remove it in your final app):
for (SKPaymentTransaction* transaction in [[SKPaymentQueue defaultQueue] transactions]) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
When finishTransaction is called, the observer's removeTransaction method will be called when each one is moved.
I had 30 in my queue and I ended up getting 30 calls to that method.
I think your issue has to do with the Sandbox User accounts in iTunes Connect. If you don't have any sandbox users, this guide was really helpful getting them setup:
https://support.magplus.com/hc/en-us/articles/203809008-iOS-How-to-Test-In-App-Purchases-in-Your-App
If you have Sandbox Users:
You can try calling finishTransaction: however, you need to make sure the SKPaymentTransactionState of the transaction is acceptable.
Per the documentation on finishTransaction :
// Asynchronous.
// Remove a finished (i.e. failed or completed) transaction from the queue.
// Attempting to finish a purchasing transaction will throw an exception.
So to safely remove completed transactions:
for transaction in SKPaymentQueue.default().transactions {
guard
transaction.transactionState != .purchasing,
transaction.transactionState != .deferred
else {
//Optionally provide user feedback for pending or processing transactions
return
}
//Transaction can now be safely finished
SKPaymentQueue.default().finishTransaction(transaction)
}
The documentation on .purchasing and .deferred is fairly vague:
case purchasing // Transaction is being added to the server queue.
case deferred // The transaction is in the queue, but its final status is pending external action.
From what I understand, handling pending and/or processing transactions should be fairly passive. The app has done everything it has needed to and is waiting on a response from the iTunes Store server, or some other dependency(i.e. payment authorization).
paymentQueue: updatedTransactions: will get called on the queue's SKPaymentTransactionObserver when the transaction is updated.
As far as how your transaction queue got stuck in limbo, I'm willing to bet all the transactions in your queue are in state .purchasing. This is most likely a bug within iTunes Connect/Sandbox users/Production iTunes Accounts. Others, including myself, have had this issue as well. There is a bug report filed for it. Try recreating/changing the password of your sandbox user, or create a new Sandbox User for testing.
More info here:
https://forums.developer.apple.com/thread/70418
You have to be sure you finish the transaction every time you make a purchase or you restore a product. If you run into this problem, you should clean up your queue and then develop the logic appropriately. A quick clean up could be obtained by running something like this in swift 3. (which is the same as the previous answer). But it is not supposed to be in your real app.
func cleanUp() {
for transaction in SKPaymentQueue.default().transactions {
SKPaymentQueue.default().finishTransaction(transaction)
}
}
Also you have to add and remove your observer in appDelegate. That is the recommendation and the best to avoid problems.

Can't successfully complete In App purchase transactions on next launch

For my unfinished transactions due to interruptions(incoming call etc.), I am getting updates when I relaunch the application as I am adding an observer to the payment queue. But how do I know which is the transaction that's being sent down the wire. I have tried this
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchasing:
//Show a loading View
NSLog(#"Purchase Ongoing");
break;
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
}
}
-(void) completeTransaction:(SKPaymentTransaction *) transaction
{
NSString *productIdentifier = transaction.originalTransaction.payment.productIdentifier;
NSLog (productIdentifier);
//this line doesn't log anything. I run it in debug mode and I see productIdentifier as InvalidCFString (0x0). I guess it's nill.
}
Is this because I couldn't finish the transaction properly before my app got killed. After killing, I do get the message that your In-App purchase was successful from Apple. It's just that I couldn't finish the transaction before my app got killed. The transaction was ongoing at that time. Trying to force finish the transaction before the app getting killed crashes the application saying "Can't finish an ongoing transaction" as I had not yet received the SKPaymentTransactionStatePurchased notification.
Now when I relaunch, I get some response but I am not able to read it and unlock the functionality which I must as the amount has already been debited to the user's itunes account. How to tackle this?
Thanks in advance.

restoreCompletedTransactions broken?

Is restoreCompletedTransactions broken in SDK 4.3 ?
I am trying to restore my auto-renewable subscription. It is not resulting in callback to updatedTransactions. Here is my code.
{
....
[appDelegate.inapp loadStore];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
....
}
Expecting callback to updatedTransactions, but do not receive it.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
NSLog(#"IN updatedTransactions, transaction.transactionState");
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
...
...
case SKPaymentTransactionStateRestored:
NSLog(#"IN updatedTransactions, SKPaymentTransactionStateRestored");
[self restoreTransaction:transaction];
break;
}
}
}
But I do receive call to this at the end.
-(void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
}
This can still(1) happen if you're testing in the Sandbox environment and your Test User is broken. Create a new Test User in iTunes Connect, and try again.
It's not entirely clear what makes Test Users go bad; from what I gather, using them once in a non-sandbox environment can do this, but there may be other reasons as well.
(1) Xcode 4.3.1, iOS SDK 5.1
Make sure to add observer before using [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
Your code should be something like:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
I was able to get around this but its very odd. On my testing device im getting bugged by iTunes to log in to multiple test user accounts. I usually just hit cancel and test my restore with the account that I want to restore. This time I decided to enter the password for these other annoying login boxes. It worked and next time I relaunched my app and hit restore it did not bug me to log in to the other test accounts, just the one I was testing. I proceeded with the restore and it did restore the items I wanted to see.
Id say were experinecing stupid sandbox issues and also we know apple has changed in app purchasing in general. Keep playing around with it, you'll get it to work out.
Hope this helps you out.
Products Consumable cannot be restored.
Products Non-Consumable can be restored.
Check if your products are type Non-Consumable.
You should not need the updatedTransactions callback if you are getting paymentQueueRestoreCompletedTransactionsFinished. The "queue" has a list of your transactions and you can loop thru those.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
for (SKPaymentTransaction *transaction in queue.transactions)
if ([myItem.productID isEqualToString:transaction.payment.productIdentifier])
myItem.purchased = YES;
}
Make sure your build number in Xcode does not have a space in it. For example "1.0 Beta". With a space updatedTransactions will not be called. Also receipt verification can fail.
Two additional points which weren't mentioned here:
If you are using Firebase make sure you add an observer before you initialize Firebase, https://firebase.google.com/docs/analytics/get-started?platform=ios
Make sure that you call finishTransaction method while handling the purchase. You won't be able to restore not completed transactions.

iPhone consumable product is behaving like a non-consumable product (already purchased...)

Our app has a list of locked products that share the same consumable product id (i.e. one consumable product id for many products). Our server provides me with a list of products and the product id associated with them:
item name="itemA" iphoneProductId="consumable.test.1"
item name="itemB" iphoneProductId="consumable.test.1"
item name="itemC" iphoneProductId="consumable.test.1"
We chose consumable because our items are created dynamically and need to be available to the user instantly (please don't reply suggesting that we use non-consumable, there are a lot of other reasons that are too hard to explain without me giving away private details about the company we are working with, as to why we are using consumable). This allows us to have multiple products share the same price.
When the user purchases itemA (for example), the item is unlocked. However, sometimes, when the user then tries to be itemB, Apple return with 'You have already purchased this but it hasn't been downloaded. Tap OK to download it now’. This should surely never happen for a consumable item. I know our system is quite complex but as far as the apple store kit is concerned, are simply just buying the same product again.
Could this just be a sandbox issue? We can't test in live as the app isn't released yet. In fact, this whole problem is holding off the release as our client is as concerned as we are about this problem.
I've followed the same code from the iphone documentation and the few in app purchase tutorials out there. I see that a lot of people on the forums seem to have witnessed the 'already purchased' dialog above for consumable products, but none of them ever get answered.
Please help! Thanks
The problem is that you are never finishing the transaction. You need to remove it from the queue.
Like:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
I'll assume you've called -[SKPaymentQueue finishTransaction:].
Off the top of my head, there are a few possibilities:
The store hasn't received the "finish" message yet (I don't know what ordering guarantees there are).
You're trying to purchase a second item before calling finishTransaction: on the first, and the App Store thinks it's a replay.
The App Store detects when you appear to be purchasing the same item repeatedly, and assumes that something must have gone wrong.
A hack is to cycle through a list of several (10? 100?) consumables, assuming that when you get back to one earlier in the list, it will have finished processing.
An alternative solution is to pre-allocate a lot of non-consumables ("product.1", "product.2", ..., "product.100") and assign ones with the appropriate price to products on the server. Changing prices can then be done on iTunes Connect, or by assigning additional product IDs as necessary.
Thanks for your quick response, but my app does seem to be finishing the transaction, etc.
My project's in app purchase classes have become quite complex so I reverted to creating a new basic project with a test consumable product and the standard implementation of in app purchase manager/observer taken from this open source:
http://blog.mugunthkumar.com/coding/iphone-tutorial-%E2%80%93-in-app-purchases/#idc-cover
The same problem occurs. This is the order:
1. Buy the consumable product for the first time (below is the debug I printed out)
_MKStoreManager: buyFeature:test.consumable.1
__MKStoreObserver: SKPaymentTransactionStatePurchased
__MKStoreObserver: completeTransaction
_MKStoreManager: provideContent
2. "Thank you for your purchase" dialog is displayed by Apple
3. Buy the consumable product for the second time:
_MKStoreManager: buyFeature:test.consumable.1
__MKStoreObserver: SKPaymentTransactionStatePurchased
__MKStoreObserver: completeTransaction
_MKStoreManager: provideContent
__MKStoreObserver: SKPaymentTransactionStateFailed
__MKStoreObserver: failedTransaction
4. "You've already purchased this but it hasn't been downloaded. Tap OK to download it now. [Environment: Sandbox]" dialog is displayed by apple.
It just doesn't make sense in step 3 that the transaction is both purchased then failed at the same time. Do you have any ideas.
It is not worth using consumable product for non-cosumable, because Apple will reject it. Here is what happened to me:
I have had the same issue. The message explaining that you have already purchased the item appears only once in a while and mostly if you purchase a lot of things - one after the other immeadiately.
We have put it for review in the App Store anyway and got the following answer:
.....
We have completed the review of your in-app purchase but cannot post it to the App Store because the Purchasability Type is not set correctly. For information on Purchasing and Currency guidelines, please see section 11 of the App Store Review Guidelines [ https://developer.apple.com/appstore/resources/approval/guidelines.html ].
.....
The purchase of a [magazine issue] is set to "consumable", however based on product functionality it should be set as non-consumable instead.
.........
You are required to create a new in-app purchase product with the correct purchasability type.
..........
I hope this saves someone time and headaches.
I was facing the same problems. The mistake I did was deallocating the purchase before the finishTransaction was sent. Make sure you handle the transaction result after finishing the transaction
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
BOOL success = YES;
for (SKPaymentTransaction *transaction in transactions){
switch (transaction.transactionState){
case SKPaymentTransactionStatePurchased:
success = YES;
break;
case SKPaymentTransactionStateFailed:
if (transaction.error.code == SKErrorPaymentCancelled){
if(DEBUG) NSLog(#"Transaction failed => Payment cancelled.");
}else if (transaction.error.code == SKErrorPaymentInvalid){
if(DEBUG) NSLog(#"Transaction failed => Payment invalid.");
}else if (transaction.error.code == SKErrorPaymentNotAllowed){
if(DEBUG) NSLog(#"Transaction failed => Payment not allowed.");
}else if (transaction.error.code == SKErrorClientInvalid){
if(DEBUG) NSLog(#"Transaction failed => client invalid.");
}else if (transaction.error.code == SKErrorUnknown){
if(DEBUG) NSLog(#"Transaction failed => unknown error.");
}else{
if(DEBUG) NSLog(#"I have no idea.");
}
success = NO;
break;
case SKPaymentTransactionStateRestored:
success = YES;
break;
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
NSLog(#"transaction finished: %#", transaction);
}
if(success){
// do something
}
}
Hope that helps some of you.
Cheers,
K.
You should put this line [[SKPaymentQueue defaultQueue] finishTransaction:transaction] after the transaction over whether you are doing fresh purchase or restore purchase.
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
make sure you have added this method on the controller handling requests.