该系列的其他文章:

【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位;

密保问题随意填;生日随意填;

地区最好挑选中国,测验便利。

苹果内购(IAP)从入门到精通(3)- 商品充值流程(非订阅型)

(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】聊聊运用内购买