Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

paymentQueue: updatedTransactions: not called during restore

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

like image 257
alexgophermix Avatar asked Jul 13 '17 17:07

alexgophermix


4 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

like image 150
yblinov Avatar answered Oct 14 '22 07:10

yblinov


have you implemented below method:

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error NS_AVAILABLE_IOS(3_0);

It's one of the optional methods from SKRequestDelegate.

We also were facing the same problem of missing restore purchase call. Handling this delegate helped us. All the requests which were not even being delivered to queue due to whatever reason, were delivered in this failure delegate.

So,I think you might be facing the same issue.

like image 34
Vikas Dadheech Avatar answered Oct 14 '22 07:10

Vikas Dadheech


Start the restore process -

-(void)restore{
    isRestored = false;
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

If any transaction is successfully restored the following method is called:

-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
       case SKPaymentTransactionStateRestored:

            DDLogVerbose(@"Restored");
            //Check with your product id if it is the right product that you want to restore
            if ([transaction.payment.productIdentifier isEqualToString:IAP_PRODUCT_ID]) {
                isRestored = true;
                // Successfully restored the payment, provide the purchased content to the user.
            }
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
}

When payment queue has finished sending restored transactions, following method is called (If it is called means its completed the transaction and not that restore is success)-

-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
    DDLogVerbose(@"Restore completed");

    if (isRestored) {
        // Successfully restored
        } else {
        // No transaction to restore
    }
}

paymentQueueRestoreCompletedTransactionsFinished

When any error occurred while restoring transactions, following method is called -

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error{

    DDLogVerbose(@"Error in restoring:%@",error);

    if (error.code == 0) {
        // unable to connect to iTunes
    }
}
like image 3
Pooja Gupta Avatar answered Oct 14 '22 07:10

Pooja Gupta


Few steps to make your application behaviour as expected :

1. Add transaction observers in AppDelegate which keeps track of delayed response & whenever your application gets launched it will update & finish the transaction in queue

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

Remove observer in applicationWillTerminate

[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

2. For second step, check in-app receipt before asking from user to purchase

-(void)validateReceiptsFromAppStoreFor:(NSString *)productTag completionBlock:(void (^)(NSDictionary *receiptResponse,NSError *error))completion
{
    //check if receipt exists in app bundle
    //else request for refresh receipt data..
    //if receipt exists,verify with server & check if product tag exists in receipt & send receipt response as success msg
    //else wait for refresh request success 
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    if (!receipt)
    { /* No local receipt -- handle the error. */
        refreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:nil];
        refreshRequest.delegate = self;
        [refreshRequest start];
        return;
    }

    /* ... Send the receipt data to your server ... */
    NSError *error;
    NSDictionary *requestContents = @{
                                      @"password":@"Your shared secret key",
                                        @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                      };
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];

    if (!requestData) { /* ... Handle error ... */ }

    // Create a POST request with the receipt data.
    NSString *storeURL = SANDBOX_VERIFY_RECEIPT_URL; //ITMS_PROD_VERIFY_RECEIPT_URL;
    if ([[[FFGDefaults sharedDefaults] objectForKey:@"environmentType"] isEqualToString:@"prod"])
    {
        storeURL = PROD_VERIFY_RECEIPT_URL;
    }
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:storeURL]];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];

    // Make a connection to the iTunes Store on a background queue.
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                               if (connectionError) {
                                   /* ... Handle error ... */
                                   if (completion) {
                                       completion(nil,connectionError);
                                   }
                               } else {
                                   NSError *error;
                                   NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                                   if (completion)
                                   {
                                       jsonResponse = jsonResponse[@"receipt"];
                                       if ([jsonResponse[@"bundle_id"] isEqualToString:[NSBundle mainBundle].bundleIdentifier])
                                       {                                           
                                               //check if product was purchased earlier..
                                               NSString *str_productID = [CFCommonUtils productIDForPlanTag:productTag];
                                                NSArray *receiptArr = jsonResponse[@"in_app"];
                                               if (receiptArr && receiptArr.count>0)
                                               {
                                                   NSArray *filteredArray = [receiptArr filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"product_id = %@",str_productID]];
                                                   if (filteredArray.count>0) {
                                                       completion(jsonResponse,error);
                                                   }
                                                   else
                                                   {
                                                       NSError *err = [NSError errorWithDomain:@"" code:100 userInfo:nil];
                                                       completion(nil,err);
                                                   }

                                               }
                                               else
                                               {
                                                   NSError *err = [NSError errorWithDomain:@"" code:100 userInfo:nil];
                                                   completion(nil,err);
                                               }

                                       }
                                       else
                                       {
                                           NSError *err = [NSError errorWithDomain:@"" code:100 userInfo:nil];
                                           completion(nil,err);

                                       }
                                   }

                               }
                               }];

}

3. Handle Receipt refresh delegate methods to check receipt from updated one

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
#ifdef DEBUG
    NSLog(@"SKRequest : didFailWithError :%@",error);
#endif
    if ([request isMemberOfClass:[SKReceiptRefreshRequest class]] && refreshRequest && delegate && [delegate respondsToSelector:@selector(receiptRefreshed:error:)])
    {
        [self receiptRefreshed:self error:error];
        refreshRequest = nil;
    }
    else
    {

    }
}

- (void)requestDidFinish:(SKRequest *)request
{
#ifdef DEBUG
    NSLog(@"SKRequest : requestDidFinish ");
#endif
    if ([request isMemberOfClass:[SKReceiptRefreshRequest class]] && refreshRequest && delegate && [delegate respondsToSelector:@selector(receiptRefreshed:error:)])
    {
        [self receiptRefreshed:self error:nil];
        refreshRequest = nil;
    }
    else
    {
    }
}



-(void) receiptRefreshed:(CFStorekitManager*)ebp error:(NSError *)error
{
    if (error)
    {
    }
    else
    {
        [self validateSubscriptionReceiptsFromAppStoreWithRefreshReceipt:YES completion:^(NSDictionary *receiptResponse, NSError *error)
     {
         dispatch_async(dispatch_get_main_queue(), ^{
             if (error)
             {
                 //show subscription for purchase
             }
             else
             {
             }
         });

     }];

    }
}
like image 3
Ellen Avatar answered Oct 14 '22 06:10

Ellen