PAYATHON 2026

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 在服务端查询订阅状态和交易历史。常见流程如下:

  1. 客户端在首次购买或恢复购买后取得已验证交易。
  2. 客户端将 originalTransactionId 和必要的交易信息发送到服务端。
  3. 服务端查询订阅状态或交易历史。
  4. 服务端通过 App Store Server Notifications V2 接收续订、过期、退款、撤销和订阅变更通知。
  5. 服务端根据签名交易、通知和自身保存的状态计算用户权益。

这样不需要用户每次打开应用时都重新上传 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。

备注:内容仅供参考。