通过苹果开发文档、搜索与挣扎摸索,我已经在App上实现了【消耗型商品】的内购。总结流程如下:

  1. 完成必须的准备工作
  2. 用有效的商品id创建SKProductRequest请求苹果服务器返回商品(SKProduct)。
  3. 用返回的有效的商品创建payment,并将payment加入苹果的支付队列。
  4. 监听队列,并根据监听到的交易状态进行适当的处理,尤其需要在交易完毕时关闭交易。

这方面的文档,我觉得苹果官方的Programming Guide就写的很好。可以仔细读一下。

我实际遇到的坑

  1. 必须先签署协议才能进行内购开发,否则无法返回商品。

  2. 创建沙箱测试员时,密码必须是强密码,即同时包含大写字母、小写字母和数字。否则,会报错:Unknown Errors while creating Sandbox Tester, Please check Error Log, email=xxx。

  3. 请求商品的SKProductRequest实例的delegate必须在离开页面时viewDidDisappear设置为nil,因为即使你已经离开了页面,苹果依然会尝试向SKProductRequest的代理发送消息,然后App就崩溃了。

  4. 必须在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

获取商品id,这个可以请求服务器,也可以保存在App上。我的App是用的后者。

首先创建一个名为ProductIDplist文件。文件内容是一个Array,里面是保存的商品id字符串。

这里需要说明的是,商品id,即product identifier,就是你在iTunes Connect 内购列表页面上看到的商品id,不需要再拼接包名bundle identifier。

1
2
3
4
5
6
// 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协议,否则无法收到通知。

1
2
3
4
5
6
7
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()
}

在协议中处理商品

1
2
3
4
5
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("received product response")
    products = response.products	// 将获取到的商品存储在界面实例变量,再次购买时不需要重复请求商品

  // 下面是继续进行支付请求

请求支付

在获取了有效商品后,就可以用商品创建SKPayment,并将payment实例加入苹果支付队列中。

1
2
3
4
func requestPayment(with product: SKProduct) {
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
}

处理支付

这个步骤核心的点就是创建并添加SKPaymentTransactionObserver

AppDelegate添加observer

  1. 在AppDelegate声明遵循SKPaymentTransactionObserver协议。
  2. 实现协议方法,实现后实现者就可以作为observer。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
*// 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)
        }
    }
}
  1. didFinishLaunchingWithOptions方法中添加观察者: SKPaymentQueue.default().add(self)

其他界面添加observer

假如用户一直呆在内购界面等待结束,我们很可能需要在内购界面也要一套前端的展示机制。跟AppDelegate的流程一样,我们将内购界面也作为SKPaymentTransactionObserver添加到队列中。这样,内购界面也能收到通知了。

离开其他界面时,记得调用SKPaymentQueue.default().remove(self)来移除observer,否则可能会崩溃。

当内购界面可用的时候,就可以让内购界面去处理一些前端交互,比如弹出窗口,关闭当前界面等。

恢复支付

如果用户购买的是非消耗型商品,比如是一个去广告服务。用户卸载后,如果重新安装回来,是可以申请恢复记录的。我使用的办法比较简陋,没有用加密和服务端验证,只是对比了下transaction.payment.productIdentifier

首先,在用户发起「恢复购买记录」后,调用SKPaymentQueue.default().restoreCompletedTransactions()

然后,在updateTransactions协议方法中,在case .restored中验证用户的transaction.payment.productIdentifier,若符合,则修改UerDefaults中的去广告标记。

本人初学,有不当或错漏之处,感谢斧正!