美文网首页
iOS - IAP总结

iOS - IAP总结

作者: lieon | 来源:发表于2021-06-09 00:08 被阅读0次

IAP的实现的流程

IAP流程
  • app端通过产品Id发送创建SKProductsRequest请求获取产品
  • 获取到产品后创建SKPayment发起支付请求
  • 监听支付回调
  • 支付成功获取支付凭证
  • 通过支付凭证,向服务端发起请求,校验凭证的正确性,创建订单
  • app端接收校验后的服务器响应,接收数据回调

IAP自动续期订阅代码实战

  • 以设计一个IAPService模块举例说明

IAPService的对外接口

  • 直接购买
  • 注意:怎么才叫购买成功:Apple端支付成功,自己的服务端校验成功,订单生产成功,才算完成了购买,所以SKPaymentQueue.default().finishTransaction(lastTansation)要在这些流程都跑通之后,才去调用,如果不调这个finish方法,则可以从 SKPaymentQueue.default().transactions获取到这相关的交易,唤醒操作校验未完成的订单或者订阅续费成功之后,都可以在这个transactions中获取到,但是有可能有延迟,所以要在每次app启动的时候,执行一个唤醒操作
func buyProduct(_ productId: String) {
        if SKPaymentQueue.canMakePayments() {
            let prefetched = prefetchProducts[productId]
            let lastTansation = SKPaymentQueue.default().transactions.filter { $0.payment.productIdentifier == productId }.first
            if let lastTansation = lastTansation, lastTansation.transactionState == .purchased { /// 购买成功过,直接去校验
                let productId = lastTansation.payment.productIdentifier
                let isFirstBuy = lastTansation.original == nil
                verifyBuySession(productId, isFirstBuy: isFirstBuy) {
                    SKPaymentQueue.default().finishTransaction(lastTansation)
                }
            } else if let prefetched = prefetched {
                let payment = SKPayment(product: prefetched)
                SKPaymentQueue.default().add(payment)
            } else {
                var products = Set<String>()
                products.insert(productId)
                let request = SKProductsRequest(productIdentifiers: products)
                request.delegate = self
                request.start()
            }
        } else {
            buyFailure.call(nil)
            NotificationCenter.default.post(name: NSNotification.Name.iapBuyFailure, object: nil)
        }
    }

 func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        if response.products.isEmpty {
            return
        }
        if request == preFetchRequest {
            /// 注意线程安全
            lock.lock()
            response.products.forEach { product in
                prefetchProducts[product.productIdentifier] = product
            }
            lock.unlock()
            return
        }
        
        if let product = response.products.first {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
        } else {
            buyFailure.call(nil)
            NotificationCenter.default.post(name: NSNotification.Name.iapBuyFailure, object: nil)
        }
    }

 func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        transactions.forEach { (transation) in
            switch transation.transactionState {
            /// 购买成功
            case .purchased:
                let productId = transation.payment.productIdentifier
                let isFirstBuy = transation.original == nil
                verifyBuySession(productId, isFirstBuy: isFirstBuy) {
                    SKPaymentQueue.default().finishTransaction(transation)
                }
            case .failed:
                handleFailed(transation)
            case .restored:
                handleRestored(transation)
            case .purchasing:
                break
            case .deferred:
                break
            @unknown default:
                debugPrint("IAPService - @unknown default")
            }
        }
    }
  • 冷启动校验(唤醒操作)
    • 如果有在苹果服务器购买成功,但是在自己的服务器都没有创建好订单,则冷启动重新校验未完成的订单
    • 如果没有未完成的订单,则获取一下当前凭证的最新信息,保持信息的同步,比如订阅过期了,app端有可能没有接收到回调信息,这样就会导致APP端一直处于订阅状态,当然注册了交易状态的监听也可以知道,但是信息有可能会延迟,这样做,相当于一个保险操作
    • 唤醒操作的目的:1.初始化IAPService单例,注册监听,2.校验续费或者已经在Apple购买成功,但是没有在自己服务端生成订单的交易
    /// 冷启动校验未完成的订单
    func wakeUp() {
        guard SKPaymentQueue.canMakePayments() else {
            return
        }
        guard let lastTansition = SKPaymentQueue.default().transactions.filter({ $0.transactionState == .purchased}).first  else {
            return
        }
        let productId = lastTansition.payment.productIdentifier
        let isFirstBuy = lastTansition.original == nil
        verifyBuySession(productId, isFirstBuy: isFirstBuy) {
            SKPaymentQueue.default().finishTransaction(lastTansition)
        }
    }
  • 恢复购买
    • 发起回复购买
    • paymentQueueRestoreCompletedTransactionsFinished中接收回调的结果
    • 如果凭证有效,直接调取服务端校验接口,返回IAP数据信息
    • 如果凭证无效, 发起刷新凭证请求, 监听请求回调结果
    • 再次尝试判断凭证是否为空,若不为空,则进行凭证校验,返回IAP数据信息
 func restore() {
        guard SKPaymentQueue.canMakePayments() else {
            return
        }
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

 func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        /// 凭证无效则刷新一下
        if reciepString == nil {
            let refresh = SKReceiptRefreshRequest()
            refresh.delegate = self
            refresh.start()
        } else {
            verifyRestoreSession()
        }
    }
    
  func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        restoreFailure.call(error)
        NotificationCenter.default.post(name: NSNotification.Name.iapRestoreFailure, object: nil)
    }

 func requestDidFinish(_ request: SKRequest) {
        /// 如果是恢复购买时刷新凭证的request
        if request is SKReceiptRefreshRequest {
            if reciepString == nil {
                restoreFailure.call(AppError(message: "error.SKReceiptRefreshRequest", code: ErrorCode(rawValue: -100) ?? .none))
                NotificationCenter.default.post(name: NSNotification.Name.iapRestoreFailure, object: nil)
            } else {
                verifyRestoreSession()
            }
        }
        if request == preFetchRequest {
            preFetchRequest = nil
        }
    }
  • 预先拉取商品
  func prefetchProducts(_ productIds: Set<String>) {
        guard SKPaymentQueue.canMakePayments() else { return }
        preFetchRequest = SKProductsRequest(productIdentifiers: productIds)
        preFetchRequest?.delegate = self
        preFetchRequest?.start()
    }
  • 信息回调(通知的形式)

内部接口

  • IAPService模块创建时添加支付状态的监听
 private override init() {
        super.init()
        SKPaymentQueue.default().add(self)
    }
  • 处理产品请求,交易状态更新的回调

extension IAPService: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        if response.products.isEmpty {
            return
        }
        if request == preFetchRequest {
            /// 注意线程安全
            lock.lock()
            response.products.forEach { product in
                prefetchProducts[product.productIdentifier] = product
            }
            lock.unlock()
            return
        }
        
        if let product = response.products.first {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
        } else {
            buyFailure.call(nil)
            NotificationCenter.default.post(name: NSNotification.Name.iapBuyFailure, object: nil)
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        buyFailure.call(error)
        NotificationCenter.default.post(name: NSNotification.Name.iapBuyFailure, object: nil)
    }
    
    func requestDidFinish(_ request: SKRequest) {
        /// 如果是恢复购买时刷新凭证的request
        if request is SKReceiptRefreshRequest {
            if reciepString == nil {
                restoreFailure.call(AppError(message: "error.SKReceiptRefreshRequest", code: ErrorCode(rawValue: -100) ?? .none))
                NotificationCenter.default.post(name: NSNotification.Name.iapRestoreFailure, object: nil)
            } else {
                verifyRestoreSession()
            }
        }
        if request == preFetchRequest {
            preFetchRequest = nil
        }
    }
}

extension IAPService: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        transactions.forEach { (transation) in
            switch transation.transactionState {
            /// 购买成功
            case .purchased:
                let productId = transation.payment.productIdentifier
                let isFirstBuy = transation.original == nil
                verifyBuySession(productId, isFirstBuy: isFirstBuy) {
                    SKPaymentQueue.default().finishTransaction(transation)
                }
            case .failed:
                handleFailed(transation)
            case .restored:
                handleRestored(transation)
            case .purchasing:
                break
            case .deferred:
                break
            @unknown default:
                debugPrint("IAPService - @unknown default")
            }
        }
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        /// 凭证无效则刷新一下
        if reciepString == nil {
            let refresh = SKReceiptRefreshRequest()
            refresh.delegate = self
            refresh.start()
        } else {
            verifyRestoreSession()
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        restoreFailure.call(error)
        NotificationCenter.default.post(name: NSNotification.Name.iapRestoreFailure, object: nil)
    }
}
  • 校验凭证,生成订单,获取当前凭证的最新信息
  /// 校验购买时使用
    fileprivate func verifyBuySession(_ productId: String? = nil, isFirstBuy: Bool, success: (() -> Void)? = nil) {
        let provider = MoyaProvider<IAPNetWorkTarget>()
        guard let reciepString = reciepString else {
            return
        }
        
        func paraseReponse(_ response: Result<Response, MoyaError>, success: (() -> Void)? = nil) {
            switch response {
            case .success(let res):
                guard let resModel = try? res.model(IAPResponse.self) else {
                    return
                }
                if let data = resModel.data {
                    self.buySuccess.call(data)
                    NotificationCenter.default.post(name: NSNotification.Name.iapBuySuccess, object: data)
                    success?()
                } else {
                    let error = AppError(message: resModel.message ?? "", code: ErrorCode(rawValue: resModel.status) ?? .none)
                    self.buyFailure.call(error)
                    NotificationCenter.default.post(name: NSNotification.Name.iapBuyFailure, object: nil)
                }
            case .failure(let error):
                self.buyFailure.call(AppError(message: error.localizedDescription, code: ErrorCode(rawValue: -100) ?? .none))
                NotificationCenter.default.post(name: NSNotification.Name.iapBuyFailure, object: nil)
            }
        }
        if isFirstBuy, let productId = productId { /// 订阅
            provider.request(.purchase(productId, reciepString)) { response in
                paraseReponse(response, success: success)
            }
        } else { /// 续费
            provider.request(.checkReceipt(reciepString)) { response in
                paraseReponse(response, success: success)
            }
        }
    }
    
    /// 校验恢复时使用
    fileprivate func verifyRestoreSession(_ success: (() -> Void)? = nil) {
        let provider = MoyaProvider<IAPNetWorkTarget>()
        guard let reciepString = reciepString else {
            return
        }
        provider.request(.checkReceipt(reciepString)) { response in
            switch response {
            case .success(let res):
                guard let resModel = try? res.model(IAPResponse.self) else {
                    return
                }
                if let data = resModel.data {
                    self.restoreSuccess.call(data)
                    NotificationCenter.default.post(name: NSNotification.Name.iapRestoreSuccess, object: data)
                    success?()
                } else {
                    let error = AppError(message: resModel.message ?? "", code: ErrorCode(rawValue: resModel.status) ?? .none)
                    self.restoreFailure.call(error)
                    NotificationCenter.default.post(name: NSNotification.Name.iapRestoreFailure, object: nil)
                }
            case .failure(let error):
                self.restoreFailure.call(AppError(message: error.localizedDescription, code: ErrorCode(rawValue: -100) ?? .none))
                NotificationCenter.default.post(name: NSNotification.Name.iapRestoreFailure, object: nil)
            }
        }
    }
    
    ///获取当前凭证的最新信息:比如订阅过期,Apple知道订阅过期了,但是app端有可能不知道,所以需要主动获取一次
    fileprivate func checkCurrentReciept() {
        let provider = MoyaProvider<IAPNetWorkTarget>()
        guard let reciepString = reciepString else {
            return
        }
        provider.request(.checkReceipt(reciepString)) { response in
            switch response {
            case .success(let res):
                guard let resModel = try? res.model(IAPResponse.self) else {
                    return
                }
                if let data = resModel.data {
                    NotificationCenter.default.post(name: NSNotification.Name.iapInfoUpdated, object: data)
                }
            case .failure:
                break
            }
        }
    }

如何防止凭证被伪造?(面试经常问)

  • app端采用的是将凭证发送给我们自己的服务器,通过自己的服务器再向苹果验证,苹果返回的信息会决定这个凭证是否有效,如果无效则自己的服务器则返回错误给客户端。即把验证凭证这个操作,交由我们自己的服务器,我们和自己的服务器交互

  • 自己服务器的数据安全由Https(对称加密和非对称加密)和一些自己约定的通信方式来保证(对称加密)

  • 完整代码传送门

相关文章

  • ios内购IAP相关内容

    ios内购IAP相关内容 iOS IAP应用内购详细步骤和问题总结指南 - 简书https://www.jians...

  • iOS开发 AIP支付总结

    iOS开发 IAP支付总结 一、IAP介绍 1.1、简介 这里先把官方文档给大家 App 内购买项目配置流程[ht...

  • iOS - IAP总结

    IAP的实现的流程 app端通过产品Id发送创建SKProductsRequest请求获取产品 获取到产品后创建S...

  • ruby实现ios和google内购

    一、ios 和 google 内购 ios IAP二次验证 IAP(In App purchase):App和Ap...

  • iOS 内购 IAP

    1. iOS内购IAP(一) —— 基础配置篇(一)2. iOS内购IAP(二) —— 工程实践(一)3. iOS...

  • 苹果常见规则整理

    苹果设备操作订阅/取消订阅 IAP 内部支付 Unity IAP插件使用 4 . IOS 打包不产生二进制文件

  • iOS内购(iap)总结

    刚刚做了内购, 记录一下这里直接上代码, 至于写代码之前的一些设置工作参考以下文章:http://www.jian...

  • 应用内支付 IAP支付流程 自动续期订阅 丢单处理 问题 流程详

    应用内支付 IAP支付流程 自动续期订阅 丢单处理 问题 流程详解 关于iOS的应用内支付(IAP), 我曾在项目...

  • iAP 的一些简介

    iAP 的小指南 in-App Purchase 内购的缩写为 iAP。是 iOS 自带的一种应用内数字产品的支...

  • iOS iap

    1.申请appleid 时填写in-app-purchesed2.填写税务信息3.编写代码4.由于build 为2...

网友评论

      本文标题:iOS - IAP总结

      本文链接:https://www.haomeiwen.com/subject/efyleltx.html