iOS 如何获取已取消或变更订阅的 receiptData
结论
可以获取,但 iOS 没有与 Android PurchaseHistoryRecord 完全对应的接口,也不能为某一笔历史订阅单独获取 receiptData。
在 StoreKit 1 中,receiptData 是整个 App Receipt 的二进制数据。将它发送到服务端验证后,可以从验证结果中读取自动续期订阅的历史交易,包括:
- 已过期或已关闭自动续订的订阅
- upgrade、downgrade 或 crossgrade 产生的订阅变更
- 续订、退款和撤销等交易状态
使用 StoreKit 2 时,可以直接读取签名交易记录,也可以由服务端通过 App Store Server API 查询订阅历史,不必再依赖客户端解析传统 receipt。
StoreKit 1:读取并刷新 App Receipt
先读取应用包中的 App Receipt:
import Foundation
func loadReceiptData() -> Data? {
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
return nil
}
return try? Data(contentsOf: receiptURL)
}
if let receiptData = loadReceiptData() {
let base64Receipt = receiptData.base64EncodedString()
// 将 base64Receipt 发送到自己的服务端验证
}
如果本地没有 receipt,可以通过 SKReceiptRefreshRequest 刷新:
import StoreKit
final class ReceiptRefresher: NSObject, SKRequestDelegate {
private var request: SKReceiptRefreshRequest?
func refresh() {
let request = SKReceiptRefreshRequest()
request.delegate = self
self.request = request
request.start()
}
func requestDidFinish(_ request: SKRequest) {
guard
let receiptURL = Bundle.main.appStoreReceiptURL,
let receiptData = try? Data(contentsOf: receiptURL)
else {
return
}
let base64Receipt = receiptData.base64EncodedString()
// 将 base64Receipt 发送到自己的服务端
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Receipt refresh failed: \(error)")
}
}
刷新 receipt 时,系统可能要求用户登录 Apple ID。应用不应反复刷新 receipt,以此静默查询用户的购买历史。
如何识别历史订阅
传统 App Receipt 不是某一笔订阅的 token,而是一份包含多笔内购交易的凭证。服务端完成验证后,可以根据这些字段整理订阅历史:
original_transaction_id:标识同一条订阅关系transaction_id:标识某一次具体交易或续订product_id:订阅商品purchase_date_ms:购买时间expires_date_ms:到期时间cancellation_date_ms:退款或撤销时间,不等同于用户关闭自动续订web_order_line_item_id:自动续期订阅交易链中的项目标识
例如,先按 original_transaction_id 分组,再按购买时间或到期时间排序:
struct SubscriptionTransaction: Decodable {
let productId: String
let transactionId: String
let originalTransactionId: String
let purchaseDateMs: String
let expiresDateMs: String?
enum CodingKeys: String, CodingKey {
case productId = "product_id"
case transactionId = "transaction_id"
case originalTransactionId = "original_transaction_id"
case purchaseDateMs = "purchase_date_ms"
case expiresDateMs = "expires_date_ms"
}
}
整理自动续期订阅的续订历史时,验证结果中的 latest_receipt_info 通常比只检查顶层的 receipt.in_app 更合适。不过,具体返回哪些内容,取决于 receipt 中已有的交易和验证接口的响应。不能假设它一定包含该 Apple ID 在所有应用、所有时间产生的完整购买记录。
StoreKit 2:读取交易历史
如果应用使用 StoreKit 2,可以通过 Transaction.all 遍历当前 App 能够访问的交易:
import StoreKit
func loadTransactionHistory() async {
for await verificationResult in Transaction.all {
switch verificationResult {
case .verified(let transaction):
print("Product:", transaction.productID)
print("Transaction:", transaction.id)
print("Original transaction:", transaction.originalID)
print("Purchase date:", transaction.purchaseDate)
print("Expiration date:", transaction.expirationDate as Any)
print("Revocation date:", transaction.revocationDate as Any)
case .unverified(let transaction, let error):
print(
"Unverified transaction:",
transaction.id,
"Error:",
error
)
}
}
}
Transaction.currentEntitlements 只用于判断当前有效的权益:
for await result in Transaction.currentEntitlements {
// 处理当前仍有效的权益
}
查询历史交易时,应使用 Transaction.all,不能只使用 Transaction.currentEntitlements。
读取交易后必须检查验证结果,不要根据 .unverified 交易授予订阅权益。
服务端方案
在生产环境中,更合适的做法是保存 originalTransactionId,并通过 App Store Server API 在服务端查询订阅状态和交易历史。常见流程如下:
- 客户端在首次购买或恢复购买后取得已验证交易。
- 客户端将
originalTransactionId和必要的交易信息发送到服务端。 - 服务端查询订阅状态或交易历史。
- 服务端通过 App Store Server Notifications V2 接收续订、过期、退款、撤销和订阅变更通知。
- 服务端根据签名交易、通知和自身保存的状态计算用户权益。
这样不需要用户每次打开应用时都重新上传 receipt,也便于处理 upgrade、downgrade、退款和跨设备恢复。
Upgrade 和 Downgrade 的处理
发生订阅变更时,不能简单地认为旧订阅已被删除。
在同一 subscription group 中:
- Upgrade 通常会让新方案较快生效,旧方案的剩余权益由 Apple 按订阅规则处理。
- Downgrade 通常在下一个续订周期生效。
- 交易链中仍能看到历史交易,但判断当前权益时,需要结合最新的已验证交易、到期时间、撤销状态和续订状态。
- 不能只根据
cancellation_date_ms判断用户是否主动取消订阅。关闭自动续订通常意味着到期后不再续费,当前订阅在到期前仍可能有效。
注意事项
- App Receipt 属于整个应用,并不是某个订阅专属的
receiptData。 - 应用只能访问自身对应的交易,无法查询其他应用的购买历史。
- 用户必须使用购买过该订阅的 Apple ID,不同 Apple ID 的购买历史不会合并。
- 删除并重新安装应用后,本地 receipt 可能需要刷新,或者通过恢复购买重新建立。
SKPaymentQueue.restoreCompletedTransactions()主要用于恢复可恢复的非消耗型商品和自动续期订阅,不应作为应用每次启动时查询历史的普通接口。- 已过期、已退款、已撤销和已关闭自动续订是不同的状态,需要分别判断。
- 不能只根据客户端数据发放权益。至少需要验证 receipt 或 StoreKit 2 的签名交易。安全要求较高的业务应在服务端完成验证和状态管理。
- 旧项目可能仍在使用 StoreKit 1 的 receipt 验证方案。新项目应优先考虑 StoreKit 2、App Store Server API 和 App Store Server Notifications。
备注:内容仅供参考。
