iOS 接入 IAP 指南

Feb 14, 2020 • 预计阅读时间 3 分钟

如果 APP 里需要购买虚拟商品,只能通过苹果的 IAP,否则审核会被拒绝。

接入 IAP 不复杂,只要遵守以下流程,就不会出现“掉单”的情况。

全局监听支付通知

接入 IAP 需要引用的库是 StoreKit。为了比较方便的处理逻辑,建议定制一个单例来处理 IAP 相关的业务。

发起支付后不管成功或者失败,系统都会给 APP 发送通知告知支付的状态,要接收这个通知首先需要注册。

我定制的单例类是 IAPService

import StoreKit

class IAPService: NSObject, SKPaymentTransactionObserver {
    static let shared = IAPService()

    func addObserver() {
        SKPaymentQueue.default().add(self)
    }

    // 这里接收所有支付事务的状态:成功、失败或者正在支付
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    }
}

需要 APP 启动的时候就进行监听,以便及时处理业务。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        IAPService.shared.addObserver()
}

处理支付事务

需要关注的状态只有两个,成功和失败。

如果状态是失败,可以直接完成这次事务,不会有任何问题,如果不关闭,下次启动 APP 时又会收到回调。

如果成功了,又分为两种情况:已购买和已恢复,已购买是指新的购买,已恢复是用于非消耗物品或者订阅制的服务。

对于状态是已购买和已恢复的交易事务,需要缓存起来稍后在合适的地方进行凭证校验。

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        handleTransaction(transaction)
    }
}

func handleTransaction(_ transaction: SKPaymentTransaction) {
    switch transaction.transactionState {
        // 购买成功,缓存这个事务
        case .purchased:
            unfinishedTransactions.append(transaction)
            break

        // 成功恢复购买,缓存这个事务
        case .restored:
            unfinishedTransactions.append(transaction.original!)
            break

        case .failed:
            SKPaymentQueue.default().finishTransaction(transaction)

        case .purchasing, .deferred:
            fallthrough

        @unknown default:
            break
    }
}

校验凭证

购买成功以后,系统就会把凭证存储在 APP 的沙盒里,路径需要通过以下方法获取:

guard let receiptURL = Bundle.main.appStoreReceiptURL else {
    return
}

不管购买多少次,都是用这个地址获取,凭证就相当于商场里的购物小票,里面记录了所有购买的物品以及状态。

校验凭证就是把这个文件经过 base64 编码后发送到苹果服务器,苹果服务器会返回 json 格式的信息,里面记录的是所购买的物品详情。 这个验证过程只能放在业务服务器来做,由 APP 来验证会有被篡改的风险。

把凭证 base64 编码的代码如下:

var receiptBase64String: String? {
    get {
        guard let receiptURL = Bundle.main.appStoreReceiptURL else {
            return nil
        }

        guard let receiptData = try? Data(contentsOf: receiptURL) else {
            return nil
        }

        return receiptData.base64EncodedString()
    }
}

开发环境和 TestFlight 测试的版本,只能通过沙盒环境验证:

https://sandbox.itunes.apple.com/verifyReceipt

只有上架到 App Store 了,才能使用正式环境验证成功:

https://buy.itunes.apple.com/verifyReceipt

关闭交易事务

验证凭证成功后,业务服务器就可以给 APP 进行充值虚拟货币或者虚拟物品了。

APP 收到业务服务器返回的充值成功回应,才可以关闭交易事务,否则就会发生掉单的情况。

所以避免掉单很简单,只有在业务服务器返回了成功的时候才关闭交易事务就可以了。 只要不关闭事务,每次 APP 启动都能收到通知,都还有机会进行补单操作。 而且不关闭交易事务,用户就不能购买同一个产品ID的商品,避免了用户会被重复扣费的问题。

一些经验

  • 如果交易事务还没有结束,APP 被卸载,在安装的时候获取凭证路径会不存在,需要重启一次 APP。
  • 如果 AppleID 未绑定支付方式,购买的时候会先取消 APP 里的交易然后跳到 App Store 内添加购买方式后才发起购买,这个时候的交易事务是由系统创建的而不是 APP。购买成功后 APP 还是可以收到通知。
  • 可以使用 applicationUsername 绑定用户 ID 的方式防止窜单,但是如果交易是由系统发起的(比如上面提到的情况),这个字段会是空的,所以只有这个字段长度大于 0 的时候才需要判断是否窜单。
iOS
版权声明:如果转发请带上本文链接和注明来源。

lvv.me

iOS/macOS Developer

Swift 创建纯代码的 macOS & iOS 应用

iOS13 使用系统里的第三方字体