Alfonso Tesauro
Alfonso Tesauro

Reputation: 1792

In App Purchase receipt validation with trusted server. Any ideas why I am not getting the latest receipt from the server?

I am developing a iOS application in swift that uses In App Purchase. I have only one product, which is an auto-renewable subscription, that also provides a 7 days trial. According to most of the documentation I have found, first of all Apple docs, the best strategy to follow is to use a trusted-server for receipt validation. I have set up everything in my project and on my trusted server, and the communication now works, at least without errors. My code in the app is the following:

func performServerToServerValidation() {

guard let receiptUrl = Bundle.main.appStoreReceiptURL,
   let receiptData = try? Data(contentsOf: receiptUrl)
   else {
            return
    }

let baseUrlString = "https://<my.trusted.server>/validation/verifyReceipt.php"

let theRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: baseUrlString, parameters: nil, error: nil)

theRequest.timeoutInterval = 20.0

theRequest.httpBody = receiptData.base64EncodedString().data(using: String.Encoding.utf8)

operation = AFHTTPRequestOperation(request: theRequest as URLRequest)

operation?.responseSerializer = AFHTTPResponseSerializer()

var dict: Dictionary<String,Any>!

operation?.setCompletionBlockWithSuccess({ operation, responseObject in
        if let responseObject = responseObject as? Data {

    do {
        dict = try JSONSerialization.jsonObject(with: responseObject,                           options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, Any>

        print("Finished connection with the trusted server")

        if let receiptBase64String = dict["latest_receipt"] as? String {

           let decodedData = Data(base64Encoded: receiptBase64String)!

           let newReceipt = Receipt.init(data: decodedData)

           print("\(newReceipt!)")

           self.validateReceipt(receipt: newReceipt)

           self.analyzeInAppPurchasesInReceipt()
     }
   } catch {

   }

}

})

// 5
operation?.start()

}

My code on the server is the following:

<?php
function getReceiptData($receipt)
{
    $fh = fopen('showme.txt',w);
    fwrite($fh,$receipt);
    fclose($fh);
    $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';

    $ch = curl_init($endpoint);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $receipt);
    $response = curl_exec($ch);
    $errno = curl_errno($ch);
    $errmsg = curl_error($ch);
    curl_close($ch);
    $msg = $response.' - '.$errno.' - '.$errmsg;
    echo $response;
}

foreach ($_POST as $key=>$value){
$newcontent .= $key.' '.$value;
}

$new = trim($newcontent);
$new = trim($newcontent);
$new = str_replace('_','+',$new);
$new = str_replace(' =','==',$new);

if (substr_count($new,'=') == 0){
  if (strpos('=',$new) === false){
    $new .= '=';
  }
}

$new = '{"receipt-data":"'.$new.'","password":"<my_shared_secret>"}';
$info = getReceiptData($new);

?>

Now comes my problem. I would like to take advantage of the server-in-the-middle, for example to determine if a user has subscribed on another device, by using the server validation. My testing flow is as follows:

1) I have two devices, A and B.

2) I delete any existing app build from both devices. Ok.

3) I create a new sandbox user on iTunesConnect, and I use that to log in on both device's sandbox account in settings. Ok.

4) I launch the app from Xcode on device A, as it is the first launch it does not have a receipt, so it gets one through SKRefreshReceiptRequest. Ok.

5) I launch the app from Xcode on device B, and it also does not have a receipt since it is the first launch, so it gets one through SKRefreshReceiptRequest. Ok.

6) I buy the subscription on device B. Features are delivered immediately. All Ok.

7) Now, I launch the app again on device A. It has a receipt (we launched it at point 4)) but that receipt is outdated because it does not include the product I have purchased on device B. PerformServerToServerValidation is called, but, unfortunately, the returned JSON does not contain a "latest_receipt" field. it only contains the "receipt", "status", and "environment" keys:

Xcode Debugger Screenshot

So I cannot ascertain if the subscription has already been purchased on another device. I must be missing something important, but shouldn't be one of the advantages of Server receipt validation to have always the latest receipt ? One important thing to know is that if I restore purchases or try to purchase again, then, if I call the server validation code, it works perfectly and has the "latest_receipt" key. But what is the sense of this all ? I would like to use the server validation to avoid to force the user to do a restore purchases or purchase the item again, even if not charged. Thanks for any help.

Upvotes: 0

Views: 272

Answers (1)

Paulw11
Paulw11

Reputation: 114826

The purpose of server-side receipt validation is to protect yourself against users injecting a forged receipt. The server simply validates the receipt that you send it. It doesn't use the receipt you send to check if there is a later corresponding receipt available.

The flow you describe, where you need to restore purchases on Device B, is the expected process.

You may be confusing server-side receipt validation with server notifications, where Apple can send your server subscription status updates

Upvotes: 2

Related Questions