【问题标题】:paymentQueue: updatedTransactions: not called during restorepaymentQueue:updatedTransactions:恢复期间未调用
【发布时间】:2017-07-13 17:37:22
【问题描述】:

在极少数情况下,我的一些用户似乎无法进行非消耗性购买。当他们尝试购买时,它不会激活“高级”,并且当他们从当前安装或全新安装恢复时,paymentQueue: updatedTransactions: 不会被调用。

我专门添加了许多日志记录,以尝试确定还原未遵循预期流程的原因。在还原失败期间,不会触发任何“RESTORE”类别事件。

供参考,[self success]; 只显示内容视图,[self fail:] 向用户显示错误消息。

viewDidLoad 中调用[[SKPaymentQueue defaultQueue] addTransactionObserver:self]; 并且在按下按钮时调用[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

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

任何建议将不胜感激

【问题讨论】:

  • 您解决了这个问题吗?我有相同的问题。 paymentQueue:updatedTransactions:在恢复操作期间未调用。没有错误并且调用了 paymentQueueRestoreCompletedTransactionsFinished 所以我的观察者必须是正确的。
  • 很遗憾没有。只有少数少数用户受到影响,我们无法查明发生了什么。老实说,他们似乎购买了并且购买被默默地取消/失败,并且不会让他们再次购买,给他们一种他们已经购买但没有收到产品的印象。可能是某种拒付利用?老实说,我不知道,我只能说他们说他们购买了并且没有得到恢复。
  • 我只是想通了(我想)。事实证明,我们有两种 IAP,一种是可下载的,一种是不可下载的。使用的代码只是测试是否 transaction.downloadable 为 nil 以区分两者。我相信可下载的数组可以是非零但为空的,你必须允许这种情况。如果我们使用不可下载的 IAP 进入可下载代码,则交易永远不会完成。
  • 签入 iOS 13 以上版本stackoverflow.com/a/66343882/239485

标签: ios in-app-purchase skpaymenttransaction


【解决方案1】:

您是否有机会在您的应用中使用 Firebase 分析?

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

如果您要跟踪应用内购买,则必须初始化您的 应用程序中的事务观察者:didFinishLaunchingWithOptions: 在初始化 Firebase 之前,否则您的观察者可能不会收到所有 购买通知。请参阅 Apple 的应用内购买最佳实践 更多信息。

在这种情况下,建议在初始化 Firebase 分析之前初始化您的观察者。

这是一篇包含更多详细信息的博文:https://www.greensopinion.com/2017/03/22/This-In-App-Purchase-Has-Already-Been-Bought.html

【讨论】:

    【解决方案2】:

    你有没有实现以下方法:

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

    这是来自 SKRequestDelegate 的可选方法之一。

    我们也面临着丢失恢复购买电话的相同问题。处理这个委托对我们有帮助。由于某种原因甚至没有被传递到队列的所有请求都在这个失败委托中传递。

    所以,我认为您可能面临同样的问题。

    【讨论】:

    • 我确实有这个方法。我认为此方法仅针对失败的SKProductRequest 触发,而不是针对付款或恢复案例
    • 就是这样,所有传送到 iTunes 的请求,都会在 updatedTransaction 方法中得到回调。在我们的案例中,甚至没有发送到 iTunes 的请求是造成麻烦的原因。所以你可以尝试从这里处理失败的请求。
    【解决方案3】:

    让您的应用程序行为符合预期的几个步骤:

    1.AppDelegate 中添加事务观察器,以跟踪延迟响应,并且无论何时启动您的应用程序,它都会更新并完成队列中的事务

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    

    在 applicationWillTerminate 中移除观察者

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

    【讨论】:

      【解决方案4】:

      开始恢复过程 -

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

      【讨论】:

      • 据我所知,这正是我正在做的事情。但是restoreCompletedTransactionsFailedWithError 没有被调用,SKPaymentTransactionStateRestored 的情况也没有被击中
      • 您是否也面临新事务或仅还原事务的问题?
      • 我相信这也会影响新交易,但过去几天我只处理了两个恢复案例。如上所述,尽管绝大多数用户似乎不受影响
      【解决方案5】:

      我不确定这是否与您的问题有关,但这可能是原因。 (或者至少这是苹果推荐的做法)。

      Apple Documentation

      Apple 建议将 SKPaymentQueue 注册为 AppDelegate 中的观察者,而不是特定类(除非您在 AppDelegate 本身中调用此类)

      这意味着:

      [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
      

      实际上应该进入 AppDelegate 中的 applicationDidFinishLaunchingWithOptions 方法。

      为什么这很重要?你提到过:

      在还原失败期间,没有任何“RESTORE”类别事件 被解雇了。

      这让我相信您的听众没有正确或按时注册。 (或者在极少数情况下,用户可能会被重定向到您的应用程序之外以正确登录或其他原因,并且由于内存问题,您的应用程序可能会被杀死?无论哪种方式,这都可以确保一旦他们返回您的应用程序,总会有准备处理 Apple 发送的任何通知的观察者)


      使用 App Receipt 恢复购买

      文档here

      或者,通过刷新应用收据然后根据用户购买的内容交付内容,实现恢复购买逻辑要容易得多。

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

      这将调用您的委托方法:

      func requestDidFinish(SKRequest)
      

      func request(SKRequest, didFailWithError: Error)
      

      请求成功完成后,您可以解析收据以授予用户之前购买的所有商品。收据解析指南描述为here

      【讨论】:

      • 这是一个很好的建议,但是 HUD 对用户隐藏,因此至少调用了一个委托方法。我的猜测是,对于恢复情况,paymentQueueRestoreCompletedTransactionsFinished[SKPaymentQueue defaultQueue].transactions.count != 0 触发 OR updatedTransactions 被调用,并且 switch 语句落入SKPaymentTransactionStateRestored 以外的情况。 paymentQueueRestoreCompletedTransactionsFinished 是否有可能被 1 个或多个事务触发但 updatedTransactions 被跳过?
      • 您恢复购买的方式是 ios 7 之前的版本,这样恢复过程会再次模仿用户购买东西的行为,因此购买被视为新购买。 “paymentQueueRestoreCompletedTransactionsFinished”用于告诉您所有购买交易都已发送给您,因此我认为您不应该检查那里的队列数。无论如何,我强烈建议您更改恢复逻辑以使用“收据”来恢复购买。这也是苹果推荐的方式。它也更简单,更可靠。检查我更新的答案。
      • 据我所知,检查队列数并在那里处理是可以的。我以前看过收据,但恢复交易似乎没有被弃用。我可能错过了旧版本中的 FinishTransanction 案例。这些用户是否有未完成的交易导致他们无法购买或恢复?如果是,我将如何检查?
      • 如果有任何未完成的事务,它们会在启动时自动传送到您的应用程序,这就是为什么您的侦听器应该在您的应用程序委托中注册的原因。不过,我认为这不是问题,因为当您按下恢复过程时,它们会被传送到您当前实现的方法中。正如您所说,您使用的方式肯定不会被弃用,但是从 iOS 7 开始检查收据更好。
      【解决方案6】:

      据我所知,如果 restoreCompletedTransactions() 调用完成但没有导致任何事务被恢复paymentQueue(_:, updateTransactions:)不会被调用

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

      所以我们可以检查queue.transactions.count == 0 来确定是否会调用另一个委托方法。

      【讨论】:

      • 如果我对此有误解,请告诉我。将进行更多测试。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-08-10
      • 1970-01-01
      • 2022-07-20
      • 1970-01-01
      • 1970-01-01
      • 2018-04-01
      • 1970-01-01
      相关资源
      最近更新 更多