都2032年了,你还没使用StoreKit2吗?

8,239 阅读6分钟

StoreKit2对iOS内购进行了重构,相较于原版本StoreKit,API改动较大,且更高效简洁。 StoreKit2抛弃了Objective-C,仅支持Swift且最低支持版本为iOS 15.0。

/// Contains properties and methods to facilitate interactions between your app and the App Store.
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)

其中,使用了Swift语言的一些新的特性:

  • @aync/@await: Swift5.5推出的多线程编程API。
  • @MainActor: 防止应用在多线程中造成数据竞争,是保证多线程安全性的新类型。
  • JWS:JSON Web Signature,是一套加密校验体系,在StoreKit2中通过此校验体系来校验订单。

使用StoreKit2,可以更简单地实现内购流程,所有请求可以在回调中返回,而无需通过监听代理方法而造成发起支付和支付结果返回的代码割裂。内购流程也更加清晰,再配合最新的App Store Server API,可以为iOS内购流程带来很大程度上的优化。

客户端实现可参考Demo工程

获取商品

@MainActor
/// 通过 productIds 请求 Product 列表
/// - Parameter productIds: product ids
/// - Returns: Product 列表
func requestProducts(productIds: [String]) async -> [Product]? {
    products = try? await Product.products(for: Set.init(productIds))
    return products
}

判断商品类型

    /// Array of consumable products
    public var consumableProducts: [Product]? {
        guard products != nil else {
            return nil
        }
        
        return products?.filter({ product in
            product.type == .consumable
        })
    }
    
    /// Array of nonConsumbale products
    public var nonConsumbaleProducts: [Product]? {
        guard products != nil else {
            return nil
        }
        
        return products?.filter({ product in
            product.type == .nonConsumable
        })
    }
    
    /// Array of subscriptio products
    public var subscriptionProducts: [Product]? {
        guard products != nil else {
            return nil
        }
        
        return products?.filter({ product in
            product.type == .autoRenewable
        })
    }
    
    /// Array of nonSubscription products
    public var nonSubscriptionProducts: [Product]? {
        guard products != nil else {
            return nil
        }
        
        return products?.filter({ product in
            product.type == .nonRenewable
        })
    }

发起支付

我们可以通过productId获取到Product对象,再发起支付。


    public func purchase(from productid: String, uid: String) async throws -> Transaction? {
        do {
            let product = try await product(from: productid)!
            return try await purchase(product: product, uid: uid)
        } catch PurchaseException.noProductMatched {
            throw PurchaseException.noProductMatched
        }
    }
    
    /// 发起支付
    /// - Parameter product: Product对象
    public func purchase(product: Product, uid: String) async throws -> Transaction? {
        guard purchaseState != .inProgress else {
            throw PurchaseException.purchaseInProgressException
        }
        
        purchaseState = .inProgress
        
        //App account token
        //用于将用户 ID 绑定到交易(Transcation)中,即可建立苹果的交易订单数据与用户信息的映射关系,方便数据整合与追溯
        let uuid = Product.PurchaseOption.appAccountToken(UUID.init(uuidString: uid)!)
        //发起支付流程
        guard let res = try? await product.purchase(options: [uuid]) else {
            purchaseState = .failed
            throw PurchaseException.transactionVerificationFailed
        }
        
        var validateTransaction: Transaction? = nil
        
        switch res {
        case .success(let verificationResult):
            //购买状态:成功
            
            print("用户购买成功")
            purchaseState = .complete
            
            let checkResult = checkTransactionVerificationResult(verificationResult)
            if !checkResult.verified {
                purchaseState = .failedVerification
                throw PurchaseException.transactionVerificationFailed
            }
            
            validateTransaction = checkResult.transaction
            
            //结束交易
            await validateTransaction!.finish()
            
        case .userCancelled:
            //购买状态:用户取消
            print("用户取消购买")
            purchaseState = .cancelled
            
        case .pending:
            //购买状态:进行中
            print("用户购买中")
            purchaseState = .pending
            
        default:
            //购买状态:未知
            print("用户购买状态:未知")
            purchaseState = .unknown
        }
        
        return validateTransaction
    }

这里新提供了appAccountToken参数用于透传用户 ID ,用于将用户 ID 绑定到交易(Transcation)中,可建立苹果的交易订单数据与用户信息的映射关系。

完成支付流程后,从返回的Transaction中可获取appAccountToken参数,方便数据整合与追溯,用于处理掉单。

这里的appAccountToken是36位的UUID,类似于E621E1F8-C36C-495A-93FC-0C247A3E6E5F

票据验证

StoreKit2可在客户端本地验证票据,而无需依赖服务端。

private func checkTransactionVerificationResult(_ result: VerificationResult<Transaction>) -> (transaction: Transaction, verified: Bool) {
        //Check whether the JWS parses StoreKit verification.
        switch result {
        case .unverified(let transaction, _):
            //StoreKit parses the JWS, but it fails verification.
            return (transaction: transaction, verified: false)
        case .verified(let transaction):
            //The reult is verified. Return the unwrapped value.
            return (transaction: transaction, verified: true)
        }
    }

监听订单

我们可以添加对支付的监听事件,监听支付结果,当完成购买流程时,我们调用票据验证方法,验证无误后结束整个购买流程。

 /// 支付监听事件
 private func listenForTransaction() -> Task<Void, Error> {
        return Task.detached {
            for await verificationResult in Transaction.updates {
                // 验证票据
                let checkResult = self.checkTransactionVerificationResult(verificationResult)
                
                if checkResult.verified {
                    let validatedTransaction = checkResult.transaction
                    await validatedTransaction.finish() // 结束购买流程
                } else {
                    print("Transaction failed verification.")
                }
            }
        }
    }

发起退款

StoreKit2支持开发者在沙盒环境进行退款测试,只需传入完成订单的transactionId,即可发起退款请求。

    /// 发起退款
    /// - Parameters:
    ///   - transactionId: transaction.originalID
    ///   - scene: Window scene
    public func refunRequest(for transactionId: UInt64, scene: UIWindowScene!) async {
        do {
            let res = try await Transaction.beginRefundRequest(for: transactionId, in: scene)
            switch res {
            case .userCancelled:
                // Customer cancelled refund request.
                print("用户取消退款。")
            case .success:
                print("退款提交成功。")
                // Refund request was successfully submitted.
            @unknown default:
                print("退款返回错误:未知")
            }
        }
        catch StoreKit.Transaction.RefundRequestError.duplicateRequest {
            print("退款请求错误:重复请求")
        }
        catch StoreKit.Transaction.RefundRequestError.failed {
            print("退款请求错误:失败")
        }
        catch {
            print("退款请求错误:其他")
        }
    }

调用

  1. 支付

func toPurchase(_ sender: Any) {
        Task { @MainActor in
            let res = await purchaseManager.purchase(uid: "68753A44-4D6F-1386-8C69-0050E4C00067", productId: "com.lyxzoxun.fhshanxkdd.rmb1.6")
            print(res)
        }
 }

    /// 发起支付
    /// - Parameters:
    ///   - uid: UUID String
    ///   - productId: Product ID
    /// - Returns: 支付结果
    public func purchase(uid: String, productId: String) async -> Dictionary<String, Any> {
        let purchaseManager: Store = Store()
        
        // 发起内购
        Task.init {
            do {
                let transaction = try await purchaseManager.purchase(from: productId, uid: uid)
                
                //支付完成,发送凭据给服务端验证,请求发货。
                if transaction != nil {
                    print("支付完成")
                    print("transaction id: \(transaction!.originalID),  purchase date:\(transaction!.originalPurchaseDate), user id:\(String(describing: transaction!.appAccountToken!))")
                    
                    return ["transactionId": transaction!.originalID,
                            "uuid": transaction!.appAccountToken!.uuidString]
                }
                
                return [:]
            } catch PurchaseException.noProductMatched {
                return ["code": PurchaseException.noProductMatched,
                        "error": "商品不存在,请检查Product ID"]
            } catch PurchaseException.transactionVerificationFailed {
                return ["code": PurchaseException.transactionVerificationFailed,
                        "error": "凭据验证失败"]
            } catch PurchaseException.purchaseInProgressException {
                return ["code": PurchaseException.purchaseInProgressException,
                        "error": "等待(家庭用户才有的状态)"]
            } catch PurchaseException.purchaseException {
                return ["code": PurchaseException.purchaseException,
                        "error": "服务器异常"]
            }
        }
        
        return ["info" : "Purchase started."]
    }
  1. 退款
Task { @MainActor in
    await purchaseManager.refunRequest(for: 2000000572764080, scene: self.view.window?.windowScene)
}

新功能,调用refunRequest()即可拉起退款交互界面,可在沙盒环境测试退款流程。

实际效果如下图:

IMG_0416.JPG

Objective-C调用Swift

  1. 先在Swift的类名前加上@objcMembers,并将该类继承于ObjectNSObject,并使用public声明类、属性和方法。
@objcMembers public class PurchaseManager: NSObject {
    public func purchase(uid: String, productId: String) async -> Dictionary<String, Any> {
        let purchaseManager: Store = Store()

        //发起内购
        Task.init {
            do {
                let transaction = try await purchaseManager.purchase(from: productId, uid: uid)

                //支付完成,发送凭据给服务端验证,请求发货。
                print("transaction id: \(transaction!.originalID), \(transaction!.originalPurchaseDate)")
                return ["transactionId": transaction!.originalID,
                        "uuid": transaction!.appAccountToken!.uuidString]
            } catch PurchaseException.noProductMatched {
                return ["code": PurchaseException.noProductMatched,
                        "error": "商品不存在,请检查productId"]
            } catch PurchaseException.transactionVerificationFailed {
                return ["code": PurchaseException.transactionVerificationFailed,
                        "error": "凭据验证失败"]
            } catch PurchaseException.purchaseInProgressException {
                return ["code": PurchaseException.purchaseInProgressException,
                        "error": "等待(家庭用户才有的状态)"]
            } catch PurchaseException.purchaseException {
                return ["code": PurchaseException.purchaseException,
                        "error": "服务器异常"]
            }
        }

        return [:]
    }
}
  1. 在Build Setting中设置Objective-C Generated Interface Header Name为项目名-Swift.h。
  2. 再创建Objective-C文件,引用项目名-Swift.h文件,我们的项目名为iapinstorekit2,那么
#import "iapinstorekit2-Swift.h"

即可调用自动生成的方法。 自动生成的.h文件中提供的方法如下所示:

SWIFT_CLASS("_TtC14iapinstorekit215PurchaseManager")

@interface PurchaseManager : NSObject

- (void)purchaseWithUid:(NSString * _Nonnull)uid productId:(NSString * _Nonnull)productId completionHandler:(void (^ _Nonnull)(NSDictionary<NSString *, id> * _Nonnull))completionHandler;

- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;

@end

在OC中可以调用:

    PurchaseManager *manager = [[PurchaseManager alloc] init];
    NSString *uid = @"";
    NSString *productId= @"";

    [manager purchaseWithUid:@"" productId:@"" completionHandler:^(NSDictionary<NSString *,id> * _Nonnull transactionInfo) {
        //支付结果
    }];

新的内购时序图

in-app-purchase-with-storekit2.png

支付结果的验证其实客户端可调用checkTransactionVerificationResult自行认证,但该过程是由客户端借助StoreKit2向Apple Server发起请求完成的,客户端不负责该过程的具体实现。

因此为了安全性和稳定性考虑,需要支付Server介入对支付结果进行验证,再由支付Server根据验证的结果决定是否发货,再通知客户端完成支付流程。

一些思考

  • 如果你的App仅支持iOS 15.0+,建议客户端使用StoreKit2实现内购;
  • 如果你的App仍需支持iOS 15.0以下版本,建议客户端使用StoreKit或StoreKit+StoreKit2兼容。
  • 服务端请务必使用V2版本的Server to Sever接口。

都2032年了,你还没用StoreKit2吗?如果还没有,没有关系,其实你可以先试试App Store Server API

参考

Apple官网-In App Purchase

StoreKit