美文网首页
Swift苹果内购IAP(余额充值场景)

Swift苹果内购IAP(余额充值场景)

作者: 雨打芭蕉落 | 来源:发表于2019-05-23 20:18 被阅读0次

前言

老夫接到一个项目,需要苹果内购做充值,经过一周多的努力把核心代码完成,希望对大家有所帮助。

效果图

废话不多说,直接上图

余额充值页面

流程说明

  • 正常充值流程
    1、【可选】App询问服务端是否可以请求充值ACG_PAY_50(50元)
    2、若允许,App向苹果发起支付(50元)
    3、苹果支付成功,返回凭证(payload)到App
    4.1、App使用payload请求本接口,
    4.2、服务端使用payload到苹果拿到下面Json数据
    4.3、校验Json数据中transaction_id是否已经使用
    4.4、若未使用,则给用户充值(50元),生成充值记录
    4.5、处理结束,返回成功(code=success)
    5、App把收到凭证从本地清除,完成支付,刷新余额

  • 异常情况:
    1、若充值过程(第3步)退出App,可充值成功,余额未给用户增加,再次打开App完成后续步骤即可
    2、若校验过程(第4步)网络断开,则可能校验成功,余额增加,App未收到校验信息,再次打开App仍然会发布充值,服务端判断transaction_id为重复,忽略即可。
    3、校验返回码每次都不成功时,App不会清除凭证,会造成每次打开App都校验,该商品也无法再次购买成功(系统提示:已经支付,可以恢复购买)。

核心代码,稍作修改即可复用

  • IAPHelper.swift
//
//  IAPHelper.swift
//  IAP
//
//  Created by lin bo on 2019/5/13.
//  Copyright © 2019 appTech. All rights reserved.
//

import UIKit
import StoreKit

/// 商品列表
enum ACG_PAY_ID: String {
    
    case pay50 = "ACG_PAY_50"
    case pay98 = "ACG_PAY_98"
    case pay148 = "ACG_PAY_148"
    case pay198 = "ACG_PAY_198"
    case pay248 = "ACG_PAY_248"
    case pay298 = "ACG_PAY_298"

    func price() -> Int {
        
        switch self {
            
        case .pay50: return 50
        case .pay98: return 98
        case .pay148: return 148
        case .pay198: return 198
        case .pay248: return 248
        case .pay298: return 298
        }
    }
}

/// 回调状态
enum IAPProgress: Int {
    
    /// 初始状态
    case none
    /// 开始
    case started
    /// 购买中
    case purchasing
    
    /// 支付成功
    case purchased
    /// 失败
    case payFailed
    /// 重复购买
    case payRestored
    /// 状态未确认
    case payDeferred
    /// 其他
    
    case payOther
    /// 开始后端校验
    case checking
    /// 后端校验成功
    case checkedSuccess
    /// 后端校验失败
    case checkedFailed

}

enum IAPPayCheck {
    
    case busy /// 有支付正在进行
    case notInit /// 未初始化
    case initFailed /// 初始化失败
    case notFound /// 没有找到该商品,中断
    case systemFailed /// 系统检测失败
    case ok /// 可以进行

}

class IAPHelper: ATBaseHelper {
    
    static let shared = IAPHelper()
    
    /// 检测初始化回调
    fileprivate var checkBlock: ((_ b: IAPPayCheck) -> ())?
    /// 支付过程回调
    var resultBlock: ((_ type: IAPProgress, _ pID: ACG_PAY_ID?) -> ())?
    
    /// 是否正在支付
    fileprivate var isBusy: Bool {
        get {
            switch progress {
            case .none:
                return false
            default:
                return true
            }
        }
    }
   
    /// 购买的状态
    fileprivate var progress: IAPProgress = .none {
        didSet {
            /// 状态改变回调
            if let block = resultBlock {
                block(progress, currentPID)
            }
        }
    }
    
    /// 当前付费的ID
    fileprivate var currentPID: ACG_PAY_ID?
    /// 商品列表
    fileprivate var productList: [SKProduct]?

    /// 初始化配置,请求商品
    func config() {
        
        SKPaymentQueue.default().add(self)
        requestAllProduct()
    }
    
    /// 初始化,请求商品列表
    func initPayments(_ block: @escaping ((_ b: IAPPayCheck) -> ())) {
        
        let c = checkPayments()
        
        if c == .notInit {
            
            requestAllProduct()
            checkBlock = block

        }else {
            
            block(c)
        }
    }
    
    /// 检测支付环境,非.ok不允许充值
    func checkPayments() -> IAPPayCheck {
        
        guard isBusy == false else {
            return .busy
        }
        
        guard let plist = productList, !plist.isEmpty else {
            return .notInit
        }
        
        guard SKPaymentQueue.canMakePayments() else {
            return .systemFailed
        }
        
        return .ok
    }
    
    /// 请求商品列表
    private func requestAllProduct() {
        
        let set: Set<String> = [ACG_PAY_ID.pay50.rawValue,
                        ACG_PAY_ID.pay98.rawValue,
                        ACG_PAY_ID.pay148.rawValue,
                        ACG_PAY_ID.pay198.rawValue,
                        ACG_PAY_ID.pay248.rawValue,
                        ACG_PAY_ID.pay298.rawValue]
        
        let request = SKProductsRequest(productIdentifiers: set)
        request.delegate = self
        request.start()
    }
    
    /// 支付商品
    @discardableResult
    func pay(pID: ACG_PAY_ID) -> IAPPayCheck {
        
        let c = checkPayments()
        
        if c == .ok {
            
            guard let plist = productList, !plist.isEmpty else {
                return .notInit
            }

            let pdts = plist.filter {
                return $0.productIdentifier == pID.rawValue
            }
            
            guard let product = pdts.first else {
                return .notFound
            }
            
            currentPID = pID
            requestProduct(pdt: product)
        }
        
        return c
    }
    
    /// 请求充值
    fileprivate func requestProduct(pdt: SKProduct) {
        
        progress = .started

        let pay: SKMutablePayment = SKMutablePayment(product: pdt)
        SKPaymentQueue.default().add(pay)
    }
    
    /// 重置
    fileprivate func payFinish() {
        
        currentPID = nil
        progress = .none
    }
    
    /// 充值完成后给后台校验
    func completeTransaction(_ checkList: [SKPaymentTransaction]) {
        
        if resultBlock == nil {
            showAlert("充值校验中...")
        }
        ALog("充值校验中...")
        progress = .checking
        
        guard let rURL = Bundle.main.appStoreReceiptURL, let data = try? Data(contentsOf: rURL) else {
            ALog("appStoreReceiptURL error")
            
            progress = .checkedFailed
            payFinish()
            return
        }
        
        let str = data.base64EncodedString()
        print(str)
        
        OrderServer.shared.requestCheckIAP(str) { [weak self] (code, msg, result) in
            
            guard let helper = self else {
                return
            }
            
            if result { // 成功则删除
                checkList.forEach({ (transaction) in
                    SKPaymentQueue.default().finishTransaction(transaction)
                })
            }
            
            if helper.resultBlock == nil {
                showAlert(result ? "校验成功" : "校验失败")
            }
            ALog(result ? "充值成功" : "充值失败")
            
            helper.progress = result ? .checkedSuccess : .checkedFailed
            helper.payFinish()
        }
    }
}

// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        ALog("---IAP---")

        if currentPID == nil {
            // 列表赋值
            productList = response.products
        }
    }
    
    func requestDidFinish(_ request: SKRequest) {
        ALog("---IAP---")

        if currentPID == nil {
            
            if let block = checkBlock {
                
                if let pList = productList, !pList.isEmpty {
                    block(.ok)
                }else {
                    block(.initFailed)
                }
                checkBlock = nil
            }
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        ALog("---IAP---")

        if currentPID == nil {
            
            if let block = checkBlock {
                block(.initFailed)
                checkBlock = nil
            }
        }
    }
}

// MARK: - SKPaymentTransactionObserver
extension IAPHelper: SKPaymentTransactionObserver {
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        ALog("---IAP---")

        var checkList: [SKPaymentTransaction] = []
        var type: IAPProgress = progress

        for transaction in transactions {
            
            ALog("支付结果: \(transaction.description)")

            let pid = transaction.payment.productIdentifier
            switch transaction.transactionState {
                
            case .purchasing:
                
                ALog("支付中:\(pid)")
                type = .purchasing

            case .purchased:
                
                checkList.append(transaction)
                ALog("支付成功:\(pid)")
                type = .purchased

            case .failed:
                
                ALog("支付失败:\(pid)")
                type = .payFailed
                SKPaymentQueue.default().finishTransaction(transaction)

            case .restored:
                
                checkList.append(transaction)
                ALog("支付已购买过:\(pid)")
                type = .payRestored

            case .deferred:
                
                ALog("支付不确认:\(pid)")
                type = .payDeferred
                SKPaymentQueue.default().finishTransaction(transaction)

            @unknown default:
                
                ALog("支付未知状态:\(pid)")
                type = .payOther
                SKPaymentQueue.default().finishTransaction(transaction)
            }
        }
        
        progress = type
        
        if !checkList.isEmpty {
            // 有内购已经完成
            completeTransaction(checkList)
            
        }else if type == .purchasing {
            // 正常情况:内购正在支付
            // 特殊情况:若该商品已购买,未执行finishTransaction,系统会提示(免费恢复项目),回调中断
            // 解决方法:在应用开启的时候捕捉到restored状态的商品,提交后台校验后执行finishTransaction

        }else { // 其他状态
            
            payFinish()
        }
    }

    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
        ALog("---IAP---")
        return true
    }
}

  • ViewController调用

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // 回调支持过程,处理HUD显隐和用户提示
        IAPHelper.shared.resultBlock = { [weak self] (result, pID) in
            
            guard let vc = self else {
                return
            }
            
            switch result {

            case .none:
                break
                
            case .started:
                vc.updateHUD(true, text: "支付中")
                
            case .purchasing:
                break
                
            case .purchased:
                break
                
            case .payFailed:
                vc.updateHUD(false, text: "支付取消")

            case .payRestored:
                vc.updateHUD(false)

            case .payDeferred:
                vc.updateHUD(false)

            case .payOther:
                vc.updateHUD(false)

            case .checking:
                vc.updateHUD(true, text: "充值中")
                
            case .checkedSuccess:
                vc.updateHUD(false, text: "充值成功")
                vc.updateData()
                
            case .checkedFailed:
                vc.updateHUD(false, text: "充值失败,请检测网络")
                vc.updateData()
            }
        }
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        
        IAPHelper.shared.resultBlock = nil
    }

    @IBAction func payAction(_ sender: Any) {
        
        for bt in sumBtns {
            if bt.isSelected == true {
 
                switch bt.tag {
                case 1:
                    pay(id: .pay50)
                case 2:
                    pay(id: .pay98)
                case 3:
                    pay(id: .pay148)
                case 4:
                    pay(id: .pay198)
                case 5:
                    pay(id: .pay248)
                case 6:
                    pay(id: .pay298)
                default:
                    break
                }
                break
            }
        }
    }

    func pay(id: ACG_PAY_ID) {
        
        // 请求支付
        let p = IAPHelper.shared.pay(pID: id)
        
        switch p {
            
        case .ok:
            break
            
        case .notInit:
            IAPHelper.shared.initPayments { (c) in
                
                if c == .ok {
                    
                    if IAPHelper.shared.pay(pID: id) != .ok {
                        showAlert("暂时无法支付,请稍后再试")
                    }
                    
                }else {
                    showAlert("暂时无法支付,请稍后再试")
                }
            }
            break

        default:
            showAlert("暂时无法支付,请稍后再试")
            break
        }
    }

  • AppDelegate需要初始化一下
IAPHelper.shared.config()

附件:

  • 支付成功后的交易凭证数据很大,后端接收这个数据,跟苹果校验后,判断用户即可给该用户充值。


        guard let rURL = Bundle.main.appStoreReceiptURL, let data = try? Data(contentsOf: rURL) else {
            ALog("appStoreReceiptURL error")
        }
        
        let str = data.base64EncodedString()
        print(str)


  • 打印出来是这样的(很长)

MIIVKgYJKoZIhvcNAQcCoIIVGzCCFRcCAQExCzAJBgUrDgMCGgUAMIIEywYJKoZIhvcNAQcBoIIEvASCBLgxggS0MAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQECAQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADALAgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQEAgIAiTANAgENAgEBBAUCAwHViDANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAyNTIwGAIBBAIBAgQQEXxl0gRk5NBqMO8/VFkmNzAZA... ...

  • 苹果返回Json

1、收到这个数据表示,里面的项目肯定付费成功
2、通过transaction_id判断是否重复校验
3、通过product_id * quantity 判断支付金额
4、通过environment判断所在环境

注意:用户信息通过判断App登录用户,透传App用户信息和App订单信息都是不可靠的。


{
  "environment": "Sandbox",
  "receipt": {
    "in_app": [{
        "transaction_id": "1000000529594470",
        "original_purchase_date": "2019-05-21 08:25:55 Etc/GMT",
        "quantity": "1",
        "original_transaction_id": "1000000529594470",
        "purchase_date_pst": "2019-05-21 01:25:55 America/Los_Angeles",
        "original_purchase_date_ms": "1558427155000",
        "purchase_date_ms": "1558427155000",
        "product_id": "ACG_PAY_50",
        "original_purchase_date_pst": "2019-05-21 01:25:55 America/Los_Angeles",
        "is_trial_period": "false",
        "purchase_date": "2019-05-21 08:25:55 Etc/GMT"
      },
      {
        "transaction_id": "1000000529074541",
        "original_purchase_date": "2019-05-20 02:29:04 Etc/GMT",
        "quantity": "1",
        "original_transaction_id": "1000000529074541",
        "purchase_date_pst": "2019-05-19 19:29:04 America/Los_Angeles",
        "original_purchase_date_ms": "1558319344000",
        "purchase_date_ms": "1558319344000",
        "product_id": "ACG_PAY_98",
        "original_purchase_date_pst": "2019-05-19 19:29:04 America/Los_Angeles",
        "is_trial_period": "false",
        "purchase_date": "2019-05-20 02:29:04 Etc/GMT"
      }
    ],
    "adam_id": 0,
    "receipt_creation_date": "2019-05-21 08:51:54 Etc/GMT",
    "original_application_version": "1.0",
    "app_item_id": 0,
    "original_purchase_date_ms": "1375340400000",
    "request_date_ms": "1558430410539",
    "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
    "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
    "receipt_creation_date_pst": "2019-05-21 01:51:54 America/Los_Angeles",
    "receipt_type": "ProductionSandbox",
    "bundle_id": "com.xxx.acg",
    "receipt_creation_date_ms": "1558428714000",
    "request_date": "2019-05-21 09:20:10 Etc/GMT",
    "version_external_identifier": 0,
    "request_date_pst": "2019-05-21 02:20:10 America/Los_Angeles",
    "download_id": 0,
    "application_version": "1"
  },
  "status": 0
}

后端实现参考文章

1、流程写得很详细,php源码也有:
http://www.cnblogs.com/wangboy91/p/7162335.html
2、Java端的支持:
https://blog.csdn.net/jianzhonghao/article/details/79343887

相关文章

  • Swift苹果内购IAP(余额充值场景)

    前言 老夫接到一个项目,需要苹果内购做充值,经过一周多的努力把核心代码完成,希望对大家有所帮助。 效果图 废话不多...

  • IAP(苹果充值)内购整理文档

    参考借鉴了一下几篇文章(在这里很感谢各位作者大大做出的总结和方案): http://blog.csdn.net/a...

  • 苹果内购(iap In-App Purchase)

    什么是iap iap 是 In-App Purchase 的缩写, 即苹果内购. 在苹果内购买虚拟产品需要通过ia...

  • 苹果内购IAP 简单总结

    1. 何为苹果内购IAP IAP(in-app-purchase),指苹果平台上所有在应用内购买的虚拟商品(商品的...

  • iOS 苹果内购(In-App Purchase)

    内购简介 IAP 全称:In-App Purchase,是指苹果 App Store 的应用内购买,是苹果为 Ap...

  • 苹果内购In-app purchase

    关于苹果内购(IAP)的一些问题以及那些坑: 最近在研究苹果内购功能,所以,在网上找了一些资料,进行学习。但是,内...

  • ios内购注意事项

    内购两种方式 ios内购及一些常用的破解手段 iap内购破解原理 苹果官方内购demo 内购的消耗性和非消耗性购买说明

  • 细说苹果内购IAP

    花了快10个工作日,终于完成了内购(IAP)功能。必须写篇文章来记录一下这十天来的心得体会,更是为了避免后续的开发...

  • 走进苹果内购IAP

    马上要过年了,而且还是本命年,为了庆祝下大鸡年,放一张苹果官网的中国元素(不多说设计此图的人NB). 1,了解苹果...

  • 苹果内购(IAP)使用

    0.前言: 实在躲不过去了,只能妥协了。? 1.准备工作: 1.1.在ASC配置协议、税务和银行业务的信息,这一步...

网友评论

      本文标题:Swift苹果内购IAP(余额充值场景)

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