iOS - IAP總結(jié)

IAP的實(shí)現(xiàn)的流程

IAP流程
  • app端通過產(chǎn)品Id發(fā)送創(chuàng)建SKProductsRequest請(qǐng)求獲取產(chǎn)品
  • 獲取到產(chǎn)品后創(chuàng)建SKPayment發(fā)起支付請(qǐng)求
  • 監(jiān)聽支付回調(diào)
  • 支付成功獲取支付憑證
  • 通過支付憑證,向服務(wù)端發(fā)起請(qǐng)求,校驗(yàn)憑證的正確性,創(chuàng)建訂單
  • app端接收校驗(yàn)后的服務(wù)器響應(yīng),接收數(shù)據(jù)回調(diào)

IAP自動(dòng)續(xù)期訂閱代碼實(shí)戰(zhàn)

  • 以設(shè)計(jì)一個(gè)IAPService模塊舉例說明

IAPService的對(duì)外接口

  • 直接購買
  • 注意:怎么才叫購買成功:Apple端支付成功,自己的服務(wù)端校驗(yàn)成功,訂單生產(chǎn)成功,才算完成了購買,所以SKPaymentQueue.default().finishTransaction(lastTansation)要在這些流程都跑通之后,才去調(diào)用,如果不調(diào)這個(gè)finish方法,則可以從 SKPaymentQueue.default().transactions獲取到這相關(guān)的交易,喚醒操作校驗(yàn)未完成的訂單或者訂閱續(xù)費(fèi)成功之后,都可以在這個(gè)transactions中獲取到,但是有可能有延遲,所以要在每次app啟動(dòng)的時(shí)候,執(zhí)行一個(gè)喚醒操作
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 { /// 購買成功過,直接去校驗(yàn)
                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")
            }
        }
    }
  • 冷啟動(dòng)校驗(yàn)(喚醒操作)
    • 如果有在蘋果服務(wù)器購買成功,但是在自己的服務(wù)器都沒有創(chuàng)建好訂單,則冷啟動(dòng)重新校驗(yàn)未完成的訂單
    • 如果沒有未完成的訂單,則獲取一下當(dāng)前憑證的最新信息,保持信息的同步,比如訂閱過期了,app端有可能沒有接收到回調(diào)信息,這樣就會(huì)導(dǎo)致APP端一直處于訂閱狀態(tài),當(dāng)然注冊(cè)了交易狀態(tài)的監(jiān)聽也可以知道,但是信息有可能會(huì)延遲,這樣做,相當(dāng)于一個(gè)保險(xiǎn)操作
    • 喚醒操作的目的:1.初始化IAPService單例,注冊(cè)監(jiān)聽,2.校驗(yàn)續(xù)費(fèi)或者已經(jīng)在Apple購買成功,但是沒有在自己服務(wù)端生成訂單的交易
    /// 冷啟動(dòng)校驗(yàn)未完成的訂單
    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)
        }
    }
  • 恢復(fù)購買
    • 發(fā)起回復(fù)購買
    • paymentQueueRestoreCompletedTransactionsFinished中接收回調(diào)的結(jié)果
    • 如果憑證有效,直接調(diào)取服務(wù)端校驗(yàn)接口,返回IAP數(shù)據(jù)信息
    • 如果憑證無效, 發(fā)起刷新憑證請(qǐng)求, 監(jiān)聽請(qǐng)求回調(diào)結(jié)果
    • 再次嘗試判斷憑證是否為空,若不為空,則進(jìn)行憑證校驗(yàn),返回IAP數(shù)據(jù)信息
 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) {
        /// 如果是恢復(fù)購買時(shí)刷新憑證的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
        }
    }
  • 預(yù)先拉取商品
  func prefetchProducts(_ productIds: Set<String>) {
        guard SKPaymentQueue.canMakePayments() else { return }
        preFetchRequest = SKProductsRequest(productIdentifiers: productIds)
        preFetchRequest?.delegate = self
        preFetchRequest?.start()
    }
  • 信息回調(diào)(通知的形式)

內(nèi)部接口

  • IAPService模塊創(chuàng)建時(shí)添加支付狀態(tài)的監(jiān)聽
 private override init() {
        super.init()
        SKPaymentQueue.default().add(self)
    }
  • 處理產(chǎn)品請(qǐng)求,交易狀態(tài)更新的回調(diào)

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) {
        /// 如果是恢復(fù)購買時(shí)刷新憑證的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)
    }
}
  • 校驗(yàn)憑證,生成訂單,獲取當(dāng)前憑證的最新信息
  /// 校驗(yàn)購買時(shí)使用
    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 { /// 續(xù)費(fèi)
            provider.request(.checkReceipt(reciepString)) { response in
                paraseReponse(response, success: success)
            }
        }
    }
    
    /// 校驗(yàn)恢復(fù)時(shí)使用
    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)
            }
        }
    }
    
    ///獲取當(dāng)前憑證的最新信息:比如訂閱過期,Apple知道訂閱過期了,但是app端有可能不知道,所以需要主動(dòng)獲取一次
    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
            }
        }
    }

如何防止憑證被偽造?(面試經(jīng)常問)

  • app端采用的是將憑證發(fā)送給我們自己的服務(wù)器,通過自己的服務(wù)器再向蘋果驗(yàn)證,蘋果返回的信息會(huì)決定這個(gè)憑證是否有效,如果無效則自己的服務(wù)器則返回錯(cuò)誤給客戶端。即把驗(yàn)證憑證這個(gè)操作,交由我們自己的服務(wù)器,我們和自己的服務(wù)器交互

  • 自己服務(wù)器的數(shù)據(jù)安全由Https(對(duì)稱加密和非對(duì)稱加密)和一些自己約定的通信方式來保證(對(duì)稱加密)

  • 完整代碼傳送門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容