背景
随着CSDN APP用户 iOS 15以上系统占比覆盖度到98%,我们与支付中台决定接入StoreKit 2,彻底告别掉单,恶意退款等iOS内购遇到的问题。本文不阐述,StoreKit2 的详细说明,如果有兴趣的可以查看官方文档。本文只演示项目接入代码。
实现方案
因为StoreKit2 只支持 Swift语言,下面案例均用Swift来实现,如有OC项目需要混编,可以暴漏了方法给OC调用就行。
获取商品列表
swift
/// 通过 productIds 请求 Product 列表
/// - Parameter productIds: product ids
/// - Returns: Product 列表
public func requestProducts(productIds: [String]) async -> [Product]? {
products = try? await Product.products(for: Set.init(productIds))
return products
}
拉起应用内购买(唤起支付)
StoreKit 2 支持传入一个appAccountToken
这个值必须是UUID格式,这个UUID可以关联我们的订单和苹果的交易。我们可以将我们的订单编号,补充转换成UUID格式。这样我们就可以保证我们的订单与苹果订单唯一绑定,如果有发生掉单,用户可以拿着交易凭证上面的订单号,通过苹果给的查询交易的接口,先查询到苹果的交易id,然后再找到我们的订单号,完成补单操作。
swift
/// 发起支付
/// - Parameter product: Product对象
public func purchase(product: Product, uid: String) async throws -> Transaction? {
guard purchaseState != .inProgress else {
throw PurchaseException.purchaseInProgressException
}
purchaseState = .inProgress
//App account token
//用于将用户订单ID 绑定到交易(Transcation)中,即可建立苹果的交易订单数据与用户信息的映射关系,方便数据整合与追溯
let uuid = Product.PurchaseOption.appAccountToken(UUID.init(uuidString: uid)!)
//发起支付流程
guard let res = try? await product.purchase(options: [uuid]) else {
purchaseState = .failed
throw PurchaseException.transactionVerificationFailed
}
var validateTransaction: Transaction? = nil
switch res {
case .success(let verificationResult):
//购买状态:成功
print("用户购买成功")
purchaseState = .complete
//可以将交易ID回传给服务端,服务端通过调用Appstore API来验证交易的可信,然后下发对应权益
let checkResult = checkTransactionVerificationResult(verificationResult)
if !checkResult.verified {
purchaseState = .failedVerification
throw PurchaseException.transactionVerificationFailed
}
validateTransaction = checkResult.transaction
//结束交易
await validateTransaction!.finish()
case .userCancelled:
//购买状态:用户取消
print("用户取消购买")
purchaseState = .cancelled
throw PurchaseException.purchaseUserCancelled
case .pending:
//购买状态:进行中
print("用户购买中")
purchaseState = .pending
default:
//购买状态:未知
print("用户购买状态:未知")
purchaseState = .unknown
}
return validateTransaction
}
交易信息校验
swift
/// 校验
/// - Parameter result: 支付返回结果
/// - Returns: 是否验证成功
private func checkTransactionVerificationResult(_ result: VerificationResult<Transaction>) -> (transaction: Transaction, verified: Bool) {
//Check whether the JWS parses StoreKit verification.
switch result {
case .unverified(let transaction, _):
//StoreKit parses the JWS, but it fails verification.
return (transaction: transaction, verified: false)
case .verified(let transaction):
//The reult is verified. Return the unwrapped value.
return (transaction: transaction, verified: true)
}
}
交易状态监听
我们测试发现,沙盒环境会将已经完成的订单也会通过该方法通知过来,所以服务端要对已经验证过,下发权益的交易做处理。
swift
/// 支付监听事件
public func listenForTransaction(completion:@escaping (Transaction) -> Void) -> Void {
Task.detached {
for await verificationResult in Transaction.updates {
let checkResult = self.checkTransactionVerificationResult(verificationResult)
if checkResult.verified {
let validatedTransaction = checkResult.transaction
await validatedTransaction.finish()
//有未完成的订单,需要重新发送给服务端验证,是否下发权益
completion(validatedTransaction)
} else {
print("Transaction failed verification.")
}
}
}
}
恢复购买
swift
/// 恢复购买
public func reStorePurchase(){
Task {
try? await AppStore.sync()
}
}
发起退款
StoreKit 2 也提供了退款方法,使得退款流程更加简洁。沙盒测试退款,也更加容易。
swift
/// 发起退款
/// - Parameters:
/// - transactionId: transaction.originalID
/// - scene: Window scene
public func refunRequest(for transactionId: UInt64, scene: UIWindowScene!) async {
do {
let res = try await Transaction.beginRefundRequest(for: transactionId, in: scene)
switch res {
case .userCancelled:
// Customer cancelled refund request.
print("用户取消退款。")
case .success:
print("退款提交成功。")
// Refund request was successfully submitted.
@unknown default:
print("退款返回错误:未知")
}
}
catch StoreKit.Transaction.RefundRequestError.duplicateRequest {
print("退款请求错误:重复请求")
}
catch StoreKit.Transaction.RefundRequestError.failed {
print("退款请求错误:失败")
}
catch {
print("退款请求错误:其他")
}
}
StoreKit 1 兼容
因为StoreKit2 只支持iOS 15以上系统设备,那对于低于iOS 15的设备还需要使用StoreKit1,并且StoreKit2 不支持 AppStore 活动与AppStore 订阅SKU的推广,对于有这个需求的也是需要使用StoreKit1来实现。
swift
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product {
// product里存放的有我们配置在App Store的产品ID,以及价格等等
return NO;
}
StoreKit1 中,如果给SKPayment
设置了applicationUsername
并且这个值是UUID类型的话,服务端在调用苹果API的时候,也是可以拿到这个值的。这块可以在官网看到具体介绍。所以我们可以和StoreKit2 一样,给applicationUsername
增加UUID类型的订单号绑定。这样不管是StoreKit1 还是StoreKit2,都不会有掉单找不到订单的风险。
swift
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
if ([payment respondsToSelector:@selector(setApplicationUsername:)])
{
payment.applicationUsername = uuid;
}
[[SKPaymentQueue defaultQueue] addPayment:payment];
服务端
服务端不再采用原来的上传沙盒票据校验的方式,统一使用App Store Server API JWS方式来验证。这块具体查看官方文档就行。对于自动续订,及退款,使用 App Store Server Notifications 来实现,续订权益下发,及退款的权益回收。