iOS内购(InAppPurchase)的一些操作笔记
1.苹果官网的配置
a.创建一种商品类目,选择对应的价格,添加一种描述,这种描述在支付的时候会弹出,沙盒测试的时候可体验,这种商品会有一个对应的id
ipa2.pngb.添加一个沙盒账号,一般使用一个不常用的邮箱,这个邮箱和apple账号无关,只要这个邮箱能收到验证码就能绑定成功,沙盒账号就是这个邮箱,密码自己随便设置
ipa1.png
2.需要熟悉的一些流程上的东西(20210817补充)
1.用户选择商品
客户端记录用户选择的商品对应的Apple的唯一标识符并向自己公司的服务端请求接口来创建订单,
请求参数是价格,和对应的商品的在Apple端的唯一标识
服务端返回的数据包含订单号orderId,客户端记录这个orderId
2.客户端检测用户当前设备是否允许内购
3.客户端将当前App所有的商品类目列表信息发送给Apple服务端,request start,Apple服务端返回可用的商品列表,payment.applicationUsername最好使用的是自己服务器的orderId
4.客户端遍历Apple服务端返回的商品列表,判断第1步中记录的商品唯一标识符是否存在于第3步Apple返回的商品列表中
5.客服端使用苹果提供的方法发起购买请求,这时候需要用户输入AppleID的账号和密码,输入正确后会走到支付完成的回调中
6.客户端检查,客户端根据苹果返回的支付回调的购买凭证通过接口请求向Apple端进行验证
7.验证成功之后将购买凭证通过接口发送给自己公司的服务端来表示当前订单已经付款了
8.服务端收到这个验证请求,再次使用该购买凭证向Apple端验证是否已经付款
9.公司的服务端在通过Apple的服务端验证完成之后,表示用户已经完成了内购支付,做虚拟商品发放,做验证接口的返回处理操作.
需要考虑到的异常情况:
第5步执行完之后,如果用户没有来的及检查购买状态和进行下一步的操作的时候,手机断电等异常情况出现时,下次打开app或者进入到充值页面时候,应当自动来检测一下,当前用户是否有正在执行中的支付订单,如果有,则从第6步开始继续执行支付流程
3.撸代码
控制器里面导入
#import <StoreKit/StoreKit.h>
这里的URL分为两个,一个是沙盒用的,一个是正式用的
//#ifdef DEBUG
//#define yz_AppStore_URL @"https://sandbox.itunes.apple.com/verifyReceipt"
//#else
//#define yz_AppStore_URL @"https://buy.itunes.apple.com/verifyReceipt"
//#endif
沙盒用的验证:就是可以用沙盒账号来购买,不用实际话费金额,就直接走了购买成功的流程
正式用的:需要实际现金支付
这里要特别注意,一定要保证给AppleStore审核人员用的是用沙盒验证的而不是正式的
而且服务端的验证方式也需要和移动端的验证方式一致
这里说下当前我们的处理逻辑:
进入付款流程的时候先默认是正式的验证url,然后进行接口请求,接口返回一个版本号,这个版本号和当前的app里面的版本号做对比,如果版本号一致,那么久将验证地址的url改为沙盒的
这样当提交一个新的版本给苹果审核的时候,将app审核过后自动发布改为手动发布.
等苹果那边审核通过之后,先让服务端将接口返回的版本号改成0.0.0(只要不一致就行了)
然后再进行手动发布新版本,这样就保证用户手里的一定是正式的,审核人员用的就是沙盒的
获取版本号,对比之后看是否需要开启沙盒模式验证
基础操作:
- (void)viewDidLoad {
[super viewDidLoad];
_appleUrl = @"https://buy.itunes.apple.com/verifyReceipt";
NSString * vFromSelfServer = [self getAppleInfoFromSelfServer];
NSString * vCurrIPA = @"当前安装包的版本号";
if([vFromSelfServer isEqualToString:vCurrIPA]){
// 当前安装包的版本号和服务端返回的版本号一致,就将appleUrl的值改为沙盒验证,因为这种情况下表示app正在被审核中
_appleUrl = @"https://sandbox.itunes.apple.com/verifyReceipt";
}
// 添加交易监听
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[self.view addSubview:self.payLab];
}
#pragma mark --------- self server request
/**
* 用接口从自己服务器获取版本号
* 1.发布更新提交审核时候,从itunes中选项改为手动发布
* 2.让服务端将getAppleInfoFromSelfServer这个接口返回的版本号和发布给苹果审核的版本号一致
* 3.等待苹果审核通过,先让服务端将getAppleInfoFromSelfServer的值改一下成无限大(这里只要不等于线上所有用户的版本号,也不等于当前已审核通过的版本号即可)
* 4.itunes手动发布已审核通过的ipa包
*/
-(NSString *)getAppleInfoFromSelfServer {
return @"自己服务器返回的一个版本号";
}
当用户点击了购买某个商品的时候,调用自己服务端接口进行订单创建
-(void)clickPayAction {
// 自己在苹果商城申请的类别的ID列表为
// bundleid+xxx 就是你添加内购条目设置的产品ID
// zydj.product.pay_6 6元4.2豆
// zydj.product.pay_40 40元28豆
// zydj.product.pay_60 60元48豆
// 假设选中了6元4.2豆则通过zydj_product_pay_6在自己服务端创建订单
self.currentProducId = @"zydj.product.pay_6";
NSString * orderId = [self creatOrderFromSelfServer:self.currentProducId];
self.orderId = orderId;
[self startInAppPay];
}
/**
* 创建订单
*/
-(NSString *)creatOrderFromSelfServer:(NSString *)priceID {
return @"服务端根据priceID返回的一个orderId";
}
先验证用户当前使用的设备是否支持内购,允许就走到productsRequest
/**
* Apple Operation
*/
-(void)startInAppPay {
//判断是否允许内购
if ([SKPaymentQueue canMakePayments]) {
NSLog(@"用户允许内购");
//bundleid+xxx 就是你添加内购条目设置的产品ID
NSArray *product = [[NSArray alloc] initWithObjects:@"zydj.product.pay_6",@"zydj.product.pay_40",@"tv.zydj.app_pay_6",@"zydj.product.pay_60", nil];
NSSet *nsset = [NSSet setWithArray:product];
//初始化请求
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
//开始请求
[request start];
// 注意,这里请求成功之后,会走到)productsRequest:
}else{
// NSLog(@"用户不允许内购");
// [ToastUtils showMessage:@"当前设备不允许内购" duration:1 position:Toast_Point_Center];
}
}
接收到产品的返回信息,然后用返回的商品信息进行发起购买请求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE_IOS(3_0)
{
NSArray *product = response.products;
//如果苹果服务器没有产品,那就return
if([product count] == 0){
NSLog(@"没有该商品");
return;
}
SKProduct *requestProduct = nil;
NSString * currentProducId = self.currentProducId;
for (SKProduct *pro in product) {
NSLog(@"%@", [pro description]);
NSLog(@"%@", [pro localizedTitle]);
NSLog(@"%@", [pro localizedDescription]);
NSLog(@"%@", [pro price]);
NSLog(@"%@", [pro productIdentifier]);
//如果后台消费条目的ID与我这里需要请求的一样(用于确保订单的正确性)
if([pro.productIdentifier isEqualToString:currentProducId]){
// 到这一步就说明商品存在,开始发送购买请求
requestProduct = pro;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
payment.applicationUsername = self.orderId;//可以是userId,也可以是订单id,跟你自己需要而定
[[SKPaymentQueue defaultQueue] addPayment:payment];
// 如果请求成功就会弹出输入密码等页面,输入完密码之后会出现一个支付成功的弹窗,点击了弹窗之后会进入paymentQueue:方法中
}
}
}
paymentQueue:方法监听,有两种方式会进入这个方法回调中,
方式1:正常流程,用户输入密码,支付成功之后,点击了系统弹出的支付成功弹窗,就会进入这个方法回调中,
方式2:异常流程,当用户输入完密码之后,苹果实际扣款成功,用户手机异常退出(闪退或者直接杀掉APP,断电等情况异常离开app),下次打开APP进入到充值页面之后,viewDidLoad中添加了支付回调监听之后,也会进这个方法回调中
- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions {
dispatch_async(dispatch_get_main_queue(), ^{
for(SKPaymentTransaction *tran in transactions){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
NSLog(@"交易完成");
// 接下来的流程
[self completeTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已经购买过商品");
//[[SKPaymentQueue defaultQueue] finishTransaction:tran]; 消耗型商品不用写
break;
case SKPaymentTransactionStateFailed:{
//这里说明支付失败了啊.密码输入错误或者没扣款成功,那就finish掉这笔交易
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
NSLog(@"交易失败");
}
break;
default:
break;
}
}
});
}
交易结束,当交易结束后还要去appstore上验证支付信息是否都正确,只有所有都正确后,我们就可以给用户方法我们的虚拟物品了。为了防止上一步的方式2进入这个方法中所需要的操作,存入identifier和recepit信息, NSData * data = [NSKeyedArchiver archivedDataWithRootObject:model];和 SAVEDEFAULTS(data, transaction.transactionIdentifier);整体实现如下
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.orderId.length != 0) {
// 这里需要展示一个加载框,展示出来之后要禁止掉用户的点击其他view区域的操作,包括左滑返回也给禁止掉
// [MBProgressHUD showLoadHUD:@"加载中..." toView:self.view];
}
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
// 验证凭据,获取到苹果返回的交易凭据
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
//发送POST请求,对购买凭据进行验证
NSLog(@"before = %@",receiptURL);
NSURL *url = [NSURL URLWithString:_appleUrl];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0f];
urlRequest.HTTPMethod = @"POST";
NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
_receipt = encodeStr;
//防止异常退出做的操作
if (self.orderId.length != 0) {
YZChargeDefaultsModel * model = [[YZChargeDefaultsModel alloc] init];
model.orderId = self.orderId;
model.receipt = encodeStr;
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:model];
SAVEDEFAULTS(data, transaction.transactionIdentifier);
// 这里存到userdefaults
}
NSString *payload = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", encodeStr];
NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
urlRequest.HTTPBody = payloadData;
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:urlRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// [MBProgressHUD hideHUD];
if (data == nil) {
NSLog(@"验证失败");
return;
}
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
NSLog(@"请求成功后的数据:%@",dic);
//这里可以通过判断 state == 0 验证凭据成功,然后进入自己服务器二次验证,,也可以直接进行服务器逻辑的判断。
//本地服务器验证成功之后别忘了 [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
NSString *productId = transaction.payment.productIdentifier;
NSString *applicationUsername = transaction.payment.applicationUsername;
NSLog(@"applicationUsername++++%@",applicationUsername);
NSLog(@"payment.productIdentifier++++%@",productId);
if (dic != nil) {
NSLog(@"order id == null");
NSArray * array = dic[@"receipt"][@"in_app"];
if (array.count <= 0) {
return;
}
self.orderId = applicationUsername;
//服务器二次验证
NSLog(@"%@",transaction);
for (int i = 0; i < array.count; i ++) {
NSDictionary * dic = array[i];
NSString * transaction_id = dic[@"transaction_id"];
if ([transaction.transactionIdentifier isEqualToString:transaction_id]) {
if (self.orderId.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:self.receipt transactionOrderId:self.orderId];
}else {
NSData * data = GETDEFAULTS(transaction.transactionIdentifier);
YZChargeDefaultsModel * model = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if (model.orderId.length != 0 && model.receipt.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:model.receipt transactionOrderId:model.orderId];
}else {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}
}
}
}];
[task resume];
});
}
上面代码中的遍历和二次处理,方式1直接发起验证请求,方式2从本地拿到一些信息数据,向服务端发起验证请求
for (int i = 0; i < array.count; i ++) {
NSDictionary * dic = array[i];
NSString * transaction_id = dic[@"transaction_id"];
if ([transaction.transactionIdentifier isEqualToString:transaction_id]) {
if (self.orderId.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:self.receipt transactionOrderId:self.orderId];
}else {
NSData * data = GETDEFAULTS(transaction.transactionIdentifier);
YZChargeDefaultsModel * model = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if (model.orderId.length != 0 && model.receipt.length != 0) {
[self vertifyApplePayRequestWith:transaction receipt:model.receipt transactionOrderId:model.orderId];
}else {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}
}
根据一些参数,向服务端请求验证当前订单是否支付
transaction.transactionIdentifier 唯一的
receipt 一般这个数据很长很长很长, md5一下,传输方便
orderId
服务端的一些处理
先判断这个transaction.transactionIdentifier对应的订单是否支付成功且发放了虚拟物品
如果是就直接返回一个状态比如状态1
客户端在状态1的处理就是直接finish掉这笔交易
如果发送虚拟物品的记录中没有这条记录,那么先通过transaction.transactionIdentifier向apple再次验证一下是否支付成功,如果支付成功,那么就发放虚拟物品给用户,并在当前接口中返回状态2
客户端在状态2的处理就是 finish掉这笔交易,并删除本地的transactionIdentifier对应的YZChargeDefaultsModel,并记录一下transactionIdentifierd对应的交易已经成功,为了应对状态1的情况
如果发送虚拟物品的记录中没有这条记录,那么先通过transaction.transactionIdentifier向apple再次验证一下是否支付成功,如果支付失败,那么返回状态3给客户端,然后什么都不用记录
客户端在状态3的处理就是直接finish掉这笔交易,因为服务端验证获取到的信息是用户支付的资金没有到账
/**
* 验证订单
*/
-(void)vertifyApplePayRequestWith:(SKPaymentTransaction *)transaction receipt:(NSString *)receipt transactionOrderId:(NSString *)orderId{
// NSMutableDictionary * dic = [NSMutableDictionary dictionary];
// [dic setObject:receipt forKey:@"receipt_data"];
// NSString * alltransId = [NSString stringWithFormat:@"%@%@",YZ_Transaction_ID,transaction.transactionIdentifier];
// NSString * needtransID = [alltransId md5String];
// [dic setObject:orderPhpId forKey:@"orderNum"];
// [dic setObject:needtransID forKey:@"transactionID"];
NSInteger status = 0;
if(status == 1){
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}else if (status == 2){
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}else if(status == 3) {
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
if (self.orderId.length != 0) {
// [ToastUtils showMessage:responseObjc[@"msg"] duration:1 position:Toast_Point_Center];
self.orderId = @"";
[[NSUserDefaults standardUserDefaults] removeObjectForKey:transaction.transactionIdentifier];
}
}
整体流程大概是这样实现的,Demo