The iOS purchase receipt server verification usually works like this:
User purchase on iPhone
Your app sent the purchase recipt to server
Server receive the recipt and send it to Apple to verify
Server gets the verify results from Apple .
Server sents the verify result to app
BUT what if only Step 1 is successful? For example, The app can't send request to the server in Step 2 or app can't get response from server in Step 5.
The problem is user already paid. What is the best way to handle this problem?
If you are using SKPaymentQueue, then it's easy. All you have to do is to keep the transaction in SKPaymentQueue until 'step 5' when you get a success/failure verify result from your server.
If anything goes wrong in between step 1 to 5 your app still has access to the transaction in the SKPaymentQueue and can 'reprocess' it.The reprocessing of incomplete transactions could kick in at start of your app (or some time interval as you prefer). Just check SKPaymentQueue to get the pending/incomplete transactions and send them to your server (just like 'step 2'). If your server is still not accessible obviously you won't get to step 5, therefore you don't remove the transaction from the queue and this reprocessing happens again and again every time at the next app start (or next queue check time interval) until is fulfilled.
Implementation
The implementation is also easy, you need to have a 'transaction observer class' of SKPaymentTransactionObserver.
At app start create an instance of the 'transaction observer class' and that should register itself by call to:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self]
Then 'transaction observer class' gets the transactions in method:
(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
This method is where you can handle and reprocess all incomplete transactions.
Note that your server must be idempotent (i.e able to handle repeated transactions if they already been processed)
Once the server processes and completes steps 2 to 4, then it comes to app with success/failure result and that's the only time when you want to remove that transaction from the queue by call to:
[[SKPaymentQueue defaultQueue] finishTransaction: transaction]
And finally give your user the premium feature they purchased at this point.
Related
I am working on a mobile app using Ionic (with Cordova purchase plugin) but this question is more general. We are using In App Purchases (IAP) and are currently getting an error when we try to finish the consumable purchase. Our current flow is like this:
Get list of our Products from Apple and render on our IAP page
User clicks consumable IAP they want and it fire off a message to StoreKit initiating the purchase
We get a response with a consumable IAP object with the state set to approved.
We initiate the verification procedure with a callback to our own server where we hit apple up to verify the purchase and then log it on our database and send the app a 200 response (not sure if we need to send back the IAP object here with receipt from our server or we just work with the one already inside the app?)
We try finish the purchase where we get an error saying (InAppPurchase[objc]: Cannot finish transaction)
My question is assuming this is the correct flow what does the finish method do? Looking in the source code of the Cordova Purchase Plugin wrapper I can see it sets the state of the object to finished but I am assuming (I couldn't find the code where this happens) it also talks to Apple so that Apple marks the purchase as finished on their side? If we manually set the state to finished the IAP error goes away but the consumable can still not be purchased multiple times which means to me that Apple also need to close it. Is this a correct assumption? Any other tips to getting this to work would also be appreciated.
I'm not sure how cordova handles IAP, but on the apple side, the transaction needs to be finished by calling finishTransaction. It would appear thats the step thats not working.
One thing that might happen due to the delay in going to the server to validate is that the original transaction object has expired, and calling finishTransaction with it does nothing. At this point you might be able to search for your transaction in: [[SKPaymentQueue defaultQueue] transactions]
If you can grab it from there then call finishTransaction then it should work. Not sure how you do this with cordova but I hope this helps.
I have been testing In App Purchases and can only assume that there are transactions stuck in the queue.
I have called [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; but every time I start the app it asks me to login to iTunes.
I have checked within -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{ but even if I place an NSLog in the first line, this is NOT being called. Thus I don't believe this is being called.
I have also called NSLog(#"PAYMENTQUEUE:%#",[[SKPaymentQueue defaultQueue] transactions]); which shows null.
Can someone advise why I would constantly be asked to login to iTunes every time I start the app all of a sudden? As I say, I can only assume it is a transaction, but how can I track it down?
Your device has been infected with the "endless loop" for failing to have called finishTransaction. It will reoccur every week like clockwork. Check out the IAP Developer's Forum for more info. The queue is empty because you need to log in as the 'infected' user.
Just in case anyone comes across this later on with the same issue. I left the device and stopped trying to sort it out. I deleted the app too.
Around half an hour later I was prompted to logon to the iTunes sandbox store (remember there was no app on anymore).
I logged in, and this scenario occurred another time.
After that all seemed to go quiet.
I have tried installing the app again today (12 hours later) and the issue has gone away.
I can only assume something got jammed up?
I'm developing an ios Application with IAP feature. It works well. But I encountered a strange issue today. It always show me a message to ask me to "Sign In to iTunes Store" for some reasons. Here is the screenshot:
It always shows this every time when I start the application or resume from background. It even still shows this after I delete and reinstall the application. When I setup breakpoints in my source code. There is no any transaction,payment delegate callbacks. Can anybody tell me what the reason is ? Could it be the problem of Apple IAP Sandbox? (I run the same application in other devices without any problem. I can purchase, restore in Sandbox.)
I've had this problem too a couple of times, it turned out I was stopping processing of transactions before calling finishTransaction on all queued transactions. I was forgetting to finish failed transactions in the particular case.
So, you could try letting your application run once and call finishTransaction for each and every one in your observer delegate like this:
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
foreach(SKPaymentTransaction *tr in transactions) [queue finishTransaction:tr];
}
i.e. finish every transaction irrespective of transaction state.
After the test run revert to your original handling code.
Hope this helps.
I tried multiple things :
call finishTransaction for all queued transactions as explained by mpramat (in my case, the array was empty)
Reboot the iDevice
Delete the test account in iTunes Connect.
Finally, the only thing that surprisingly worked for me was to delete the app in the iDevice then re-install it via XCode.
I'm simulating purchases of an Auto-renewing subscription in my app on an iPhone. My issue is that the purchase is considered to be done by the App store while it is not.
Here is what is going on:
The user presses a button to purchase the renewing subscription
The user gives his iTunes password and confirms the purchase
The app submits the receipt received from the app store to my server to check validity
The server returns a "ok" or "not ok" string. The app calls finishTransaction only on "ok"
I have an issue when there is a network failure on step 3. I can't validate the receipt. But if the user tries to purchase a second time, the app store tells him that he has already subscribed, even though I didn't call the finishTransaction method to complete the purchase!
Is this an expected behavior? Shouldn't the app-store treat non-finished transactions as non-finished, or am I missing something?
I would welcome any suggestion to solve this issue.
-(void) userPurchase:(SKProduct*) product{
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
NSLog(#"paymentQueue updatedTransaction");
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
break;
case SKPaymentTransactionStatePurchased:
[self recordSubscription:transaction];
break;
case SKPaymentTransactionStateFailed:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self recordSubscription:transaction];
break;
default: NSLog(#"Default");
break;
}
};
}
-(void) recordSubscription:(SKPaymentTransaction*)transaction{
NSString *jsonObjectString = [self encode:(uint8_t *)transaction.transactionReceipt.bytes length:transaction.transactionReceipt.length];
NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithObjectsAndKeys:jsonObjectString,#"receiptdata", nil];
[[AFNetworkSubClass sharedClient] postPath:#"myserver" params:params
success:^(AFHTTPRequestOperation *operation, id output) {
/* some code */
if([valstring isEqualToString:#"ok"]){
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"validation failed");
}
I think before you call recordSubscription method, you can call finish finishTransaction method to remove payment from payment queue because you have finished the purchase action with apple server.
If network failure make you can't validate app receipt, just record the receipt and validate the receipt to your own server again or validate when network is reachable another time.
I hope it can help you.
Users cannot purchase same subscription until it expires. In your case, the purchasing transaction was finished, but you didn't provide the service because of the network failure.
I used to have similar problem. You may need to ask the users to restore all completed transactions.
Then completeTransaction function will be called. Good luck!
If you haven't called finishTransaction that doesn't meant the transaction has not taken money from the user. All that call does is prevent StoreKit from notifying your App on the transaction every launch. Its just a means for you to tell StoreKit that you have finished with the transaction i.e. unlocked the users content and saved that to your backend etc.
So even with a network error you should be retrying your API call. If you close the App and reopen you should get a transaction update to indicate it has been 'purchased' giving you time to submit to your server again:)
This is the expected behaviour otherwise you would be double/triple billing users.
Just a heads up if your App doesn't fit within the following statement then you will not be able to use Auto-renewing Subscriptions. The following is from the App Review Guidelines;
11.15 Apps may only use auto renewing subscriptions for periodicals (newspapers, magazines), business Apps (enterprise, productivity,
professional creative, cloud storage) and media Apps (video, audio,
voice), or the App will be rejected
In case your app does fit into this bracket then what you could do setup your app so it considers itself to be temporarily "Subscribed" and it keeps trying to authenticate with the server (notify the user if it has been too long since it was connected to the internet).
I used to have issues like this with both the app store purchases and the game center. And I know this is going to sound like a non answer, but it worked for me.
I deleted the app and all its data from my device, restarted xcode and cleaned the project. Turned the device on and setup a fresh deployment. And the app started to receive the correct response from the App Store. I'm not sure if it was because something was cached on the device or if the App Store just needed more time to work correctly but things just fell into place after that.
Although I have never worked with subscription based purchases, nothing in your code stands out as incorrect as far as I can tell.
Good luck.
I have an issue with a live app where incomplete purchases are being mishandled. I am trying to test my new code to make sure that this will be taken care of, so I download the live app, cause the problem, then load my development app (or Ad-Hoc app) hoping that the StoreKit Observer will catch the incomplete purchase notification. No matter how I do this (development or Ad-Hoc) the observer does not fire a notification.
My general question is: How can I simulate incomplete and interrupted purchases in the App Store testing environment?
My more specific question is: Can I simulate the specific issue where the user must leave the app to confirm their current credit card pin number on the app store?
According to this helpful page:
Test an Interrupted Transaction
Set a breakpoint in your transaction queue observer’s
paymentQueue:updatedTransactions: method so you can control whether it
delivers the product. Then make a purchase as usual in the test
environment, and use the breakpoint to temporarily ignore the
transaction—for example, by returning from the method immediately
using the thread return command in LLDB. Terminate and relaunch your
app. Store Kit calls the paymentQueue:updatedTransactions: method
again shortly after launch; this time, let your app respond normally.
Verify that your app correctly delivers the product and completes the
transaction.
Hope this helps someone.
To your general question:
SKPaymentTransaction provides several transaction states like SKPaymentTransactionStateFailed
According to the Documentation you can check out the error property to see what happened.
For example you can check it in -(void)paymentQueue:(SKPaymentQueue *)updatedTransactions:(NSArray *)transactions callback like so
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState)
{
case SKPaymentTransactionStateFailed:
...
break;
default:
break;
}
};
}
Hope this helps.