Implementing iOS In‑App Purchase Subscription Verification and Renewal Handling in PHP
This article walks through the server‑side implementation of iOS In‑App Purchase (IAP) subscription verification, order creation, and automatic renewal handling using PHP, including detailed code for receipt validation, sandbox/production switching, and processing Apple server notifications.
The author shares a practical guide for integrating iOS In‑App Purchase (IAP) subscription payments into a company's app, focusing on the backend logic written in PHP.
Step 1 – Order creation and receipt verification : The client sends a receipt token, which the server validates against Apple’s verification endpoint (sandbox or production). If the receipt is valid, an order is generated and returned to the client.
<code>public function pay()
{
$uid = $this->request->header('uid');
$receipt_data = $this->request->post('receipt');
if (!$uid || !$receipt_data) return $this->rep(400);
$info = $this->getReceiptData($receipt_data, $this->isSandbox); // verify with Apple
Log::info(['uid'=>$uid,'receipt'=>$receipt_data,'iap_info'=>$info]);
if (is_array($info) && $info['status'] == 0) {
// process order creation logic
} elseif (is_array($info) && $info['status'] == 21007) {
$new_info = $this->getReceiptData($receipt_data, true); // sandbox re‑verify
// process order creation logic
}
}</code> <code>private function getReceiptData($receipt, $isSandbox = false)
{
if ($isSandbox) {
$endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt'; // sandbox
} else {
$endpoint = 'https://buy.itunes.apple.com/verifyReceipt'; // production
}
$postData = json_encode(['receipt-data' => $receipt, 'password' => 'abde7d535c']);
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$response = curl_exec($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno != 0) {
$order['status'] = 408; // curl error
} else {
$data = json_decode($response, true);
if (isset($data['status'])) {
$order = isset($data['receipt']) ? $data['receipt'] : [];
$order['status'] = $data['status'];
} else {
$order['status'] = 30000;
}
}
return $order;
}</code>Step 2 – Automatic subscription renewal handling : Apple sends server‑to‑server notifications for renewal events. The backend provides an endpoint that parses the notification, validates the shared secret, extracts the latest receipt information, and records renewal or cancellation events.
<code>/* Automatic renewal subscription callback */
public function renew()
{
$resp_str = $this->request->post();
Log::info(['resp_str'=>$resp_str]);
if (!empty($resp_str)) {
$data = $resp_str['unified_receipt'];
$notification_type = $resp_str['notification_type']; // e.g., INITIAL_BUY, RENEWAL, CANCEL
$password = $resp_str['password'];
if ($password == "abde7d5353") {
$receipt = isset($data['latest_receipt_info']) ? $data['latest_receipt_info'] : $data['latest_expired_receipt_info'];
$receipt = self::arraySort($receipt, 'purchase_date', 'desc');
$original_transaction_id = $receipt['original_transaction_id'];
$transaction_id = $receipt['transaction_id'];
$purchaseDate = str_replace(' America/Los_Angeles','',$receipt['purchase_date_pst']);
$orderinfo = Order::field('uid,original_transaction_id,money,order_no,pay_time')
->where(['original_transaction_id' => $original_transaction_id])->find();
$user_info = User::field('app_uid,device_id,unionid')->get($orderinfo['uid']);
if ($notification_type == 'CANCEL') {
IpaLog::addLog($orderinfo['uid'], $orderinfo['order_no'], $receipt, $resp_str);
} else {
if (in_array($notification_type, ['INTERACTIVE_RENEWAL','RENEWAL','INITIAL_BUY'])) {
IapRenew::addRenew($orderinfo['uid'], $receipt, $data['latest_receipt'], 1, $notification_type, $user_info['app_uid'], $purchaseDate);
} else {
IapRenew::addRenew($orderinfo['uid'], $receipt, $data['latest_receipt'], 0, $notification_type, $user_info['app_uid'], $purchaseDate);
}
IpaLog::addLog($orderinfo['uid'], $orderinfo['order_no'], $receipt, $resp_str);
}
} else {
Log::info('通知传递的密码不正确--password:' . $password);
}
}
}
private function toTimeZone($src, $from_tz = 'Etc/GMT', $to_tz = 'Asia/Shanghai', $fm = 'Y-m-d H:i:s')
{
$datetime = new \DateTime($src, new \DateTimeZone($from_tz));
$datetime->setTimezone(new \DateTimeZone($to_tz));
return $datetime->format($fm);
}
private static function arraySort($arr, $key, $type='asc')
{
$keyArr = [];
foreach ($arr as $k=>$v){
$keyArr[$k] = $v[$key];
}
if($type == 'asc'){
asort($keyArr);
} else {
arsort($keyArr);
}
foreach ($keyArr as $k=>$v){
$newArray[$k] = $arr[$k];
}
$newArray = array_merge($newArray);
return $newArray[0];
}</code>The article concludes with a reminder to share and bookmark the guide.
php中文网 Courses
php中文网's platform for the latest courses and technical articles, helping PHP learners advance quickly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.