StoreKit2 Transaction交易
StoreKit2 Transaction交易

StoreKit2 Transaction交易

Transaction是StoreKit 2的一个结构体,负责管理App Store每一笔交易凭证。每个用户应用内购买,经过App Store服务验证后,都会返回一个Transaction交易凭证。

它包含:userID(隐式)、productID(产品 ID)、purchaseDate(购买时间)、expirationDate(订阅到期时间)、revocationDate(退款时间)、ownershipType(家庭共享 or 自己买的)、appAccountToken, offerID, renewalInfo 等等字段,记录所有的交易细节。

因为 Apple 的凭证是加密的,不能直接显示,必须使用 StoreKit 提供的验证逻辑:

let verified = try checkVerified(transaction)

验证通过的Transaction才能使用,保证交易数据不被篡改、不是越狱的FakeStoreKit、不是设备伪造,而是官方 App Store 真实发生的交易收据。

常用参数

1、productID:商品 ID,例如 “com.example.app.premium”。

2、purchaseDate:购买时间。

3、expirationDate(订阅才有):订阅到期时间,用它判断用户是否仍然是会员。

Date() < expirationDate → 订阅有效

3、revocationDate:如果用户退款,系统会填上退款时间。

4、revocationReason:退款原因(用户主动退款、问题退款等)。

5、id:交易ID。

6、originalID:首次购买的交易ID(订阅续费会发生变化)。

7、ownershipType:是自己买的,还是家庭共享得到的,通常是 .purchased 和 .familyShared。

8、subscriptionGroupID:订阅组。

9、signedDate:交易签名生成时间,一般和 purchaseDate 几乎一致。

10、renewalInfo:订阅续费的信息,包含是否自动续订、下次续费时间、使用的优惠。

renewalInfo不是Transaction的属性,需要单独查询:

let renewalInfo = try await transaction.renewalInfo

11、appAccountToken:用户ID,识别哪个用户购买。

12、environment:购买环境,通常为 .production 和 .sandbox。

13、deviceVerification:关于设备的安全校验信息。

14、storefrontCountryCode:用户购买地区,例如“USA”,“CHN”,用于地区价格差异判断。

15、signedTransactionInfo:交易凭证的完整结构(包含所有加密字段)。

16、productType:商品类型。通常为 .nonConsumable(非消耗型)、.consumable(消耗型)、.autoRenewable(自动订阅)和 . nonRenewable(非自动订阅),用来判断买断或订阅商品。

使用方法

1、finish():表示已经处理完成这笔交易。如果不调用,交易会反复回调。

await transaction.finish()

2、verifying(隐式)

let verified = try checkVerified(result)

Transaction 自身带有加密数据,需要通过 StoreKit 的 Verification API 验证。

3、beginRefundRequest(in:):在应用内发起退款流程(iOS 15.5+)。

系统会弹出Apple官方退款界面。

静态属性

1、Transaction.updates:实时推送新交易(购买、续费、退款)。

for await update in Transaction.updates {
    let verified = try checkVerified(update)
}

系统会要求必须调用 updates 属性,否则会漏掉关键状态变化。

2、Transaction.all:用户的历史购买记录(用于恢复购买)。

for await transaction in Transaction.all { ... }

包含当前账号下所有“仍然有效”的交易,例如有效订阅、已过期订阅、已退款/撤销交易、永久购买(非消耗型)。

它可能不包含消耗型商品(未实际验证)。

Transaction.updates和Transaction.all区别

1、Transaction.updates用于处理后台交易:例如当订阅的商品自动订阅时,updates方法会自动处理订阅的变化。

如果用户的订阅商品到期后,自动续订该商品。updates会自动接收新的续订周期,可以根据订阅商品的expirationDate,处理新的内购商品到期时间。

如果用户退款,updates也会自动获取退款交易,可以根据内购商品revocationDate退款日期,移除内购商品标识。

如果不使用updates方法,那么当订阅商品自动续费或者退款时,用户就无法了解到这一实时交易变化,因此这一方法是 StoreKit规定必须实现的。

2、Transaction.all用户处理历史交易记录:例如用户点击“恢复购买”按钮时,可以根据这一方法获取所有的历史交易记录。

如果用户需要恢复一个非消耗性商品,可以根据Transaction.all遍历所有的商品交易,如果有非消耗性商品交易记录,则恢复内购标识。

Transaction.updates和Transaction.all指责不同,updates用于实时监听商品的订阅、退款等交易记录,对于非实时的交易记录,则通常使用all来获取。

注意事项

1、checkVerified验证

Transaction必须使用checkVerified方法进行验证:

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
    case .unverified:    // unverified校验失败,StoreKit不能确定交易有效
        print("校验购买结果失败")
        throw StoreError.failedVerification
    case .verified(let signedType):    // verfied校验成功
        print("校验购买结果成功")
        return signedType    // StoreKit确认本笔交易信息由苹果服务器合法签署
    }
}

例如,使用Transaction.updates监听实时交易:

func handleTransactions() async {
    for await result in Transaction.updates {
        do {
            let transaction = try checkVerified(result) // 验证交易
            updatePurchasedState(from: transaction)
            await transaction.finish()
        } catch {
            print("交易处理失败:\(error)")
        }
    }
}

使用for-in获取实时交易的VerificationResult<Transaction>,但是Apple发送的Transaction凭证是加密的,不能直接使用。

必须使用 StoreKit提供的验证逻辑:

let verified = try checkVerified(transaction)

验证通过后的Transaction才能使用。

2、Transaction.finish()

当成功购买后,必须调用Transaction.finish()方法:

func handleTransactions() async {
    for await result in Transaction.updates {
        do {
            let transaction = try checkVerified(result)
            updatePurchasedState(from: transaction) // 处理交易
            await transaction.finish()  // 完成交易
        } catch {
            print("交易处理失败:\(error)")
        }
    }
}

Transaction.finish() 方法标识这笔交易已经处理完毕。

如果处理交易后,不调用Transaction.finish() 方法,就会到重复调用商品交易。

3、Transaction.updates()

App 必须在启动时,调用 Transaction.updates() 方法,监听交易变化。

@main
struct pigletApp: App {
  @State private var appStorage = AppStorageManager.shared
  var body: some Scene {
    WindowGroup {
        ContentView()
            .task {
                await iapManager.loadProduct()   // 加载产品信息
                await iapManager.handleTransactions()   // 监听交易变化
            }
    }
  }
}

AppStorageManager是Transaction的管理类,管理整个交易流程,在App入口文件中,设置handleTransactions方法监听交易变化。

func handleTransactions() async {
  for await result in Transaction.updates { // 监听交易变化
    do {
        let transaction = try checkVerified(result) // 验证交易
        updatePurchasedState(from: transaction) // 处理交易
        await transaction.finish()  // 完成交易
    } catch {
        print("交易处理失败:\(error)")
    }
  }
}

handleTransactions方法包含Transaction.updates、处理交易流程和完成交易。

如果不使用Transaction.updates,Xcode会提示报错:

Making a purchase without listening for transaction updates risks missing successful purchases. Create a Task to iterate Transaction.updates at launch.

该提示是关于应用内购买 (IAP) 的警告信息,表明在应用启动时没有监听 Transaction.updates,可能会导致错过用户在后台成功完成的购买。为了确保不会漏掉任何交易,应该在应用启动时设置一个 Task 来监听 Transaction.updates。

4、沙盒环境中的订阅周期

沙盒环境(Sandbox)中,订阅商品的周期是加速的。它会将“1个月订阅”压缩为几分钟,常见规则:

AppStore订阅时长:1周(7天)订阅,沙盒环境实际时长:3分钟。

AppStore订阅时长:1月订阅,沙盒环境实际时长:5分钟。

AppStore订阅时长:2月订阅,沙盒环境实际时长:10分钟。

AppStore订阅时长:3月订阅,沙盒环境实际时长:15分钟。

AppStore订阅时长:6月订阅,沙盒环境实际时长:30分钟。

AppStore订阅时长:1年订阅,沙盒环境实际时长:1小时。

沙盒环境下,Xcode做的是“诈骗式刷新”, 为了方便订阅商品时间的测试

沙盒中的订阅商品周期很快。

上线到正式环境后,它会严格按照自然时间返回。

总结

Transaction是App Store的交易凭证,它记录了商品ID、购买时间、订阅到期时间、退款情况等信息。

Transaction.updates:实时推送新交易(购买、续费、退款)。

Transaction.all:一次性读取所有历史交易,用于恢复购买。

App 要在启动时检查历史(all),运行中监听变化(updates)。

判断订阅商品有效性主要看:expirationDate(订阅) 和 revocationDate(退款)。

相关文章

1、iOS通过StoreKit 2实现应用内购:https://fangjunyu.com/2024/05/30/ios%e5%ae%9e%e7%8e%b0%e5%ba%94%e7%94%a8%e5%86%85%e8%b4%ad/

2、StoreKit2结构体Product:https://fangjunyu.com/2025/07/16/storekit2%e7%bb%93%e6%9e%84%e4%bd%93product/

扩展知识

Transaction交易JSON

1、订阅商品:

订阅商品信息:{
  "appTransactionId" : "704934904065052961",
  "bundleId" : "com.fangjunyu.piglet",
  "currency" : "CNY",
  "deviceVerification" : "iDfuEAMmwI0FzcsLDSXIgSqq9Z1Rnt+krlAXxHhOYVdgIElB1U95UMi+QBAAdSHY",
  "deviceVerificationNonce" : "972da387-deeb-450b-a1c1-1322f5bc6dde",
  "environment" : "Sandbox",
  "expiresDate" : 1763132646000,
  "inAppOwnershipType" : "PURCHASED",
  "originalPurchaseDate" : 1763129004000,
  "originalTransactionId" : "2000001056943097",
  "price" : 8000,
  "productId" : "com.fangjunyu.Banklet.monthly",
  "purchaseDate" : 1763132346000,
  "quantity" : 1,
  "signedDate" : 1763160207044,
  "storefront" : "CHN",
  "storefrontId" : "143465",
  "subscriptionGroupIdentifier" : "21815913",
  "transactionId" : "2000001056987164",
  "transactionReason" : "RENEWAL",
  "type" : "Auto-Renewable Subscription",
  "webOrderLineItemId" : "2000000118511650"
}

2、非消耗性商品:

订阅商品信息:{
  "appTransactionId" : "704934904065052961",
  "bundleId" : "com.fangjunyu.piglet",
  "currency" : "CNY",
  "deviceVerification" : "Oso3KworFYl6i\/eGuXywAKwE06xcQhvgAc5bat5bC1lewi3Zc2ItGkq7mNx5PH4A",
  "deviceVerificationNonce" : "ce3eb1d7-80c6-415b-975a-ff8ee0e46340",
  "environment" : "Sandbox",
  "inAppOwnershipType" : "PURCHASED",
  "originalPurchaseDate" : 1760429578000,
  "originalTransactionId" : "2000001033622153",
  "price" : 8000,
  "productId" : "20240523",
  "purchaseDate" : 1760429578000,
  "quantity" : 1,
  "signedDate" : 1762413366946,
  "storefront" : "CHN",
  "storefrontId" : "143465",
  "transactionId" : "2000001033622153",
  "transactionReason" : "PURCHASE",
  "type" : "Non-Consumable"
}

Transaction交易JSON字段

1、appTransactionId:此次 App 内事务的全局唯一 ID,用于追踪整条交易链路。

2、bundleId:这笔交易所属的 App bundle ID。

3、currency:购买时的货币。

4、deviceVerification:服务器验证此设备可信性的 cryptographic token(加密验证串)。用于 Server-to-Server 通信中确认交易来自真实设备。

5、deviceVerificationNonce:随机 UUID,配合 deviceVerification 使用,保证一次请求一次随机值,避免重放攻击。同样主要用于服务器验证。

6、environment:表示交易来自的所属环境,例如真实环境(Production)或者沙盒环境(Sanbdox)。

7、inAppOwnershipType:一般是PURCHASED(用户购买)或者FAMILY_SHARED(家庭共享)。

8、originalPurchaseDate:这个商品第一次被购买的时间(毫秒时间戳)。

9、originalTransactionId:第一次购买的 ID,对非消耗型来说,它与 transactionId 相同。

10、price:价格(单位是“厘”),你的 8000 代表 8 元人民币。

11、productId:App Store Connect 里设置的商品 ID。

12、purchaseDate:这笔交易发生的时间(毫秒)。

13、quantity:数量,非消耗型必定为 1。

14、signedDate:苹果签发这份 JWS(交易票据)的时间。通常比 purchaseDate 晚,因为它表示服务器何时生成这个结构,而不是用户何时付款。

15、storefront:购买来自的地区商店代码。CHN = 中国大陆。

16、storefrontId:App Store 的内部区域 ID,与 storefront 一一对应。

17、transactionId:这条交易记录的唯一 ID。

18、transactionReason:交易原因:”PURCHASE”(用户主动购买)”RENEWAL”(订阅自动续费)和”RESTORE”(通过恢复购买获得)。

非消耗型一般只有 PURCHASE。

19、type:商品类型。

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注