iOS内购中碰到的问题与解决方案
公司项目中需要做一个会员购买功能,当时一听到这个需求就知道要跟内购扯上关系了,于是乎开始在网上大量找相关资料,也包括其他开发者遇到的问题。不过....但是....在实际上线后还是碰到了意想不到的问题,真是措手不及啊!
委屈啊.jpg在网上找到了一个比较厉害的第三方库RMStore,虽然我项目中确实最终导入了它,但是感觉它针对我们普通购买功能来说,代码显的有些冗余了。介于人家良好的口碑,最终采用他了,不过需要在它里面增加自己的代码哦。
我们采用的是服务器接口验证流程,这样应该更加靠谱一些。
发起内购
并将我们服务器生成的预订单号存储起来且一并传入
- (void)addPayment:(NSString*)productIdentifier
user:(NSString*)userIdentifier
success:(void (^)(SKPaymentTransaction *transaction))successBlock
failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock
{
SKProduct *product = [self productForIdentifier:productIdentifier];
if (product == nil)
{
RMStoreLog(@"unknown product id %@", productIdentifier)
if (failureBlock != nil)
{
NSError *error = [NSError errorWithDomain:RMStoreErrorDomain code:RMStoreErrorCodeUnknownProductIdentifier userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"Unknown product identifier", @"RMStore", @"Error description")}];
failureBlock(nil, error);
}
return;
}
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
if ([payment respondsToSelector:@selector(setApplicationUsername:)])
{
payment.applicationUsername = userIdentifier;
}
RMAddPaymentParameters *parameters = [[RMAddPaymentParameters alloc] init];
if(userIdentifier){
parameters.userid = userIdentifier;
}
parameters.successBlock = successBlock;
parameters.failureBlock = failureBlock;
_addPaymentParameters[productIdentifier] = parameters;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
在支付成功后,将预订单号保存起来,并与苹果的订单号绑定起来,并存储到keychain中
#pragma mark Transaction State
- (void)didPurchaseTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
{
RMStoreLog(@"transaction purchased with product %@", transaction.payment.productIdentifier);
if(transaction.payment.productIdentifier != nil){
RMAddPaymentParameters *parameters = _addPaymentParameters[transaction.payment.productIdentifier];
if(parameters){
//如果这个参数存在,则肯定是通过主动发起购买请求引起的
//在支付成功后,将parameters中的预订单号存起来,并与苹果的订单号绑定起来,并存储到keychain中
if(parameters.userid && transaction.transactionIdentifier){
[SAMKeychain setPassword:parameters.userid forService:@"qixiubaodian.server" account:transaction.transactionIdentifier];
NSString *openid = [[BDUserManager manager] getCurrentUserInfo].tokenInfo.uid;
if(openid.length > 0){
///每次用户支付完之后,将记录保存一份到有盟平台
[[BDEventMonitor monitor] event:Event_Purchase_Buy attributes:@{@"openid":openid,@"orderNumber":parameters.userid.copy}];
///本地数据库也保存一份
[[BDLocalityDataManager manager] savePruchaseTransactionIdentifier:transaction.transactionIdentifier orderNumber:parameters.userid openId:openid];
}
}
}
}
if (self.receiptVerifier != nil)
{
[self.receiptVerifier verifyTransaction:transaction success:^{
[self didVerifyTransaction:transaction queue:queue];
} failure:^(NSError *error) {
[self didFailTransaction:transaction queue:queue error:error];
}];
}else{
RMStoreLog(@"WARNING: no receipt verification");
[self didVerifyTransaction:transaction queue:queue];
}
}
最后就仿照RMStore中的验证代理,实现我们服务器验证接口即可。
重点来了...可能的原因
1、我们在前面将服务器生产的预订单号存在applicationUsername上,但是在实际上线中,发现在需要验证的时候,一定概率上取出来的为nil值。
2、根据很多用户的反馈,发现用户在成功支付后,竟然没有支付成功的回调,就是)didPurchaseTransaction:(SKPaymentTransaction )transaction queue:(SKPaymentQueue)queue没有被调用。当然这个问题猜测是这样的,因为线上的问题只能根据事先埋下的点来分析了。既然在验证的时候取不到预订单号,那么最终我们采取的是在内购发起之前就将生成号的预订单号保存到本地,只有当用户取消或者支付验证成功后,才从本地移除。
我这边做了一个持续验证的管理着,只有队列中有没有完成的订单,则间隔一段时间不停的去验证,直到验证成功为止,因此,就算本地有多个预订单号也没有关系。
这一操作上线后,丢单率从30%一下子降到了几乎为0。
然后还是有些用户比较极端,在付款成功后,可能由于网络差的缘故没能充值会员成功,用户竟然将app卸载然后重装了。针对这个问题我们采取了如下思路:
1、只有用户点击购买的时候,将获取的预订单号存储到keychain中
2、假如用户取消支付,则在取消支付的回调中将keychain值删除,或者更新为空字符串
3、假设用户成功支付后,并且有回调成功,且后台充值回调也完成,则将keychain值更新或者删除
经过以上步骤,即使漏单了,并且用户卸载后重装app,都可以重新调用找回预订单号进行重新充值会员。当然了,如果用户卸载后换手机那就没辙了。
苹果内购反正就是这样坑,我们只能尽可能降低丢单率,但是谁也无法保证没有丢单。
下面再贴一下我这边验证的方法:
- (void)verifyTransaction:(SKPaymentTransaction*)transaction
success:(void (^)())successBlock
failure:(void (^)(NSError *error))failureBlock
{
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
NSString *resultText = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
if(resultText.length == 0){
NSString *errorText = [NSString stringWithFormat:@"获取的凭证为空,transactionIdentifier:->%@",transaction.transactionIdentifier];
if(transaction.payment.applicationUsername != nil){
errorText = [errorText stringByAppendingFormat:@"applicationUsername:->%@",transaction.payment.applicationUsername];
}
NSError *error = [NSError errorWithDomain:errorText code:RMStoreErrorCodeUnableToCompleteVerification userInfo:@{NSLocalizedDescriptionKey:@"获取的凭证为空"}];
if(failureBlock){
failureBlock(error);
}
//加入任务队列,持续进行验证请求
[[BDPurchaseManager manager] verifityPurchaseWhenFailWithParams:nil transaction:transaction];
[Bugly reportError:error];
return;
}
//苹果的订单ID
NSString *identifier = transaction.transactionIdentifier;
// 2.5.1第一版内购中,由于applicationUsername返回空值,导致漏单,针对部分用户后台手动添加了,所以有些则需要进行数据清除
NSString *uid = [[BDUserManager manager] getCurrentUserInfo].tokenInfo.uid;
if(uid.length > 0){
if(([identifier isEqualToString:@"420000514257547"] && [uid isEqualToString:@"269378"])){
NSError *error = [NSError errorWithDomain:@"已经支付过" code:BDPruchaseManagerERrorCodeVerifiHadPay userInfo:@{NSLocalizedDescriptionKey:@"已经支付过"}];
if(failureBlock){
failureBlock(error);
}
return;
}
}
//自家服务器生成的预订单号
NSString *orderId = transaction.payment.applicationUsername;
__block BOOL orderNumberFromFirstTable = false;
if(orderId.length == 0){
//则从本地数据库进行查找
orderId = [[BDLocalityDataManager manager] purchaseOrderNumberWithTransactionIdentifier:identifier];
if(orderId.length == 0){
//从keychain上找找有没有这个对应的预订单号
NSString *savedOrderNumber = [SAMKeychain passwordForService:@"qixiubaodian.server" account:identifier];
if(savedOrderNumber != nil && savedOrderNumber.length != 0){
orderId = savedOrderNumber;
}
//如果keychain上还是没有,则从本地的初始预订单中寻找
if(orderId.length == 0){
if(uid.length > 0){
orderId = [[BDLocalityDataManager manager] getFirstOrderNumberWithUserId:uid];
if(orderId.length > 0){
orderNumberFromFirstTable = true;
}
}
}
}
}
if(orderId.length == 0){
NSError *error = [NSError errorWithDomain:@"预订单号为空" code:RMStoreErrorCodeUnableToCompleteVerification userInfo:@{NSLocalizedDescriptionKey:@"预订单号为空"}];
if(failureBlock){
failureBlock(error);
}
//加入任务队列,持续进行验证请求
[[BDPurchaseManager manager] verifityPurchaseWhenFailWithParams:nil transaction:transaction];
NSError *buglyError = [NSError errorWithDomain:[NSString stringWithFormat:@"预订单号为空 -> transactionIdentifier:%@,票据:...",identifier] code:0 userInfo:nil];
[Bugly reportError:buglyError];
return;
}
//生成一个加密Key,用resultText拼接key之后在md5加密即可
NSString *secretText = [resultText stringByAppendingString:BD_PURCHASE_SGIN_KEY];
//生成了加密value
NSString *sginValue = [BDPublicWays MD5EncodedString:secretText];
///接下去就是那这些参数调用自家服务器验证接口了
.....
....
如何在App Store中显示要推荐的内购商品
1、需要在App Store后台勾选推广按钮
勾选推广
2、需要实现- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product
一般返回false即可,表示自己处理,如果返回true,则表示系统帮你处理,(我没有试过哈,别人说的...)