23

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?

Nicolas Miari
  • 16,006
  • 8
  • 81
  • 189
  • Benjamin's answer provides the more elegant solution that you asked for. Could you mark his answer as the accepted answer so it comes out on top for everyone to find? – Timo Feb 14 '13 at 10:57
  • Hi @NicolasMiari, how did you solve your problem – Ranjit Feb 20 '13 at 09:54

4 Answers4

12

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.

nervous-energy
  • 367
  • 6
  • 10
  • delegate behavior applies to objc using Xcode 8/latest SDK also. same check of transaction count workaround. – drshock May 29 '17 at 12:31
11

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).

Benjamin
  • 618
  • 2
  • 6
  • 17
  • won't queue.transactions.count return only the pending transactions ([link](http://developer.apple.com/library/ios/documentation/StoreKit/Reference/SKPaymentQueue_Class/Reference/Reference.html#//apple_ref/occ/instp/SKPaymentQueue/transactions))? In this case, always returning 0 (having finished any transactions)? – Andrei Filip Feb 14 '13 at 16:01
  • 1
    i keep a flag _didRestore that i set to true when i get paymentQueue:updatedTransactions: call with transactionState == SKPaymentTransactionStateRestored. then in paymentQueueRestoreCompletedTransactionsFinished: i check for that flag – Joris Weimar Oct 14 '14 at 07:47
11

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?".

Purchase Screen 1

But here is the screen I'm more interested in, the result of pressing "Already Purchased?" when there is nothing to restore:

Purchase Screen 2

"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

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.}
catlan
  • 25,100
  • 8
  • 67
  • 78
  • 1
    @catlan, are you really seeing restoreCompletedTransactionsFailedWithError getting called when a user hits restore but has never previously made a purchase? Because in the simulator I'm seeing paymentQueueRestoreCompletedTransactionsFinished getting called with no transactions in the queue (i.e. nothing to restore). – jeffjv Aug 30 '16 at 07:00
0

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.

Oatz
  • 9
  • 1