iOS收据验证-自动续订订阅

4
我遇到了一个验证自动续订IAP收据的问题。以下是我的订阅验证代码摘要。我使用Parse作为后端,同时包括一些相关的代码。在开发模式下,一切都运行得很完美,没有任何问题。它已经在App Store上线,但是所有的测试用户和一些真实用户在应用程序尝试进行验证时经历了崩溃。我发现崩溃是在函数的最后一步发生的,当我尝试将数据保存回Parse时,它告诉我我要保存的键值是null。(base64,info,expirationDate)
在我的设备上,我有一个沙盒购买收据,并且当我尝试针对实时URL进行验证时,我会收到21007的响应。当我切换到沙盒URL时,它每次都能验证并工作。
此外,我知道这种精确的购买验证方式并不安全,我通过我的服务器执行验证,所以不存在问题。
我正在努力找出是否缺少步骤或者是否应该采取不同的方法?
伪代码:
Get the receipt from NSBundle
Get the receipt from Parse database
If neither of them exist:
    end the function
else:
    if Parse receipt exists:
        use it, but first just check the expirationDate stored in Parse
    else:
        use NSBundle receipt for validation, 
    If expired based on date from Parse:
        build request to send to Apple (my server in production)
        Get JSON response, and perform switch statement for response codes
    Check for errors or expiration
    Save new data to Parse // <-- Cause of the crash is here because the keys are null for some users

以下是实际代码示例:

PFUser *user = [PFUser currentUser];

//Load the receipt from the app bundle
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

//Load the receipt from Parse, already encoded
NSString *saved64 = user[@"base64"];

//Check for at least one instance of a receipt... Either Parse or appBundle
if (!receipt && saved64.length == 0) {
//if (!receipt) {
    //No receipt
    NSLog(@"No Receipt");
    return;
}

//Base 64 encode appBundle receipt
NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];

//String to hold base64 receipt (either from Parse or appBundle)
NSString *temp64;

//See if Parse base64 exists
if (saved64.length == 0) {
    //Not a receipt in Parse yet, use appBundle
    NSLog(@"Using appBundle receipt.");
    temp64 = receipt64;
} else {
    //Receipt in Parse, use it
    NSLog(@"Using Parse receipt.");
    temp64 = saved64;

    //Check expiration date stored in Parse
    NSDate *parseExpDate = user[@"expirationDate"];
    if ([[self todayGMT] compare:parseExpDate] == NSOrderedAscending) {
        //Active based on Parse, no need to validate...
        NSLog(@"Active based on Parse receipt... No need to validate!");
        return;
    }
}

    //Base 64 encode appBundle receipt
    NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];


    //Create the request
    NSString *sharedSecret = @"[shared-secret]";
    NSError *error;
    //Request with receipt data and shared secret from iTunesConnect
    NSDictionary *requestContents = @{@"receipt-data":receipt64, @"password": sharedSecret};
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
    if (!requestData) {
        //Handle error
        NSLog(@"Error: %@", error);

        return;
    }

    //Create a POST request with the receipt data.
    NSURL *storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];                      
    //NSURL *sandboxURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];                
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];                        
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];

    //Make a connection to the iTunes Store
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (connectionError) {
            //Error
        } else {
            //Success
            NSError *error;
            NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
            if (!jsonResponse) {
                //Error
                NSLog(@"Error at !jsonResponse: %@", error);
                return;
            }

            NSString *base64 = jsonResponse[@"latest_receipt"];
            NSArray *info = jsonResponse[@"latest_receipt_info"];
            BOOL isPro;

            //Switch statement for subscription status
            switch ([jsonResponse[@"status"] intValue]) {
                case 21002: {
                    //The data in the receipt-data property was malformed or missing.
                    NSLog(@"21002 : The data in the receipt-data property was malformed or missing.");
                    isPro = NO;
                    break;
                }
                case 21003: {
                    //The receipt could not be authenticated.
                    NSLog(@"21003 : The receipt could not be authenticated.");
                    isPro = NO;
                    break;
                }
                case 21004: {
                    //The shared secret you provided does not match the shared secret on file for your account.
                    NSLog(@"21004 : The shared secret you provided does not match the shared secret on file for your account.");
                    isPro = NO;
                    break;
                }
                case 21005: {
                    //The receipt server is not currently available.
                    NSLog(@"21005 : The receipt server is not currently available.");
                    isPro = NO;
                    break;
                }
                case 21006: {
                    //This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
                    NSLog(@"21006 : This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.");
                    isPro = NO;
                    break;
                }
                case 21007: {
                    //This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
                    NSLog(@"21007 : This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead..");
                    isPro = NO;
                    break;
                }
                case 21008: {
                    //This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
                    NSLog(@"21008 : This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead..");
                    isPro = NO;
                    break;
                }
                case 0: {
                    //Valid and active
                    NSLog(@"0 : Valid and active subscription.");
                    isPro = YES;
                    break;
                }
                default: {
                    isPro = NO;
                    break;
                }
            }

            //Set user info to database (Parse)
            user[@"base64"] = base64;
            user[@"info"] = info;
            user[@"expirationDate"] = expirationDate;
            user[@"isPro"] = [NSNumber numberWithBool:isPro];
            [user saveEventually];
        }
    }];

我正在处理一个类似的问题。你对此有什么想法? - blwinters
@blwinters 由于某些奇怪的原因,有时候 base64infoexpirationDate 会是空的... 因此我的解决方案是在保存之前检查它们是否存在。它们对检查用户订阅没有影响,仍然可以返回正确的订阅状态。私信我以了解更多。 - tcd
好的,那很有道理,谢谢。我使用Swift和可选绑定来保存所有数据到Parse,所以应该可以工作。 - blwinters
没错,那会起作用! - tcd
1个回答

0

沙盒 URL - https://sandbox.itunes.apple.com/verifyReceipt

沙盒 URL 只在使用开发者证书的开发模式下有效,因此它从服务器获取响应。

线上 URL - https://buy.itunes.apple.com/verifyReceipt

线上 URL 只在使用分发证书的分发模式下有效,因此它无法在开发模式下工作并且无法返回响应。

因此,如果您想要在调试模式下使用线上 URL,则应处理异常情况。

如果您使用可选绑定的 Swift,您可以在不崩溃的情况下解析。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接