内购全面总结
苹果IAP最大的坑点:applicationUsername=nil,你懂得
另外:IAP和第三方支付最大的不同点
第三方支付:客户端只要给服务器传商品参数给服务器让我们服务器向第三方支付服务器请求交易订单这样的好处是安全,可控制,可查询然后我们客户端根据服务器给我们的交易订单来拉起支付
但是IAP:如果也向第三方流程一样由服务器创建订单再下发给客户端然后调用IAP的话我们无法控制这笔订单是否成功不可控制因素太多,比如这笔交易没有成功而服务器已经生产了订单然后客户端根据苹果的交易结果去服务器验证在有服务器去向IAP验证延时太长。所以内购我们不能按照这样的流程来
而是客户端先向IAP发起请求支付,交易完成以后客户端再去我们自己的服务器创建订单然后再让服务器根据证订单再向苹果服务器去验证这笔交易是否完成最后通知客户端
第三方支付的流程图(此图来源简书:NewPan)
一:协议
1.协议,税务和银行业务信息的填写
2.内购商品的添加
3.添加沙盒测试账号
4.内购代码具体实现
5.内购注意事项
二.协议.税务和银行信息的填写入口
• 2.2、选择申请合同类型
• 进入协议、税务和银行业务页面后,会有3种合同类型,如果你之前没有主动申请过去合同,那么一般你现在激活的合同只有iOS Free Application一种。
• 页面内容分为两块:
Request Contracts(申请合同)
Contracts In Effect(已生效合同)。
• 合同类型分为3种:
iOS Free Application(免费应用合同)
iOS Paid Application(付费应用合同)
iAd App NetNetwork(广告合同)
2.3、申请iOS Paid Application合同(协议、税务和银行业务3个都要填写)
先点击Contact Info 的Set Up
有些银行通过下面的Look up CNAPS Code方法查不到,就需要借助百度了,一定要准确查询,否则会有问题。推荐一个地址
https://e.czbank.com/CORPORBANK/query_unionBank_index.jsp如果查不到自己的银行cnaps code可以打电话给银行客服
货币类型可能有歧义,看你是想收美元还是人民币了,都说美元合适。不过,我做的时候为了避免事情,还是选择了CNY,支持国产。还有一点,银行账号如果是对公的账号,需要填写公司的英文名称,如果没有的话,上拼音!然后点击保存银行信息就算ok了,然后退回到最开始的页面
如果以上信息填写完毕,状态一直是Processing不要怀疑自己填写出错,那是需要审核一般1到3天就能通过
二、为app添加内购产品
在你点击添加内购产品按钮后会有弹框,提示你选择类型,这个就要看你app的需求了
填写完审核信息后,点击右上角的“存储”按钮,就添加了一个内购产品~
三、添加沙盒技术测试员
在iTunes Connect的用户和智能中选择“沙盒技术测试员”,填写信息保存以后就有一个测试员了
四、具体实现
//收到产品返回信息这个时获取苹果服务器的产品列表根据id来的
- (void)productsRequest:(SKProductsRequest )request didReceiveResponse:(SKProductsResponse )response{
// NSLog(@”————–收到产品反馈消息———————”);
NSArray *product = response.products;
if([product count] == 0){
// [SVProgressHUD dismiss];
// NSLog(@”————–没有商品——————”);
return;
}
// NSLog(@”productID:%@”, response.invalidProductIdentifiers);
// NSLog(@”产品付费数量:%lu”,(unsigned long)[product count]);
SKProduct *p = nil;
for (SKProduct *pro in product) {
NSLog(@"%@", [pro description]);
NSLog(@"%@", [pro localizedTitle]);
NSLog(@"%@", [pro localizedDescription]);
NSLog(@"%@", [pro price]);
NSLog(@"%@", [pro productIdentifier]);
if([pro.productIdentifier isEqualToString:@"jianyinyue6"]){
p = pro;
}
}
SKPayment *payment = [SKPayment paymentWithProduct:p];
// NSLog(@”发送购买请求”);
[[SKPaymentQueue defaultQueue] addPayment:payment];
// _hud.hidden = YES;
}
//请求失败
- (void)request:(SKRequest )request didFailWithError:(NSError )error{
// [SVProgressHUD showErrorWithStatus:@”支付失败”];
_hud.hidden = YES;
// NSLog(@”——————错误—————–:%@”, error);
}
//正式环境验证
//
-(void)verifyPurchaseWithPaymentTransaction{
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
//创建请求到苹果官方进行购买验证
NSURL *url=[NSURL URLWithString:AppStore];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody=bodyData;
requestM.HTTPMethod=@"POST";
//创建连接并发送同步请求
NSError *error=nil;
NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
return;
}
NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
// NSLog(@”%@”,dic);
if([dic[@”status”] intValue]==0){
// NSLog(@”购买成功!”);
//验证过
NSDictionary *dicReceipt= dic[@"receipt"];
NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识
//如果是消耗品则记录购买数量,非消耗品则记录是否购买过
NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
if ([productIdentifier isEqualToString:@" "]) {
// 以后确认购买成功了
[self cfingMBhud];
[[NSUserDefaults standardUserDefaults] setValue:productIdentifier forKey:@”appidsting”];
[[NSUserDefaults standardUserDefaults]synchronize];
}else{
[defaults setBool:YES forKey:productIdentifier];
}
//在此处对购买记录进行存储,可以存储到开发商的服务器端
}else{
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
//创建请求到苹果官方进行购买验证
NSURL *url=[NSURL URLWithString:SANDBOX];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody=bodyData;
requestM.HTTPMethod=@"POST";
//创建连接并发送同步请求
NSError *error=nil;
NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
return;
}
NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
NSLog(@"%@",dic);
if([dic[@"status"] intValue]==0){
// NSLog(@"购买成功!");
//验证过
_hud.hidden = YES;
NSDictionary *dicReceipt= dic[@"receipt"];
NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识
//如果是消耗品则记录购买数量,非消耗品则记录是否购买过
NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
if ([productIdentifier isEqualToString:@"jianyinyue6"]) {
// 以后确认购买成功了
[self cfingMBhud];
[[NSUserDefaults standardUserDefaults] setValue:productIdentifier forKey:@"appidsting"];
[[NSUserDefaults standardUserDefaults]synchronize];
}else{
[defaults setBool:YES forKey:productIdentifier];
}
}else{
_hud.hidden = YES;
}
}
}
//监听购买结果
- (void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transaction{
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
// NSLog(@”交易完成”);
_hud.hidden = YES;
// 发送到苹果服务器验证凭证
[self verifyPurchaseWithPaymentTransaction];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
// NSLog(@”商品添加进列表”);
break;
case SKPaymentTransactionStateRestored:{
// NSLog(@”已经购买过商品”);
_hud.hidden= YES;
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStateFailed:{
// NSLog(@”交易失败%@”,tran);
// [self verifyPurchaseWithPaymentTransaction];
_hud.hidden = YES;
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
default:
break;
}
}
}
//可以知道恢复购买购买了哪些东西这里可以喝服务器做交互
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
NSMutableArray *ar = [[NSMutableArray alloc] init];
NSLog(@"received restored transactions: %lu", (unsigned long)queue.transactions.count);
//没有购买过
if (queue.transactions.count==0) {
_hud.hidden = YES;
_hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
_hud.mode = MBProgressHUDModeText;
_hud.removeFromSuperViewOnHide=NO;
_hud.label.text = @"哦哦,你还没购买过此项目,赶快去购买吧!";
_hud.bezelView.color = [UIColor blackColor];
[_hud hideAnimated:YES afterDelay:2.0];
}
//购买过
for (SKPaymentTransaction *transaction in queue.transactions)
{
NSString *productID = transaction.payment.productIdentifier;
[ar addObject:productID];
[[NSUserDefaults standardUserDefaults] setValue:ar[0] forKey:@"appidsting"];
[[NSUserDefaults standardUserDefaults]synchronize];
[self cfingMBhud];
}
}
//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
// NSLog(@”交易结束”);
_hud.hidden = YES;
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
购买成功后我们iOS前端可以单独在客户端完成订单正确性的验证。但是因为有的项目后台要Android和iOS两端生成账单便于对账。所以我们请求后台接口,服务器处验证是否支付成功,依据后台返回结果做相应逻辑处理。
订单正确性的验证本来可以是:iOS客户端(购买成功)→ 前端到苹果服务器验证→处理苹果返回结果做相应逻辑处理; 现在:iOS客户端(购买成功)→ 后台→后台到苹果服务器验证→处理后台返回结果做相应逻辑处理)
服务器要做的是:
1.接收iOS前端发过来的购买凭证。
2.判断凭证是否已经存在或验证过,然后存储该凭证。
3.将该凭证发送到对应环境下的苹果服务器验证,并将验证结果返回给客户端。
4.根据需求,是否修改用户相应信息。
官方文档应该也是支持的这么做的→In-App Purchase Programming Guide
- (void)verifyTransactionResult{
//验证凭据,获取到苹果返回的交易凭据
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
//从沙盒中获取到购买凭据
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
//传输的是BASE64编码的字符串
BASE64常用的编码方案,通常用于数据传输,以及加密算法的基础算法,传输过程中能够保证数据传输的稳定性,BASE64是可以编码和解码的。
NSDictionary *requestDict =@{@”receipt-data”: [receipt base64EncodedStringWithOptions:0],@”sandbox”:@”1”};
请求后台接口,服务器处验证是否支付成功,依据返回结果做相应逻辑处理
与后台协调好,让后台根据你的“sandbox”字段的1,0来区分请求是正式环境还是测试环境
当然“sandbox”这个字段也可以替换为你想要的,但是“receipt-data”不能替换,要注意!)
//请求成功的response自己输出看一下吧,status是0就成功了,这里就不贴出来了,因为有一些敏感数据,比如你的bundleID,product_id之类的
}
下面是两种环境下的苹果服务器验证地址
测试环境(审核用这个)
//正式环境验证
五、要注意的事项!
1.bundleID要与iTunes Connect上你App的相同,不然是请求不到产品信息的
2.在沙盒环境进行测试内购的时候,要使用没有越狱的苹果手机。
3.在沙盒环境下真机测试内购时,请去app store中注销你的apple ID,不然发起支付购买请求后会直接case:SKPaymentTransactionStateFailed。使用沙盒测试员的账号时不需要真正花钱的。
4.如果只添加了一个沙盒测试员账号,当一个真机已经使用了这个账号,另一个真机再使用这个账号支付也是会发生错误的。那就去多建几个沙盒测试员账号使用不同的,反正也是免费的,填写也很快。
5.监听购买结果,当失败和成功时代码中要调用:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
该方法通知苹果支付队列该交易已完成,不然就会已发起相同 ID 的商品购买就会有此项目将免费恢复的提示
六、请在本地做一下凭证存储!
现在订单正确性的验证是:iOS客户端(购买成功)→ 后台→后台到苹果服务器验证→处理后台返回结果做相应逻辑处理。
我们前端购买成功后,凭证本地保留一份,当与后台验证成功后,再将本地保留的凭证删除。否者一直使用本地已经保留的凭证与后台交互。
注:由于以前我在的iTunes Connect中填写过协议,税务,和银行业务步骤无法复现所以部分图片来自简书作者:睡不着的叶-《iOS开发 内购流程 手把手教你还不学?》文章。
七:出现的问题
如果在真机上运行代码请将真是apple id账号退出用测试账号登入
App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的时候需要判断返回Status Code决定是否去沙盒进行二次验证,为了线上用户的使用,验证的顺序肯定是先验证正式环境,此时若返回值为21007,就需要去沙盒二次验证,因为此购买的是在沙盒进行的。
审核不通过:原因没有提供测试账号,或者审核时用的是正式环境验证链接
如果年会员或者月会员的话选择是—消耗性商品,订阅和消耗性返回数据有区别
生成的订单怎么对应上这个订单呢-》后台来做(推荐)也可以前台来做
凭证存储一定要在本地做一下(防止网络原因或者和后台中断访问)然后等和后台交互过确认以后再删除
八:坑点(转自简书NewPan)
最大的一个就是,从 IAP 交易结果出来到通知 APP,只有一次。
1.如果用户后买成功以后,网络就不行了,那么苹果的 IAP 也收不到支付成功的通知,就没法通知 APP,我们也没法给用户发货。
2.如果 IAP 通知我们支付成功,我们驱动服务器去 IAP 服务器查询失败的话,那就要等下次 APP 启动的时候,才会重新通知我们有未验证的订单。这个周期根本没法想象,如果用户一个月不重启 APP,那么我们可能一个月没法给用户发货
3.有人反馈,IAP 通知已经交易成功了,此时去沙盒里取收据数据,发现为空,或者出现通知交易成功那笔交易没有被及时的写入到沙盒数据中,导致我们服务器去 IAP 服务器查询的时候,查不到这笔订单。
4.如果用户的交易还没有得到验证,就把 APP 给卸载了,以后要怎么恢复那些没有被验证的订单?
5.越狱手机有无数奇葩的收据丢失或无效或被替换的问题,应该怎样酌情处理?
解决:越狱手机一律不准内购
检查是否越狱:友盟统计有一个方法#import