StoreKit2 有这么香?嗯,我试过了,真香

2022-08-04 14:19:05 浏览数 (1)

前文

PurchaseX 迎来首次新的更新啦!此次更新引入了 Apple 新推出的 StoreKit2 框架。

想必开发过 In-App-Purchase 的同学肯定都应该体会过被他生涩难懂的 API,复杂的消息回调,不合理的数据结构以及莫名其妙的丢单等问题折磨过,于是 Apple 针对 StoreKit 做了一次全面的升级,推出了 Storekit2 框架。

在阅读下面内容之前,我先将一些在下面的文章中会涉及到的 Swift 语言的新特性和大家做一下说明:

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

接下来,就让我带领大家来看下,StoreKit2 相比 StoreKit 有哪些重大的变化吧!

请求商品

在 StoreKit2 中,请求商品的 API 变得简洁无比,配合上使用 @aync/@await,只要简简单单的一行代码,即可从 AppStore 获得内购商品。代码如下:

代码语言:javascript复制
@MainActor public func requestProductsFromAppstore(productIds: [String]) async -> [Product]? {
    products = try? await Product.products(for: Set.init(productIds))
    return products
}

再来看下旧版本内购是如何获取商品信息的,代码如下:

代码语言:javascript复制
    // MARK: - requestProductsFromAppstore
    /// - Request products form appstore
    /// - Parameter completion: a closure that will be called when the results returned from the appstore
    public func requestProductsFromAppstore(productIds: [String]?, completion: @escaping (_ notification: PurchaseXNotification?) -> Void) {
        // save request products info
        requestProductsCompletion = completion
        
        guard productIds != nil || productIds!.count > 0 else {
            PXLog.event(.productIdArrayEmpty)
            DispatchQueue.main.async {
                completion(.productIdArrayEmpty)
            }
            return
        }
        
        if products != nil {
            products?.removeAll()
        }
                
        configuredProductIdentifiers = Set(productIds!)
        
        // 1. Cancel pending requests
        productsRequest?.cancel()
        // 2. Init SKProductsRequest
        productsRequest = SKProductsRequest(productIdentifiers: configuredProductIdentifiers!)
        // 3. Set Delegate to receive the notification
        productsRequest!.delegate = self
        // 4. Start request
        productsRequest!.start()
    }

对比完代码后,你就可以看出使用 StoreKit2 得有多方便了。

首先,利用 @aync/@await 新特性,我们的代码可以像同步执行一样获取商品信息了,再也不用因为获取商品是异步执行的方式,而去写那些地狱级的闭包嵌套了;StoreKit2 里面商品对象已经由原来的 SKProduct 变化为 Product,请求商品也只需要仅仅一行代码即可,简单易懂。

其次,利用 StoreKit2,我们可以根据 Product 对象里的 type 类型,来获取返回的商品中的商品类型,代码如下:

代码语言:javascript复制
    /// 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
        })
    }

在老的内购里面,我们是无法通过 SKProduct 对象来分辨商品类型的,这一步只能由我们开发者自行去判断了,现在 Apple 在 StoreKit2 中已经帮我们做好了。

发起支付

接下来,再来说一下支付功能。

不得不说 StoreKit2 提供的新的 API 都非常的精简,都只需要一句代码就可以完成功能,代码如下:

代码语言:javascript复制
let result = try await product.purchase()

是不是非常的简单,在 StoreKit2 中已经不再需要用到 SKPaymentTransactionObserver 代理了。上述代码它的返回值 result 是 Product.PurchaseResult 类型,它是一个枚举类型,定义了此次购买的订单状态,分别为:

代码语言:javascript复制
public enum PurchaseResult {

    /// The purchase succeeded with a `Transaction`.
    case success(VerificationResult<Transaction>)

    /// The user cancelled the purchase.
    case userCancelled

    /// The purchase is pending some user action.
    ///
    /// These purchases may succeed in the future, and the resulting `Transaction` will be
    /// delivered via `Transaction.updates`
    case pending
}

success 表明此次购买成功,userCancelled 表明用户取消了此次购买,pending 表明此次购买被挂起。

我们可以通过 switch 条件 语句,来分别处理这些状态,代码如下:

代码语言:javascript复制
switch result {
        case .success(let verificationResult):
            let checkResult = checkTransactionVerificationResult(result: verificationResult)
            if !checkResult.verified {
                purchaseState = .failedVerification
            }
            
            let validatedTransaction = checkResult.transaction
            
            await validatedTransaction.finish()
                        
        case .userCancelled:
            purchaseState = .cancelled
            
        case .pending:
            purchaseState = .pending
            
        default:
            purchaseState = .unknown
        }

购买成功也就是状态为 success 的时候,该枚举还返回了一个 VerificationResult类型参数,这个参数是用来干嘛的呢!别急,这部分内容我放在了下面的内容中做说明。最终完成购买,我们还需要进行最后的一步结束交易,还是用一句代码来结束,那就是:

代码语言:javascript复制
await validatedTransaction.finish()

validatedTransaction 是一个 Transaction 类型,由枚举参数返回。

验证票据

看到这里,有的同学可能会问,在上一版本的内购中,我们需要对购买的商品订单 进行票据验证,而且验证的过程还非常的麻烦,但是在新版本中怎么没有体现出来呢!难道 Apple 已经默默地帮你完成了?

说的没错,在上一版本的内购中,苹果提供了俩种验证方式给开发者对票据进行验证,分别是本地验证和远程验证。想必看过我 PurchaseX 第一版本的同学都应该清楚本地验证有多麻烦,我们要借用第三方的 OpenSSL 库去解析票据的各种属性和值,然后去一一验证,在这里我就不多做阐述了,感兴趣的可以去看下我的代码。

在新版本中,苹果引入了 JWS 来帮助我们校验订单的安全性,发起支付后,purchase() 函数会返回给我们一个枚举类型 PurchaseResult,并且当枚举值为 success 的时候,我们即可通过它的回调参数 VerificationResult 来判断当前的订单是 verified 还是 unverified。VerificationResult 这个类型其实也是个枚举类型,代码如下:

代码语言:javascript复制
@frozen public enum VerificationResult<SignedType> {

    /// The associated value failed verification for the provided reason.
    case unverified(SignedType, VerificationResult<SignedType>.VerificationError)

    /// The associated value passed all automatic verification checks.
    case verified(SignedType)

    ...
}

当你拿到 PurchaseResult 的时候,苹果就已经默默的帮我们在背后完成了 JWS 校验。

在新版本中,发起购买的完成代码如下:

代码语言:javascript复制
    // MARK: - purchase
    /// Start the process to purchase a product.
    /// - Parameter product: Product object
    public func purchase(product: Product) async throws -> (transaction: Transaction?, purchaseState: PurchaseXState){
        guard purchaseState != .inProgress else {
            throw PurchaseXException.purchaseInProgressException
        }
        
        purchaseState = .inProgress
        
        // Start a purchase transaction
        guard let result = try? await product.purchase() else {
            purchaseState = .failed
            throw PurchaseXException.purchaseException
        }
        
        switch result {
        case .success(let verificationResult):
            let checkResult = checkTransactionVerificationResult(result: verificationResult)
            if !checkResult.verified {
                purchaseState = .failedVerification
                throw PurchaseXException.transactionVerificationFailed
            }
            
            let validatedTransaction = checkResult.transaction
            
            await validatedTransaction.finish()
            
            // Because consumable's transaction are not stored in the receipt, So treat it differently.
            if validatedTransaction.productType == .consumable {
                if !PXDataPersistence.purchase(productId: product.id){
                    PXLog.event(.consumableKeychainError)
                }
            }
            purchaseState = .complete
            return (transaction: validatedTransaction, purchaseState: .complete)
        case .userCancelled:
            purchaseState = .cancelled
            return (transaction: nil, purchaseState: .cancelled)
        case .pending:
            purchaseState = .pending
            return (transaction: nil, purchaseState: .pending)
        default:
            purchaseState = .unknown
            return (transaction: nil, purchaseState: .unknown)
        }
    }

其他

在上一个版本的内购中,如果你的应用包含了非消耗品,那么开发者就需要为此提供一个“恢复购买”的按钮,来保证用户在新设备上能同步这些非消耗品。

但是在 StoreKit2 中,就不再需要这个恢复按钮了,因为在 StoreKit2 中, 我们可以直接获取所有已经购买过的非消耗品和订阅类商品的记录,只需要简单的通过调用

代码语言:javascript复制
Transaction.currentEntitlements

即可获取。但是该 API 返回的数据并不包括消耗品的购买记录,所以如果想统计消耗品的购买记录,需要开发者单独的统计。

其次,在上一版本中,我们若想去管理订阅类的商品,需要去系统的设置中查看,但是该步骤个人觉得内嵌的太深,相信现在还是有很多人不清楚该如何去手动关闭订阅。但是在 StoreKit2 中,它直接提供了一个 API 可以在应用内弹出管理订阅类商品的界面,也仅需一行代码:

代码语言:javascript复制
try await AppStore.showManageSubscriptions(in: scene)

如图所示:

image

很方便吧!

最后,StoreKit2 还提供了为内购商品退款的 API,原先退款的方式需要玩家在苹果官方网站上登录自己的 AppleID 来申请退款,非常的不方便;现在可以直接在应用中进行退款操作,开发者只需要调用下方的 API 就可以在应用中弹出退款界面,相当的人性化:

代码语言:javascript复制
@inlinable public func beginRefundRequest(in scene: UIWindowScene) async throws -> Transaction.RefundRequestStatus

如图所示:

image

结尾

经过上述的一番对比,可以发现 StoreKit2 相比于之前的版本,已经发生了翻天覆地的变化,它的 API 简洁直观,配合使用 @aync/@await 这一新特性,使得它的内购代码阅读起来更加的简单,非常容易上手。说了几个它的优势,再来说说它唯一的一个硬伤吧!那就是 StoreKit2 目前只支持 iOS15。对于需要支持 iOS15 以下的机器,还得使用原先的那一套内购逻辑。

0 人点赞