Skip to content

Instantly share code, notes, and snippets.

@jamesstout
Last active August 5, 2021 08:49
Show Gist options
  • Save jamesstout/5073237 to your computer and use it in GitHub Desktop.
Save jamesstout/5073237 to your computer and use it in GitHub Desktop.
Verify iOS in-app purchase receipts
<?php
include("/var/www/vhosts/xxxxxcom/httpdocs/PHPUtils/dbConfig.php");
include("/var/www/vhosts/xxxxxcom/httpdocs/PHPUtils/DButils.php");
// verifies receipt from iOS in-app purchase
// returns:
// 0 - if params missing
// 1 - if receipt is valid
// 2 - if invalid receipt, or invalid response from verification server
// or bundle/in-app IDs are incorrect
// or if transaction ID has been used before
// get the parmas from the URL
$testing = htmlspecialchars($_GET["testing"]);
$receiptData = $_GET["receipt"];
if ( $testing == "" or $receiptData == "" ){
echo "0";
exit;
}
$testing = (bool)$testing;
// verify the receipt
try {
// quick check
if(strpos($receiptData,'{') !== false){
$receiptData = base64_encode($receiptData);
}
$info = getReceiptData($receiptData, $testing);
if(checkInfo($info) == true){
echo "1";
}
else{
echo "2";
}
}
catch (Exception $ex) {
// unable to verify receipt, or receipt is not valid
echo "2";
}
exit;
/**
* Verify a receipt and return receipt data
*
* @param string $receipt Base-64 encoded data
* @param bool $isSandbox Optional. True if verifying a test receipt
* @throws Exception If the receipt is invalid or cannot be verified
* @return array Receipt info (including product ID and quantity)
*/
function getReceiptData($receipt, $isSandbox = false)
{
// determine which endpoint to use for verifying the receipt
if ($isSandbox) {
$endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
}
else {
$endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
}
// build the post data
$postData = json_encode(
array('receipt-data' => $receipt)
);
// create the cURL request
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
// execute the cURL request and fetch response data
$response = curl_exec($ch);
$errno = curl_errno($ch);
$errmsg = curl_error($ch);
curl_close($ch);
// ensure the request succeeded
if ($errno != 0) {
throw new Exception($errmsg, $errno);
}
// parse the response data
$data = json_decode($response);
// ensure response data was a valid JSON string
if (!is_object($data)) {
throw new Exception('Invalid response data');
}
// ensure the expected data is present
if (!isset($data->status) || $data->status != 0) {
throw new Exception('Invalid receipt');
}
// build the response array with the returned data
return array(
'quantity' => $data->receipt->quantity,
'status' => $data->status,
'product_id' => $data->receipt->product_id,
'transaction_id' => $data->receipt->transaction_id,
'original_transaction_id' => $data->receipt->original_transaction_id,
'purchase_date' => $data->receipt->purchase_date,
'bid' => $data->receipt->bid,
'bvrs' => $data->receipt->bvrs
);
}
/**
* Check transactionID has not been used before
*
* @param string $transactionID transactionId to check
* @return bool true if new ID (or there is an error, for safety), false if not
*/
function checkTransactionID($transactionID)
{
/*
from PHPUtils/dbConfig.php
assoc array containing db connection info
e.g.
$dbConfig['xxxxxx']['server'] = 'localhost';
$dbConfig['xxxxxx']['user'] = 'username';
$dbConfig['xxxxxx']['password'] = 'password';
$dbConfig['xxxxxx']['database'] = 'database';
*/
global $dbConfig;
/*
from PHPUtils/DButils.php
just does a mysqli_connect with the data from $dbConfig
with some error checking
*/
$link = connectToDBMYSQLI($dbConfig['xxxxxx'], __FILE__, true);
if (!$link) {
// for safety, return true, in case db is down
return true;
}
else{
// in the iOS app, store successful transactions to a db
$sql = "select count(*) as `tranCount` from `ReceiptsTable` where `transactionID` = '$transactionID'";
$res = mysqli_query($link, $sql);
if ($res) {
$row = mysqli_fetch_assoc($res);
if($row[tranCount] == 0){
// this is what we want
return true;
}
else{
// transactionID already used - a pirate!
return false;
}
}
else{
// for safety, return true, in case db is down
return true;
}
mysqli_close($link);
}
// for safety, return true, shouldn't get here
return true;
}
/**
* Check receipt for valid in-appProductID, appBundleID and unique transactionID
*
* @param array $infoArray Array returned from getReceiptData()
* @return bool true if all OK, false if not
*/
function checkInfo($infoArray)
{
$inAppPurchaseID = "com.xxxx.xxxx.inappid";
$bundleID = "com.xxxxxx.appid";
if($infoArray[product_id] != $inAppPurchaseID){
return false;
}
if($infoArray[bid] != $bundleID){
return false;
}
if(checkTransactionID($infoArray[transaction_id]) == false){
return false;
}
// probably won't get here - getReceiptData throws Ex on status == 0
if($infoArray[status] != 0){
return false;
}
return true;
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment