因为之前做的项目都是电商的,交易的也都是实体的,所以都会采用微信,支付宝等第三方支付,现在这个公司的项目是个付费小说的软件,要搞支付只能选择苹果内购了。
具体怎么在ituens connect开通就不在介绍了,网上也有很多资料,这篇文章主要分享一下app接入内购的代码,已经防丢单的处理策略。
下面先介绍一下 内购的主要流程
- 首先拿到productid去请求苹果服务器,请求产品信息
- 得到苹果服务器的返回信息,如果产品存在且为可售状态,则生成内购订单,加入到苹果的交易队列
- 苹果检查到支付成功或者失败后,会通过代理,回调支付的结果
- 根据结果,如果成功则取出交易凭证,一般是交给自己的服务器去验证
- 验证完成之后的一些处理
// 去苹果服务器请求产品信息
- (void)requestProductData:(NSString *)productId {
NSArray *productArr = [[NSArray alloc]initWithObjects:productId, nil];
NSSet *productSet = [NSSet setWithArray:productArr];
SKProductsRequest *request = [[SKProductsRequest alloc]initWithProductIdentifiers:productSet];
request.delegate = self;
[request start];
#pragma mark - SKProductsRequestDelegate //这个是向苹果服务器请求产品的代理
- (void)requestDidFinish:(SKRequest *)request
{
_timeIntervalStart = [[NSDate date] timeIntervalSince1970];
[KR_RECHARGELOG appendingString:@"-> SKProductReqFinish"];
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
[KR_PROGRESS toast:STRING_LOCALIZE(@"recharge_pay_failed")];
[KR_RECHARGELOG appendingString:@"-> SKProductReqFail"];
}
/**
收到产品返回信息
*/
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSArray *productArr = response.products;
if ([productArr count] == 0)
{
return;
}
SKProduct *product = nil;
for (SKProduct *pro in productArr) {
if ([pro.productIdentifier isEqualToString:_orderModel.data[@"product_id"]]) {
product = pro;
break;
}
}
if (product)
{
//此处为创建内购订单,加入到苹果内购iap的交易队列
SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
//此处是把订单号 存到 payment的applicationUsername字段上
payment.applicationUsername = _orderModel.orderNum;
//发送内购请求
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
else
{
NSLog(@"没有此商品");
}
}
#pragma mark - SKPaymentTransactionObserver
// 监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased://交易完
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed://交易失败
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored://已经购买过该商品
[self restoreTransaction:transaction];
break;
case SKPaymentTransactionStatePurchasing://商品添加进列表
break;
case SKPaymentTransactionStateDeferred://状态未确定
default:
break;
}
}
}
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
//交易失败
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
[KR_PROGRESS toast:STRING_LOCALIZE(@"recharge_reinstate")];
// 对于已购商品,处理恢复购买的逻辑
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
// 验证购买
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
// 验证凭据,获取到苹果返回的交易凭据
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
//这个recptData就是交易的凭证,你可以选择是客户端自己去向苹果验证,但更多情况是 把凭证传给服务器,让服务器去验证
}
在开发测试的时候可以先客户端验证,先验证能不能完整走完内购流程
//沙盒测试环境验证
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
// 验证购买
- (void)verifyPurchaseWithPaymentTrasaction {
// 验证凭据,获取到苹果返回的交易凭据
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
// 发送网络POST请求,对购买凭据进行验证
//测试验证地址:https://sandbox.itunes.apple.com/verifyReceipt
//正式验证地址:https://buy.itunes.apple.com/verifyReceipt
NSURL *url = [NSURL URLWithString:SANDBOX];
NSMutableURLRequest *urlRequest =
[NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];
urlRequest.HTTPMethod = @"POST";
NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSString *payload = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", encodeStr];
NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
urlRequest.HTTPBody = payloadData;
// 提交验证请求,并获得官方的验证JSON结果 iOS9后更改了另外的一个方法
NSData *result = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:nil error:nil];
// 官方验证结果为空
if (result == nil) {
NSLog(@"验证失败");
return;
}
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:result options:NSJSONReadingAllowFragments error:nil];
if (dict != nil) {
// 比对字典中以下信息基本上可以保证数据安全
// bundle_id , application_version , product_id , transaction_id
// NSLog(@"验证成功!购买的商品是:%@", @"_productName");
NSLog(@"验证成功%@",dict);
}
}
以上基本上就是内购的流程了,但是有可能由于个种原因,导致苹果内购的- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
回调不及时,或者当次根本就没回调成功,等等照成订单丢失的现象。
下面我结合我们项目的实际情况来说一下对app内购丢单的处理,我们的app逻辑是在下单的同时,后台会给你一个订单号,等app支付完成后,在苹果回调时,把凭证和订单号一块传给后台,如果后台验证成功,则会根据订单号增加对应的用户id的看点。刚上线的时候,每天都收到很多看点不到账的问题反馈,原因是因为苹果回调的不及时,等苹果回调了,订单号已经不存在了,所以造成,扣款成功,服务器却无法知道成功状态,导致订单丢失。
后来我们进行了优化处理,比如在上述- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
方法中,将订单号保存到对应的SKMutablePayment,然后等到苹果内购回调的时候,取出订单号,然后将交易凭证和订单号一块发给服务器处理。
还有就是在苹果回调的时候,将订单号以及凭证本地化存储,我做项目的时候采用的是,每有一个交易就存储到本地的plist文件数组中,在很多地方去主动检查有没有存储的订单号,如果有就依次取出去拿到服务器认证,认证完成之后在本地删除对应的凭证及订单号。
经过优化之后投诉量基本上在几天一个,对内购丢单有疑问的同学可以互相交流一下