Some times I get this callback
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
Even before I receive this
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
I don't even get to say finishTransaction:. I find this superweird. I've all this time been assuming paymentQueueRestoreCompletedTransactionsFinished: wouldn't be triggered before I had marked every unfinished transaction as finished.
Is this expected behavior?
This is what I found in documentation
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue
*)queue Description
Tells the observer that the payment queue has finished sending restored transactions. This method is called after
all restorable transactions have been processed by the payment queue.
Your application is not required to do anything in this method.
I'm not sure what it means.
I found the header file itself to be most clear on this part.
#interface SKPaymentQueue : NSObject
...
// Array of unfinished SKPaymentTransactions. Only valid while the queue has observers. Updated asynchronously.
#property(nonatomic, readonly) NSArray *transactions;
#end
#protocol SKPaymentTransactionObserver
// Sent when all transactions from the user's purchase history have successfully been added back to the queue.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
...
#end
These two in conjunction make it unambiguous.
paymentQueueRestoreCompletedTransactionsFinished is triggered once StoreKit has added all transactions to SKPaymentQueue. -[SKPaymentQueue transactions] only has a list of unfinished transactions.
I think the docs could have been more clear about this.
Related
I'm struggling with if I need to add a Transaction observer and also if I need to remove the transaction observer and where and what all this is ...
My inherited code includes the app delegate w/ an observer...
AppDelegate ->
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
SKPaymentQueue *paymentQueue = [self.injector getInstance:[SKPaymentQueue class]];
[paymentQueue addTransactionObserver:self.purchaseHelper];
}
and I'm working on the restore part of the purchase helper...
PurchaseHelper ->
-(void) beginRestorePurchases:(BOOL)serverRestore {
self.serverRestore = serverRestore;
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
-(void) paymentQueueRestoreCompletedTransactionsFinished : (SKPaymentQueue *) queue {
for (SKPaymentTransaction *transaction in queue.transactions) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
My concern at the moment is with Restoring purchases. ( I realize that i'm using a dependency injection pattern for the other portion(the purchasing part) of the code so it different. I'm not sure if this is having an effect on my problem as well.)
The problem is that I have no idea if I should be adding another observer for restoring or not. If I do I get some weird behavior with iTunes prompting multiple times upon restoring. In SO posts I see some mention of removing the observer. When would you do this if I'm creating it on the app delegate level?
Perhaps the my SKPaymentQueue * paymentQueue object is not a defaultQueue object???? No idea....
You always need to add your transaction observer, and early in the app lifecycle. Actions outside your control can cause transactions to fail to complete and the OS level queue will re-feed these to your app. I can't think of any common, legitimate reason to remove the observer.
You only need one observer, and that observer should be the gatekeeper to handling all the IAP events.
You also need to make certain that you're calling finish on each legitimately completed transaction. If you don't, you can wind up stacking lots of duplicated (from the SKU perspective) transactions on top of each other.
I check & process IAP receipts (of consumables) on server side, and only call [[SKPaymentQueue defaultQueue] finishTransaction:transaction] on the app when it gets an okay back.
When the server does not return this okay (for whatever reason), the app correctly won't finish the transaction.
My question now is: Is there anything the app needs to do to receive the receipt again for a retry, or does iOS take care of this by invoking - (void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transactions`? When I reran my app, the item was re-submitted; but a user should not need to restart the app.
Related question: When I tried to buy a still pending item again (on the sandbox), I got an iOS alert saying that I bought it earlier but was not downloaded. Why is this? I would expect (and have actually seen) this for non-consumables. I could buy another consumable, with this previous one still pending.
Once you finish the transaction of CONSUMABLE IN-APP the following method will fire there add this statement "[[SKPaymentQueue defaultQueue] finishTransaction:transaction]" to remove the purchased product.Using this statement you can avoid the alert message(I bought it earlier but was not downloaded).
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
if (transaction.downloads)
{
[[SKPaymentQueue defaultQueue]
startDownloads:transaction.downloads];
} else {
//Add the following line to remove purchased observer
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
break;
case SKPaymentTransactionStateFailed:
[[SKPaymentQueue defaultQueue]
finishTransaction:transaction];
break;
}
}
}
The next time your App adds a transaction observer the transaction will appear in the queue.
And perhaps this is why you get the 'not yet downloaded' message - that means 'the app store has not yet gotten a finishTransaction'.
I am restoring completed transactions (recurring) with
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
and in
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
I got a list of history transactions restored, made by the app, but the method:
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
is not even called once, so I wonder which one should I use? I did some research and found that updatedTransactions: method should be used with checking transaction state, but if I got a list of transactions restored, it is just meaningless to treat them all as transactions. Which one should I use? Does paymentQueueRestoreCompletedTransactionsFinished only gives me the latest one (ie sandbox subscription expires in several minutes and I got a full list of history transactions made when testing, in updatedTransactions).
There is an excellent WWDC Video about using StoreKit, it is WWDC2012 Session 302.
To isolate each purchase, your updatedTransactions method could look something like this:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch(transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
// Unlock content
//... Don't forget to call `finishTransaction`!
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStatePurchasing:
// Maybe show a progress bar?
break;
case SKPaymentTransactionStateFailed:
// Handle error
// You must call finishTransaction here too!
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
// This is the one you want ;)
// ...Re-unlock content...
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
}
}
Once you've determined that the purchase is being restored, you can make content available as you see fit - preferably by calling a separate method from within that switch statement and passing the transaction as a parameter. The implementation is up to you of course.
Call [[SKPaymentQueue defaultQueue] addTransactionObserver:self] in (void)viewDidLoad or equivalent if applicable.
Then call [[SKPaymentQueue defaultQueue] restoreCompletedTransactions].
(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions will be called accordingly through (2).
If you don't call the method in (1), the application will never reach (3) to restore transactions in the first place.
I had this same issue with paymentQueueRestoreCompletedTransactionsFinished never getting called. I fixed by going to iTunes & Aoo Stores in Settings and logged out of the sandbox test account and tried it again. Worked as expected the next time after being prompted to login again on a restore.
From the Apple Docs:
paymentQueueRestoreCompletedTransactionsFinished: This method is called
after all restorable transactions have been processed by the payment
queue. Your application is not required to do anything in this method.
As discussed in this question and everywhere else, Apple now requires apps to include a means for the user to restore completed transactions for In App Purchases.
I'm all for this. The first version of my app somehow made it past review without it (I wasn't aware of this rule at the time, and/or it wasn't being enforced yet), but then I started receiving lots of e-mails from users asking about missing content (there is the Data Storage Guidelines too, and the heavy, downloadable contents aren't backed up).
So let's say I include a 'restore' button somewhere in my UI, that when tapped calls:
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
So far, so good. The user is prompted his AppleID and/or password, and the restoring process begins.
The problem I have is: If there is no transactions to restore, after the AppleID prompt essentially nothing happens in my app, and that may be confusing to the user or make the app look unresponsive or broken.
I would like to be able to display an alert view along the lines of "All purchases are up to date" or something.
Is there anything I can do in my Transaction Observer code to detect this case?
Does anybody think it would be a bad design, UX-wise?
This is still an issue in the latest SDK / xCode 8.0, Swift 3 - if a user who hasn't made any purchases attempts to 'restore', the following method:
SKPaymentQueue.default().restoreCompletedTransactions()
does not trigger the usual delegate method that handles purchases / restoring:
paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {...}
Interestingly, the method that probably should catch the error is also NOT called:
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error){...}
And instead, the optional method is triggered, as if the restore worked fine:
func paymentQueueRestoreCompletedTransactionsFinished()
This can cause the app to look like it is hanging / not doing anything.
As discussed in the other answers, the cause of this is that the SKPaymentQueue doesn't contain any transactions.
in Swift, this problem can be overcome by using the following:
//Optional Method.
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue)
{
let transactionCount = queue.transactions.count
if transactionCount == 0
{
print("No previous transactions found")
//You can add some method to update your UI, indicating this is the problem e.g. use notification centre:
NotificationCenter.default.post(name: "restoreFailedNoPrevIAP", object: nil)
}
}
Importantly, if a user has made previous purchases, the transaction queue will not be empty, hence updateTransaction delegate method will be called, and will process the restore request normally.
You could also implement the following delegate functions:
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
-(void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
Then you would know when the restore process was finished or if it failed.
You can mix in the usage of queue.transactions.count in paymentQueueRestoreCompletedTransactionsFinished to see if any transactions was restored.
Remember to handle SKPaymentTransactionStateRestored in
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
You might also want to handle the restored transaction(s) the way same as you did with SKPaymentTransactionStatePurchased the transaction(s).
I was interested in the right/best wording for restoring purchase.
I have seen enough "Unknown Error" alerts, just using [error localizedDescription] inside -(void)paymentQueue:restoreCompletedTransactionsFailedWithError:. (todo: fill radar)
So I took a look at how Apple does it. The only app from Apple with Non-Consumable In-App Purchases right now is GarageBand (Dec, 2014).
Instead of "Restore Purchase", "Restore Previous Purchases" or ... they go with "Already Purchased?".
But here is the screen I'm more interested in, the result of pressing "Already Purchased?" when there is nothing to restore:
"There are no items available to restore at this time." Not revolutionary, but beats the hell out of "Unknown Error"
So lets look at -(void)paymentQueue:restoreCompletedTransactionsFailedWithError:.
iOS:
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
if ([error.domain isEqual:SKErrorDomain] && error.code == SKErrorPaymentCancelled)
{
return;
}
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(#"There are no items available to restore at this time.", #"")
message:nil
delegate:nil
cancelButtonTitle:NSLocalizedString(#"OK", #"")
otherButtonTitles:nil];
[alert show];
}
OS X:
I'm not happy with just the same text on OS X. An NSAlert with just the messageText and no informativeText just looks empty and wrong.
One option for me is to let the user know that he needs to purchase it, with something like "To use it, you need to buy “%#”.".
Another option I came up with is letting them browser there Purchase History. I found that you can directly link to it with itms://phobos.apple.com/purchaseHistory. In all honesty Purchase History in the iTunes Store is a piece of shit, it gonna take you for ever do find something.
But maybe it helps reinsuring people that we don't try to make them repurchase something. Always assume that your customers don't know or can't tell the difference between Non-Consumable and Consumable. And don't know that they can't get charged twice for a Non-Consumable.
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
{
if ([error.domain isEqual:SKErrorDomain] && error.code == SKErrorPaymentCancelled)
{
return;
}
NSAlert *alert = nil;
alert = [NSAlert alertWithMessageText:NSLocalizedString(#"There are no items available to restore at this time.", #"")
defaultButton:NSLocalizedString(#"OK", #"")
alternateButton:NSLocalizedString(#"Purchase History", #"")
otherButton:nil
informativeTextWithFormat:#"You can see your purchase history in the iTunes Store."];
NSModalResponse returnCode = [alert runModal];
if (returnCode == NSAlertAlternateReturn)
{
NSURL *purchaseHistory = [NSURL URLWithString:#"itms://phobos.apple.com/purchaseHistory"];
[[NSWorkspace sharedWorkspace] openURL:purchaseHistory];
}
}
Example on OS X
Testing Notes (OS X, itunesconnect sandbox user):
When user clicks cancel:
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
Error Domain=SKErrorDomain Code=2 "The payment was canceled by the user" UserInfo=0x600000470a40 {NSLocalizedDescription=The payment was canceled by the user}
When there is nothing to restore:
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
Error Domain=SKErrorDomain Code=0 "Unknown Error." UserInfo=0x60800007fb80 {NSLocalizedDescription=Unknown Error.}
I ran into the same issue in the app I'm working on now. My workaround is to use a X second timer. It starts when you tap the 'Restore Purchases' button and restarts if a restored transaction event comes in. Once it reaches the X second mark I have a popup saying "Purchases Restored". So if you have no transactions you should only have to wait X seconds. Hope that helps.
Sorry for that millionth question on autorenewable subscriptions, but i don't get it.
I've done everything as describet in Apples In-App Purchase Guidelines but it didn't solve the problem.
My problem is that i have created autorenewable subscriptions but they won't be autorenewed.
I've create a Payment Transaction Observer class, which implements the SKPaymentTransactionObserver interface. This class will be installed as a paymentObserver at Application startup in the viewDidLoad: method.
PaymentTransactionObserver *observer = [[PaymentTransactionObserver alloc] init];
[[SKPaymentQueue defaultQueue] addTransactionObserver:observer];
In the paymenttransactionobserver i have the paymentQueue:updateTransactions method: (same as describet in Apple's documentation)
(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
break;
default:
break;
}
}
When i buy a autorenewable product, the product will successfully be purchase.
But it will never be autorenewed. I thought of, that the transaction observer, somehow will get deallocated, but it won't (Otherwhise, i would be notified by the debugger). I also though, i did remove the observer but it will never be removed.
I used the debugger to ensure, that the updateTranscations: method will get called, but nothing. When i buy a test product (in sandbox-mode) with autorenewal time of one week, the method should get called after 3 minutes, but it wont.
What am i doing wrong?
Can anybody help?
Br Nic
If a subscription is autorenewed, the transaction won't pass the paymentQueue:updateTransactions method. The renew just happens on the Store. If you want to test for it you have to either:
Revalidate the receipt on your application server, if you store the receipt there.
Revalidate the receipt on ur iOS client
(http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/VerifyingStoreReceipts/VerifyingStoreReceipts.html#//apple_ref/doc/uid/TP40008267-CH104-SW1)
In order to avoid testing for an autorenew each launch/activation you should store the endDate of the subscription period to test for a renew afterwards.
Also see:
http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/RenewableSubscriptions/RenewableSubscriptions.html#//apple_ref/doc/uid/TP40008267-CH4-SW4
However, there seems to be a bug in the sandbox. Subscriptions sometimes get renewed, sometimes not. Hard to test....
Auto-renewals only get posted to your app's purchase queue on app launch or when it cycles back from being backgrounded. Try clicking the home button and then returning to your app.