这两天因为appstore审核被拒的原因,不得不把微信支付宝等支付替换成苹果内购。经过两天的研究和学习我发现了内购的好多个坑,我在这里做了一个总结,希望能对大家有所帮助,有不对的地方还请大家无情指出并嘲讽之。最后还有我最终的解决方案分享给大家。
一、内购的坑
- app被卸载后使用SKReceiptRefreshReques重新获取内购票据。
{
SKReceiptRefreshRequest *receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:@{}];
receiptRefreshRequest.delegate = self;
[receiptRefreshRequest start];
return;
}
#pragma mark - SKRequestDelegate
- (void)requestDidFinish:(SKRequest *)request{
// 刷新成功票据的代理
}
这个的坑在于重新获取的票据虽然客户端能验证成功,但是验证成功的信息里面有一个关键字段“in_app”为空数组。把这个票据传给后台后是不能验证通过的,所以就不能为用户充值成功,所以这个方法大家慎用。
- finishTransaction
[[SKPaymentQueue defaultQueue] finishTransaction: transaction]
finishTransaction是去告诉苹果这次交易已经结束,如果不执行或者执行失败(为什么说会执行失败呢?因为这个方法是异步网络请求,网络不好的时候就会失败)下次用户购买同样的商品的时候购买成功后会提示“您已经免费恢复”的字样。我看了好多博客,好多博主都说一定要在给用户充值完以后在执行这个方法,我这里有不同意见,因为如果网络不好充值失败了,没有执行到这个方法的话,不仅有上面的问题,因为没执行成功这个方法的话,下次进入app或者进入充值界面(取决你在哪里[[SKPaymentQueue defaultQueue] addTransactionObserver:self]
)的时候就会自动回调paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
,这让我的充值流程变得非常不可控也不可见。下面的我的解决方案会提到怎么解决这个问题。
3.异常订单存入数据库
数据库是会随着app卸载而删除的,我开始没有想到这一点,后来测试发现了才把这个方案取消了,费时又费力,希望大家不要踩坑 。
二、我的解决方案
内购的步骤
不要把问题想得那么复杂,大象装进冰箱需要三步呢,可内购在我看来就两步。
- 用户付钱给苹果完毕
- 客户端发送请求给服务器验证票据进行充值成功。
思考?
- 什么时候会漏单呢?很简单,第一步完成,第二步没完成。
- 什么时候第一步完成呢?因该是在
paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
里面transaction.transactionState = SKPaymentTransactionStatePurchased
。- 什么时候第二步没完成?不管什么原因,肯定没来到充值接口成功的回调里面。
开始解决
用户付钱给苹果完毕 使用钥匙串(钥匙串不会随着app卸载而删除,会永久性的存储,除非用户换手机,我这里使用的是SAMKeychain)存储用户付款的凭证(receipt)和交易id(transactionId)。储存凭证在付款成功的回调里面,存储的时候要区分用户,所以监听苹果队列的代码
[[SKPaymentQueue defaultQueue] addTransactionObserver:self]
要写在登录之后,否则无法正确存储凭证。充值接口成功的回调里面删除用户付款的凭证(receipt)和交易id(transactionId)。
由于各种原因导致充值失败漏单了怎么办?在用户再次想充值的时候,判断钥匙串中有没有存储的数据,如果有的话给一个拦截的操作,我这边是给一个弹窗,提示用户处理异常订单。这里很关键,这个操作保证了用户在进行内购操作的时候,最多只能有一个异常订单所以我们不担心有串单的风向,也不会有大量漏单的情况出现。点击弹窗的按钮“立即处理异常订单”会从钥匙串中获取之前存储的凭证和交易id继续向公司的服务器发起验证并充值,如果充值成功删除凭证和交易id。如果失败了,不用进行任何操作。当用户点击充值的时候还是会有一个弹窗提示。
finishTransaction的调用,上面的坑说到了这个方法。所以我们为了能够自己控制充值流程,摆脱苹果的控制,我们不用在乎他什么时候调用,但一定尽可能保证他调用完成。我是怎么做的呢?
- 用户付钱给苹果完毕调用一次。
- 充值成功调用一次。
- 充值失败调用一次。
- 充值前把当前所有的transaction进行finish操作。
如果这样还不能避免由于网络等原因没有执行完[[SKPaymentQueue defaultQueue] finishTransaction: transaction]
怎么办呢?这我也做了处理。我加了一个属性isNewtranstion
,意思就是这个交易是新的吗,或者换句话说,这个transtion(交易)是从buyProduct:(SKProduct *)productIdentifier onCompletion:(IAPbuyProductCompleteResponseBlock)completion
这个方法中来的吗?
所以我在这个方法中为这个字段进行了赋值操作。
- (void)buyProduct:(SKProduct *)productIdentifier onCompletion:(IAPbuyProductCompleteResponseBlock)completion {
SKPayment *payment = [SKPayment paymentWithProduct:productIdentifier];
if ([SKPaymentQueue defaultQueue]) {
[[SKPaymentQueue defaultQueue] addPayment:payment];
[CHBUserDataCenterModel sharedInstance].isNewtranstion = YES;
}
}
如果是旧的交易,就是没有被finish的交易的话我就再finish一下(我就不信干不了你!!!)
if (![CHBUserDataCenterModel sharedInstance].isnewtranstion) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return;
}
但是这么做会引发一个问题,用户付钱给苹果完毕之前杀死app,但是因为是发送给苹果的请求还是会弹出您的购买已完成,就无法响应paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
中将凭证和交易id保存到钥匙串的操作了,所以要把保存放到第一步,而且要加判断,防止凭证或者交易id为空的transtion进入回调坏事。
// 获取小票
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
NSString *payload = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSString* payload1 = [SAMKeychain passwordForService:receiptservice account:[CHBUserDataCenterModel sharedInstance].passportId];
NSString* transactionId1 = [SAMKeychain passwordForService:transactionIdservice account:[CHBUserDataCenterModel sharedInstance].passportId];
if (payload1 == nil) {
BOOL issaved = [SAMKeychain setPassword:payload forService:receiptservice account:[CHBUserDataCenterModel sharedInstance].passportId];
while (!issaved) {
[SAMKeychain setPassword:payload forService:receiptservice account:[CHBUserDataCenterModel sharedInstance].passportId];
}
}
if (transactionId1 == nil){
BOOL issaved1 = [SAMKeychain setPassword:transaction.transactionIdentifier forService:transactionIdservice account:[CHBUserDataCenterModel sharedInstance].passportId];
while (!issaved1) {
[SAMKeychain setPassword:transaction.transactionIdentifier forService:transactionIdservice account:[CHBUserDataCenterModel sharedInstance].passportId];
}
}
- 还有一个问题:调用充值接口的时候,app被杀死了。
这种情况的话会产生一种现象,post请求发出了,后台接收到了,充值成功了,但是客户端收不到响应了,也不能执行充值成功后的代码了。这时需要后台提供一个接口:验证钥匙串中的凭证和交易id充值成功过没有,如果充值成功过,就删除钥匙串中内容,避免用户充值的时候有个提示框拦截操作,如果没有充值成功过,那就是多虑了,还是按之前的流程走。
三、相关思路
-
内购流程图
-
内购测试点