iOS通过StoreKit2应用收据验证交易
iOS通过StoreKit2应用收据验证交易

iOS通过StoreKit2应用收据验证交易

苹果新的 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
}

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

发表回复

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