该系列的其他文章:
【1】苹果内购(IAP)从入门到通晓(1)-内购产品类型与装备
【2】苹果内购(IAP)从入门到通晓(2)-银行卡与税务信息装备
【4】苹果内购(IAP)从入门到通晓(4)- 订阅、续订、退订、康复订阅
【5】苹果内购(IAP)从入门到通晓(5)- 掉单处理、防hook以及一些坑
【6】苹果内购(IAP)从入门到通晓(6)- 实践事务结合&线上异常状况处理
以下为非订阅型的产品(包括最常用的耗费型,以及不怎么用到的非耗费型、非续期订阅产品)的充值流程。
1. 初始化IAP->获取产品->创建订单
(1)发动付出行列监听
承继协议,不必去设置delegate。然后去发动付出行列监听:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
(可选)检测是否能够进行付出
用户能够禁用在程序内部付出的功用。在发送付出恳求之前,程序应该检查该功用是否被敞开。
App可在显示商铺界面之前就检查该设置(没启用就不显示商铺界面了);
也能够在主张付出前,检查是否关闭付出功用(假如关闭就弹出相应提示)。
if([SKPaymentQueue canMakePayments]){
...//Display a store to the user
}
else{
...//Warn the user that purchases are disabled.
}
(2)增加产品到付出行列中
增加产品有两种办法。
第一种办法:是去苹果后台恳求获取产品,成功后将获取到的SKPayment目标增加到充值行列中。这样有个优点时,假如回调成功,阐明你这个产品是有用的,这样设置到行列里必定也是能够正常充值的。
- (void) requestProductData{
NSArray *arr = [[NSArray alloc]initWithObjects:@"com.test.pay6", nil]; //com.test.pay6是产品ID,苹果后台现已装备了的
NSSet *productSet = [NSSet setWithArray:arr];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productSet];
request.delegate = self; //需求承继<SKProductsRequestDelegate>协议
[request start];
}
//SKProductsRequestDelegate Methods
//恳求成功
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
for (int i = 0; i < response.products.count; ++i)
{
SKProduct* product = [response.products objectAtIndex:i];
NSLog(@"苹果后台取得产品ID:%@ 产品描绘:%@",product.productIdentifier,product.localizedDescription);
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = 1;
payment.applicationUsername = orderId; //透传参数,一会儿会说
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
//恳求失利
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
NSLog(@"苹果后台产品恳求失利:%@",error.description);
}
但这儿有个 坑 :苹果的接口,在线上环境极端不稳定,并且很慢。有些时分你或许要等候3、4秒才会回调;或许在网络状况一般的状况下,或许会直接回调你失利,导致你无法充值(即便你这个产品ID是有用的)。所以,为了提高用户体验,不主张运用第一个办法。
第二种办法:直接将产品ID设置到SKPayment目标中。实践上,只需产品ID是有用的(苹果后台装备了的),那么直接设置后增加到付出行列中,是没问题的。这样,减少了一个向苹果恳求SKProduct目标的时刻。
SKMutablePayment *payment = [[SKMutablePayment alloc] init];
payment.applicationUsername = _orderId; //透传参数。能够传你自己的订单号,后续或许用得到
payment.productIdentifier = _productId; //产品ID
payment.quantity = _count; //产品数量,一般默许都传1
[[SKPaymentQueue defaultQueue] addPayment:payment];
但假如你这个产品ID是无效的,那么增加进付出行列后,代码层并不会报错,但苹果自己会去检测,检测到是无效的产品,就会直接回调充值失利。但作为开发者而言,在提审、上线之前,理应是确保咱们的产品ID是合法的,以及 检查银行卡后是否现已装备经过 ,所以基本不会存在什么问题。
2. 付款
测验环境下输入沙盒账号和暗码进行付款,TestFlight环境下运用下载app的Apple id进行付款。这两种测验环境,都仅仅模仿充值,即不会扣真正的钱。以下主要解说沙盒账号的装备与运用。
(1)装备沙盒账号: 沙盒账号的邮箱,除了有必要是邮箱的格局外(xxx@xxx.com),没有其他要求。这个邮箱不一定是实在存在的,能够是test@test.com这种。但前提是这个沙盒邮箱没有在其他地方装备过。所以能够随意命名了;
暗码有必要包括大小写字母与数字,至少8位;
密保问题随意填;生日随意填;
地区最好挑选中国,测验便利。
(2)设置运用沙盒账号
假如你曾经登录过沙盒账号,那么在充值时的界面上是不会显示账号的,只会让你输入暗码。这个时分,你需求检查这个沙盒账号是否是当时App绑定的沙盒账号而不是其他开发者账号下绑定的。
检查沙盒账号,去手机上的设置 -> App Store -> 沙盒账号(拉到最下面)。 iOS12等低版本下,你需求退掉你的个人Appleid。然后点击产品充值时,在app内输入沙盒账号(之后这个沙盒账号会出现在你“设置”里的appleid上)。
3. 获取收据
(1)监听付出状况
由于承继了协议,监听付出状况的代理办法是有必要完成的。
- (void)paymentQueue:(nonnull SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions {
for (int i = 0; i < [transactions count]; ++i)
{
SKPaymentTransaction *transaction = [transactions objectAtIndex:i];
if (transaction.transactionState != SKPaymentTransactionStatePurchasing)
{
NSLog(@"updatedTransactions with tid: %@", transaction.transactionIdentifier);
}
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchasing: //消费中
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inAppAlertAppeared) name:UIApplicationWillResignActiveNotification object:nil];
break;
case SKPaymentTransactionStatePurchased: //消费成功
[self verifyTransactionReceipt:transaction];
break;
case SKPaymentTransactionStateFailed: //消费失利
[self transactionFailed:transaction];
break;
case SKPaymentTransactionStateRestored: //康复已购买的产品(耗费型产品不能康复)
[self transactionRestored:transaction];
break;
default: //购买处于待定状况,比方iOS的家长监管功用(小孩子购买,需求家长同意);之后会依据逻辑变成purchased或许failed状况。所以这个位置能够不必去操作
break;
}
}
}
检查付出状况的源码,咱们发现,付出状况有以下几种:
typedef NS_ENUM(NSInteger, SKPaymentTransactionState) {
SKPaymentTransactionStatePurchasing, // Transaction is being added to the server queue.(付出中)
SKPaymentTransactionStatePurchased, // Transaction is in queue, user has been charged. Client should complete the transaction.(付出完成)
SKPaymentTransactionStateFailed, // Transaction was cancelled or failed before being added to the server queue.(付出失利)
SKPaymentTransactionStateRestored, // Transaction was restored from user's purchase history. Client should complete the transaction.(康复购买)
SKPaymentTransactionStateDeferred API_AVAILABLE(ios(8.0), macos(10.10)), // The transaction is in the queue, but its final status is pending external action.(待处理)
}
SKPaymentTransactionStateDeferred为付出待处理状况,跟付出中不太相同。iOS有一个所谓的家长控制(小孩子购买,需求家长同意),在需求家长承认时就会走到这个状况来。购买之后会依据付出的成果回调purchased或许failed状况。一般的App不必监听这个状况做什么操作。
SKPaymentTransactionStateRestored为康复购买的状况。耗费型产品、非续期订阅都不会走到这个状况里来。只有主动订阅和一次性产品,在发动restore监听的时分会走到这儿来。这个地方的逻辑咱们会在后面讲“主动订阅产品”的时分具体打开阐明。
SKPaymentTransactionStatePurchasing为购买中的状况。addPayment之后,就会走到这个状况中。由于弹出沙盒付出界面、正式付出界面,都算是当时运用跳出活跃状况(跟跳到桌面是相同的),所以这个时分需求增加监听办法:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inAppAlertAppeared) name:UIApplicationWillResignActiveNotification object:nil];
但由于这个状况下可操作性的逻辑比较少。开发者能够依据自己的事务需求,挑选性地完成这个inAppAlertAppeared办法。没有逻辑,就不必去管这个状况。
SKPaymentTransactionStatePurchased为购买成功状况。这个时分仅仅表明,你这个app现已成功付钱了。但付钱不代表产品到账。这个时分,咱们需求去处理苹果回来给咱们的一个叫“收据”的东西。这个在下一段会解说到。
SKPaymentTransactionStateFailed为购买失利状况。比方你手动取消购买、付款不成功、网络恳求失利,都算是“购买失利”。这个时分,你需求finish掉你这个SKPaymentTransaction。这个很重要。否则,你下次发动付出行列监听,这个SKPaymentTransaction又会跑出来。
- (void)transactionFailed:(SKPaymentTransaction *)transaction
{
if (transaction.error.code == SKErrorPaymentCancelled){
NSLog(@"您取消了内购操作.");
}else{
NSLog(@"内购失利,原因:%@",[transaction.error localizedDescription]);
}
NSLog(@"transactionFailed with tid: %@ and code:%li and msg:%@", transaction.transactionIdentifier,(long)transaction.error.code,transaction.error.localizedDescription);
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
4. 收据校验
当咱们付款成功,付出状况回调SKPaymentTransactionStatePurchased,之后,咱们需求校验收据。
收据,是苹果将付出的相关信息,整理成了一个json回来给咱们。里面包括比较常用的一些数据段是产品ID、付出时刻、苹果的订单ID(transactionId),以及主动订阅产品的优惠政策、过期时刻、续订时刻等。
逻辑是,咱们去拿到这个收据receipt(看着像base64格局的,但实践不是base64加密的),然后去恳求苹果的收据验证接口。成功后会回调你一个json格局的数据。咱们依据自己的服务端逻辑,去判断这个收据是否是有用、合法的。
(1)获取receipt:
NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString *receiptNewStr = [receiptData base64EncodedStringWithOptions:0];
留意:这个API是iOS7之后的新的API。鉴于iOS7以下的系统,当时的市面上现已不适配了,这儿我就不阐明原来的API是什么的了。但这儿需求提示一下:旧API和新API,获取到的收据结构是不相同的(旧的收据里是没有in_app相关字段的)。并且,旧的API只能获取到耗费型产品的收据,主动订阅的产品恳求收据校验接口的时分会报错。
(2)恳求苹果接口进行收据校验
苹果有两个收据校验的接口。一个是沙盒环境的(sandbox.itunes.apple.com/verifyRecei… ,一个是正式环境的(buy.itunes.apple.com/verifyRecei… 。由于客户端不便利在提审和过审之后,别离运用不同的校验接口去做校验。所以一般状况下,这个收据校验的逻辑,都是客户端将receipt传给服务器,服务端去做校验。
苹果也是主张这个校验逻辑由服务端完成。服务器需求先去恳求正式环境。假如receipt是正式环境的,那么这个时分苹果会回来(21007)奉告咱们这个是沙盒的receipt,那么服务器再去恳求sandbox环境。
以下,我在客户端去模仿这个收据校验。(实践开发中客户端不必去做哈)
- (void)localReceiptVerifyingWithUrl:(NSString *)requestUrl AndReceipt:(NSString *)receiptStr AndTransaction:(SKPaymentTransaction *)transaction
{
NSDictionary *requestContents = @{
@"receipt-data": receiptStr,
};
NSError *error;
// 转换为 JSON 格局
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
NSString *verifyUrlString = requestUrl;
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[[NSURL alloc] initWithString:verifyUrlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
// 在后台对列中提交验证恳求,并取得官方的验证JSON成果
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:storeRequest completionHandler:(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"链接失利");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
NSLog(@"验证失利");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
NSLog(@"验证成功");
//TODO:取这个json的数据去判断,道具是否下发
}
}];
[task resume];
}
假如验证成功,就依据回调的json数据去匹配判断购买的道具是否应该下发;假如验证失利,就finishTransaction。
关于耗费型产品,回来的收据的json是最规范的。非耗费型产品(一次性产品)、非续期订阅产品,收据的json和耗费型产品相同。如下所示:
{
environment = Sandbox; //阐明是沙盒环境
receipt = {
"adam_id" = 0;
"app_item_id" = 0;
"application_version" = 1;
"bundle_id" = "com.mytest.test"; //bundle id
"download_id" = 0;
"in_app" = (
{
"is_trial_period" = false; //是否有优惠(这个一般是主动订阅和一次性产品会运用到,耗费型产品是没用到的)
"original_purchase_date" = "2019-09-18 06:38:46 Etc/GMT"; //购买时刻
"original_purchase_date_ms" = 1568788726000; //购买时刻戳
"original_purchase_date_pst" = "2019-09-17 23:38:46 America/Los_Angeles";
"original_transaction_id" = 1000000569411111; //购买时的收据ID
"product_id" = "com.test.pay6"; //产品ID
"purchase_date" = "2019-09-18 06:38:46 Etc/GMT";
"purchase_date_ms" = 1568788726000;
"purchase_date_pst" = "2019-09-17 23:38:46 America/Los_Angeles";
quantity = 1; //产品数量
"transaction_id" = 1000000569411111;
}
);
"original_application_version" = "1.0";
"original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
"original_purchase_date_ms" = 1375340400000;
"original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
"receipt_creation_date" = "2019-09-18 06:38:46 Etc/GMT";
"receipt_creation_date_ms" = 1568788726000;
"receipt_creation_date_pst" = "2019-09-17 23:38:46 America/Los_Angeles";
"receipt_type" = ProductionSandbox;
"request_date" = "2019-09-18 06:39:00 Etc/GMT";
"request_date_ms" = 1568788740085;
"request_date_pst" = "2019-09-17 23:39:00 America/Los_Angeles";
"version_external_identifier" = 0;
};
status = 0; //付款状况(0为已付款)
}
这个收据的处理,由服务端负责。
实践上咱们发现,收据里有很多字段,value都是相同的。那究竟用哪个呢?
在in_app中,original_purchase_date代表你第一次购买产品付款的时刻,purchase_date表明你当时付款的时刻。关于耗费型产品,这个值是相同的。但关于主动订阅产品,当这个主动订阅产品续期后,收据里的会有一组数据的purchase_date表明当时续订的时刻,original_purchase_date表明第一次订阅的时刻。purchase_date会大于original_purchase_date。
相同,关于主动订阅而言,original_transaction_id表明第一次订阅的收据ID,transaction_id表明当时续订时的收据ID。这个时分两个收据ID就是不同的。这个也是会在后面“主动订阅”具体打开解说。
关于服务器而言,假如耗费型产品,需求判断如下几个值:
status需求等于0,表明付出成功;status也有其他状况码:
状况码 | 描绘 |
---|---|
0 | 收据校验成功 |
21000 | 未运用HTTP POST恳求办法向App Store发送恳求。 |
21001 | 此状况代码不再由App Store发送。 |
21002 | receipt-data特点中的数据格局过错或丢掉。 |
21003 | 收据无法认证。 |
21004 | 您提供的同享暗码与您帐户的文件同享暗码不匹配。 |
21005 | 收据服务器当时不可用。 |
21006 | 该收据有用,但订阅已过期。当此状况代码回来到您的服务器时,收据数据也会被解码并作为呼应的一部分回来。仅针对主动续订的iOS 6样式的买卖收据回来。 |
21007 | 该收据来自测验环境,但已发送到生产环境以进行验证。 |
21008 | 该收据来自生产环境,可是已发送到测验环境以进行验证。 |
21009 | 内部数据访问过错。稍后再试。 |
21010 | 找不到或删除了该用户帐户。 |
bundle_id为当时包体的bundle id(由于或许有人反编译重签,然后用沙盒账号充值);
in_app内,purchase_date要大于服务器订单的创建时刻;
in_app内,transaction_id要等于恳求前SKPaymentTransaction目标的transaction_id;
in_app内,product_id要等于充值时的产品ID;
in_app内,quantity要等于充值时的数量(一般都是1);
假如以上参数都能匹配,阐明当时收据时合法的。就算服务端的真正的校验经过(并不是说恳求苹果接口回来成功就算校验经过了)。
校验收据的官方文档
5. 产品下发
由于前端恳求服务器的收据校验接口时,服务器获取恳求后,还需求去恳求苹果的接口。所以这个时分,服务器只会回来你这个接口恳求是否成功,无法回来你这个收据是否合法。所以这个时分,客户端在收到恳求成功之后,就能够依据前端的逻辑进行界面展现,然后在恰当的时分finishTransaction。
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
(以下为可选功用)
假如客户端需求拿到这个收据的实在校验状况,能够延时处理,去向服务端获取校验成果。
咱们app设置然后在恰当的时分finishTransaction之后,等候10s进行第一次获取校验成果。假如正在处理中,则持续等候30s、120s…即轮询获取校验成果。直到校验回调成功或许失利。
int64_t delayInSeconds = 10.0; //推迟10s再去后台校验订单成果,防止研制那边的产品还未下发
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), (void){
//第一次校验
[承认服务器校验收据状况:(id response) {
if (收据合法) {
//TODO:(假如是客户端做的话)下发道具
}else{
int64_t delayInSeconds1 = 30.0; //推迟30s再去后台进行第2次校验订单成果
dispatch_time_t popTime1 = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds1 * NSEC_PER_SEC);
dispatch_after(popTime1, dispatch_get_main_queue(), (void){
//第2次校验
[承认服务器校验收据状况:(id response) {
if (收据合法) {
//TODO:(假如是客户端做的话)下发道具
}else{
int64_t delayInSeconds2 = 120.0; //推迟120s再去后台进行第2次校验订单成果
dispatch_time_t popTime2 = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds2 * NSEC_PER_SEC);
dispatch_after(popTime2, dispatch_get_main_queue(), (void){
//第三次校验
[承认服务器校验收据状况:(id response) {
if (收据合法) {
(假如是客户端做的话)下发道具
}else{
NSLog(@"订单校验失利");
}
}];
});
}
}];
});
}
}];
});
当服务器回传给客户端,阐明收据是合法的(充值的对应的产品且付款),那么就能够下发充值的道具或许权益了。一般而言,这些权益由于要跟用户绑定,所以服务端必定还有一堆其他逻辑要处理。奉告客户端后,客户端依据自身事务需求,更改客户端的UI、角色权益等等。假如你们是游戏App,那么能够由客户端告诉游戏端(比方unity或许cocos)进行对应功用修正;或许是server to server,sdk server告诉game server进行权益修正,game server告诉unity层进行功用修正,而oc(或许swift)层面不必做操作。
参考资料:
【1】促进您的运用内购买
【2】聊聊运用内购买