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.
Related
I am developing an application in which user can purchase auto-renewable subscription. Buying part is woking alright but problem occurs when user deletes the application and tries to restore his purchases. following is the code I wrote to handle that.
I have given a button titled "Already a Subscriber". When user tap that I call following code.
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
And this is how I handle restore in - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions method.
case SKPaymentTransactionStateRestored:
duration = [strDuration intValue];
if(transaction.transactionReceipt != nil){
receipt = [[NSString alloc] initWithData:[b64 dataByBase64EncodeFromData:transaction.transactionReceipt] encoding:NSASCIIStringEncoding];
[userDefault setObject:transaction.transactionReceipt forKey:#"LastReceipt"];
[queue finishTransaction:transaction];
[self callReceiptInfoImpl];
}
break;
Following is delegate method when restore transaction is completed.
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
NSLog(#"COMPLETED TRANSACTION RESTORED");
}
Problem is that, when user clicks button "Already a subscriber" then Step 1. gets called but Step 2. never gets called. Finally I can see message "COMPLETED TRANSACTION RESTORED" on screen.
If anybody has faced similar problem then please guide.
Thanks for reading.....
Within your class, you have to implement the callback
function paymentQueue:updatedTransactions:
This function will receive updates on the transactions as and when
it’s made. Because your transactions take place even when the app is
closed, you should be ready to receive these notifications as soon as
you open the app. So the best place is to initialize it in
applicationDidFinishLaunching or equivalent method.
from this link
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.
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.
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];
}
I'm debugging restoring transactions and in my debug configuration everything works normally:
IE I call:
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
sometime later the queueCalls:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
and sometime after that it calls:
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
and everyone is happy.
BUT in my release configuration, I never see the call to updatedTransactions and so I never actually restore the purchases.
possibly related, after I attempt the restore and it doesn't work. I restart the application and I find that I don't get a response when I ask the store for a product list.
This error condition had nothing to do with the configuration. It is an intermittent bug in store kit.
Carl, as you said it seems to be an intermittent bug.
However, I have also found out that it doesn't happen (or at least I haven't seen it yet) if I test it using the US Store. I've been using a UK test user and today it was failing miserably every single time. Created a US test user, and, after being switched to the US store automatically, it works perfectly again.
It is not a fix, but it may be useful ;)
Do you add your payment object in the payment queue in this way?
SKPayment *payment = [SKPayment paymentWithProductIdentifier:"Your Product identifier"];
[[SKPaymentQueue defaultQueue] addPayment:payment];
If you are using this way,then the UpdatedTransactions is called right after adding the payment object in the payment queue.You dont have to call it explicitly.It is handeled by store kit.