苹果新的 StoreKit2 API 设计中,将购买信息的验证与凭证的处理更加简化和模块化。还需要注意的是,本篇文章实际作为《iOS通过StoreKit2实现应用内购》文章的扩展篇,本文的代码大多与前文相关,不了解的代码逻辑可以尝试从前文中搜素。
整个StoreKit2收据交易流程都是围绕下面的流程图的步骤进行的。
获取凭证的正确方式
在 StoreKit2 中,需要获取 应用内收据(App Receipt),以下是处理方式:
1、添加属性和协议
1、添加 receiptRefreshCompletion 属性
在 IAPManager 类中添加 receiptRefreshCompletion 属性,用于保存刷新收据完成后的回调:
private var receiptRefreshCompletion: ((Bool) -> Void)?
2、继承自 NSObject
修改 IAPManager 类的定义,让它继承自 NSObject,以满足 SKRequestDelegate 的要求:
@available(iOS 15.0, *)
@MainActor
class IAPManager: NSObject, ObservableObject {
// 其他代码...
}
3、修复后的代码
import StoreKit
@available(iOS 15.0, *)
@MainActor
class IAPManager: NSObject, ObservableObject {
static let shared = IAPManager()
private override init() {}
private var receiptRefreshCompletion: ((Bool) -> Void)?
...
}
这里的变动就是遵循NSObject协议后,init()需要使用override关键字。
2、获取应用内收据
应用内收据是整个应用的交易历史记录,存储在设备中,可以通过 Bundle.main.appStoreReceiptURL 获取。
在IAPManager方法中,新增getAppReceipt方法:
// 读取收据
func getAppReceipt(completion: @escaping (Data?) -> Void) {
if let receiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: receiptURL.path) {
do {
let receiptData = try Data(contentsOf: receiptURL)
completion(receiptData)
} catch {
print("读取收据失败: \(error)")
completion(nil)
}
} else {
print("未找到收据文件,尝试刷新")
refreshReceipt { [weak self] success in // 使用 [weak self] 防止循环引用
guard let self = self else {
completion(nil)
return
}
if success,
let refreshedReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: refreshedReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: refreshedReceiptURL)
completion(receiptData)
} catch {
print("读取刷新后的收据失败: \(error)")
completion(nil)
}
} else {
print("刷新收据失败")
completion(nil)
}
}
}
}
在调用 getAppReceipt() 时,先尝试直接读取收据;如果收据不存在,自动调用 refreshReceipt刷新收据。
为什么需要刷新收据?
在 StoreKit 中,应用内收据可能因以下原因不存在或不可用:
1、首次安装后未获取收据:重新安装应用时,App Store 并不会自动生成收据,必须通过调用 SKReceiptRefreshRequest 请求更新收据。
2、用户未完成交易:即便提示购买成功,实际可能是挂起状态,未完成的交易可能未写入收据。
3、沙盒环境异常:在开发或测试中,沙盒环境偶尔会出现不生成或未更新收据的情况。
3、刷新收据逻辑
在IAPManager方法中,新增refreshReceipt方法,同时扩展IAPManager方法遵循SKRequestDelegate协议:
// 刷新收据
func refreshReceipt(completion: @escaping (Bool) -> Void) {
let request = SKReceiptRefreshRequest()
request.delegate = self
self.receiptRefreshCompletion = completion
request.start()
}
这段代码的作用为,刷新 App Store 的收据 (Receipt)。刷新收据的目的是重新从 App Store 获取最新的交易信息,以便验证用户的购买历史或订阅状态。
// 实现 SKRequestDelegate 方法
extension IAPManager: SKRequestDelegate {
func requestDidFinish(_ request: SKRequest) {
print("收据刷新成功")
self.receiptRefreshCompletion?(true)
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("收据刷新失败: \(error)")
self.receiptRefreshCompletion?(false)
}
}
这里的扩展代码在IAPManager方法的外面实现,具体可查看最后的完整代码部分。
4、将收据发送到服务端
使用获取到的 App Receipt 数据(Base64 编码)发送到服务端。
在IAPManager方法中,新增sendReceiptToServer方法:
// 同步服务器数据
func sendReceiptToServer(_ receiptData: Data) {
guard let url = URL(string: "https://yourserver.com/validateReceipt") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"receipt-data": receiptData.base64EncodedString(),
"password": "your_shared_secret" // 如果需要 App 专用共享密钥
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("收据验证请求失败: \(error)")
return
}
guard let data = data else {
print("收据验证返回数据为空")
return
}
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("收据验证结果: \(json)")
}
}
task.resume()
}
5、校验整个收据方法
在IAPManager方法中,新增verifyPurchase方法。
当用户点击内购并成功购买时,通过调取verifyPurchase方法,将依次执行前面提到的获取、刷新收据代码。在成功获取收据代码后,调用sendReceiptToServer方法,将收据传递给服务器。
// 校验整个收据流程
func verifyPurchase() {
getAppReceipt { [weak self] receiptData in // 使用 [weak self]
guard let self = self else { return }
if let receiptData = receiptData {
print("获取到收据: \(receiptData.base64EncodedString())")
self.sendReceiptToServer(receiptData)
} else {
print("未能获取到收据,尝试刷新")
self.refreshReceipt { [weak self] success in
guard let self = self else { return }
if success {
self.getAppReceipt { refreshedReceipt in
if let refreshedReceipt = refreshedReceipt {
print("刷新后获取到的收据: \(refreshedReceipt.base64EncodedString())")
self.sendReceiptToServer(refreshedReceipt)
} else {
print("刷新后仍未能获取到收据")
}
}
} else {
print("刷新收据失败")
}
}
}
}
}
调用 verifyPurchase() 方法即可实现收据的获取、刷新、以及验证的完整流程。
6、修改 purchaseProduct 方法
注意,在purchaseProduct方法在《iOS通过StoreKit2实现应用内购》一文中,可以跳转查看对应的方法。
在 purchaseProduct 中调用 sendReceiptToServer 方法:
// purchaseProduct:购买商品的方法,返回购买结果
func purchaseProduct(_ product: Product) {
// 在这里输出要购买的商品id
print("Purchasing product: \(product.id)")
Task { @MainActor in
do {
let result = try await product.purchase()
switch result {
case .success(let verification): // 购买成功的情况,返回verification包含交易的验证信息
let transaction = try checkVerified(verification) // 验证交易
verifyPurchase() // 调用校验整个收据方法
savePurchasedState(for: product.id) // 更新UserDefaults中的购买状态
await transaction.finish() // 告诉系统交易完成
print("交易成功:\(result)")
case .userCancelled: // 用户取消交易
print("用户取消交易:\(result)")
case .pending: // 购买交易被挂起
print("购买交易被挂起:\(result)")
default: // 其他情况
throw StoreError.failedVerification // 购买失败
}
} catch {
print("购买失败:\(error)")
await resetProduct() // 购买失败后重置 product 以便允许再次尝试购买
}
DispatchQueue.main.async {
self.loadPurchased = false // 隐藏内购时的加载画布
}
print("loadPurchased:\(loadPurchased)")
}
}
收据数据
在调用verifyPurchase方法后,实际返回的是一段Base64编码的字符串,它实际上是一个二进制数据文件的文本表示形式。
获取到收据: MIIURwYJKoZIhvcNA...
这段 Base64 数据包含了购买记录和设备相关信息。在调用sendReceiptToServer方法后,将这段Base64编码的字符串发送到自己的服务器上去,然后通过自己的服务器与苹果服务器进行校验。
收据校验流程概述
1、客户端生成收据:客户端在完成内购后,通过 StoreKit 获取 Base64 编码的收据数据。
2、客户端发送收据到自己的服务器:为了安全性,收据数据需要发送到自己的服务器,而不是直接在客户端调用苹果的验证接口。
3、服务器转发收据到苹果服务器:服务器将收据发送给苹果提供的验证接口(沙盒或生产环境)。
4、服务器解析苹果返回的结果:根据苹果服务器返回的 JSON 数据,确定购买是否有效并提取相关信息。
5、服务器将结果返回客户端:服务器解析完验证结果后,将处理后的信息返回客户端。
校验收据数据
方法1:发送到苹果服务器验证
苹果提供了内购收据的验证服务。将 Base64 编码的收据数据发送到苹果的验证服务器,苹果会返回一个 JSON 格式的解析结果。
服务器端验证步骤
1、构建请求数据
发送到苹果服务器的数据结构如下:
{
"receipt-data": "Base64EncodedReceipt",
"password": "YourAppSpecificSharedSecret" // 如果需要共享密钥验证
}
2、选择验证环境
验证接口地址:
沙盒环境(开发测试):
https://sandbox.itunes.apple.com/verifyReceipt
生产环境(正式发布):
https://buy.itunes.apple.com/verifyReceipt
以下是服务器端的伪代码(以 Python 为例):
import requests
import json
def validate_receipt(receipt_data, is_sandbox=False):
url = "https://sandbox.itunes.apple.com/verifyReceipt" if is_sandbox else "https://buy.itunes.apple.com/verifyReceipt"
shared_secret = "your_shared_secret" # 替换为你的 App 专用共享密钥
payload = {
"receipt-data": receipt_data,
"password": shared_secret
}
response = requests.post(url, json=payload)
return response.json()
# 示例调用
receipt_data = "Base64EncodedReceipt" # 客户端传递来的收据
validation_result = validate_receipt(receipt_data, is_sandbox=True)
print("验证结果:", json.dumps(validation_result, indent=4))
3、解析返回结果
苹果服务器会返回一个包含验证状态和收据信息的 JSON 数据:
{
"status": 0,
"receipt": {
"bundle_id": "com.example.app",
"in_app": [
{
"product_id": "com.example.product",
"purchase_date": "2023-12-25T12:00:00Z",
"transaction_id": "1000000123456789"
}
]
}
}
status:
0: 验证成功
其他值:验证失败(如收据无效)
environment:
“Sandbox” 或 “Production” 表示环境。
receipt:包含收据的详细信息(产品 ID、交易 ID 等)。
4、服务器解析结果并返回客户端
服务器解析验证结果后,将必要的信息返回客户端。例如:
{
"valid": true,
"product_id": "com.example.product",
"purchase_date": "2024-12-25T12:00:00Z"
}
客户端根据这个结果确认是否解锁相关功能。
实际校验情况
因为缺失服务器,所以我的测试环境是通过Postman应用使用POST调用接口。
调用地址(沙盒环境):
https://sandbox.itunes.apple.com/verifyReceipt
调用格式:Body – raw – JSON。
调用内容(测试数据):
{
"receipt-data": "MIIURwYJKoZI..."
}
最后提示调取成功。
完整的返回信息:
{
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.fangjunyu.ERdepot",
"application_version": "1.0.4",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2024-12-25 05:31:29 Etc/GMT",
"receipt_creation_date_ms": "1735104689000",
"receipt_creation_date_pst": "2024-12-24 21:31:29 America/Los_Angeles",
"request_date": "2024-12-26 13:57:44 Etc/GMT",
"request_date_ms": "1735221464174",
"request_date_pst": "2024-12-26 05:57:44 America/Los_Angeles",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms": "1375340400000",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"original_application_version": "1.0",
"in_app": [
{
"quantity": "1",
"product_id": "supportERdepot20241019",
"transaction_id": "2000000751300248",
"original_transaction_id": "2000000751300248",
"purchase_date": "2024-10-22 15:40:10 Etc/GMT",
"purchase_date_ms": "1729611610000",
"purchase_date_pst": "2024-10-22 08:40:10 America/Los_Angeles",
"original_purchase_date": "2024-10-22 15:40:10 Etc/GMT",
"original_purchase_date_ms": "1729611610000",
"original_purchase_date_pst": "2024-10-22 08:40:10 America/Los_Angeles",
"is_trial_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
},
"environment": "Sandbox",
"status": 0
}
核心字段解析
1、status
值为 0 表示验证成功,没有任何错误。
如果 status 不为 0,需要根据 Apple 文档 查找对应的错误代码。
2、environment
值为 “Sandbox”,表示你验证的是沙箱环境的收据(适用于测试期间)。
如果是在生产环境下验证,需要使用生产接口,返回的 environment 应该是 “Production”。
3、receipt
bundle_id: 应与应用的包标识符一致(如 “com.fangjunyu.ERdepot”)。
application_version: 应与你应用的 CFBundleShortVersionString 一致(如 “1.0.4”)。
in_app: 包含了用户的应用内购买信息。
4、in_app
product_id: 表示用户购买的商品 ID,例如 “supportERdepot20241019″。
transaction_id: 唯一的交易 ID,用于标识这次购买。
purchase_date: 表示交易时间(UTC 格式),如 “2024-10-22 15:40:10 Etc/GMT”。
is_trial_period: 表示用户是否处于试用期(此例为 false,即非试用)。
in_app_ownership_type: 这里的值为 “PURCHASED”,表示用户拥有该商品(非赠送)。
如果想要进行测试,可以使用我的沙盒内购测试数据,但因为我可能会清理测试内购数据,因此这个文件仅供参考使用,可能会失效。
需要注意的是,因为我没有共享密钥,所以在调取时,没有添加password字段,如果在没有共享密钥的情况下,使用password字段,可能返回21003的报错:
{"environment":"Sandbox", "status":21003}
验证成功的标志
status: 0
收据中的 bundle_id、product_id 和应用内的配置一致。
收据中 in_app 的数组包含了有效的购买信息。
后续处理
1、服务器端处理:
确保存储了 transaction_id,避免重复处理同一交易(防止重复消费)。
如果需要自动续订订阅,可以存储并定期验证 original_transaction_id,以跟踪订阅的状态。
2、沙箱与生产环境:
在开发和测试中使用沙箱环境的验证接口。
上线时切换到生产环境接口:
https://buy.itunes.apple.com/verifyReceipt
3、安全性:
确保所有收据验证都在服务器端完成,避免将共享密钥暴露在客户端。
4、解析与响应用户:
使用 in_app 信息,提供对应的购买功能或权限。
方法2: 使用本地解析工具
测试数据
如果想要进需要在本地解析收据,可以使用开源工具,如 SwiftyStoreKit 或苹果官方的 ASN.1 工具解析收据。
收据的本地结构
苹果收据是一个使用 ASN.1 编码 的二进制数据。可以用 ASN.1 解码器解析它,获取内购记录、产品 ID、交易 ID 等信息。
在 Swift 中解析收据可能比较复杂,通常更推荐服务器端解析。
方法 3:混合方式(本地校验 + 服务器解析)
1、本地检查收据文件是否存在,并解析基本信息(如 bundle_id)。
2、将收据发送到自己的服务器,由服务器转发到苹果验证接口。
这种方式的优点是增强了安全性,并避免在客户端暴露共享密钥。
总结
通过读取 App Store Receipt(位于 Bundle.main.appStoreReceiptURL)来获取收据,并将其发送到服务端验证。通过这种方式,可以完成交易后的服务器通信逻辑。
相关文章
1、iOS通过StoreKit2实现应用内购: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、App Store Server API:https://developer.apple.com/documentation/appstoreserverapi
3、SwiftyStoreKit:https://github.com/bizz84/SwiftyStoreKit
4、ASN.1编码:https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
扩展知识
Apple共享密钥验证
共享密钥验证是 App Store 的一种额外安全机制,用于验证内购收据的合法性,尤其是订阅类型的内购(如自动续订订阅)。通过这种方式,开发者可以确保收据验证的请求确实来自自己的应用程序,而不是伪造的请求。
共享密钥 (Shared Secret) 的定义
共享密钥 是 Apple 提供的一串唯一的字符串(类似密码),专属于开发者的 App。
这个密钥必须保密,通常用于验证订阅类内购收据。
共享密钥的用途
1、增强验证安全性:通过共享密钥,Apple 的服务器能够确认收据验证请求是由合法的开发者发起的,而不是恶意的第三方。
2、订阅管理:对于自动续订订阅类型,收据中包含订阅的详细信息,而共享密钥是 Apple 用来授权提供这些信息的关键。
验证时的工作流程
1、本地获取收据:
App 在用户完成购买后,会生成一个 Base64 编码的收据数据 (receipt-data)。
可以通过 Bundle.main.appStoreReceiptURL 获取该收据。
2、发送验证请求到 Apple 服务器:
开发者服务器将收据数据和共享密钥一起发送给 Apple 的验证接口。
3、Apple 服务器验证:
Apple 的服务器检查以下内容:
收据是否有效。
收据是否与指定共享密钥匹配。
收据是否被篡改或伪造。
返回验证结果。
共享密钥的设置
1、登录 App Store Connect。
2、选择App > 应用内购买。
3、找到 App 专用共享密钥,并复制或生成一个新的密钥。
4、将密钥安全地存储在服务器端,不要硬编码在客户端。
共享密钥仅适用于自动续订订阅类型。如果 App 中的内购项目不是自动续订订阅(例如消耗型商品或非消耗型商品),则无需使用共享密钥。
注意事项
1、服务器验证:共享密钥应始终存储在服务器端,避免直接在客户端中使用,以防被反编译和盗取。
2、订阅管理:如果你的 App 使用了自动续订订阅,则共享密钥是必需的,用于验证订阅状态、到期时间等信息。
3、错误代码:Apple 返回的验证响应中可能包含错误代码(如 21007、21008 等),需要正确处理以判断问题来源。
常见错误代码
21007: 使用沙盒收据请求了生产环境接口,应切换到沙盒环境验证。
21008: 使用生产环境收据请求了沙盒接口,应切换到生产环境验证。
21002: 提交的 JSON 数据无效(通常是 receipt-data 或 password 格式有问题)。
完整代码
import StoreKit
@available(iOS 15.0, *)
@MainActor
class IAPManager:NSObject, ObservableObject {
static let shared = IAPManager()
private override init() {}
@Published var productID = [""] // 需要内购的产品ID数组
@Published var products: [Product] = [] // 存储从 App Store 获取的内购商品信息
@Published var loadPurchased = false // 如果开始内购流程,loadPurchased为true,View视图显示加载画布
private var receiptRefreshCompletion: ((Bool) -> Void)?
// 视图自动加载loadProduct()方法
func loadProduct() async {
print("调取loadProduct方法")
do {
// 传入 productID 产品ID数组,调取Product.products接口从App Store返回产品信息
// App Store会返回对应的产品信息,如果数组中个别产品ID有误,只会返回正确的产品ID的产品信息
let fetchedProducts = try await Product.products(for: productID)
if fetchedProducts.isEmpty { // 判断返回的是否是否为空
// 抛出内购信息为空的错误,可能是所有的产品ID都不存在,中断执行,不会return返回products产品信息
throw StoreError.IAPInformationIsEmpty
}
DispatchQueue.main.async {
self.products = fetchedProducts // 将获取的内购商品保存到products变量
print("成功加载产品: \(fetchedProducts)") // 输出内购商品数组信息
}
} catch {
print("加载产品失败:\(error)") // 输出报错
}
}
// purchaseProduct:购买商品的方法,返回购买结果
func purchaseProduct(_ product: Product) {
// 在这里输出要购买的商品id
print("Purchasing product: \(product.id)")
Task { @MainActor in
do {
let result = try await product.purchase()
switch result {
case .success(let verification): // 购买成功的情况,返回verification包含交易的验证信息
let transaction = try checkVerified(verification) // 验证交易
verifyPurchase()
savePurchasedState(for: product.id) // 更新UserDefaults中的购买状态
await transaction.finish() // 告诉系统交易完成
print("交易成功:\(result)")
case .userCancelled: // 用户取消交易
print("用户取消交易:\(result)")
case .pending: // 购买交易被挂起
print("购买交易被挂起:\(result)")
default: // 其他情况
throw StoreError.failedVerification // 购买失败
}
} catch {
print("购买失败:\(error)")
await resetProduct() // 购买失败后重置 product 以便允许再次尝试购买
}
DispatchQueue.main.async {
self.loadPurchased = false // 隐藏内购时的加载画布
}
print("loadPurchased:\(loadPurchased)")
}
}
// 验证购买结果
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确认本笔交易信息由苹果服务器合法签署
}
}
// handleTransactions处理所有的交易情况
func handleTransactions() async {
for await result in Transaction.updates {
// 遍历当前所有已完成的交易
do {
let transaction = try checkVerified(result) // 验证交易
// 处理交易,例如解锁内容
savePurchasedState(for: transaction.productID)
await transaction.finish()
} catch {
print("交易处理失败:\(error)")
}
}
}
// 当购买失败时,会尝试重新加载产品信息。
func resetProduct() async {
self.products = []
await loadProduct() // 调取loadProduct方法获取产品信息
}
// 保存购买状态到用户偏好设置或其他存储位置
func savePurchasedState(for productID: String) {
UserDefaults.standard.set(true, forKey: productID)
print("Purchased state saved for product: \(productID)")
}
// 通过productID检查是否已完成购买
func loadPurchasedState(for productID: String) -> Bool{
let isPurchased = UserDefaults.standard.bool(forKey: productID) // UserDefaults读取购买状态
print("Purchased state loaded for product: \(productID) - \(isPurchased)")
return isPurchased // 返回购买状态
}
// 刷新收据
func refreshReceipt(completion: @escaping (Bool) -> Void) {
let request = SKReceiptRefreshRequest()
request.delegate = self
self.receiptRefreshCompletion = completion
request.start()
}
// 读取收据
func getAppReceipt(completion: @escaping (Data?) -> Void) {
if let receiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: receiptURL.path) {
do {
let receiptData = try Data(contentsOf: receiptURL)
completion(receiptData)
} catch {
print("读取收据失败: \(error)")
completion(nil)
}
} else {
print("未找到收据文件,尝试刷新")
refreshReceipt { [weak self] success in // 使用 [weak self] 防止循环引用
guard let self = self else {
completion(nil)
return
}
if success,
let refreshedReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: refreshedReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: refreshedReceiptURL)
completion(receiptData)
} catch {
print("读取刷新后的收据失败: \(error)")
completion(nil)
}
} else {
print("刷新收据失败")
completion(nil)
}
}
}
}
// 同步服务器数据
func sendReceiptToServer(_ receiptData: Data) {
guard let url = URL(string: "https://yourserver.com/validateReceipt") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"receipt-data": receiptData.base64EncodedString(),
"password": "your_shared_secret" // 如果需要 App 专用共享密钥
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("收据验证请求失败: \(error)")
return
}
guard let data = data else {
print("收据验证返回数据为空")
return
}
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("收据验证结果: \(json)")
}
}
task.resume()
}
// 校验整个收据流程
func verifyPurchase() {
getAppReceipt { [weak self] receiptData in // 使用 [weak self]
guard let self = self else { return }
if let receiptData = receiptData {
print("获取到收据: \(receiptData.base64EncodedString())")
self.sendReceiptToServer(receiptData)
} else {
print("未能获取到收据,尝试刷新")
self.refreshReceipt { [weak self] success in
guard let self = self else { return }
if success {
self.getAppReceipt { refreshedReceipt in
if let refreshedReceipt = refreshedReceipt {
print("刷新后获取到的收据: \(refreshedReceipt.base64EncodedString())")
self.sendReceiptToServer(refreshedReceipt)
} else {
print("刷新后仍未能获取到收据")
}
}
} else {
print("刷新收据失败")
}
}
}
}
}
}
// 实现 SKRequestDelegate 方法
extension IAPManager: SKRequestDelegate {
func requestDidFinish(_ request: SKRequest) {
print("收据刷新成功")
self.receiptRefreshCompletion?(true)
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("收据刷新失败: \(error)")
self.receiptRefreshCompletion?(false)
}
}
// 定义 throws 报错
enum StoreError: Error {
case IAPInformationIsEmpty
case failedVerification
}