iOS 应用内购(内付费)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_31928443/article/details/52863484

简介:

IOS 内购支付两种模式:

  • 内置模式

  • 服务器模式

内置模式的流程:

  1. app从app store 获取产品信息

  2. 用户选择需要购买的产品

  3. app发送支付请求到app store

  4. app store 处理支付请求,并返回transaction信息

  5. app将购买的内容展示给用户

服务器模式的流程:

  1. app从服务器获取产品标识列表

  2. app从app store 获取产品信息

  3. 用户选择需要购买的产品

  4. app 发送 支付请求到app store

  5. app store 处理支付请求,返回transaction信息

  6. app 将transaction receipt 发送到服务器

  7. 服务器收到收据后发送到app stroe验证收据的有效性

  8. app store 返回收据的验证结果

  9. 根据app store 返回的结果决定用户是否购买成功

上述两种模式的不同之处主要在于:交易的收据验证,内建模式没有专门去验证交易收据,而服务器模式会使用独立的服务器去验证交易收据。内建模式简单快捷,但容易被破解。服务器模式流程相对复杂,但相对安全。


开发之初,苹果方就很负责的告知:我们的服务器不稳定。真正开发之后,发现苹果方果然是很负责的,不仅是不稳定,而且足够慢。app store server验证一个收据需要3-6s时间。

  1. 用户能否忍受3-6s的等待时间

  2. 如果app store server 宕机,如何确保成功付费的用户能够得到正常服务。

对于第一个问题,我们有理由相信用户完全无法忍受,所以采用异步验证的方式,服务器收到客户端的请求后,就将请求放到MCQ中去处理。

对于第二个问题,由于苹果人员很负责人的告知:我们的服务器不稳定,所以不排除收据验证超时的情况。对于验证超时的收据,保存到数据库中并标记为验证超时,定时任务每隔一定的时间去app store验证,确保能够获取收据的验证结果。

在开发过程中,需要测试应用是否能够正常的进行支付,但是又不能进行实际的支付,因此需要使用苹果提供的sandbox Store测试。Store Kit不能在iOS模拟器中使用,测试Store必须在真机上进行。

在sandbox中验证receipt:  
https://sandbox.itunes.apple.com/verifyReceipt

在生产环境中验证receipt:  
https://buy.itunes.apple.com/verifyReceipt

在实际开发过程中,服务器端通过issandbox字段标识客户端传递的收据是沙盒环境中的收据还是生产环境中的收据。在提交苹果审核前,沙盒测试均无问题。提交苹果审核后,被告知购买失败,审核未通过。通过查询日志发现,客户端发送的交易收据为沙盒收据,但是issandbox字段却标识为生产环境。

结论:

苹果审核app时,仍然在沙盒环境下测试。但是客户端同事在app提交苹果审核时,将issandbox字段写死,设置为生产环境。这样就导致沙盒收据发送到https://buy.itunes.apple.com/verifyReceipt去验证。

那么如何自动的识别收据是否是sandbox receipt呢?

识别沙盒环境下收据的方法有两种:

1.根据收据字段 environment = sandbox。

2.根据收据验证接口返回的状态码,如果status=21007,则表示当前的收据为沙盒环境下收据, t进行验证。

苹果反馈的状态码;

  • 21000App Store无法读取你提供的JSON数据
  • 21002 收据数据不符合格式
  • 21003 收据无法被验证
  • 21004 你提供的共享密钥和账户的共享密钥不一致
  • 21005 收据服务器当前不可用
  • 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
  • 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
  • 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

先生产验证后测试验证,可以避免来回切换接口的麻烦。测试验证只要用你自己申请的测试appid的时候才会用到,用户不会拥有测试appid,所以不会走到测试验证这一步。即使生产验证出错,应该也不回返回21007状态吗。测试验证通过的用户名,和充值金额最好用数据库记录下来,方便公司资金核对。

好了,介绍的差不多了,实际的操作如下:

首先进入苹果的ItunesConnection(https://itunesconnect.apple.com)点击左上角的加号新建一个App应用,点击后该网站会弹出一个信息编辑框,大家只要将上面的信息填充完毕点击save即可在苹果的app平台上拥有一个属于自己的App。



在套装ID的上,需要提前为该App申请一个AppID以及BundleID,只要是申请成功了就会在选择列表中显示出来。

这里顺便多说一句这个ItunesConnect是用来干嘛的,它是苹果公司给个人或企业提供管理自己App的一个平台。在这个平台上开发者可以新建,删除和管理自己的App应用,开发者可以根据需求对App应用进行上架与下架,编辑App信息,生成测试app所需的信息,例如账号,邀请码等,还有就是我们今天要讲的内付费功能。当然啦,他的功能可不止我讲的这些,我大致说一下这个平台的作用,如果你经常跟它打交道的话就会慢慢熟悉了。


接下来,我就来为大家演示一下如何添加付费道具,首先打开iTunesConnect,显示如下页面:



选择红圈所圈起来的选项,然后将里面的相关信息补充完毕,如果缺少这一步,内购功能是不会成功的。

假如你已经完成了上述相关银行账户的设置,就点击你的App,选择上面标题栏中的"App 内购买项目"



随后点击左上角的 "create new"选项,如下图所示,进入到下一个界面:




这个界面是让你选择消费道具的种类,现在改版的网站是有简体中文翻译的,所以不像以前打开一看都不知道选哪一个,甚至都不知道每个代表的什么意思(比如我第一次遇到的时候,在领导面前真是囧)。它的种类分为如下几种:





一般对项目来说大多数都是选择“消耗型项目”这个种类,比如游戏中购买金币,宝石balabala~之类的,选中之后就会到这个界面中来:



在上图所示的编辑框中输入,商品名称,产品ID以及价格等级,在这边说明一下:

1.商品名称根据你的消费道具的实际意义来说明,比如“100颗宝石”,“100金币”等。

2.产品ID是比较重要的,由项目自定义,只要唯一即可,像我一般都是用App的bundleID加一个后缀来表示,这样既跟项目关联又具有唯一性。

3.价格等级的话“查看价格表”中有对应的说明,可以对照着表中每个国家的货币价格与等级来选择。


我们继续,在这个网页的接下来部分如图所示

最后一步就是点击“选取文件”提交一张苹果它指定像素(640*920)的商品图片,当他上传完毕后点击“save”按钮,我们这第二部分就大工告成了。提交的商品最后会在内购的页面上显示为如图:



这个图是我在已经发布的app上面截取的,添加了3个商品,已经是通过的的状态了(显示绿色),当您刚提交的时候,因为通过苹果的审查需要一段时间所以会显示黄色的等待状态,所以不必担心是不是商品编辑错了。如图


这部分,我主要给大家演示一下,如何申请测试账号,利用苹果的沙盒测试环境来模拟AppStore的购买流程。

在ItunesConnect中选择“用户和职能”选项~



随后在左上角的选项中选择沙盒测试者,点击左上角的加号图标增加一位测试者,如图:


编辑好相应的内容,点击保存,就创建了一个测试账号,是不是很简单啊!当然这个账号如果你忘记了密码可以重新生成一个,无关紧要。

顺带多句嘴,不要在正式的appstore上面用沙盒测试的账号来登录,千万要牢记在心,此账号只用于测试环境下~

接下来就是大家最关心的问题:直接上代码
首先在项目工程中加入“storekit.framework”,加入头文件#import <StoreKit/StoreKit.h>
添加SKPaymentTransactionObserver,SKProductsRequestDelegate监听机制的协议
.h文件中

#import <StoreKit/StoreKit.h>


@interface MallInterfaceViewController : BaseViewController<SKPaymentTransactionObserver,SKProductsRequestDelegate>


@property (nonatomic,strong)UITextField *productID;//产品id


//点击事件

- (void)imageClickOhePayImage:(UIButton *)sender;

主要都是.m文件中:

//沙盒测试环境验证

#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"

//正式环境验证

#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view from its nib.

    [[SKPaymentQueue defaultQueue]addTransactionObserver:self];//监听购买结果

}

- (void)viewDidUnload {

    [super viewDidUnload];

    self.productID = [[UITextField alloc]init];

    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

}

//点击事件

- (void)imageClickOhePayImage:(UIButton *)sender{

     self.productID.text = @"刚才的产品ID";

     [self initMallInterfaceMoney];

}

- (void)initMallInterfaceMoney{

    NSString *product = self.productID.text;

    if ([SKPaymentQueue canMakePayments]) {

        [self requestProductData:product];

    }else{

        NSLog(@"不允许程序内付费");

    }

}

//去苹果服务器请求商品

- (void)requestProductData:(NSString *)type{

    

    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){

//没有产品

        return;

    }


    NSLog(@"productID:%@", response.invalidProductIdentifiers);

    NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);

    

    SKProduct *p = nil;

    for (SKProduct *pro in product) {

        NSLog(@"描述信息 %@", [pro description]);

        NSLog(@"产品标题 %@", [pro localizedTitle]);

        NSLog(@"产品描述信息 %@", [pro localizedDescription]);

        NSLog(@"价格  %@", [pro price]);

        NSLog(@"id   %@", [pro productIdentifier]);

        

        if([pro.productIdentifier isEqualToString:self.productID.text]){

            p = pro;

        }

    }

    

    SKPayment *payment = [SKPayment paymentWithProduct:p];

    

    //发送购买请求

    [[SKPaymentQueue defaultQueue] addPayment:payment];

}

//请求失败

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{

    [MBProgressHUD showError:@"支付失败" toView:self.view];

    [LBProgressHUD hideAllHUDsForView:self.view animated:YES];

}


- (void)requestDidFinish:(SKRequest *)request{

    //反馈信息结束

}

//监听购买结果

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{

    for(SKPaymentTransaction *tran in transaction){

        switch (tran.transactionState) {

            case SKPaymentTransactionStatePurchased:{

                // 发送到苹果服务器验证凭证(支付完成)

                [self verifyPurchaseWithPaymentTransaction];

                [[SKPaymentQueue defaultQueue] finishTransaction:tran];

                

            }

                break;

            case SKPaymentTransactionStatePurchasing:

                NSLog(@"商品添加进列表");

                

                break;

            case SKPaymentTransactionStateRestored:{

                NSLog(@"已经购买过商品");

                [[SKPaymentQueue defaultQueue] finishTransaction:tran];

            }

                break;

            case SKPaymentTransactionStateFailed:{

                NSLog(@"交易失败 %@",transaction);

                [[SKPaymentQueue defaultQueue] finishTransaction:tran];

            }

                break;

            default:

                break;

        }

    }

}


-(void)verifyPurchaseWithPaymentTransaction{

    //从沙盒中获取交易凭证并且拼接成请求体数据

    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];

    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];

    NSError *error;

    

    //创建请求到苹果官方进行购买验证(判断是走那个方法,沙箱还是走APPStore)

    NSURL *url;

    if (receiptUrl) {

        //沙箱中的方法

        url = [NSURL URLWithString:SANDBOX];

    }else{

        //不是沙箱

//        //创建请求到苹果官方进行购买验证

        url = [NSURL URLWithString:AppStore];

    }

    NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串

    

    NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];

    

    //拼接请求数据

    NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];

    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];

    requestM.HTTPBody = bodyData;

    requestM.HTTPMethod = @"POST";

    //创建连接并发送同步请求

    NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];

    

    if (error) {

        NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);

        return;

    }

    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];

    if([dic[@"status"] intValue] == 0){

        NSDictionary *dicReceipt = dic[@"receipt"];

        NSDictionary *dicInApp = [dicReceipt[@"in_app"] firstObject];

        NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识

        //如果是消耗品则记录购买数量,非消耗品则记录是否购买过

        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

        if ([productIdentifier isEqualToString:self.productID.text]) {

            int purchasedCount = [defaults integerForKey:productIdentifier];//已购买数量

            [[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount + 1) forKey:productIdentifier];

        }else{

            [defaults setBool:YES forKey:productIdentifier];

        }

        //在此处对购买记录进行存储,可以存储到开发商的服务器端

    }

}


//交易结束(进入页面的时候检查状态)

- (void)completeTransaction:(SKPaymentTransaction *)transaction{

    //交易验证

    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];

}

//移除

- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions{


}

//重复支付失败

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error{

}


- (void)dealloc{

    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

    

}

/*

 1. 代码中的_currentProId所填写的是你的购买项目的的ID,这个和第二步创建的内购的productID要一致;本例中是 123

 2. 在监听购买结果后,一定要调用[[SKPaymentQueue defaultQueue] finishTransaction:tran];来允许你从支付队列中移除交易。

 3. 沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。

 4. 请务必使用真机来测试,一切以真机为准。

 5. 项目的Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。

 6. 真机测试的时候,一定要退出原来的账号,才能用沙盒测试账号

 7. 二次验证,请注意区分宏, 测试用沙盒验证,App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的时候需要判断返回Status Code决定是否去沙盒进行二次验证,为了线上用户的使用,验证的顺序肯定是先验证正式环境,此时若返回值为21007,就需要去沙盒二次验证,因为此购买的是在沙盒进行的。

 */

讲了这么多,附上几张测试截屏给大家展示一下:

请求商品时的打印日志:


交易成功后:

手机截屏:

要求输入AppStore帐密,使用测试生成的即可:

确定购买:

交易完成:

好了,iOS的代码基本就完了, 赶这篇博客的时间比较匆忙,如果有童鞋还有什么疑问或者我写的那个地方不对欢迎私信我或者评论,我会在第一时间回复。谢谢

猜你喜欢

转载自blog.csdn.net/qq_31928443/article/details/52863484