美文网首页
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总结

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