paymentQueue: updatedTransactions: not called during restore

Viewed 6111

In rare cases it seems that some of my users are unable to make a non-consumable purchase. When they attempt to purchase it doesn't activate "premium" and when they restore from either their current install or a fresh install paymentQueue: updatedTransactions: is not called.

I've added a lot of logging specifically to try and determine why the restore is not following an expected flow. During a failed restore none of the "RESTORE" category events are fired.

For reference [self success]; just displays the content view and [self fail:] displays an error message to the user instead.

Also [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; is called in viewDidLoad and [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; is called on button press.

- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
    // COMPLETION POINT - RESTORE COMPLETE***
    [MBProgressHUD hideHUDForView:self.view animated:TRUE];

    if ([SKPaymentQueue defaultQueue].transactions.count == 0) {
        [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
                                                                   action:@"failure_hard"
                                                                    label:@"no_purchases"
                                                                    value:nil] build]];
        [self fail:@"There are no items available to restore at this time."];
    } else {
        [self success];
    }
}

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
    // COMPLETION POINT - RESTORE FAILED
    [MBProgressHUD hideHUDForView:self.view animated:TRUE];

    [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
                                                               action:@"failure_hard"
                                                                label:error.localizedDescription
                                                                value:nil] build]];
    [self fail:error.localizedDescription];
}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    // Make sure completion states call [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    // in order to prevent sign in popup
    // http://stackoverflow.com/a/10853107/740474
    [MBProgressHUD hideHUDForView:self.view animated:TRUE];
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                break;
            case SKPaymentTransactionStateDeferred:
                break;
            case SKPaymentTransactionStateFailed:
                // COMPLETION POINT - PURCHASE FAILED
                [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"PURCHASE"
                                                                           action:@"failure_hard"
                                                                            label:transaction.error.localizedDescription
                                                                            value:nil] build]];
                if (transaction.error.code != SKErrorPaymentCancelled) {
                    // only show error if not a cancel
                    [self fail:transaction.error.localizedDescription];
                }
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchased:
                // COMPLETION POINT - PURCHASE SUCCESS
                if ([transaction.payment.productIdentifier isEqualToString:(NSString*)productID]) {
                    // premium purchase successful
                    [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"PURCHASE"
                                                                               action:@"success"
                                                                                label:nil
                                                                                value:nil] build]];
                    [Utils setPremium:YES];
                    [self success];
                } else {
                    [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"PURCHASE"
                                                                               action:@"failure_hard"
                                                                                label:@"no_id"
                                                                                value:nil] build]];
                    [self fail:@"The item you purchased was not returned from Apple servers. Please contact us."];
                }
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                if ([transaction.payment.productIdentifier isEqualToString:(NSString*)productID]) {
                    // premium purchase restored
                    [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
                                                                               action:@"restore_success"
                                                                                label:nil
                                                                                value:nil] build]];
                    [Utils setPremium:YES];
                } else {
                    [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
                                                                               action:@"failure_hard"
                                                                                label:@"no_id"
                                                                                value:nil] build]];
                }
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
            default:
                // For debugging
                   [self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"STORE"
                                                                           action:@"transaction_weird"
                                                                               label:[NSString stringWithFormat:@"Unexpected transaction state %@", @(transaction.transactionState)]
                                                                            value:nil] build]];
                break;
        }
    }
}

Any suggestions would be appreciated

6 Answers

Is there any chance you are using Firebase analytics in your app?

https://firebase.google.com/docs/analytics/ios/start says

If you're tracking in-app purchases, you must initialize your transaction observer in application:didFinishLaunchingWithOptions: before initializing Firebase, or your observer may not receive all purchase notifications. See Apple's In-App Purchase Best Practices for more information.

In this case the suggestion is to init your observer before initializing Firebase analytics.

Here is a blogpost with additional details: https://www.greensopinion.com/2017/03/22/This-In-App-Purchase-Has-Already-Been-Bought.html

From what I can see, if the restoreCompletedTransactions() call completes but does not result in any transactions being restored, paymentQueue(_:, updateTransactions:) is not called.

func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
    if queue.transactions.count == 0 {
        // does NOT call paymentQueue:updatedTransactions:
    } else {
        // should call paymentQueue:updatedTransactions:
    }
}

So we can check queue.transactions.count == 0 to determine if the other delegate method will be called.

Related