最近開發(fā)一個項目涉及到內購, 也遇到過一些問題. 這里拿出來分享一下, 避免一些人走彎路.
開頭先聊一聊最近蘋果關于2017年新的審核機制和沸沸揚揚的微信和蘋果的撕逼
1. 2017新的審核機制:
ipv6: 使用國內阿里云的app上架, 大都會遇到ipv6被拒的郵件:
解決方案:
方案1. 服務端解決: ?配置阿里云ECS支持IPv6, 添加AAAA解析
方案2. 客戶端解決: 手機端配置ipv6環(huán)境測試, 錄制APP內的操作視頻, 上傳到YouTobe, 將網(wǎng)址發(fā)送給審核人員即可通過審核 (ps: 錄制時候一定要錄制APP所在的網(wǎng)絡環(huán)境: 設置中->無線網(wǎng)絡->DNS: 2001:2:0:aab1: :: 1 ,DNS為這種格式則為ipv6)內購:
說一說這個項目內購有趣的事情:
a. 首先做這個項目的時候, 我們充值虛擬幣方案定的是: 后臺做一個開關, app在審核期間走蘋果內購, 在上線后, 走微信和支付寶支付, 并向低版本兼容. 達到繞過蘋果審核的目的. 結果被拒了, 郵件中提到了支付寶, 當時很懵逼, 就留下了老大的聯(lián)系方式和蘋果溝通, 第二天蘋果打來電話: 說內購的同時不可以使用第三方支付. 由此看來: 第三方支付的相關相關代碼或SDK被掃描到了. 遂移除掉, 只使用內購方式
b. 審核期間, 蘋果發(fā)來一封郵件大概意思是問:你們確定內購的最高價格是你們期望的嗎? 回復以后才可以繼續(xù)審核, 這里我的理解是: 我們的內購的最高價格定得很高149美元的那一檔, 所以蘋果要確認一下, 經過回復郵件說明了一下這個最高價格確定是我們自己定的最高價格, 沒有錯誤, 第二天蘋果又恢復了審核, 變成了審核中...
c. app被拒后, 內購項目變成了需要開發(fā)人員操作, 盜圖一張:

這時候一般只需要進入需要開發(fā)人員操作的內購項目中, 修改一下描述, 重新提交即可, 然后重新提交app. (ps: 一般這里我只是將描述中添加或刪除空格, 就可以重新提交了)
d. 關于項目中: app內購商品返回列表為空, 返回的都是無效產品
即: [response.products count]始終為0, [response.invalidProductIdentifiers] 有值
這個的原因是: 協(xié)議、稅務和銀行業(yè)務中必須通過才可以(盜圖一張):

2. 談一談微信和蘋果的撕逼
新的審核協(xié)議將打賞列為了內購
我的觀點和這個仁兄一樣
3. 閑話扯完了, 看一下怎么做內購并處理掉單問題:
蘋果官方提供的內購的正確姿勢
蘋果這一文中說明兩點:
a. 在appdelegate中添加觀察者, 在購買成功后提交給自己的服務器, 由自己服務器提交憑證到蘋果服務器驗證正確后, 返回給客戶端之后, 這筆交易才完成, 這時候再queue.finishTransaction(transaction), 如果這期間蘋果的服務器還沒返回結果 或者 購買成功了,我們提交憑證給自己服務器的時候網(wǎng)斷掉了(錢空了, 但是虛擬物品沒有到賬, 丟單了), 則這筆交易都沒有完成, 方法queue.finishTransaction(transaction)都沒有調用, 所以再次打開app的時候, 因為appdelegate中添加了觀察者, 就會再次調用
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])方法
b. 蘋果推薦進入內購項目表單頁面的時候先請求appstore,根據(jù)返回的可銷售商品來進行展示(但是很多app的做法都是調用自己的接口取得商品價格列表進行展示, 但是我們不能確定我們自己的服務器返回的和蘋果返回的不同), 這里非常抱歉的說明一下: 我們的app也是按照自己服務器的api返回的數(shù)據(jù)展示的商品價格列表, 哈哈哈
c. 關于內購和服務端的接口參數(shù), 我們設置為:
- 此次交易的用戶的唯一標示符(accountID):
- 交易成功的憑證
- 此次交易的訂單號
- 服務端也要處理重復請求該接口的情況(不要每次請求成功都給用戶加錢..)
說明: 用戶的唯一標示符的作用: 如果用戶購買成功, 但是將憑證給自己服務端的時候斷掉了, 然后自己切換了賬號, 下次打開app的時候檢測, 我們需要這個表示符知道誰買的..不要將虛擬貨幣充錯用戶
ios7 蘋果增加了一個屬性applicationusername,SKMutablepayment的屬性,所以用戶在發(fā)起支付的時候可以指定用戶的username及自己生成的訂單,這樣用戶再下次得到回調的時候就知道,此交易是哪個訂單發(fā)起的了進而完成交易。回調中獲取username。
上代碼: (內購工具類)
import Foundation
import StoreKit
enum InpurchaseError: Error {
/// 沒有內購許可
case noPermission
/// 不存在該商品: 商品未在appstore中\(zhòng)商品已經下架
case noExist
/// 交易結果未成功
case failTransactions
/// 交易成功但未找到成功的憑證
case noReceipt
}
typealias Order = (productIdentifiers: String, applicationUsername: String)
class Inpurchase: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate {
static let `default` = Inpurchase()
/// 掉單/未完成的訂單回調 (憑證, 交易, 交易隊列)
var unFinishedTransaction: ((String, SKPaymentTransaction, SKPaymentQueue) -> ())?
private var sandBoxURLString = "https://sandbox.itunes.apple.com/verifyReceipt"
private var buyURLString = "https://buy.itunes.apple.com/verifyReceipt"
private var isComplete: Bool = true
private var products: [SKProduct] = []
private var failBlock: ((InpurchaseError) -> ())?
/// 交易完成的回調 (憑證, 交易, 交易隊列)
private var receiptBlock: ((String, SKPaymentTransaction, SKPaymentQueue) -> ())?
private var successBlock: (() -> Order)?
private override init() {
super.init()
SKPaymentQueue.default().add(self)
}
deinit {
SKPaymentQueue.default().remove(self)
}
/// 開始向Apple Store請求產品列表數(shù)據(jù),并購買指定的產品,得到Apple Store的Receipt,失敗回調
///
/// - Parameters:
/// - productIdentifiers: 請求指定產品
/// - successBlock: 請求產品成功回調,這個時候可以返回需要購買的產品ID和用戶的唯一標識,默認為不購買
/// - receiptBlock: 得到Apple Store的Receipt和transactionIdentifier,這個時候可以將數(shù)據(jù)傳回后臺或者自己去post到Apple Store
/// - failBlock: 失敗回調
func start(productIdentifiers: Set<String>,
successBlock: (() -> Order)? = nil,
receiptBlock: ((String, SKPaymentTransaction, SKPaymentQueue) -> ())? = nil,
failBlock: ((InpurchaseError) -> ())? = nil) {
guard isComplete else { return }
defer { isComplete = false }
let request = SKProductsRequest(productIdentifiers: productIdentifiers)
request.delegate = self
request.start()
self.successBlock = successBlock
self.receiptBlock = receiptBlock
self.failBlock = failBlock
}
//MARK: - SKProductsRequestDelegate
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
products = response.products
guard let order = successBlock?() else { return }
buy(order)
}
/// 購買給定的order的產品
private func buy(_ order: Order) {
let p = products.first { $0.productIdentifier == order.productIdentifiers }
guard let product = p else { failBlock?(.noExist); return }
guard SKPaymentQueue.canMakePayments() else { failBlock?(.noPermission); return }
let payment = SKMutablePayment(product: product)
/// 發(fā)起支付時候指定用戶的username, 在掉單時候驗證防止切換賬號導致充值錯誤
payment.applicationUsername = order.applicationUsername
SKPaymentQueue.default().add(payment)
}
//MARK: - SKPaymentTransactionObserver
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
// appStoreReceiptURL iOS7.0增加的,購買交易完成后,會將憑據(jù)存放在該地址
guard let receiptUrl = Bundle.main.appStoreReceiptURL,
let receiptData = NSData(contentsOf: receiptUrl) else { failBlock?(.noReceipt);return }
let receiptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
if let receiptBlock = receiptBlock {
receiptBlock(receiptString, transaction, queue)
}else{ // app啟動時恢復購買記錄
unFinishedTransaction?(receiptString, transaction, queue)
}
isComplete = true
case .failed:
failBlock?(.failTransactions)
queue.finishTransaction(transaction)
isComplete = true
case .restored: // 購買過 對于購買過的商品, 回復購買的邏輯
queue.finishTransaction(transaction)
isComplete = true
default:
break
}
}
}
}
appdelegate中的監(jiān)聽使用方式:
appdelegate中:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Inpurchase.default.unFinishedTransaction = {(receipt, transaction, queue) in
// 如果存在掉單情況就會走這里
let data = InpurchaseAPIData(accountID: transaction.payment.applicationUsername, //用戶唯一標示
transactionID: transaction.transactionIdentifier, //交易流水
receiptData: receipt)// 憑證
LPNetworkManager.request(Router.verifyReceipt(data)).showToast().loading(in: self.view).success {[weak self] in
showToast("恢復購買成功")
// 記住一定要請求自己的服務器成功之后, 再移除此次交易
queue.finishTransaction(transaction)
}.fail {
print("向服務器發(fā)送憑證失敗")
}
}
return true
}
點擊購買的代碼:
// 點擊購買
let productIdentifiers: Set<String> = ["a", "b", "c"]
Inpurchase.default.start(productIdentifiers: productIdentifiers, successBlock: { () -> Order in
return (productIdentifiers: "a", applicationUsername: "該用戶的id或改用戶的唯一標識符")
}, receiptBlock: { (receipt, transaction, queue) in
//交易成功返回了憑證
let data = InpurchaseAPIData(accountID: transaction.payment.applicationUsername,
transactionID: transaction.transactionIdentifier,
receiptData: receipt)
LPNetworkManager.request(Router.verifyReceipt(data)).showToast().loading(in: self.view).success {[weak self] in
showToast("購買成功")
// 記住一定要請求自己的服務器成功之后, 再移除此次交易
queue.finishTransaction(transaction)
}.fail {
print("向服務器發(fā)送憑證失敗")
}
}, failBlock: { (error) in
print(error)
})
demo地址 能點個star也是極好的, 打不打賞無所謂, 能幫到你就好
還有一種實踐方式, 個人并不推薦, 因為太繁瑣了:
思路: 購買成功后在本地將訂單的用戶, 憑證等信息存儲到本地(UserDefaults, 數(shù)據(jù)庫,keyChain等), 將憑證發(fā)送給自己服務器成功之后再移除此條交易記錄, 每次打開app的時候, 在本地掃描是否有未完成的訂單, 循環(huán)發(fā)送給自己的服務器進行二次驗證
補充:
- 關于上線:
錯誤做法: 上線審核的時候使用沙箱測試地址, 審核通過后, 手動發(fā)布上線, 上線后讓服務器切換到蘋果的正式測試地址
說明: 這種做法第一次上架可以使用, 但是到第二次迭代審核的時候, 蘋果測試員使用的是沙盒環(huán)境, 但是我們服務器是正式環(huán)境, 會導致報錯誤碼: 21007
正確的做法: 判斷蘋果正式驗證服務器的返回code,如果是21007表示環(huán)境不對,則再一次連接測試服務器進行驗證即可.. (這一步驟即: 先判斷蘋果的環(huán)境, 根據(jù)蘋果環(huán)境切換沙盒地址還是正式地址)
- 關于蘋果二次驗證返回的參數(shù):
服務端\客戶端對蘋果發(fā)送請求進行驗證有時會返回多個交易記錄
說明: 蘋果驗證會返回: 一個未完成交易的數(shù)組(一般只有一個, 就是當前操作購買的這個), 如果有多個為完成的交易,就會返回多個 (這種情況一般是代碼寫的不對造成的), 服務端根據(jù)
transactionIdentifier找到當前購買的交易或者取最后一個也是當前購買的交易來做判斷和驗證....經過測試發(fā)現(xiàn)如果在當前手機請求發(fā)現(xiàn)出現(xiàn)多個未完成的交易, 則換另外一部手機和賬號等, 仍然會返回那些未完成的交易, 看來每次對商品進行購買, 蘋果會把所有未完成的交易都返回(不管這個商品是其他用戶的還是其他手機的)
demo地址 能點個star也是極好的, 打不打賞無所謂, 能幫到你就好