前言
老夫接到一個(gè)項(xiàng)目,需要蘋(píng)果內(nèi)購(gòu)做充值壤躲,經(jīng)過(guò)一周多的努力把核心代碼完成城菊,希望對(duì)大家有所幫助。
效果圖
廢話不多說(shuō)碉克,直接上圖
流程說(shuō)明
正常充值流程
1凌唬、【可選】App詢問(wèn)服務(wù)端是否可以請(qǐng)求充值A(chǔ)CG_PAY_50(50元)
2、若允許漏麦,App向蘋(píng)果發(fā)起支付(50元)
3客税、蘋(píng)果支付成功,返回憑證(payload)到App
4.1撕贞、App使用payload請(qǐng)求本接口更耻,
4.2、服務(wù)端使用payload到蘋(píng)果拿到下面Json數(shù)據(jù)
4.3捏膨、校驗(yàn)Json數(shù)據(jù)中transaction_id是否已經(jīng)使用
4.4秧均、若未使用食侮,則給用戶充值(50元),生成充值記錄
4.5目胡、處理結(jié)束锯七,返回成功(code=success)
5、App把收到憑證從本地清除誉己,完成支付眉尸,刷新余額異常情況:
1、若充值過(guò)程(第3步)退出App巨双,可充值成功效五,余額未給用戶增加,再次打開(kāi)App完成后續(xù)步驟即可
2炉峰、若校驗(yàn)過(guò)程(第4步)網(wǎng)絡(luò)斷開(kāi)畏妖,則可能校驗(yàn)成功,余額增加疼阔,App未收到校驗(yàn)信息戒劫,再次打開(kāi)App仍然會(huì)發(fā)布充值,服務(wù)端判斷transaction_id為重復(fù)婆廊,忽略即可迅细。
3、校驗(yàn)返回碼每次都不成功時(shí)淘邻,App不會(huì)清除憑證茵典,會(huì)造成每次打開(kāi)App都校驗(yàn),該商品也無(wú)法再次購(gòu)買成功(系統(tǒng)提示:已經(jīng)支付宾舅,可以恢復(fù)購(gòu)買)统阿。
核心代碼,稍作修改即可復(fù)用
- 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
}
}
}
/// 回調(diào)狀態(tài)
enum IAPProgress: Int {
/// 初始狀態(tài)
case none
/// 開(kāi)始
case started
/// 購(gòu)買中
case purchasing
/// 支付成功
case purchased
/// 失敗
case payFailed
/// 重復(fù)購(gòu)買
case payRestored
/// 狀態(tài)未確認(rèn)
case payDeferred
/// 其他
case payOther
/// 開(kāi)始后端校驗(yàn)
case checking
/// 后端校驗(yàn)成功
case checkedSuccess
/// 后端校驗(yàn)失敗
case checkedFailed
}
enum IAPPayCheck {
case busy /// 有支付正在進(jìn)行
case notInit /// 未初始化
case initFailed /// 初始化失敗
case notFound /// 沒(méi)有找到該商品筹我,中斷
case systemFailed /// 系統(tǒng)檢測(cè)失敗
case ok /// 可以進(jìn)行
}
class IAPHelper: ATBaseHelper {
static let shared = IAPHelper()
/// 檢測(cè)初始化回調(diào)
fileprivate var checkBlock: ((_ b: IAPPayCheck) -> ())?
/// 支付過(guò)程回調(diào)
var resultBlock: ((_ type: IAPProgress, _ pID: ACG_PAY_ID?) -> ())?
/// 是否正在支付
fileprivate var isBusy: Bool {
get {
switch progress {
case .none:
return false
default:
return true
}
}
}
/// 購(gòu)買的狀態(tài)
fileprivate var progress: IAPProgress = .none {
didSet {
/// 狀態(tài)改變回調(diào)
if let block = resultBlock {
block(progress, currentPID)
}
}
}
/// 當(dāng)前付費(fèi)的ID
fileprivate var currentPID: ACG_PAY_ID?
/// 商品列表
fileprivate var productList: [SKProduct]?
/// 初始化配置扶平,請(qǐng)求商品
func config() {
SKPaymentQueue.default().add(self)
requestAllProduct()
}
/// 初始化,請(qǐng)求商品列表
func initPayments(_ block: @escaping ((_ b: IAPPayCheck) -> ())) {
let c = checkPayments()
if c == .notInit {
requestAllProduct()
checkBlock = block
}else {
block(c)
}
}
/// 檢測(cè)支付環(huán)境蔬蕊,非.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
}
/// 請(qǐng)求商品列表
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
}
/// 請(qǐng)求充值
fileprivate func requestProduct(pdt: SKProduct) {
progress = .started
let pay: SKMutablePayment = SKMutablePayment(product: pdt)
SKPaymentQueue.default().add(pay)
}
/// 重置
fileprivate func payFinish() {
currentPID = nil
progress = .none
}
/// 充值完成后給后臺(tái)校驗(yàn)
func completeTransaction(_ checkList: [SKPaymentTransaction]) {
if resultBlock == nil {
showAlert("充值校驗(yàn)中...")
}
ALog("充值校驗(yàn)中...")
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 ? "校驗(yàn)成功" : "校驗(yàn)失敗")
}
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("支付結(jié)果: \(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("支付已購(gòu)買過(guò):\(pid)")
type = .payRestored
case .deferred:
ALog("支付不確認(rèn):\(pid)")
type = .payDeferred
SKPaymentQueue.default().finishTransaction(transaction)
@unknown default:
ALog("支付未知狀態(tài):\(pid)")
type = .payOther
SKPaymentQueue.default().finishTransaction(transaction)
}
}
progress = type
if !checkList.isEmpty {
// 有內(nèi)購(gòu)已經(jīng)完成
completeTransaction(checkList)
}else if type == .purchasing {
// 正常情況:內(nèi)購(gòu)正在支付
// 特殊情況:若該商品已購(gòu)買结澄,未執(zhí)行finishTransaction,系統(tǒng)會(huì)提示(免費(fèi)恢復(fù)項(xiàng)目)岸夯,回調(diào)中斷
// 解決方法:在應(yīng)用開(kāi)啟的時(shí)候捕捉到restored狀態(tài)的商品麻献,提交后臺(tái)校驗(yàn)后執(zhí)行finishTransaction
}else { // 其他狀態(tài)
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調(diào)用
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 回調(diào)支持過(guò)程,處理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: "充值失敗猜扮,請(qǐng)檢測(cè)網(wǎng)絡(luò)")
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) {
// 請(qǐng)求支付
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("暫時(shí)無(wú)法支付勉吻,請(qǐng)稍后再試")
}
}else {
showAlert("暫時(shí)無(wú)法支付,請(qǐng)稍后再試")
}
}
break
default:
showAlert("暫時(shí)無(wú)法支付破镰,請(qǐng)稍后再試")
break
}
}
- AppDelegate需要初始化一下
IAPHelper.shared.config()
附件:
- 支付成功后的交易憑證數(shù)據(jù)很大餐曼,后端接收這個(gè)數(shù)據(jù),跟蘋(píng)果校驗(yàn)后鲜漩,判斷用戶即可給該用戶充值源譬。
guard let rURL = Bundle.main.appStoreReceiptURL, let data = try? Data(contentsOf: rURL) else {
ALog("appStoreReceiptURL error")
}
let str = data.base64EncodedString()
print(str)
- 打印出來(lái)是這樣的(很長(zhǎng))
MIIVKgYJKoZIhvcNAQcCoIIVGzCCFRcCAQExCzAJBgUrDgMCGgUAMIIEywYJKoZIhvcNAQcBoIIEvASCBLgxggS0MAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQECAQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADALAgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQEAgIAiTANAgENAgEBBAUCAwHViDANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAyNTIwGAIBBAIBAgQQEXxl0gRk5NBqMO8/VFkmNzAZA... ...
- 蘋(píng)果返回Json
1、收到這個(gè)數(shù)據(jù)表示孕似,里面的項(xiàng)目肯定付費(fèi)成功
2踩娘、通過(guò)transaction_id判斷是否重復(fù)校驗(yàn)
3、通過(guò)product_id * quantity 判斷支付金額
4喉祭、通過(guò)environment判斷所在環(huán)境
注意:用戶信息通過(guò)判斷App登錄用戶养渴,透?jìng)鰽pp用戶信息和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
}
后端實(shí)現(xiàn)參考文章
1泛烙、流程寫(xiě)得很詳細(xì)理卑,php源碼也有:
http://www.cnblogs.com/wangboy91/p/7162335.html
2、Java端的支持:
https://blog.csdn.net/jianzhonghao/article/details/79343887