细说苹果内购IAP

花了快10个工作日,终于完成了内购(IAP)功能。必须写篇文章来记录一下这十天来的心得体会,更是为了避免后续的开发者重复入坑。

网上关于IAP的文章已经有很多了,我基本全都看了一遍,问题太大了。首先不说这些文章里面的内容大同小异,很多文章都是直接照抄其他文章的,就说很重要的一点,很少有文章提到移动端在内购完成后,如何通过自己的服务器向苹果后台验证此次交易的正确性。大多数的文章都在千篇一律的教你怎么去后台配置商品,怎么配置财务信息,怎么配置测试账号这些东西,然后放上大同小异的代码就算完成一篇博文了。此处只有呵呵二字......

首先我希望读到这篇文章的你至少已经学会了如何在iTunes Connect进行IAP的配置。因为本文不会再去说这些没有什么技术含量的配置流程问题。如果你不知道,请移步:
http://yimouleng.com/2015/12/17/ios-AppStore/
这是所有我看过的IAP文章中最好的一篇了,跟着这篇文章走,你会成功的配置好IAP。

本文要谈的三点内容:

  1. 获取不到商品信息的原因。
  2. 从获取到商品信息开始到苹果后台返回成功购买的信息中间发生了什么。
  3. 如何搭建自己的服务器向苹果后台验证交易的正确性。

** 第一点:获取不到商品信息 **

当你兴致冲冲的做完那些配置后,调试代码你很可能会发现请求到的商品列表为空。然后你会以为自己哪里没有配置正确,又把相关的配置博文翻来覆去的看几遍,最后很失望的发现然并卵。这里,我根据自己的经验结合网上的一些答案给大家做个总结:

  • 确定配置环节正确。
  • 确定是真机测试且手机没有越狱。
  • 确定内购商品添加到了需要内购功能的App中。
  • 确定当前运行的App的Bundle ID和后台配置的App的Bundle ID是一致的。
  • 可以尝试先删除旧App,再重新编译生成新的,避免新App未覆盖错误。

这里要提一点,沙盒的测试账号和你请求商品信息没有关系。请求商品信息的流程是,你在后台配置好了内购商品,并且将其添加到了需要集成内购功能的App中,然后你请求商品。请求到商品后的流程是这样的,苹果系统会自动弹出登录框让你登录账号。然后根据提示操作进行购买,这里的账号就是你配置的沙盒测试账号。

如果你做到了以上五点,还是获取不到商品信息。欢迎评论中留言提出问题,我会尽量帮助大家解决。

** 第二点:从开始获取商品信息到苹果后台返回成功购买的信息中间发生了什么 **

(这里只贴出核心代码讲解,带着大家理顺购买商品的整个思路,我会在文章末尾给出一些不错的封装好的工具类推荐给大家,因为思路清晰之后,你才能真正的理解IAP的购买机制,我们应该做到理解而不仅仅是会用)

在第一点中我已经说了付费购买的环节和请求商品信息的环节是相互独立的,分别通过不同的API去调用完成,并通过不同的代理和监听方法返回信息。但这里有一个顺序性,你只有请求到商品信息后,并且将商品信息发送到苹果后台(此时商品信息一定有效,因为你已经请求到了商品信息),才开始付费购买的环节。正常情况下,当需要购买的商品信息发送到苹果后台后,系统会自动弹出账号登录框,这个时候在测试环境下你需要输入沙盒测试账号,在上线版本中,需要输入用户绑定的苹果账号,接下来都是系统弹窗,按照提示购买就行了。

整个购买过程我们需要用到苹果的一个库
#import <StoreKit/StoreKit.h>

1.请求商品信息 这里需要用到SKProductsRequestDelegate,SKProductsRequestDelegate是商品请求回调,用来告诉你有没有这个商品

 #pragma mark - 请求商品信息

 //请求商品
 - (void)requestProductData:(NSString *)type{

  NSLog(@"-------------请求对应的产品信息----------------");
  [SVProgressHUD showWithStatus:@"请求产品信息中" maskType:SVProgressHUDMaskTypeBlack];

  NSArray *product = [[NSArray alloc] initWithObjects:type,nil];

  NSSet *nsset = [NSSet setWithArray:product];
  SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
  request.delegate = self;
  [request start];//开始请求

 }

//收到商品返回信息,并将其包装成SKPayment,发送购买请求。
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{

  NSArray *product = response.products;

  //没有商品
  if([product count] == 0){
      [SVProgressHUD dismiss];
      return;
  }

  //有商品 发送购买请求
  SKProduct *p = nil;
  for (SKProduct *pro in product) {
      p = pro;
  }
  SKPayment *payment = [SKPayment paymentWithProduct:p];
  [[SKPaymentQueue defaultQueue] addPayment:payment];

  }

SKProductsRequest是苹果封装好的一个对象。该对象有两个属性,products是一个数组,代表的是你获取到的所有商品信息,每个商品都是一个数组元素。invalidProductIdentifiers是无效的商品id的数组,此id对应的是你在苹果后台构建的商品id。如果你调试代码的过程中发现,product为nil,invalidProductIdentifiers有值,那请回到第一步,因为你未请求到商品。

// Array of SKProduct instances.
@property(nonatomic, readonly) NSArray<SKProduct *> *products NS_AVAILABLE_IOS(3_0);

// Array of invalid product identifiers.
@property(nonatomic, readonly) NSArray<NSString *> *invalidProductIdentifiers NS_AVAILABLE_IOS(3_0);

2.判断购买结果,这里需要用到SKPaymentTransactionObserver,SKPaymentTransactionObserver是交易观察者,用来告诉你交易进行到哪个步骤了。

//监听购买结果  购买顺序:商品添加进列表
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{

for(SKPaymentTransaction *tran in transaction){
    
    switch (tran.transactionState) {
            
        case SKPaymentTransactionStatePurchased://交易成功
            [self completeTransaction:tran];
            break;
            
        case SKPaymentTransactionStateFailed://交易失败
            [self failedTransaction:tran];
            break;
            
        case SKPaymentTransactionStatePurchasing://商品添加进列表
            break;
            
        case SKPaymentTransactionStateRestored://已购买过该商品
            break;
            
        case SKPaymentTransactionStateDeferred://交易延迟
            break;

        default:
            break;
    }

}
}

**第三点:交易成功后,向苹果后台验证 **

   //交易结束
   #pragma mark - ****************  Private Methods
    - (void)completeTransaction:(SKPaymentTransaction *)transaction {//交易成功

     NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];//这里的URL测试环境下为沙盒url,上线版本中应为苹果后台的URL

     NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];

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

     // 向自己的服务器验证购买凭证(此处应该考虑将凭证本地保存,对服务器有失败重发机制)
    /**
     服务器要做的事情:
     接收ios端发过来的购买凭证。
     判断凭证是否已经存在或验证过,然后存储该凭证。
     将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
     如果需要,修改用户相应的会员权限
     */

     [[SKPaymentQueue defaultQueue] finishTransaction:transaction];

    }

这里说的购买凭证就是receiptString。将这个作为参数传给自己的服务器,让自己的服务器去向苹果后台验证。这里说一下,有的公司做的比较好,直接让移动端传交易的订单号给服务器。这个其实都一样,看各位公司的后台怎么搭建了。我们公司的后台没做过这方面,我们是在共同阅读苹果官方文档的情况下,自己搭建出来的接口,苹果会给你返回一串信息。其中会把所有的历史购买记录返回给你,注意,这些购买记录是无序的,需要后台通过时间戳将其排序出来,这样后台才能拿到最新的请求购买信息去做验证(这是我们调试了几个小时得出来的结论,满满的干货!)

官方文档的说明对receiptString的获取。并且要求你将它发送给自己的服务器。

Snip20170310_21.png

我稍微提一下后台接口设计的要点。自己服务器像苹果后台去请求验证信息的时候,设计的字段为receipt-data。这是官方文档中写明的。receipt-data对应的字段值就为上面提到的receiptString

Snip20170310_19.png

苹果后台收到服务器发过来的验证请求后会返回以下字段,status代表此次交易的状态,receipt代表凭证信息,是一个数组,里面是每一次交易的历史记录。

Snip20170310_18.png

这是status返回的状态码,对应的值都有解释,看英文不好的同学可以谷歌翻译一下哈。

Snip20170310_20.png

关于receipt的每一个JSON对象里包含的字段及对应信息说明,因为太多,这里就不贴图一一分析了。需要说明一点的是,这些历史记录是无序的,需要后台通过时间戳排序。
https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1

这里需要注意的一点是,如果你发现请求到了商品信息,也发送了购买请求,但是监听购买结果的方法就是不执行。可以检查一下,是否在工具类初始化的时候,添加了监听。(这是我踩过的一个坑)

#pragma mark - 获取单例
+ (instancetype)sharedInstance{
static IAPPayManager* instance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
    instance = [[IAPPayManager alloc] init];
    [[SKPaymentQueue defaultQueue] addTransactionObserver:instance];//将工具栏对象添加为购买的监听对象
});
return instance;
}

最后附上几个作者在开发过程后参考的几个链接和一些不错的工具类:

http://www.cocoachina.com/special/iap.html(cocoachina上关于内购问题的整理)

https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW5(关于如何像苹果后台验证的官方文档)

https://github.com/mruegenberg/IAPManager(封装工具类)
https://github.com/saturngod/IAPHelper(封装工具类)

如果这篇文章真的帮助到了您,请顺手给个赞哈。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容