美文网首页iOS点点滴滴业务方案iOS 开发每天分享优质文章
iOS IAP应用内购详细步骤和问题总结指南

iOS IAP应用内购详细步骤和问题总结指南

作者: iii余光 | 来源:发表于2018-08-23 14:27 被阅读2次

    最近公司在做APP内购会员功能 遇到了很多问题 总结记录一下 首先一定要区分Apple pay 和IAP内购的区别
    可以先去看一下官方文档地址 有每个步骤的详细解释
    本篇文章分为:
    1、 内购支付流程;
    2、开发集成步骤;
    3、问题(遇坑)记录解决方式

    之前没看官方文档走了很多弯路 网上博客并不系统 强烈建议先过一遍官方文档

    先看一下IAP内购支付流程(官方)

    官方流程图
    1. 程序向服务器发送请求,获得一份产品列表。
    2. 服务器返回包含产品标识符的列表。
    3. 程序向App Store发送请求,得到产品的信息。
    4. App Store返回产品信息。
    5. 程序把返回的产品信息显示给用户(App的store界面)
    6. 用户选择某个产品
    7. 程序向App Store发送支付请求
    8. App Store处理支付请求并返回交易完成信息。
    9. 程序从信息中获得数据,并发送至服务器。
    10. 服务器纪录数据,并进行审(我们的)查。
    11. 服务器将数据发给App Store来验证该交易的有效性。
    12. App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识。
    13. 服务器读取返回的数据,确定用户购买的内容。
    14. 服务器将购买的内容传递给程序。

    第一步:内购账户税务协议、银行卡绑定相关

    一般都是运营或者产品经理处理这步 这篇文章图文步骤比较详细 处理税务银行相关设置 IAP,In App Purchases-在APP内部支付

    第二步:Xcode设置相关

    打开In-App Purchase开关 对应在开发者证书中心的项目证书中显示应该也是可用状态

    屏幕快照 2018-08-22 下午6.00.11.png
    屏幕快照 2018-08-22 下午6.01.35.png

    第三步:在App Store Content -> 我的APP 添加内购项目商品

    1. 首页上,点按“我的 App”,然后选择与该 App 内购买项目相关联的 App。
    2. 在工具栏中,点按“功能”,然后在左列中点按“App 内购买项目”。
    3. 若要添加 App 内购买项目,请前往“App 内购买项目”,并点按“添加”按钮(+)。
    屏幕快照 2018-08-23 上午10.06.23.png
    选择功能 添加内购项目商品
    选择功能Tab

    内购商品对应四种类型 消耗型、非消耗型、自动续订订阅型、非续订订阅型
    官方文档

    1. 选择“消耗型项目”、“非消耗型项目”或“非续订订阅”,并点按“创建”。有关自动续订订阅的信息,请参见创建自动续期订阅
    2. 添加参考名称、产品 ID 和本地化显示名称。
    3. 点按“存储”或“提交以供审核”。
    您可以在创建您的 App 内购买项目时输入所有的元数据,或稍后输入您的 App 内购买项目信息。
    
    屏幕快照 2018-08-23 上午10.09.34.png

    添加一个测试商品 其他属性都可以随意填写 产品ID一定要认真填写 项目中需要根据ID获取商品信息 价格有不同的等级可以选 最低备用等级1 == 1元
    填写完成之后储存 就完成了一个内购商品的添加

    屏幕快照 2018-08-23 上午10.16.31.png

    第四步:沙盒环境测试账号

    因为涉及到钱相关 总不能直接用money去支付吧 所以需要你去添加一个沙盒技术测试人员的账号 (这个账号是虚拟的) 付款不会扣你
    看第三步那张图 在App Store Content 选择用户和职能 进入下面页面 选择沙箱技术测试员 添加测试账号

    屏幕快照 2018-08-23 上午11.02.26.png 屏幕快照 2018-08-23 上午11.05.28.png

    Tips:Q:为什么添加沙箱技术测试员 注册不成功 Unknown Email xxxxxx
    首先这里有个坑 邮箱只要符合格式就可以 虚假邮箱也可以 但密码必须符合正式的要求要有大小写和字符 复杂就好 例如:Lh123456*

    第五步:代码实现(初步,未进行优化 有什么问题可以在评论中跟我沟通)

    //@"77"代表我项目中设置的内购产品的ID 可以自行替换 
    #import "XSApplePayManager.h"
    
    #import <StoreKit/StoreKit.h>
    #import "APIManager.h"
    @interface XSApplePayManager ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>
    
    @end
    
    @implementation XSApplePayManager
    
    + (instancetype)shareManager
    {
        static dispatch_once_t onceToken;
        static XSApplePayManager *manager = nil;
        dispatch_once(&onceToken, ^{
            manager = [[XSApplePayManager alloc]init];
        });
        return manager;
    }
    
    - (void)requestPay{
        
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        
        if([SKPaymentQueue canMakePayments]){
            [self requestProductData:@"77"];
        }else{
            [AAProgressManager showFinishWithStatus:L(@"请打开应用内支付功能")];
        }
    }
    //去苹果服务器请求商品
    - (void)requestProductData:(NSString *)type{
        
        [AAProgressManager showWithStatus:@"正在支付..."];
        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{
        
        NSArray *product = response.products;
        if([product count] == 0){
            [AAProgressManager dismiss];
            [AAProgressManager showFinishWithStatus:L(@"无法获取商品信息,请重新尝试购买")];
            return;
        }
        
        NSLog(@"productID:%@", response.invalidProductIdentifiers);
        NSLog(@"产品付费数量:%ld",product.count);
        
        SKProduct *p = product.firstObject;
        NSLog(@"商品信息 ————  %@  %@  %@ %@ %@", p.description,p.localizedTitle,p.localizedDescription,p.price,p.productIdentifier);
        
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:p];
        payment.quantity = (NSInteger)p.price;//购买次数=价钱
        if (payment.quantity == 0) {
            payment.quantity = 1;
        }
        payment.applicationUsername = @"订单号/用户ID 用来映射";
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    
    }
    
    //请求失败
    - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
        NSLog(@"------------------错误-----------------:%@", error);
    }
    - (void)requestDidFinish:(SKRequest *)request{
        [AAProgressManager dismiss];
        NSLog(@"------------反馈信息结束-----------------%@",request);
    }
    //沙盒测试环境验证
    #define kIAPSandbox @"https://sandbox.itunes.apple.com/verifyReceipt"
    #define kIAPAppStore @"https://buy.itunes.apple.com/verifyReceipt"
    /**验证购买,避免越狱软件模拟苹果请求达到非法购买问题 
    建议这一步在服务器去做*/
    -(void)verifyPurchaseWithPaymentTransaction{
        
        //从沙盒中获取交易凭证并且拼接成请求体数据
        NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
        NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
        NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
        
        NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\",\"password\":\"你的共享密钥(在续订订阅状态下会用到)\"}", receiptString];
        NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
        
        //创建请求到苹果官方进行购买验证
        NSURL *url=[NSURL URLWithString:kIAPSandbox];
        NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
        requestM.HTTPBody = bodyData;
        requestM.HTTPMethod= @"POST";
        //创建连接并发送同步请求
        NSError *error = nil;
        NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM
                                                   returningResponse:nil
                                                               error:&error];
        if (error) {
            NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
            return;
        }
        NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData
                                                          options:NSJSONReadingAllowFragments
                                                            error:nil];
        NSLog(@"%@",dic);
        if([dic[@"status"] intValue]==0){
            NSLog(@"购买成功!");
            NSDictionary *dicReceipt= dic[@"receipt"];
            NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
            NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识
            //如果是消耗品则记录购买数量,非消耗品则记录是否购买过
            NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
            if ([productIdentifier isEqualToString:@"77"]) {
                NSInteger purchasedCount=[defaults integerForKey:productIdentifier];//已购买数量
                [[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier];
            }else{
                [defaults setBool:YES forKey:productIdentifier];
            }
            //在此处对购买记录进行存储,可以存储到开发商的服务器端
        }else{
            NSLog(@"购买失败,未通过验证!");
        }
    }
    
    //监听购买结果
    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
        dispatch_async(dispatch_get_main_queue(), ^{
            for(SKPaymentTransaction *tran in transaction){
                switch (tran.transactionState) {
                    case SKPaymentTransactionStatePurchased:{
                        NSLog(@"交易完成");
                        // 发送到苹果服务器验证凭证
                        [self verifyPurchaseWithPaymentTransaction];
                        [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                    }
                        break;
                    case SKPaymentTransactionStatePurchasing:
                        NSLog(@"商品添加进列表");
                        
                        break;
                    case SKPaymentTransactionStateRestored:{
                        [AAProgressManager showFinishWithStatus:L(@"已经购买过商品")];
                        [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                    }
                        break;
                    case SKPaymentTransactionStateFailed:{
                        
                        [AAProgressManager showFinishWithStatus:L(@"交易失败")];
                        [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                        break;
                    case SKPaymentTransactionStateDeferred:
                        NSLog(@"最终状态未确定");
                        break;
                    }
                        break;
                    default:
                        break;
                }
            }
        });
        
    }
    
    //交易结束
    - (void)completeTransaction:(SKPaymentTransaction *)transaction{
        NSLog(@"交易结束");
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }
    
    
    - (void)dealloc{
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
    
    第六步:IAP支付流程 & 服务器验证流程

    整个支付流程如下:
    1.客户端向Appstore请求购买产品(假设产品信息已经取得),Appstore验证产品成功后,从用户的Apple账户余额中扣费。
    2.Appstore向客户端返回一段receipt-data,里面记录了本次交易的证书和签名信息。
    3.客户端向我们可以信任的服务器提供receipt-data
    4.服务器对receipt-data进行一次base64编码
    5.服务器把编码后的receipt-data发往itunes.appstore进行验证
    6.itunes.appstore返回验证结果给服务器
    7.服务器对商品购买状态以及商品类型,向客户端发放相应的道具与推送数据更新通知

    漏单处理 确保receipt-data的成功提交与异常处理

    建立在IAP Server Model的基础上,并且我们知道手机网络是不稳定的,在付款成功后不能确保把receipt-data一定提交到服务器。如果出现了这样的情况,那就意味着玩家被appstore扣费了,却没收到服务器发放的道具。
    漏单处理:
    解决这个问题的方法是在客户端提交receipt-data给我们的服务器,让我们的服务器向苹果服务器发送验证请求,验证这个receipt-data账单的有效性. 在没有收到回复之前,客户端必须要把receipt-data保存好,并且定期或在合理的UI界面触发向服务端发起请求,直至收到服务端的回复后删除客户端的receipt账单记录。
    如果是客户端没成功提交receipt-data,那怎么办?就是玩家被扣费了,也收到appstore的消费收据了,却依然没收到游戏道具,于是投诉到游戏客服处。

    这种情况在以往的经验中也会出现,常见的玩家和游戏运营商发生的纠纷。游戏客服向玩家索要游戏账号和appstore的收据单号,通过查询itunes-connect看是否确有这笔订单。如果订单存在,则要联系研发方去查询游戏服务器,看订单号与玩家名是否对应,并且是否已经被使用了,做这一点检查的目的是 为了防止恶意玩家利用已经使用过了的订单号进行欺骗(已验证的账单是可以再次请求验证的,曾经为了测试,将账单手动发给服务器处理并成功),谎称自己没收到商品。这就是上面一节IAP Server Model中红字所提到的安全逻辑的目的。当然了,如果查不到这个订单号,就意味着这个订单确实还没使用过,手动给玩家补发商品即可。

    更多可以查看这篇博文苹果IAP安全支付与防范 receipt收据验证

    遇到的坑

    Q:21004 你提供的共享密钥和账户的共享密钥不一致 什么是共享密钥? 共享密钥从哪里获取?

    A:先看一下官方文档怎么说生成收据验证代码
    为了在验证自动续期订阅时提高您的 App 与 Apple 服务器交易的安全性,您可以在收据中包含一个 32 位随机生成的字母数字字符串,作为共享密钥。
    在 App Store Connect 中生成共享密钥。您可以生成一个主共享密钥,作为您所有 App 的单一代码,或作为针对单个 App 的 App 专用共享密钥。您也可以针对您的部分 App 使用主共享密钥,其他 App 使用 App 专用共享密钥。
    点击下面展开就可以看到共享密钥生成的方式

    Q:沙箱技术测试人员添加不成功 总是提示邮箱错误

    A: 沙箱技术测试账号用于付款测试 任意未创建过Apple ID 的邮箱都可以 假的邮箱也可以 重要的是密码格式一定要包含大小写 跟正式账号注册规则一样 (例如:Lh123456*)

    Q:自己服务器向苹果服务器验证收据/凭证参数是什么?向status code 验证apple iap sever的状态码代表什么意思?

    A:21002、21003、21004、21005、21006、21007... 具体可以查看这篇文档用App Store验证收据

    Q:Apple 和IAP的区别

    A:IAP是链接App store的内购服务 一般是虚拟商品需要走的通道(比如会员功能)
    Apple Pay是苹果跟各大银行合作的卡包形式的类似于刷卡支付服务 一般用于现实场景
    这两个一定别搞混了

    Q:怎么通过itunes-connect查看具体订单,itunes-connect中无法直接看到订单信息,可以用以下方法来查询

    1.可以通过账单向苹果发送账单验证,有效可以手动补发
    2 .用自己的服务器的记录账单列表对比
    3.利用第三方的TalkingData等交易函数,会自动记录账单数据

    还有一些问题可以借鉴一下这篇博文iOS之你一定要看的内购破解-越狱篇 他遇到的实际问题比较多 按需借鉴

    后续继续补充....

    相关文章

      网友评论

      本文标题:iOS IAP应用内购详细步骤和问题总结指南

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