web-dev-qa-db-ja.com

iOSテストアプリのレシート検証

サンドボックステスターアカウントを使用してアプリ内購入の領収書検証をテストする方法については、多くの例があります。

しかし、有料アプリ自体の領収書はどうですか?開発環境でアプリの領収書を取得するにはどうすればよいですか?

やりたいことが2つあります。

  • アプリを購入しなかったユーザーが実行するアプリの違法コピーを防ぐため。 iTuneアカウントが接続されたことを検出したアプリがアプリを所有していないことを確認しました(アプリを所有していないユーザーには警告を表示しますが、アプリを使用し続けるためにユーザーを停止できません)

  • アプリの購入領収書をサーバーに送信します。私たちはいつ彼らが私たちのアプリを買うのか、彼らが持ってきたアプリのバージョンを知りたいのです。

16
King Chan

答えの大部分は here Appleのドキュメントにあります。しかし、ギャップがあり、objective-cコードは非推奨のメソッドを使用しています。

このSwift 3コードは、アプリの領収書を取得し、検証のためにアプリストアに送信する方法を示しています。必要なデータを保存する前に、アプリストアでアプリの領収書を必ず検証する必要があります。アプリストアに検証を依頼するのは、JSONに簡単にシリアル化できるデータで応答し、そこから必要なキーの値を引き出すことです。暗号化は不要です。

As Appleはその文書で、優先フローは次のように記述されています...

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

アプリストアがサーバーに戻ったら、成功を想定して、ここで必要なデータをシリアル化して引き出し、必要に応じて保存します。以下の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;
}
_
30
Murray Sagal

InAppの購入方法を知っていることを前提としています。

トランザクションが終了した後、領収書を検証する必要があります。

- (void)completeTransaction:(SKPaymentTransaction *)transaction 
{
    NSLog(@"completeTransaction...");

    [appDelegate setLoadingText:VALIDATING_RECEIPT_MSG];
    [self validateReceiptForTransaction];
}

製品が正常に購入されたら、検証する必要があります。サーバーがこれを行います。Apple server。

-(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オブジェクトです。

status:

領収書が有効な場合は0、または以下のエラーコードのいずれか:

enter image description here

IOS 6スタイルのトランザクション領収書の場合、ステータスコードは特定のトランザクションの領収書のステータスを反映します。

IOS 7スタイルのアプリの領収書の場合、ステータスコードはアプリの領収書全体のステータスを反映しています。たとえば、期限切れのサブスクリプションを含む有効なアプリ領収書を送信した場合、領収書全体が有効であるため、応答は0です。

領収書:

検証のために送信された領収書のJSON表現。

覚えておいてください:

  • サンドボックス環境でのレシート検証の成功のために、ステイタスコード21007を取得します。

  • テスト環境では、URLとして https://sandbox.iTunes.Apple.com/verifyReceipt を使用します。本番環境では、 https://buy.iTunes.Apple.com/verifyReceipt をURLとして使用します。

  • サンドボックス環境での購入をテストするには、iTunes Connectでテストユーザーアカウントを設定する必要があります。

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. */
    }

}
5
NSPratik

アプリ内でテストする場合は、受信確認のためにサンドボックス環境に移動し、サンドボックスでは更新間隔が

1週間3分1か月5分2か月10分3か月15分6か月30分1年1時間

レシートを検証する最善の方法は、検証のためにサーバーをAppleサーバーと通信することです。

0
Shubham kapoor