通过苹果开发文档、搜索与挣扎摸索,我已经在App上实现了【消耗型商品】的内购。总结流程如下:
- 完成必须的准备工作
- 用有效的商品id创建SKProductRequest请求苹果服务器返回商品(SKProduct)。
- 用返回的有效的商品创建payment,并将payment加入苹果的支付队列。
- 监听队列,并根据监听到的交易状态进行适当的处理,尤其需要在交易完毕时关闭交易。
这方面的文档,我觉得苹果官方的Programming Guide就写的很好。可以仔细读一下。
我实际遇到的坑
- 必须先签署协议才能进行内购开发,否则无法返回商品。
- 创建沙箱测试员时,密码必须是强密码,即同时包含大写字母、小写字母和数字。否则,会报错:Unknown Errors while creating Sandbox Tester, Please check Error Log, email=xxx。
- 请求商品的
SKProductRequest
实例的delegate
必须在离开页面时viewDidDisappear
设置为nil
,因为即使你已经离开了页面,苹果依然会尝试向SKProductRequest
的代理发送消息,然后App就崩溃了。 - 必须在
AppDelegate
中声明遵守SKPaymentTransactionObserver
协议、实现协议方法updatedTransactions
、并在didFinishLaunching
方法中加入如下代码SKPaymentQueue.default().add(self)
。
这一行代码是将AppDelegate作为内购队列的监听者。这样,即使你离开了内购页面、即使上次支付未完成等情况,都能够在这里得到及时的处理。
如果不在这里这么做,可能的报错有:
This In-App purchase has already been bought. It will be restored for free.
如果需要在内购页面上实现交互,可以也在内购界面上添加监听,并进行交互上面适当的处理。但无路如何,不能省掉AppDelegate
中的那部分。
内购接入核心流程
以下是我实际接入中使用的代码。
请求商品
获取商品id
获取商品id,这个可以请求服务器,也可以保存在App上。我的App是用的后者。
首先创建一个名为ProductID
的plist
文件。文件内容是一个Array
,里面是保存的商品id字符串。
这里需要说明的是,商品id,即product identifier,就是你在iTunes Connect 内购列表页面上看到的商品id,不需要再拼接包名bundle identifier。
// Get product id from plist.
func predefinedProductIdentifiers() -> [String]? {
guard let url = Bundle.main.url(forResource: "ProductID", withExtension: "plist") else { return nil }
guard let productIdentifiers = NSArray(contentsOf: url) as? [String] else { return nil }
return productIdentifiers
}
验证商品id
请求苹果服务器验证我们提供的商品id是否有效。不过记得要把内购Controller设置为遵循SKProductsRequestDelegate
协议,否则无法收到通知。
func validateProductIdentifers(with productIdentifiers: [String]) {
guard let set = NSSet(array: productIdentifiers) as? Set<String> else { return }
let productRequest = SKProductsRequest(productIdentifiers: set)
request = productRequest // 将request强引用,避免在完成前被提前释放;不过也务必在离开页面后,将request.delegate设置为nil
productRequest.delegate = self
productRequest.start()
SVProgressHUD.setDefaultStyle(.dark)
}
在协议中处理商品
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("received product response")
products = response.products // 将获取到的商品存储在界面实例变量,再次购买时不需要重复请求商品
// 下面是继续进行支付请求
请求支付
在获取了有效商品后,就可以用商品创建SKPayment
,并将payment
实例加入苹果支付队列中。
func requestPayment(with product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
处理支付
这个步骤核心的点就是创建并添加SKPaymentTransactionObserver
。
AppDelegate添加observer
- 在AppDelegate声明遵循
SKPaymentTransactionObserver
协议。 - 实现协议方法,实现后实现者就可以作为observer。
*// MARK: - SK Product Request Delegate*
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
DispatchQueue.main.async {
SVProgressHUD.show(withStatus: NSLocalizedString("订单处理中", comment: "请求支付提示" ))
}
case .deferred:
SVProgressHUD.show(withStatus: NSLocalizedString("订单处理中", comment: "请求支付提示" ))
case .purchased:
// 购买成功,此处需要发送玩家道具、保存凭证等动作。
// 当然交易完毕后,一定要手动关闭
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
// 交易失败,也要关闭交易
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
// 恢复订单
SKPaymentQueue.default().finishTransaction(transaction)
default:
print("other situation")
SKPaymentQueue.default().finishTransaction(transaction)
}
}
}
- 在
didFinishLaunchingWithOptions
方法中添加观察者:
SKPaymentQueue.default().add(self)
其他界面添加observer
假如用户一直呆在内购界面等待结束,我们很可能需要在内购界面也要一套前端的展示机制。跟AppDelegate的流程一样,我们将内购界面也作为SKPaymentTransactionObserver
添加到队列中。这样,内购界面也能收到通知了。
当内购界面可用的时候,就可以让内购界面去处理一些前端交互,比如弹出窗口,关闭当前界面等。
本人初学,有不当或错漏之处,感谢斧正!