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:商品类型。
