iOS 测试应用收据验证

iOS test App Receipt Validation

有很多关于如何使用沙盒测试器帐户测试应用内购买收据验证的示例。

但付费应用程序本身的收据如何?开发环境如何获取App Receipt?

我想做两件事:

我假设您知道如何执行应用内购买。

我们需要在交易完成后验证收据。

- (void)completeTransaction:(SKPaymentTransaction *)transaction 
{
    NSLog(@"completeTransaction...");
    
    [appDelegate setLoadingText:VALIDATING_RECEIPT_MSG];
    [self validateReceiptForTransaction];
}

商品购买成功后, 需要验证。服务器为我们做这个,我们 只需要传递Apple服务器返回的Receipt数据即可。

-(void)validateReceiptForTransaction
{
    /* Load the receipt from the app bundle. */
    
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    
    if (!receipt) { 
        /* No local receipt -- handle the error. */
    }
    
    /* ... Send the receipt data to your server ... */
    
    NSData *receipt; // Sent to the server by the device
    
    /* Create the JSON object that describes the request */
    
    NSError *error;
    
    NSDictionary *requestContents = @{ @"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.
    
    NSURL *storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];
    
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL: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 ... */
                               } 
                               else {
                                   NSError *error;
                                   NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                                   
                                   if (!jsonResponse) { 
                                       /* ... Handle error ...*/ 
                                   }
                                   
                                   /* ... Send a response back to the device ... */
                               }
                           }];
}

响应的有效负载是一个 JSON 对象,其中包含以下键和值:

状态:

如果收据有效则为 0,或者为以下错误代码之一:

对于iOS6种风格的交易收据,状态码反映了具体交易收据的状态。

对于iOS7种风格的app收据,状态码是反映app收据整体的状态。例如,如果您发送包含过期订阅的有效应用收据,则响应为 0,因为收据作为一个整体是有效的。

收货:

JSON 表示已发送以供验证的收据。

Remember:


EDIT 1

transactionReceipt 已弃用:首先在 iOS 7.0

中弃用
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) {
    // iOS 6.1 or earlier.
    // Use SKPaymentTransaction's transactionReceipt.

} else {
    // iOS 7 or later.

    NSURL *receiptFileURL = nil;
    NSBundle *bundle = [NSBundle mainBundle];
    if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) {

        // Get the transaction receipt file path location in the app bundle.
        receiptFileURL = [bundle appStoreReceiptURL];

        // Read in the contents of the transaction file.

    } else {
        /* Fall back to deprecated transaction receipt,
           which is still available in iOS 7.
           Use SKPaymentTransaction's transactionReceipt. */
    }

}

大部分答案都可以在 Apple 的文档中找到 here。但是存在差距,objective-c 代码使用了已弃用的方法。

此 Swift 3 代码显示了如何获取应用收据并将其发送到应用商店进行验证。在保存您想要的数据之前,您一定要通过应用商店验证应用收据。要求应用程序商店进行验证的优势在于,它会响应您可以轻松序列化为 JSON 的数据,并从那里提取所需键的值。无需密码学。

正如 Apple 在该文档中所描述的那样,首选流程是这样的...

device -> your trusted server -> app store -> your trusted server -> device

当应用程序存储 returns 到您的服务器时,假设成功,您将在此处序列化并提取所需数据并按需要保存。请参阅下面的 JSON。您可以将结果和您想要的任何其他内容发送回应用程序。

在下面的 validateAppReceipt() 中,为了使其成为一个工作示例,它简单地使用了这个流程...

device -> app store -> device

要使其与您的服务器一起工作,只需将 validationURLString 更改为指向您的服务器并将您需要的任何其他内容添加到 requestDictionary

要在开发中对此进行测试,您需要:

  • 确保您在 itunesconnect 中设置了沙盒用户
  • 在您的测试设备上退出 iTunes 和 App Store
  • 在测试期间,出现提示时,使用您的沙盒用户

这是代码。快乐之路顺畅。错误和失败点只是打印或注释。根据需要处理这些。

这部分抓取应用收据。如果它不存在(这将在您测试时发生),它会要求应用商店刷新。

let receiptURL = Bundle.main.appStoreReceiptURL

func getAppReceipt() {
    guard let receiptURL = receiptURL else {  /* receiptURL is nil, it would be very weird to end up here */  return }
    do {
        let receipt = try Data(contentsOf: receiptURL)
        validateAppReceipt(receipt)
    } catch {
        // there is no app receipt, don't panic, ask apple to refresh it
        let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
        appReceiptRefreshRequest.delegate = self
        appReceiptRefreshRequest.start()
        // If all goes well control will land in the requestDidFinish() delegate method.
        // If something bad happens control will land in didFailWithError.
    }
}

func requestDidFinish(_ request: SKRequest) {
    // a fresh receipt should now be present at the url
    do {
        let receipt = try Data(contentsOf: receiptURL!) //force unwrap is safe here, control can't land here if receiptURL is nil
        validateAppReceipt(receipt)
    } catch {
        // still no receipt, possible but unlikely to occur since this is the "success" delegate method
    }
}

func request(_ request: SKRequest, didFailWithError error: Error) {
    print("app receipt refresh request did fail with error: \(error)")
    // for some clues see here: https://samritchie.net/2015/01/29/the-operation-couldnt-be-completed-sserrordomain-error-100/
}

这部分验证应用收据。这不是本地验证。请参阅注释中的注释 1 和注释 2。

func validateAppReceipt(_ receipt: Data) {

    /*  Note 1: This is not local validation, the app receipt is sent to the app store for validation as explained here:
            https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1
        Note 2: Refer to the url above. For good reasons apple recommends receipt validation follow this flow:
            device -> your trusted server -> app store -> your trusted server -> device
        In order to be a working example the validation url in this code simply points to the app store's sandbox servers.
        Depending on how you set up the request on your server you may be able to simply change the 
        structure of requestDictionary and the contents of validationURLString.
    */
    let base64encodedReceipt = receipt.base64EncodedString()
    let requestDictionary = ["receipt-data":base64encodedReceipt]
    guard JSONSerialization.isValidJSONObject(requestDictionary) else {  print("requestDictionary is not valid JSON");  return }
    do {
        let requestData = try JSONSerialization.data(withJSONObject: requestDictionary)
        let validationURLString = "https://sandbox.itunes.apple.com/verifyReceipt"  // this works but as noted above it's best to use your own trusted server
        guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return }
        let session = URLSession(configuration: URLSessionConfiguration.default)
        var request = URLRequest(url: validationURL)
        request.httpMethod = "POST"
        request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
        let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in
            if let data = data , error == nil {
                do {
                    let appReceiptJSON = try JSONSerialization.jsonObject(with: data)
                    print("success. here is the json representation of the app receipt: \(appReceiptJSON)")
                    // if you are using your server this will be a json representation of whatever your server provided
                } catch let error as NSError {
                    print("json serialization failed with error: \(error)")
                }
            } else {
                print("the upload task returned an error: \(error)")
            }
        }
        task.resume()
    } catch let error as NSError {
        print("json serialization failed with error: \(error)")
    }
}

你应该得到这样的结果。在您的情况下,这就是您将在服务器上使用的内容。

{
    environment = Sandbox;
    receipt =     {
        "adam_id" = 0;
        "app_item_id" = 0;
        "application_version" = "0";  // for me this was showing the build number rather than the app version, at least in testing
        "bundle_id" = "com.yourdomain.yourappname";  // your app's actual bundle id
        "download_id" = 0;
        "in_app" =         (
        );
        "original_application_version" = "1.0"; // this will always return 1.0 when testing, the real thing in production.
        "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
        "original_purchase_date_ms" = 1375340400000;
        "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
        "receipt_creation_date" = "2016-09-21 18:46:39 Etc/GMT";
        "receipt_creation_date_ms" = 1474483599000;
        "receipt_creation_date_pst" = "2016-09-21 11:46:39 America/Los_Angeles";
        "receipt_type" = ProductionSandbox;
        "request_date" = "2016-09-22 18:37:41 Etc/GMT";
        "request_date_ms" = 1474569461861;
        "request_date_pst" = "2016-09-22 11:37:41 America/Los_Angeles";
        "version_external_identifier" = 0;
    };
    status = 0;
}

如果您想在应用内测试,请进入沙盒环境进行收据验证,请考虑沙盒更新间隔为

1周3分钟 1个月5分钟 2个月10分钟 3 个月 15 分钟 6 个月 30 分钟 1年1小时

验证收据的最佳方法是将您的服务器与苹果服务器进行通信以进行验证。

适用于iOS13

以下是在没有任何服务器代码的情况下在设备上验证收据的步骤:

You need password before verifying the receipt. It would be the shared secret key.

如何生成:

转到 -> iTunes 连接进入 "Contracts, Tax, and Banking" 并单击 iOs 付费应用程序合同上的 "Request",然后接受合同。

访问这个link

https://appstoreconnect.apple.com

1:- 点击功能

2:- 点击应用内购买并创建您的订阅包

3:- 创建订阅成功后点击 App-Specific Shared Secret

4:- 生成特定于应用程序的共享密钥


更新代码以验证应用内订阅收据:

-(void) verifyReceipt
{
/* Load the receipt from the app bundle. */

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

if (!receipt) {
    /* No local receipt -- handle the error. */
}

/* Create the JSON object that describes the request */
NSError *error;

/* reciept data and password to be sent, password would be the Shared Secret Key from Apple Developer account for given app. */
NSDictionary *requestContents = @{
                                  @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                 ,@"password": @"2008687bb49145445457ff2b25e9bff3"};

NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                      options:0
                                                        error:&error];

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

// Create a POST request with the receipt data.
NSURL *storeURL = [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 on a background queue. */
//NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:storeRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    // handle request error
    if (error) {
        //completion(nil, error);
        return;
    } else {
        NSError *error;
        NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

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

        /* ... Send a response back to the device ... */
    }
}];
[dataTask resume];
}

希望对您有所帮助

谢谢