最近公司在做APP内购会员功能 遇到了很多问题 总结记录一下 首先一定要区分Apple pay 和IAP内购的区别
可以先去看一下官方文档地址 有每个步骤的详细解释
本篇文章分为:
1、 内购支付流程;
2、开发集成步骤;
3、问题(遇坑)记录解决方式
之前没看官方文档走了很多弯路 网上博客并不系统 强烈建议先过一遍官方文档
先看一下IAP内购支付流程(官方)
官方流程图
- 程序向服务器发送请求,获得一份产品列表。
- 服务器返回包含产品标识符的列表。
- 程序向App Store发送请求,得到产品的信息。
- App Store返回产品信息。
- 程序把返回的产品信息显示给用户(App的store界面)
- 用户选择某个产品
- 程序向App Store发送支付请求
- App Store处理支付请求并返回交易完成信息。
- 程序从信息中获得数据,并发送至服务器。
- 服务器纪录数据,并进行审(我们的)查。
- 服务器将数据发给App Store来验证该交易的有效性。
- App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识。
- 服务器读取返回的数据,确定用户购买的内容。
- 服务器将购买的内容传递给程序。
第一步:内购账户税务协议、银行卡绑定相关
一般都是运营或者产品经理处理这步 这篇文章图文步骤比较详细 处理税务银行相关设置 IAP,In App Purchases-在APP内部支付
第二步:Xcode设置相关
屏幕快照 2018-08-22 下午6.00.11.png打开In-App Purchase开关 对应在开发者证书中心的项目证书中显示应该也是可用状态
屏幕快照 2018-08-22 下午6.01.35.png
第三步:在App Store Content -> 我的APP 添加内购项目商品
屏幕快照 2018-08-23 上午10.06.23.png
- 在首页上,点按“我的 App”,然后选择与该 App 内购买项目相关联的 App。
- 在工具栏中,点按“功能”,然后在左列中点按“App 内购买项目”。
- 若要添加 App 内购买项目,请前往“App 内购买项目”,并点按“添加”按钮(+)。
选择功能 添加内购项目商品
选择功能Tab
内购商品对应四种类型 消耗型、非消耗型、自动续订订阅型、非续订订阅型
官方文档
- 选择“消耗型项目”、“非消耗型项目”或“非续订订阅”,并点按“创建”。有关自动续订订阅的信息,请参见创建自动续期订阅。
- 添加参考名称、产品 ID 和本地化显示名称。
- 点按“存储”或“提交以供审核”。
您可以在创建您的 App 内购买项目时输入所有的元数据,或稍后输入您的 App 内购买项目信息。
屏幕快照 2018-08-23 上午10.09.34.png
屏幕快照 2018-08-23 上午10.16.31.png添加一个测试商品 其他属性都可以随意填写 产品ID一定要认真填写 项目中需要根据ID获取商品信息 价格有不同的等级可以选 最低备用等级1 == 1元
填写完成之后储存 就完成了一个内购商品的添加
第四步:沙盒环境测试账号
屏幕快照 2018-08-23 上午11.02.26.png 屏幕快照 2018-08-23 上午11.05.28.png因为涉及到钱相关 总不能直接用money去支付吧 所以需要你去添加一个沙盒技术测试人员的账号 (这个账号是虚拟的) 付款不会扣你
看第三步那张图 在App Store Content 选择用户和职能 进入下面页面 选择沙箱技术测试员 添加测试账号
Tips:Q:为什么添加沙箱技术测试员 注册不成功 Unknown Email xxxxxx
首先这里有个坑 邮箱只要符合格式就可以 虚假邮箱也可以 但密码必须符合正式的要求要有大小写和字符 复杂就好 例如:Lh123456*
第五步:代码实现(初步,未进行优化 有什么问题可以在评论中跟我沟通)
//@"77"代表我项目中设置的内购产品的ID 可以自行替换
#import "XSApplePayManager.h"
#import <StoreKit/StoreKit.h>
#import "APIManager.h"
@interface XSApplePayManager ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>
@end
@implementation XSApplePayManager
+ (instancetype)shareManager
{
static dispatch_once_t onceToken;
static XSApplePayManager *manager = nil;
dispatch_once(&onceToken, ^{
manager = [[XSApplePayManager alloc]init];
});
return manager;
}
- (void)requestPay{
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
if([SKPaymentQueue canMakePayments]){
[self requestProductData:@"77"];
}else{
[AAProgressManager showFinishWithStatus:L(@"请打开应用内支付功能")];
}
}
//去苹果服务器请求商品
- (void)requestProductData:(NSString *)type{
[AAProgressManager showWithStatus:@"正在支付..."];
NSArray *product = [[NSArray alloc] initWithObjects:type,nil];
NSSet *nsset = [NSSet setWithArray:product];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}
//收到产品返回信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] == 0){
[AAProgressManager dismiss];
[AAProgressManager showFinishWithStatus:L(@"无法获取商品信息,请重新尝试购买")];
return;
}
NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%ld",product.count);
SKProduct *p = product.firstObject;
NSLog(@"商品信息 ———— %@ %@ %@ %@ %@", p.description,p.localizedTitle,p.localizedDescription,p.price,p.productIdentifier);
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:p];
payment.quantity = (NSInteger)p.price;//购买次数=价钱
if (payment.quantity == 0) {
payment.quantity = 1;
}
payment.applicationUsername = @"订单号/用户ID 用来映射";
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
[AAProgressManager dismiss];
NSLog(@"------------反馈信息结束-----------------%@",request);
}
//沙盒测试环境验证
#define kIAPSandbox @"https://sandbox.itunes.apple.com/verifyReceipt"
#define kIAPAppStore @"https://buy.itunes.apple.com/verifyReceipt"
/**验证购买,避免越狱软件模拟苹果请求达到非法购买问题
建议这一步在服务器去做*/
-(void)verifyPurchaseWithPaymentTransaction{
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\",\"password\":\"你的共享密钥(在续订订阅状态下会用到)\"}", receiptString];
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
//创建请求到苹果官方进行购买验证
NSURL *url=[NSURL URLWithString:kIAPSandbox];
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:@"77"]) {
NSInteger purchasedCount=[defaults integerForKey:productIdentifier];//已购买数量
[[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier];
}else{
[defaults setBool:YES forKey:productIdentifier];
}
//在此处对购买记录进行存储,可以存储到开发商的服务器端
}else{
NSLog(@"购买失败,未通过验证!");
}
}
//监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
dispatch_async(dispatch_get_main_queue(), ^{
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
NSLog(@"交易完成");
// 发送到苹果服务器验证凭证
[self verifyPurchaseWithPaymentTransaction];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:{
[AAProgressManager showFinishWithStatus:L(@"已经购买过商品")];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStateFailed:{
[AAProgressManager showFinishWithStatus:L(@"交易失败")];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateDeferred:
NSLog(@"最终状态未确定");
break;
}
break;
default:
break;
}
}
});
}
//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
NSLog(@"交易结束");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
第六步:IAP支付流程 & 服务器验证流程
整个支付流程如下:
1.客户端向Appstore请求购买产品(假设产品信息已经取得),Appstore验证产品成功后,从用户的Apple账户余额中扣费。
2.Appstore向客户端返回一段receipt-data,里面记录了本次交易的证书和签名信息。
3.客户端向我们可以信任的服务器提供receipt-data
4.服务器对receipt-data进行一次base64编码
5.服务器把编码后的receipt-data发往itunes.appstore进行验证
6.itunes.appstore返回验证结果给服务器
7.服务器对商品购买状态以及商品类型,向客户端发放相应的道具与推送数据更新通知
漏单处理 确保receipt-data的成功提交与异常处理
建立在IAP Server Model的基础上,并且我们知道手机网络是不稳定的,在付款成功后不能确保把receipt-data一定提交到服务器。如果出现了这样的情况,那就意味着玩家被appstore扣费了,却没收到服务器发放的道具。
漏单处理:
解决这个问题的方法是在客户端提交receipt-data给我们的服务器,让我们的服务器向苹果服务器发送验证请求,验证这个receipt-data账单的有效性. 在没有收到回复之前,客户端必须要把receipt-data保存好,并且定期或在合理的UI界面触发向服务端发起请求,直至收到服务端的回复后删除客户端的receipt账单记录。
如果是客户端没成功提交receipt-data,那怎么办?就是玩家被扣费了,也收到appstore的消费收据了,却依然没收到游戏道具,于是投诉到游戏客服处。
这种情况在以往的经验中也会出现,常见的玩家和游戏运营商发生的纠纷。游戏客服向玩家索要游戏账号和appstore的收据单号,通过查询itunes-connect看是否确有这笔订单。如果订单存在,则要联系研发方去查询游戏服务器,看订单号与玩家名是否对应,并且是否已经被使用了,做这一点检查的目的是 为了防止恶意玩家利用已经使用过了的订单号进行欺骗(已验证的账单是可以再次请求验证的,曾经为了测试,将账单手动发给服务器处理并成功),谎称自己没收到商品。这就是上面一节IAP Server Model中红字所提到的安全逻辑的目的。当然了,如果查不到这个订单号,就意味着这个订单确实还没使用过,手动给玩家补发商品即可。
更多可以查看这篇博文苹果IAP安全支付与防范 receipt收据验证
遇到的坑
Q:21004 你提供的共享密钥和账户的共享密钥不一致 什么是共享密钥? 共享密钥从哪里获取?
A:先看一下官方文档怎么说生成收据验证代码
为了在验证自动续期订阅时提高您的 App 与 Apple 服务器交易的安全性,您可以在收据中包含一个 32 位随机生成的字母数字字符串,作为共享密钥。
在 App Store Connect 中生成共享密钥。您可以生成一个主共享密钥,作为您所有 App 的单一代码,或作为针对单个 App 的 App 专用共享密钥。您也可以针对您的部分 App 使用主共享密钥,其他 App 使用 App 专用共享密钥。
点击下面展开就可以看到共享密钥生成的方式
Q:沙箱技术测试人员添加不成功 总是提示邮箱错误
A: 沙箱技术测试账号用于付款测试 任意未创建过Apple ID 的邮箱都可以 假的邮箱也可以 重要的是密码格式一定要包含大小写 跟正式账号注册规则一样 (例如:Lh123456*)
Q:自己服务器向苹果服务器验证收据/凭证参数是什么?向status code 验证apple iap sever的状态码代表什么意思?
A:21002、21003、21004、21005、21006、21007... 具体可以查看这篇文档用App Store验证收据
Q:Apple 和IAP的区别
A:IAP是链接App store的内购服务 一般是虚拟商品需要走的通道(比如会员功能)
Apple Pay是苹果跟各大银行合作的卡包形式的类似于刷卡支付服务 一般用于现实场景
这两个一定别搞混了
Q:怎么通过itunes-connect查看具体订单,itunes-connect中无法直接看到订单信息,可以用以下方法来查询
1.可以通过账单向苹果发送账单验证,有效可以手动补发
2 .用自己的服务器的记录账单列表对比
3.利用第三方的TalkingData等交易函数,会自动记录账单数据
还有一些问题可以借鉴一下这篇博文iOS之你一定要看的内购破解-越狱篇 他遇到的实际问题比较多 按需借鉴
网友评论