内购是啥
App 内购买项目允许顾客通过访问 App Store 购买您 App 中的内容、功能或服务,并安全处理来自用户的付款。
详情传送门https://help.apple.com/itunes-connect/developer/#/devb57be10e7
下面来说内购集成流程
1.协议
登录苹果开发者中心,进入iTunes Connect,再进入“协议、税务和银行业务”页面,如图
点击进入可以看到,目前共有两个分组,三种合同。(此处有坑,比如我们当前账号不能申请合同!如下图)
Request Contracts 可以申请的合同;
Contracts In Effect 已经生效的合同。
三种合同分别是
Free Applications 免费应用(默认已经生效);
Paid Applications 付费应用,需要申请;
iAd App Network 广告应用,需要申请。
内购对应的是Paid Applications 付费应用,需要申请,如图2.(如果Request按钮不显示,则说明当前账号权限有问题)
点击Request完善信息,提交就行.
2.内购集成
内购实现流程:
1.客户端向Appstore请求购买产品(假设产品信息已经取得),Appstore验证产品成功后,从用户的Apple账户余额中扣费。
2.Appstore向客户端返回一段receipt-data,里面记录了本次交易的证书和签名信息。
3.客户端向我们可以信任的服务器提供receipt-data
4.服务器对receipt-data进行一次base64编码
5.把编码后的receipt-data发往itunes.appstore进行验证
6.itunes.appstore返回验证结果给服务器
7.服务器对商品购买状态以及商品类型,向客户端发放相应的道具与推送数据更新通知
注,下图3步骤和上面流程不是一一对应
我项目里面的购买流程,加入了一点业务逻辑和后台验证流程,有什么问题欢迎大家指出.
3.去苹果开发者中心创建内购商品
如下图5,点击+号去创建内购商品,产品id最好是当前应用+数字,价格区间苹果提供了一张表,商品价格只能是表上的价格,苹果会抽取30%,商家能收到的钱是用户充值的70%.这就造成了部分平台区分安卓和苹果.两端账号不互通,也造就了代充行业,再次就不展开说了.
商品价格大于100$,提交审核的时候要说明这个金额是确认过的,不然可能会被拒
4.代码集成
建议单独建一个类来处理内购业务
.h类
//
// EMAppStorePay.h
// MobileFixCar
//
// Created by Wcting on 2018/4/11.
// Copyright © 2018年 XXX有限公司. All rights reserved.
//
/*
wct20180917 内购支付类,短视频e豆购买用到。
*/
#import
@class EMAppStorePay;
@protocol EMAppStorePayDelegate ;
@optional
/**
wct20180418 内购支付成功回调
@param appStorePay 当前类
@param dicValue 返回值
@param error 错误信息
*/
- (void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePaySuccess:(NSDictionary *)dicValue error:(NSError*)error;
/**
wct20180423 内购支付结果回调提示
@param appStorePay 当前类
@param dicValue 返回值
@param error 错误信息
*/
- (void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePayStatusshow:(NSDictionary *)dicValue error:(NSError*)error;
@end
@interface EMAppStorePay : NSObject
@property (nonatomic, weak)id delegate;/**
.m类(里面有客户端验证receipt的代码,解开注释就可以,用于调试.验证建议放后台去做)
//
// EMAppStorePay.m
// MobileFixCar
//
// Created by Wcting on 2018/4/11.
// Copyright © 2018年 XXX有限公司. All rights reserved.
//
#import "EMAppStorePay.h"
#import
//#define goods1 @"net.ejiajx.MobileFixCar06"
@interface EMAppStorePay()
@property (nonatomic, strong)NSString *goodsId;/**)
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
// NSLog(@"error:%@", error);
}
//反馈请求的产品信息结束后
- (void)requestDidFinish:(SKRequest *)request
{
// NSLog(@"信息反馈结束");
}
#pragma mark ------ SKPaymentTransactionObserver 监听购买结果
// 13.监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction
{
if (self.delegate && [self.delegate respondsToSelector:@selector(EMAppStorePay:responseAppStorePayStatusshow:error:)]) {
[self.delegate EMAppStorePay:self responseAppStorePayStatusshow:@{@"value":transaction} error:nil];
}
// if (transaction.count > 0) {
// //检测是否有未完成的交易
// SKPaymentTransaction* tran = [transaction firstObject];
// if (tran.transactionState == SKPaymentTransactionStatePurchased) {
// [self completeTransaction:tran];
// [[SKPaymentQueue defaultQueue] finishTransaction:tran];//未完成的交易在此给它结束
// return;
// }
// }
for(SKPaymentTransaction *tran in transaction){
// NSLog(@"%@",tran.payment.applicationUsername);
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
// NSLog(@"交易完成");
// 购买后告诉交易队列,把这个成功的交易移除掉。
//走到这就说明这单交易走完了,无论成功失败,所以要给它移出。finishTransaction
[self completeTransaction:tran];//这儿出了问题抛异常,导致下面一句代码没执行
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
// NSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:
// NSLog(@"已经购买过商品");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
// NSLog(@"交易失败");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateDeferred:
// NSLog(@"交易还在队列里面,但最终状态还没有决定");
break;
default:
break;
}
}
}
@end
5.沙盒测试
如下图6,点添加创建沙盒测试账号,账号未未注册成AppleID的账号,测试前先到设置里退出当前AppleID,登录沙盒测试账号,沙盒测试账号只能用来测试沙盒支付,不具备正常AppleID的功能.
准备工作
1.第一次测试内购需要卸载之前APP,找开发人员安装可测试内购的APP。防止App Store下载的app走sandbox环境走不通;
2.在iPhone设置里面,退出原有账号。登录开发人员提供的内购测试账号(可找开发申请新测试账号);
6.交易安全机制
1.双重验证
苹果审核人员审核内购的时候走的是沙盒环境对应沙盒验证接口https://sandbox.itunes.apple.com/verifyReceipt,如果验证receipt只有正式环境https://buy.itunes.apple.com/verifyReceipt,苹果审核员走内购会验证失败,交易走不通,后果就是审核被拒.所以验证的时候先默认走正式环境,如果返回21007的错误码就去沙盒环境验证,保证审核通过.
2.交易凭据receipt判重
一般我们验证支付凭据(receipt)是否有效放后台去做,如果后台不做判重,同一个凭据就可以无数次验证通过(苹果也不判重),后台就会给前端发放无数次商品,但是用户只支付了一次钱取到一个支付凭据.所以安全的做法是后台把验证通过的支付凭据做个记录,每次来新的凭据先判断是否已经使用过,防止多次发放商品.
3.本地交易流水
在测试过程中,由于苹果不提供交易流水,所以会出现无法对账的情况,会提出一些莫名bug,因为测试不知道某个单的支付状态,这时前端需要做个交易流水记录,方便对账和避免不必要的bug及撕逼.
在支付成功回调里面把当前交易数据存在本地持久化,然后去后台验证,出问题就那本地存的交易数据和后台对,找出问题.
#pragma mark - EMAppStorePayDelegate
-(void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePaySuccess:(NSDictionary *)dicValue error:(NSError *)error
{
NSString *transactionReceiptString = [ZSTools objectOrNilForKey:@"value" fromDictionary:dicValue];
NSDictionary *dic = @{@"orderCode":self.strOrderCode,
@"receipt":transactionReceiptString,
@"category":@"1"
};
// NSLog(@"222diczhi:%@",dic);
/*
//wct20180601 本地交易流水,不测试内购就给注释吧,省手机内存
NSMutableDictionary *dicRec = [NSMutableDictionary dictionaryWithDictionary:self.dicPay];
[dicRec setValue:self.strOrderCode forKey:@"orderCode"];
[dicRec setValue:transactionReceiptString forKey:@"receipt"];
[dicRec setValue:@"1" forKey:@"category"];
NSString *time = [self getCurrentTimes];
[dicRec setValue:time forKey:@"creatTime"];
[self.modelEBean addDicReconciliation:dicRec];//对应下面的实现方法
*/
[self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];//苹果支付成功,传receipt-data给后台验证
[ZSTools loadActivityIndicatorOn:self.view withCenterPoint:self.view.center withTitleString:@"正在购买..." sizeType:2];
}
存储持久化实现
-(void)addDicReconciliation:(NSDictionary *)dicEBean
{
if (![self.arrReconciliationModel containsObject:dicEBean]) {
[self.arrReconciliationModel addObject:dicEBean];
}
[self saveReconciliation];
}
- (void)saveReconciliation
{
NSString *path = [NSString stringWithFormat:@"%@/%@_reconciliation.plist", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0], [EMVideoUserSingleton sharedInstance].ugsvId];
[self.arrReconciliationModel writeToFile:path atomically:NO];
}
7.注意事项
1.对账问题
通过textflight下载的app走内购也是在sandbox环境。这时走内购不需要支付相应金额,但是对应的咱们后台是正式环境,内购走通后返回的e豆(商品,以下e豆都对应商品)是正式环境。这就会造成没支付钱,但是正式环境得到e豆了,对账的时候要作记录。
2.漏单的情况:
先看看支付流程,如下:
app iTunes app 后台 app
1发起支付--->2扣费成功--->3得到receipt(支付凭据)--->4去后台验证凭据获取e豆--->5返回数据,前端刷新数据
漏单情况1
3到4的时候出问题,比如断网。此时前端会把支付凭据持久化存储下来(期间用户卸载APP此单在前端就真漏了),下次进入购买页会先判断有无未成功的支付,有就提示用户,用户选择找回,重走4,5流程。
漏单情况2
4到5的时候出问题。此时后台其实已经成功,只是前端没获取到数据,当漏单处理,还是上面的逻辑,会把该单存储。下次进入的时候会先刷新数据(此时未获取到e的豆已经获取到了),然后提示有未完成单,此时点找回会提示无效的凭据,这是正常的,因为豆已经给了,此单已结束。
漏单情况3
2到3环节出问题属于苹果的问题,目前没做处理。
3.漏单处理
1.在后台返回商品支付回调失败里面把当前交易数据持久化存储,成功状态下移除当前单数据.并检查是否有已扣款未返商品单,对应下面checkHaveDidNotPay
}else{
if (dicPara) {
[self.modelEBean addDicEBean:dicPara];//传receipt失败,
[self checkHaveDidNotPay];
}
- (void)checkHaveDidNotPay
{
if (self.modelEBean.arrEBeanBuyModel.count) {
[EMTextAlertView title:@"温馨提示" message:@"网络不给力,e豆数据可能更新不及时,请重新加载。" leftTitle:@"下次再说" rightTitle:@"重新加载" complete:^(NSInteger index, NSString *title) {
if (index == 1){//重新获取会重新调用购买验证
for (NSDictionary *dic in self.modelEBean.arrEBeanBuyModel) {
[self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];
}
}
}];
}
}
根据需求,每次购买前先检查有无之前漏单,有先处理漏单.视需求定.
我们目前是每次到购买页面先检查有无漏单
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.bizVideoMine requestVideoMineData:nil];
[self checkHaveDidNotPay];
}
有问题下面留言,有不足的地方欢迎指正.