前言
老夫接到一个项目,需要苹果内购做充值,经过一周多的努力把核心代码完成,希望对大家有所帮助。
效果图
废话不多说,直接上图
流程说明
正常充值流程
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