一, 如何定义内购商品?
线上购买线上使用,不涉及到线下实物的交易都需要用苹果内购。简言之,就是虚拟商品,比如线上音乐,电子图书,设计作品,游戏币等。
二,Appstore Connect线上配置
1.协议、税务和银行业务填写;
2.创建内购项目;
3.设置沙箱测试账号;
由于回头总结,截图未能一一提供,在此只提示设置入口而后按引导一步步去做就行了。
三,代码集成
1.添加和移除监听设置
// 添加观察者
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
// 移除观察者
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
2.通过产品ID获取产品信息列表
- (void)getProductInfo:(NSString *)productIdentifier{
self.selectProductId = productIdentifier;
if (![SKPaymentQueue canMakePayments]){
if (self.failurePurchaseBlock) {
self.failurePurchaseBlock(@"不允许付费购买商品");
}
return;
}
if (productIdentifier.length > 0){
NSArray * product = [[NSArray alloc] initWithObjects:productIdentifier, nil];
NSSet *set = [NSSet setWithArray:product];
SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
request.delegate = self;
[request start];//开始请求
}
}
/*
查询成功后的回调(经由getProductInfo函数发起的产品信息查询,成功后返回执行的回调)。
*/
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *myProduct = response.products;
if (myProduct.count == 0){
if (self.failurePurchaseBlock) {
self.failurePurchaseBlock(@"无法获取商品信息");
}
return;
}
//选择用户选择的档位发起购买请求
for (SKProduct *sKProduct in myProduct) {
NSLog(@"pro info");
NSLog(@"SKProduct 描述信息:%@", sKProduct.description);
NSLog(@"localizedTitle 产品标题:%@", sKProduct.localizedTitle);
NSLog(@"localizedDescription 产品描述信息:%@",sKProduct.localizedDescription);
NSLog(@"price 价格:%@",sKProduct.price);
NSLog(@"productIdentifier Product id:%@",sKProduct.productIdentifier);
if([sKProduct.productIdentifier isEqualToString: self.selectProductId]){
[self buyProduct:sKProduct];
break;
}
}
}
3.发起购买请求
/*解决掉单的问题:
1.将需要传给后台服务器的参数(比如订单id,用户id)放到SKMutablePayment的applicationUsername字段里面;
*/
-(void)buyProduct:(SKProduct *)product{
// 1.创建票据
NSString *orderId = @"123";
NSString *userId = @"abc";
NSString *userName = [NSString stringWithFormat:@"%@-%@",userId,orderId];
SKMutablePayment *skpayment = [SKMutablePayment paymentWithProduct:product];
skpayment.applicationUsername = userName;
// 2.将票据加入到交易队列
[[SKPaymentQueue defaultQueue] addPayment:skpayment];
}
4.实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
/*
SKPaymentTransactionStatePurchasing, 正在购买
SKPaymentTransactionStatePurchased, 已经购买
SKPaymentTransactionStateFailed, 购买失败
SKPaymentTransactionStateRestored, 回复购买中
SKPaymentTransactionStateDeferred 交易还在队列里面,但最终状态还没有决定
*/
self.sKPaymentQueue = queue;
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:{
NSLog(@"正在购买");
}break;
case SKPaymentTransactionStatePurchased:{
NSLog(@"购买成功");
//这里可以做一个是否同一用户的判断,因为如果是更新数据时产生的订单,跟下单时的用户未必是同一个用户
//isLogin:是否登录状态; userId:该订单的用户id;currentUserId:当前登录用户的id;
NSArray *arr = [transaction.payment.applicationUsername componentsSeparatedByString:@"-"];
NSString *userId = @"";
if (arr.count>0) {
userId = arr[0];
}
if (isLogin && [currentUserId isEqualToString: userId]) {
//取出凭证
[self buyAppleStoreProductSucceedWithPaymentTransactionp:transaction];
}
}
break;
case SKPaymentTransactionStateFailed:{
NSLog(@"购买失败");
if (self.failurePurchaseBlock) {
self.failurePurchaseBlock(@"购买失败");
}
// 购买失败也要把这个交易移除掉
[queue finishTransaction:transaction];
}break;
case SKPaymentTransactionStateRestored:{
NSLog(@"回复购买中,也叫做已经购买");
// 回复购买中也要把这个交易移除掉
[queue finishTransaction:transaction];
}break;
case SKPaymentTransactionStateDeferred:{
NSLog(@"交易还在队列里面,但最终状态还没有决定");
}break;
default:
break;
}
}
}
5.获取凭证,并且将凭证发送给后台校验
-(void)buyAppleStoreProductSucceedWithPaymentTransactionp:(SKPaymentTransaction *)paymentTransactionp {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
NSString *encodedReceipt = @"";
if (!receipt) {
NSLog(@"no receipt");
/* No local receipt -- handle the error. */
} else {
/* Get the receipt in encoded format */
encodedReceipt = [receipt base64EncodedStringWithOptions:0];
}
//调接口服务器校验,该方法自行实现
// [self varifyPurchaseData:paymentTransactionp andReceipt:encodedReceipt];
}
注意:后台校验成功后,需要把这个成功的交易移除掉
[self.sKPaymentQueue finishTransaction:transaction];
四,踩过的坑汇总
1.创建沙盒测试账号的时候报错:Unknown Errors while creating Sandbox Tester, Please check Error Log, email=***
解决:密码设置复杂一些。
2.购买成功后的校验处理方案
- 为了保证安全,购买成功后,需要通过绑定产品id,订单号,凭证提供给后台,让后台调苹果接口解析凭证进行校验;
- 由于测试和审核的时候用的是沙盒测试,上线后才用正式环境,所以后台需要做二次校验。
- 即是先校验正式环境,如果成功则是上线后购买成功;
- 如果校验失败,再校验测试环境,成功的话则是沙盒测试下购买成功,失败则是沙盒测试下购买失败;
3.审核遇到的问题
(1)Guideline 3.1.1 - Business - Payments - In-App Purchase
We noticed that your app uses in-app purchase products to purchase credits or currencies that are not consumed within the app, which is not appropriate for the App Store.
通过跟苹果审核的客服电话沟通, 苹果内购不能包含“提现”等有套现或洗钱嫌疑的功能——去掉“提现”的功能即可;
(2)一个 App 内购买项目被退回,并在以下列表中以高亮显示
点击高亮的内购项目,编辑一下描述的文字,写得更加符合项目并且详细一些,点击保存。
4.掉单问题处理
网上有说把创建的订单信息存储在本地,但是思量后发现有不少的问题,如果app被删掉了,存储的订单信息一样会被同步删除,除非存储在钥匙串里面,否则还是有概率调单。
后来参考了另一篇文章提到的applicationUsername字段,于是解决了这个问题,而且不需要做各种存取操作。
下面看看如何去获取未完成凭证校验的订单(哪些情况会产生掉单):
1.苹果内购过程中,苹果服务器响应慢,用户杀掉进程的情况;
2.拿到凭证,传给公司后台服务器过程中,网络问题导致接口访问失败等情况。
解决步骤:
- 创建订单的时候,将orderId和userId存储在applicationUsername里面(可以将更多的字段转为json字符串,取出的时候再转为字典);
- 在需要更新苹果未完成订单的页面添加监听方式(即是代码集成第一步),可以在是启动app页面,或刷新金额的页面;
- 在购买成功的代理中,判断订单中的用户id和当前登录的用户id是否一致,如果一致,则取出订单id和凭证一起传给后台服务器;(注:该页面如果没有登录Apple ID的话,会弹出弹框提示用户登录)
- 当后台服务器接收到凭证等数据成功时,通过finishTransaction结束该交易。
这样下来,几乎是不会再有掉单的问题了。
参考资料:
iOS内购最新讲解
真·iOS内购的完整流程