PaymentQueue: updatedTransactions: not called during recovery

In rare cases, it seems that some of my users are unable to make a purchase without cost. When they try to purchase it, it does not activate the "premium", and when they are restored either from the current installation or from a new installation, paymentQueue: updatedTransactions:it is not called.

I have added many protocols to try to determine why the recovery does not match the expected thread. During a failed restore, none of the events in the RECOVERY category are triggered.

For reference, it [self success];simply displays a view of the content, and [self fail:]displays an error message to the user.

Also [[SKPaymentQueue defaultQueue] addTransactionObserver:self];called in viewDidLoadand [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];called when the button is pressed.

- (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
source share
5 answers

You have completed the method below:

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

This is one of the optional methods from SKRequestDelegate.

We also faced the same problem of the lack of a call for recovery. Working with this delegate helped us. All requests that were not even delivered to the queue for any reason were delivered in this deletion deletion.

So, I think you are facing the same problem.

+3
source

-

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

, -

-(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;
}

, ( , , , ) -

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

if (isRestored) {

    // Successfully restored
}else{
    // No transaction to restore
  }
}

paymentQueueRestoreCompletedTransactionsFinished

-

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

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

if (error.code == 0) {
    // unable to connect to iTunes
  }
}
+2

, :

1. AppDelegate, , ,

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

WillTerminate

[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

2. , ,

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

- (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
             {
             }
         });

     }];

    }
}
+2

, , . (, , ).

Apple

Apple SKPaymentQueue AppDelegate, ( AppDelegate)

, :

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

applicationDidFinishLaunchingWithOptions AppDelegate.

? , :

"" .

, . ( , - , - ? , , , , Apple)


App Receipt

.

, , ​​ , .

request = [[SKReceiptRefreshRequest alloc] init];
request.delegate = self;
[request start];

:

func requestDidFinish(SKRequest)

func request(SKRequest, didFailWithError: Error)

, , . .

+1

, Firebase ?

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

, : didFinishLaunchingWithOptions: Firebase, . . Apple In-App Buy Best Practices .

Firebase.

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

0
source

Source: https://habr.com/ru/post/1681382/


All Articles