15

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

我已经添加了很多专门的日志记录来尝试确定为什么还原没有遵循预期的流程。在还原失败期间,不会触发任何“RESTORE”类别事件。

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

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];被调用viewDidLoad[[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;
        }
    }
}

任何建议,将不胜感激

4

6 回答 6

8

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

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

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

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

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

于 2019-06-28T21:20:10.367 回答
4

你有没有实现以下方法:

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

它是 SKRequestDelegate 的可选方法之一。

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

所以,我想你可能会面临同样的问题。

于 2017-07-19T11:08:39.033 回答
3

开始恢复过程 -

-(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
    }
}
于 2017-07-14T07:48:27.953 回答
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
             {
             }
         });

     }];

    }
}
于 2017-07-19T14:19:40.400 回答
1

据我所知,如果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确定是否会调用另一个委托方法。

于 2019-10-31T00:14:17.080 回答
1

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

苹果文档

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

这意味着:

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

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

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

在还原失败期间,不会触发任何“RESTORE”类别事件。

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


使用 App Receipt 恢复购买

文档在这里

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

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

这将调用您的委托方法:

func requestDidFinish(SKRequest)

或者

func request(SKRequest, didFailWithError: Error)

请求成功完成后,您可以解析收据以授予用户以前购买的所有物品。收据解析指南在此处描述。

于 2017-07-18T02:12:24.503 回答