前文
PurchaseX 迎来首次新的更新啦!此次更新引入了 Apple 新推出的 StoreKit2 框架。
想必开发过 In-App-Purchase 的同学肯定都应该体会过被他生涩难懂的 API,复杂的消息回调,不合理的数据结构以及莫名其妙的丢单等问题折磨过,于是 Apple 针对 StoreKit 做了一次全面的升级,推出了 Storekit2 框架。
在阅读下面内容之前,我先将一些在下面的文章中会涉及到的 Swift 语言的新特性和大家做一下说明:
- @aync/@await: Swift5.5 新推出的多线程编程 API
- @Actor: 防止应用在多线程中造成数据竞争,是保证多线程安全性的新类型
- 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 以下的机器,还得使用原先的那一套内购逻辑。