应用内支付IAP全部流程

作者: Stark_Dylan | 来源:发表于2015-09-24 12:08 被阅读18482次

    本文Demo地址 ,内附详细文档。

    下边的文档仅供开发参考,建议到最新文档 阅读。

    (__Deprecated!)

    今天, 我们的App审核又因为支付方式被拒绝了, 所以选择了增加应用内支付这种方式。

    大致的业务逻辑是这样的。

    1.向服务器请求商品订单号码以及ituns配置的商品ID
    2.发起IAP购买请求
    3.购买流程结束后, 向服务器发起验证凭证以及支付结果的请求
    4.验证流程结束完成购买流程。

    流程

    1. 首先打开itunesconnect看一下有没有配置用户账户等信息, 点击 『协议, 税务与银行卡业务』进去配置就可以。
    2. 创建App或者选择已经有的App, 点击进入详情之后, 点击App内购项目这一选项


      屏幕快照 2015-09-24 上午11.53.06.png

      进入, 点击右上角的CreateNew按钮进行创建。 如果已经有的话可以点击进详情编辑内容。
      消耗型项目
      对于消耗型 App 内购买项目,用户每次下载时都必须进行购买。一次性服务通常属于消耗型项目,例如钓鱼 App 中的鱼饵。
      非消耗型项目
      对于非消耗型 App 内购买项目,用户仅需要购买一次。不会过期或随使用而减少的服务通常为非消耗型项目,例如游戏 App 的新跑道。
      自动续订订阅
      通过自动续订订阅,用户可以购买指定时间期限内的更新和动态内容。除非用户取消选择,否则订阅(例如杂志订阅等)会自动续订。
      免费订阅
      免费订阅是开发人员在“报刊杂志”中推广其内容的绝佳方式。用户注册免费订阅后,此订阅内容在与该用户 Apple ID 相关联的所有设备上可用。免费订阅不会过期,并且仅能在位于“报刊杂志”类别中的 App 中提供。
      非续订订阅
      非续订订阅允许有时限性的营销服务。对于 App 内购买项目中的限时访问内容,就需使用非续订订阅。例如,导航 App 中语音导航功能的一周订阅,或者年度订阅已存档的视频或音频的在线目录。

    通常我们都选择消耗形项目, 如果你要按月付费之类的就要选择非续订订阅之类的喽。

    选择消耗形项目, 然后继续,输入商品的名称, 产品的ID(自定义), 在下边添加语言的地方添加一下商品的描述信息, 然后上传一张商品界面的截图(这里可以随便, 影响不是很大)保存就可以了。

    屏幕快照 2015-09-24 上午11.57.16.png

    这是我们创建的结果, 右边的准备好去提交的提示是用于你提交App审核的时候同时提交一下内购项目的审核, 在应用程序的附加信息里边。必须要提交- - 。 这里的产品ID就是后期用于请求商品的商品ID。这时候就可以开始测试了。

    代码

    1. 导入 StoreKit.Framework 这个框架
      并在VC中
    #import <StoreKit/StoreKit.h>
    
    1. 实现SKPaymentTransactionObserver, SKProductsRequestDelegate这两个代理

    3.在ViewDidLoad中添加购买监听

    
        // 添加购买监听
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    
    1. 开始模拟购买, 要先检测是否允许内购。
            // 检测是否允许内购
            
            if([SKPaymentQueue canMakePayments]){
                
                [self requestProductData:productID];
            }else{
                
                NSLog(@"不允许程序内付费");
            }  
    
    //请求商品
    - (void)requestProductData:(NSString *)type{
        
        NSLog(@"请求商品");
        
        [SVProgressHUD showWithStatus:@"正在请求商品信息" maskType:SVProgressHUDMaskTypeGradient];
        
        NSArray *product = [[NSArray alloc] initWithObjects:type, nil];
        
        NSSet *nsset = [NSSet setWithArray:product];
        
        // 请求动作
        
        SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
        request.delegate = self;
        [request start];
    }
    
    
    //收到产品返回信息
    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
        
        NSLog(@"收到了请求反馈");
        
        NSArray *product = response.products;
        if([product count] == 0){
            
            NSLog(@"没有这个商品");
            return;
        }
        
        NSLog(@"productID:%@", response.invalidProductIdentifiers);
        
        NSLog(@"产品付费数量:%ld",[product count]);
        
        
        SKProduct *p = nil;
        
        // 所有的商品, 遍历招到我们的商品
        
        for (SKProduct *pro in product) {
            
            NSLog(@"%@", [pro description]);
            NSLog(@"%@", [pro localizedTitle]);
            NSLog(@"%@", [pro localizedDescription]);
            NSLog(@"%@", [pro price]);
            NSLog(@"%@", [pro productIdentifier]);
            
            if([pro.productIdentifier isEqualToString:productID]) {
                p = pro;
            }
        }
        
        SKPayment * payment = [SKPayment paymentWithProduct:p];
        
        NSLog(@"发送购买请求");
        
        [SVProgressHUD showWithStatus:@"正在发送购买请求" maskType:SVProgressHUDMaskTypeGradient];
        
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
    
    //请求失败
    - (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
        NSLog(@"商品信息请求错误:%@", error);
        
        [SVProgressHUD showErrorWithStatus:[error localizedDescription]];
    }
    
    - (void)requestDidFinish:(SKRequest *)request {
        NSLog(@"请求结束");
        
        [SVProgressHUD dismiss];
    }
    
    //监听购买结果
    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction {
        
        for(SKPaymentTransaction *tran in transaction){
            
            switch (tran.transactionState) {
                case SKPaymentTransactionStatePurchased:
                    NSLog(@"交易完成");
                    
                    [SVProgressHUD showSuccessWithStatus:@"交易完成"];
                    // 结束掉请求
                    [self completeTransaction:tran];
                    break;
                case SKPaymentTransactionStatePurchasing:
                    NSLog(@"商品添加进列表");
                    
                    [SVProgressHUD showWithStatus:@"正在请求付费信息" maskType:SVProgressHUDMaskTypeGradient];
                    
                    break;
                case SKPaymentTransactionStateRestored:
                    NSLog(@"已经购买过商品");
                    
                    [SVProgressHUD showErrorWithStatus:@"已经购买过商品"];
    
                    break;
                case SKPaymentTransactionStateFailed:
                    NSLog(@"交易失败");
                    
                    [SVProgressHUD showErrorWithStatus:@"交易失败, 请重试"];
                    
                    break;
                default:
                    
                    [SVProgressHUD dismiss];
                    break;
            }
        }
    }
    

    一定要在监听到购买结果的时候结束掉这个交易, 不然StoreKit不能即时的做记录

    在交易结束的时候, 要向服务器做凭证的验证, 因为要链接苹果的服务器, 所以这里的网络请求可能稍慢一点。 所以建议做一下本地化。

    
    //交易结束
    - (void)completeTransaction:(SKPaymentTransaction *)transaction{
        NSLog(@"交易结束");
        
        [SVProgressHUD dismiss];
    
        NSString * productIdentifier = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];
        NSString * receipt = [[productIdentifier dataUsingEncoding:NSUTF8StringEncoding] base64EncodedString];
        
        if ([productIdentifier length] > 0) {
            // 向自己的服务器验证购买凭证
            
      //      https://sandbox.itunes.apple.com/verifyReceipt
     //       receipt-data
        }
        
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }
    

    最后在dealloc中移除监听

    - (void)dealloc{
        
        // 移除监听
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
    

    IAP问题参考资料-唐巧
    IAP开源类库

    自己了解一下流程很重要, 顺便附上服务器端的验证代码

    <?php  
        //服务器二次验证代码  
        function getReceiptData($receipt, $isSandbox = false)     
        {     
            if ($isSandbox) {     
                $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';     
            }     
            else {     
                $endpoint = 'https://buy.itunes.apple.com/verifyReceipt';     
            }     
          
            $postData = json_encode(     
                array('receipt-data' => $receipt)     
            );     
          
            $ch = curl_init($endpoint);     
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);     
            curl_setopt($ch, CURLOPT_POST, true);     
            curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);     
           curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);  //这两行一定要加,不加会报SSL 错误  
            curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);   
      
      
            $response = curl_exec($ch);     
            $errno    = curl_errno($ch);     
            $errmsg   = curl_error($ch);     
            curl_close($ch);     
        //判断时候出错,抛出异常  
            if ($errno != 0) {     
                throw new Exception($errmsg, $errno);     
            }     
                      
            $data = json_decode($response);     
        //判断返回的数据是否是对象  
            if (!is_object($data)) {     
                throw new Exception('Invalid response data');     
            }     
        //判断购买时候成功  
            if (!isset($data->status) || $data->status != 0) {     
                throw new Exception('Invalid receipt');     
            }     
          
        //返回产品的信息             
            return array(     
                'quantity'       =>  $data->receipt->quantity,     
                'product_id'     =>  $data->receipt->product_id,     
                'transaction_id' =>  $data->receipt->transaction_id,     
                'purchase_date'  =>  $data->receipt->purchase_date,     
                'app_item_id'    =>  $data->receipt->app_item_id,     
                'bid'            =>  $data->receipt->bid,     
                'bvrs'           =>  $data->receipt->bvrs     
            );     
        }     
          
        //获取 App 发送过来的数据,设置时候是沙盒状态  
            $receipt   = $_GET['data'];     
            $isSandbox = true;     
        //开始执行验证  
        try  
         {  
             $info = getReceiptData($receipt, $isSandbox);     
             // 通过product_id 来判断是下载哪个资源  
             switch($info['product_id']){  
                case 'com.application.xxxxx.xxxx':  
                    Header("Location:xxxx.zip");  
                break;             
            }  
         }  
        //捕获异常  
        catch(Exception $e)  
        {  
            echo 'Message: ' .$e->getMessage();  
        }  
    ?>
    

    在我们公司的测试服务器中,我们会连接苹果的测试服务器https://sandbox.itunes.apple.com/verifyReceipt验证。
    在我们部署在线上的正式服务器中,我们会连接苹果的正式服务器https://buy.itunes.apple.com/verifyReceipt验证。
    我们提交给苹果审核的是正式版,我们以为苹果审核时,我们应该连接苹果的线上验证服务器来验证购买凭证。结果我理解错了,苹果在审核App时,只会在sandbox环境购买,其产生的购买凭证,也只能连接苹果的测试验证服务器。但是审核的app又是连接的我们的线上服务器。所以我们这边的服务器无法验证通过IAP购买,造成我们app的又一次审核被拒。
    解决方法是判断苹果正式验证服务器的返回code,如果是21007,则再一次连接测试服务器进行验证即可。苹果的这一篇文档上有对返回的code的详细说明 (引自 唐巧, 上边有文章地址).

    这篇文章验证讲解的比较详细

    测试

    需要添加沙箱的测试帐号, 在itunsconnect中选择用户与职能,然后添加测试帐号, 这个帐号可以用于测试购买。 另外, 在测试的时候, 可能比较慢, 所以我的项目中加入了不可交互的HUD进行提示, 避免用户进行多次商品的添加与购买。

    源码

    Github Page IAPDemo

    工程的bundleID必须同你创建内购的App的BID相同呦

    CopyRight@Dylan 2015-9-24.

    相关文章

      网友评论

      • 蛊毒_:我想请问一下本地能获取到过期时间么?能的话应该怎么获取?
      • ad193b2e3cf5:if([product count] == 0){}每次上线的时候都会有几个小时
        出现product.count = 0
        ad193b2e3cf5:@WildDylan 最近真是坑死了。当包刚发布的时候,就会出现这种情况。需要等几个小时才能好。我是在是不知道为什么
        Stark_Dylan:@ad193b2e3cf5 抱歉 没有呢
        ad193b2e3cf5:朋友遇到过吗
      • bright_moon:是否被苹果要求发现付费内容, 就必须要增加内购呢。
        Stark_Dylan:@南方杭州 虚拟物品要内购
        广州深圳:你好,我的是虚拟的金币,可以用支付宝吗,
        Stark_Dylan:@浩月难求 光明正大的使用支付宝支付就好了。
      • 洁简:商品一会没有返回,测试还需要提交吗?
      • 65f3d5653d0a:楼主 问一下 本地收到购买IAP成功后 先给服务器验证 再去finish订单 这个过程中 如果 服务未通知到app 用户卸载了程序 导致订单没有finish 重新安装app或者换设备 会导致无法继续购买!怎么搞呢?
      • Stark_Dylan:审核的时候 要链接苹果的沙箱地址。链接正式服会返回21007,收到21007的时候,再重新向沙箱地址请求一下就好了。
      • 秒赞不是偶然:问你个问题 ”所以我们这边的服务器无法验证通过IAP购买,造成我们app的又一次审核被拒“ 这句话的是啥意思? 是不是审核的时候 审核的app 链接生产环境,然后服务器会受到 21007,的错误,然后服务器验证不通过,
        解决办法就是 如果是审核阶段,服务器收到这个错误进行二次验证,这样服务器端就通过了,审核的app 也不用链接测试的验证环境吧?
      • 5a7c2642d9ee:请问,像分答那种的偷听,提问,还有打赏,订阅这几种支付情况,是不是只有订阅需要走内购,其余三种走第三方就行呢
        Stark_Dylan:@双鱼七封信 这个是自己决定的哦、 全部第三方也可以。 但是一旦被苹果要挟下架,就要都换内购了。
      • 超_iOS:楼主是不管验证成功与否都结束订单了?那怎么解决掉单问题啊?
        Stark_Dylan:@李二超 在客户端收到IAP回调之后,将凭证传给服务器,由服务器向苹果服务器做验证,确认订单的状态。 因为在客户端可能被篡改。 网络请求尽量加密。 以服务端的状态为准。
        超_iOS:@WildDylan :joy: 能详细解释下么?谢谢
        Stark_Dylan:@李二超 根据服务器的状态为准
      • f0da5b21f55d:我用自己的账号 测试是失败的 是不是第一次只能用沙盒测试账号 但是 苹果审核的时候怎么知道我的沙盒测试账号呢? 急
        f0da5b21f55d:@WildDylan 明白了 非常感谢
        Stark_Dylan:@sunchao 自己在沙箱测试。 苹果审核回调会到沙箱地址。 上线后自动变为正式。
      • f0da5b21f55d:IAP :smile: 第一次上线的时候必须要用 沙盒测试账号吗?
      • 9c0e7717d8cc:楼主,请问你们上架被拒的原因。因为我们目前在做一个付费收听的app,这种app是不是只能使用苹果的应用内支付,而不能使用微信支付宝之类的方式支付?求解答哦 :pray:
        Stark_Dylan:@甲休钟纯 支付宝可以正常通过。
      • 西楼望月:楼主你好,已经加你微信了,想请教一些IAP相关的细节问题
      • 没了蜡笔de小新:头像啊哈哈哈 :clap:
        没了蜡笔de小新:@WildDylan 楼主加我微信 交流交流吧 beautyT_TFuture :fist: :fist: :fist:
        Stark_Dylan:@coder_猿不猿 😋😋😋
      • jinstar520:可以加qq592306623交流下吗?
      • 奔跑的小菜菜:加好友 私聊下 email940671781 我微信
      • 杨世玲:我们的游戏接了第三方封装的支付SDK,啥日志都没有输出,我非常机智地在github上搜索到了楼主的demo,很快定位了问题,原来是tax信息没有填导致IAP无法使用。感谢楼主, 你是个好人。
        bright_moon:那为何也会被苹果拒绝呢。
        Stark_Dylan:@杨世玲 谢谢
      • 奔跑的小菜菜:还挺详细的,正好我也要做支付了 能加个微信或qq吗 :smile:
        b778b29f7fcf:楼主你微信多少
        奔跑的小菜菜:您号是多少
        Stark_Dylan:@奔跑的小菜菜 可以的

      本文标题:应用内支付IAP全部流程

      本文链接:https://www.haomeiwen.com/subject/zxoicttx.html