Client-side implementation should use StoreKit2
. For server-side notifications, refer to Apple’s official documentation.
Database Design (Simplified)
# Order Table
mysql> desc orders;
+------------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| no | varchar(255) | NO | UNI | NULL | |
| trade_no | varchar(255) | YES | | NULL | |
| status | tinyint(4) | NO | | NULL | |
| origin_no | varchar(255) | YES | | NULL | |
+------------------+---------------------+------+-----+---------+----------------+
# User Subscription Contracts Table
mysql> desc member_contracts;
+-----------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+---------------------+------+-----+---------+-------+
| user_id | bigint(20) unsigned | NO | PRI | NULL | |
| status | tinyint(4) | NO | | NULL | |
| contract_id | varchar(255) | NO | MUL | NULL | |
+-----------------+---------------------+------+-----+---------+-------+
Server-Side Processing Logic
- Resubscription after expiration changes both
appAccountToken
andoriginalTransactionId
(after subscription cancellation and expiration) - Resubscription during active period retains the same
appAccountToken
andoriginalTransactionId
Client-Side Order Creation
## Client requests order creation API to generate order number `$uuid`
## Return this value to client for `appAccountToken` usage
INSERT INTO orders(no) VALUES('$uuid');
Subscription Event Callback
- Handling Apple Server
notificationV2
callback && client-side verification (a JWS string)
## Parse server callback, verify notificationType=SUBSCRIBED and subtype in (INITIAL_BUY, RESUBSCRIBE, AUTO_RENEW_ENABLED)
## Key fields: appAccountToken, transactionId, originalTransactionId, expiresDate
## 1. Check if appAccountToken order exists
select * from orders where no='{$appAccountToken}' limit 1 => $id;
## 2. Update order status and payment information
update orders set trace_no='$transactionId', origin_no='$originalTransactionId' where id=$id;
## 3. Update user subscription status
update member_contracts set contract_id='$originalTransactionId';
## ... Additional membership benefits processing
Renewal Event Handling
## notificationType=DID_RENEW
## 1. Check if transactionId exists
select * from orders where trade_no='{$transactionId}' limit 1 => $id
## 2. If not exists, find subscription via originalTransactionId
select * from member_contracts where contract_id='$originalTransactionId' limit 1;
## 3. Create new paid order record
INSERT INTO orders(no, trade_no, origin_no) VALUES('$uuid', '$transactionId', '$originalTransactionId');
## ... Membership extension processing
Unsubscription Event
update member_contracts set status=xxx where contract_id='$originalTransactionId';
Client-Side Verification
- The JWS string from client callback contains identical fields to Apple server callback’s
.Data.SignedTransactionInfo
- Reuse the same event handling logic for both server callbacks and client verification
// Retrieve product information
products = try await Product.products(for: Set(productIds))
// Initiate payment with server-generated UUID
let uuid = Product.PurchaseOption.appAccountToken(UUID.init(uuidString: "$uuid")!)
let result = try await product.purchase(options: [uuid])
// Handle payment result
switch result {
case .success(let verificationResult):
// Send verificationResult to server for immediate validation
// (Avoid waiting for Apple server callback delay)
case .userCancelled:
// Handle cancellation
case .pending:
// Handle pending transactions via Transaction.updates
}
Additional Event Handling
Implement corresponding logic for other notification types (DID_FAIL_TO_RENEW
, REFUND
, etc.) as needed.