这段时间做了苹果内购IAP,做一个整理记录,主要是开发层面。
一.前期工作:在开发者账号中添加银行信息同意协议等,添加沙盒账号,添加内购商品
二.项目开发,因为项目需要支持iOS15一下的版本所以使用旧版StoreKit,新版的StoreKit2只支持iOS15以上,新的nsync同步接口。
1.获取内购商品信息,可以在自己服务器中获取商品的productId数组,根据product ID 获取价格等具体商品信息,用于显示给用户,如果商品信息不经常变化,可以把结果缓存起来,不获取商品信息也可以发起内购不影响购买
private var productFetchCallbacks = [SKProductsRequest: ([SKProduct]) -> Void]()
public func fetchProductsInfo(_ productIDs: [String],completion:@escaping ([SKProduct]) -> Void) {
let set = Set<String>.init(productIDs)
let request = SKProductsRequest.init(productIdentifiers: set)
productFetchCallbacks[request] = completion
request.delegate = self
request.start()
}
//代理回调结果,注意代理回调结果是在多线程中,根据需求是否要切换成主线程
// MARK: SKProductsRequestDelegate
extension MXLiveIAPPayment : SKProductsRequestDelegate{
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard let callback = productFetchCallbacks[request] else { return }
productFetchCallbacks[request] = nil
DispatchQueue.main.async {
callback(response.products)
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print(error.localizedDescription)
if let productsFetchRequest = request as? SKProductsRequest {
guard let callback = productFetchCallbacks[productsFetchRequest] else { return }
productFetchCallbacks[productsFetchRequest] = nil
DispatchQueue.main.async {
callback([])
}
}
}
// public func requestDidFinish(_ request: SKRequest) {
// print(request)
// }
}
2.设置代理发起内购,
let payment = SKMutablePayment()
payment.quantity = 1
payment.applicationUsername = currentOrder?.uuid
payment.productIdentifier = order.productId
payment.simulatesAskToBuyInSandbox = true // test deferred
SKPaymentQueue.default().add(payment)
//代理方法中收到支付结果
/ MARK: SKPaymentTransactionObserver
//处理未完成的交易
extension MXLiveIAPPayment : SKPaymentTransactionObserver{
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for tran in transactions {
switch tran.transactionState {
case .purchased://购买完成
//成功的未移出的transaction进入app会会掉,失败的不会回掉
self.delegate?.orderStatusChanged(order: currentOrder, status:.purchased)
currentTransaction = tran
completePay(transaction: tran)
print("-------IAP pay purchased--------------")
break
case.purchasing://商品添加进列表
// tran.transactionIdentifier此时未nil
self.delegate?.orderStatusChanged(order: currentOrder, status: .purchasing)
currentTransaction = tran
self.updatePurchaseStatus(status: "purchasing")
print("-------IAP pay purchasing--------------")
break
case.restored://已经购买过该商品
self.delegate?.orderStatusChanged(order: currentOrder, status: .failed(MXLiveIAPError(reason: "product restored", code: -1)))
self.updatePurchaseStatus(status: "restored")
currentTransaction = tran
finishCurrentOrder()
print("-------IAP pay restored--------------")
break
case.failed://购买失败
self.delegate?.orderStatusChanged(order: currentOrder, status: .failed(tran.error ?? MXLiveIAPError(reason: "purchase failed error", code: -1)))
handleFailure(tran)
self.updatePurchaseStatus(status: "failed")
//低版本iOS13以下添加观察者之后有可能直接走到此处失败的回调中
currentTransaction = tran
finishCurrentOrder()
print("-------IAP pay failed--------------")
break
case .deferred:
//https://stackoverflow.com/questions/42152560/how-to-handle-skpaymenttransactionstatedeferred
//ask permission for your parent or guardian
//ask for buy,We get transaction deferred state, if user is part of Apple family sharing & family admin enabled ASK TO BUY.
currentTransaction = tran
currentOrder?.deferedDate = Date()
currentOrder?.updateTokeyChain()
self.updatePurchaseStatus(status: "deferred")
self.delegate?.orderStatusChanged(order: currentOrder, status: .deferred)
print("-------IAP pay deferred--------------")
break
@unknown default:
()
}
}
}
private func handleFailure(_ transaction: SKPaymentTransaction) {
guard let error = transaction.error else { return }
let nsError = error as NSError
guard nsError.domain == SKError.errorDomain else { return }
switch nsError.code {
case SKError.clientInvalid.rawValue, SKError.paymentNotAllowed.rawValue:
print ("You are not allowed to make IAP payment.")
case SKError.paymentCancelled.rawValue:
print ( "IAP Payment has been cancelled.")
case SKError.unknown.rawValue, SKError.paymentInvalid.rawValue:
fallthrough
default:
print ("Something went wrong making IAP payment.")
}
}
完成的transaction要记住调用finish接口,否则下一次支付代理回调中还会收到这条transaction
3.验证支付票据,支付票据客户端可以直接调苹果的接口验证,我们是调用后台接口让后台 去验证,这样验证通过可以直接进行后续下发商品等业务
private func verifyForApple(data:Data,transaction:SKPaymentTransaction?) {
self.delegate?.orderStatusChanged(order: currentOrder, status: .receiptChecking)
let base64Str = data.base64EncodedString(options: .endLineWithLineFeed)
let params = NSMutableDictionary()
params["receipt-data"] = base64Str
let body = try? JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
var request = URLRequest.init(url: URL.init(string: receiptState == 21008 ? url_receipt_itunes : url_receipt_sandbox)!, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 20)
request.httpMethod = "POST"
request.httpBody = body
let session = URLSession.shared
let task = session.dataTask(with: request) { [weak self](data, response, error) in
guard let data = data, let dict = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSDictionary else{
self?.delegate?.orderStatusChanged(order: self?.currentOrder, status: .failed(MXLiveIAPError(reason: "receipt check failed", code: -1)))
return
}
print("receipt_info:")
print(dict)
let status = dict["status"] as? Int
switch(status){
case 0:
self?.delegate?.orderStatusChanged(order: self?.currentOrder, status: .complete)
break
case 21007:
self?.receiptState = 21007
self?.verifyForApple(data: data, transaction: transaction)
break
default:
self?.delegate?.orderStatusChanged(order: self?.currentOrder, status: .failed(MXLiveIAPError(reason: "receipt check failed", code: -1)))
break
}
}
task.resume()
}
票据验证结果事例:
可以根据transaction.payment.productIdentifier去匹配自己的业务订单
苹果做了限制,如果有相同的productIdentifier的transaction没有处理完,不能发起重复支付,话句话说,transaction数组中不会同时包含两个productID相同的item,也就是如果上一个product未finish,发起新的相同productid的内购会返回失败
在客户端层面也做了限制,当前交易未处理完之前不能发起新的交易,所以基本不会出现多个truncation的情况,
多个transaction验证结果事例:
{
receipt = {
receipt_type = "ProductionSandbox";
app_item_id = 0;
receipt_creation_date = "2022-12-08 14:16:42 Etc/GMT";
bundle_id = "com.mxplay.ios.live";
original_purchase_date = "2013-08-01 07:00:00 Etc/GMT";
in_app = (
{
quantity = "1";
purchase_date_ms = "1670508859000";
transaction_id = "2000000223045245";
is_trial_period = "false";
original_transaction_id = "2000000223045245";
purchase_date = "2022-12-08 14:14:19 Etc/GMT";
product_id = "mx_dq_00001";
original_purchase_date_pst = "2022-12-08 06:14:19 America/Los_Angeles";
in_app_ownership_type = "PURCHASED";
original_purchase_date_ms = "1670508859000";
purchase_date_pst = "2022-12-08 06:14:19 America/Los_Angeles";
original_purchase_date = "2022-12-08 14:14:19 Etc/GMT";
},
{
quantity = "1";
purchase_date_ms = "1670508919000";
transaction_id = "2000000223046251";
is_trial_period = "false";
original_transaction_id = "2000000223046251";
purchase_date = "2022-12-08 14:15:19 Etc/GMT";
product_id = "mx_dq_00002";
original_purchase_date_pst = "2022-12-08 06:15:19 America/Los_Angeles";
in_app_ownership_type = "PURCHASED";
original_purchase_date_ms = "1670508919000";
purchase_date_pst = "2022-12-08 06:15:19 America/Los_Angeles";
original_purchase_date = "2022-12-08 14:15:19 Etc/GMT";
}
);
adam_id = 0;
receipt_creation_date_pst = "2022-12-08 06:16:42 America/Los_Angeles";
request_date = "2022-12-08 14:17:15 Etc/GMT";
request_date_pst = "2022-12-08 06:17:15 America/Los_Angeles";
version_external_identifier = 0;
request_date_ms = "1670509035235";
original_purchase_date_pst = "2013-08-01 00:00:00 America/Los_Angeles";
application_version = "202202153";
original_purchase_date_ms = "1375340400000";
receipt_creation_date_ms = "1670509002000";
original_application_version = "1.0";
download_id = 0;
};
status = 0;
environment = "Sandbox";
}
验证的错误码如下
21000 App Store无法读取你提供的JSON数据
21002 收据数据不符合格式
21003 收据无法被验证
21004 你提供的共享密钥和账户的共享密钥不一致
21005 收据服务器当前不可用
21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
注意:
IAP审核时, 需要提供沙盒测试账号和一个APP的测试账号, 在审核过程时, 我们整个流程都已经切换为正式环境, 但审核人员仍然使用测试凭证去进行验证, 我们服务器需要在审核阶段, 对于此时凭证仍然去沙盒测试验证接口去验证才能验证通过, 否则会被拒绝通过。
在审核阶段可以修改服务端验证支付凭证的流程,先验证正式的如果失败再验证沙盒环境
代码地址:https://github.com/duzhaoquan/DQIAPTool
相关参阅:
https://juejin.cn/post/6974733392260644895