iOS 内购(IAP) 简单总结

1 IAP详细规则

IAP(In-App Purchase),是指苹果App Store的应用内购买,是苹果为APP内购买虚拟商品或服务提供的一套交易系统。

1.1适用范围

在APP内需要付费使用的产品功能或虚拟商品、服务。例如在斗鱼上充鱼翅、在视频APP上冲会员、在王者农药里买皮肤等....这些购买的商品或服务只能在APP内消费使用的适用IAP。反之,在京东或淘宝买东西、在滴滴上打车等...这些购买的商品或服务只能在APP外使用的情况是不适用IAP的。

1.2 IAP类型

IAP是一套商品交易系统,而非简单的支付系统。每一个购买项目都需要在App的itunes connect后台创建一个商品,提交给苹果审核,审核通过后,购买项目才会生效。
在创建IAP商品时,主要有四中类型:

  • 1.2.1 Consumable products (该类型适用于可多次购买的消耗型项目,如游戏道具、虚拟币等。)
  • 1.2.2 Non-consumable products (该类型适用于一次购买永久有效的项目,如电子书、游戏关卡等。
    该类型项目支持跨设备同步和本地restore,比如说,用户在某个App中购买了一本书,可在所有相同Apple ID设备的App中免费获取这本书,而不要需要借助App本身的帐号体系,即使在App中删除了这本书,也可免费重新获取。)
  • 1.2.3 Auto-renewable subscriptions (该类型适用于自动续费的订阅项目,如Apple Music的按月订阅,用户购买后会每月自动续费,直到用户手动取消或者开发者下架IAP项目。
    类似Non-consumable products,该类型也支持跨设备同步和本地restore机制。)
  • 1.2.4 Non-renewable subscriptions (该类型适用于固定有效期的非自动续费项目,如云音乐的会员和一些视频App的会员。没有跨设备同步和本地restore机制,用户可以多次购买。)


    IAPType.png
*********特别说明本文侧重讲解 消耗性项目 的流程*********
2 IAP设计开发要点
  • 2.1 开发之前需要 先向AppStore提交资料,填写协议、税务和银行业务 具体流程可参考 http://www.jianshu.com/p/cb1c8b4ba2c0
  • 2.2 然后需要先在itunes connect后台创建IAP商品,并按规范填写product id、商品名称、价格、截图等信息。
    如果App当前版本支持新增的IAP项目,可不用发版直接提交IAP审核。如果需要App新功能配合,则需要和App版本一起提交。
    《In-App Purchase Configuration Guide for iTunes Connect》详细介绍了IAP的创建和提交流程:https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/CreatingInAppPurchaseProducts.html#//apple_ref/doc/uid/TP40013727-CH3-SW1
    注意点:
    2.2.1尽量不要删除已创建的IAP
    已创建的IAP除了product id之外的所有信息都可以修改,如果删除了一个IAP,将无法再创建一个相同product id的IAP,也意味着该product id永久失效。而product id一般有特定的命名规则,用来标示App内的购买项目,如果命名规则下有某个product id永久失效,可能会导致整个product id命名规则都要修改,掉进坑里~
    2.2.2 注意区分reference name和display name
    eference name是给开发者自己看的,display name会在IAP支付流程的确认购买系统弹窗中展示给用户,而且不能随意修改(修改需要重新提交IAP审核),所以命名的时候要弄清楚。
  • 2.3 IAP支付流程
    IAP的支付模式分为客户端校验和服务端校验两种模式,客户端校验模式因为容易伪造支付凭证,安全性比较低,一般只有简单的单机APP才会使用,大部分APP都会采用服务端校验模式。
    不同的IAP支付流程也会有一些小差异,主要是因为restore机制,下面是最常用的Consumable products和Non-renewable subscriptions类型的支付流程(服务端校验模式)
    1.用户准备购买某个项目时,App客户端通过product id向苹果API请求支付信息
    2.手机系统弹窗验证用户的Apple ID(可能需要输入Apple ID密码或验证touch ID)
    3.Apple ID验证完成后,苹果API向App客户端返回用户将要支付的价格和货币单位
    4.手机系统弹窗提示用户确认将要购买的内容和价格,用户点击确认购买
    5.App客户端获得苹果API返回的支付成功通知以及支付凭据,向App服务端请求校验支付凭据
    6.App服务端拿到客户端的支付凭据,再向苹果服务器请求校验支付凭据(避免一些越狱插件伪造客户端支付凭据)
    7.App服务端校验支付凭据成功,通知App客户端
    8.App收到支付凭据校验成功通知,代表用户付费成功,再处理后续业务逻辑
3 具体的代码实现
  • 3.1 想要让用户购买我们的商品,首先我们得有个商品的展示界面。上面我们已经在itunes connect后台创建过我们的商品。创建过的商品都有一个唯一的产品标识 ProductID。通过ProductID我们可以拿到商品的具体信息。
    3.1.1 商品的请求
- (void)fetchProductInformationForIds:(NSArray *)productIds{
//产品ID可以以Plist的方式放在本地APP,也可以放在本地的服务器上。不过最好是放在本地服务器上,当后的产品有变化时,就不用升级我们的APP了
   SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIds]];
    request.delegate = self;
    [request start];
  SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIds]];
    request.delegate = self;
    [request start];
}
 //苹果的服务器通过此方法向我们返回商品信息
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{`
   if (response.products.count > 0) { //有效产品
   self.availableProducts = [NSMutableArray arrayWithArray:response.products];
    }
    if (response.invalidProductIdentifiers.count > 0) {//无效产品标示
        self.invalidProductIds = [NSMutableArray arrayWithArray:response.invalidProductIdentifiers];
    }
    self.status = IAPProductRequestResponse;
    [[NSNotificationCenter defaultCenter] postNotificationName:IAPProductRequestNotification object:self];
}
//产品请求失败会调用此方法
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    self.status = IAPRequestFailed;
   [[NSNotificationCenter defaultCenter] postNotificationName:IAPProductRequestNotification object:self];
    NSLog(@"Product Request Status: %@",error.localizedDescription);
}
//然后就是产品的展示了
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
   if (self.itemArr.count > 0) {
        SKProduct *product = self.itemArr[indexPath.row];
        cell.textLabel.text = product.localizedTitle;
     cell.detailTextLabel.text = [NSString stringWithFormat:@"%@元",product.price];
     }
}

3.2.2 商品的购买

- (void)buy:(SKProduct *)product{
      SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
    // 记录购买者ID 后面取到传给服务器
   NSString *userID = @"user";
   payment.applicationUsername =userID;
    payment.quantity = 1;//购买一次
    //将商品添加到购买队列
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}
//当用户点击完购买,此时付款队列中有了交易,会调用下面的方法。
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
  for (SKPaymentTransaction * transaction in transactions) {
        
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing: //交易正在被添加到付款队列
                
                NSLog(@"交易正在被添加到付款队列");
                break;
            case SKPaymentTransactionStateDeferred: //最终状态未确定
                
                [self completeTransaction:transaction forStatus:IAPPurchaseFailed];
                NSLog(@"最终状态未确定");
                break;
                
            case SKPaymentTransactionStatePurchased: //购买成功
                
                NSLog(@"购买成功");
                
                [self completeTransaction:transaction forStatus:IAPPurchaseSucceeded];
                break;
                
            case SKPaymentTransactionStateRestored: //已经购买过该商品
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];//消耗型不支持恢复
                NSLog(@"已经购买过该商品");
                break;
                
            case SKPaymentTransactionStateFailed: //交易失败
                NSLog(@"交易失败");
                [self completeTransaction:transaction forStatus:IAPPurchaseFailed];
                break;
                
            default:
                break;
        }
    }
}

//检查交易状态,做出相应操作
- (void)completeTransaction:(SKPaymentTransaction *)transaction forStatus:(NSInteger)status{
    
    self.status = status;
    NSString *detail = nil;
    if (transaction.error != nil) {
        
        switch (transaction.error.code) {
                
            case SKErrorUnknown:
                
                NSLog(@"SKErrorUnknown");
                detail = @"未知的错误,请稍后重试。";
                break;
                
            case SKErrorClientInvalid:
                
                NSLog(@"SKErrorClientInvalid");
                detail = @"当前苹果账户无法购买商品(如有疑问,可以询问苹果客服)";
                break;
                
            case SKErrorPaymentCancelled:
                
                NSLog(@"SKErrorPaymentCancelled");
                detail = @"订单已取消";
                break;
            case SKErrorPaymentInvalid:
                NSLog(@"SKErrorPaymentInvalid");
                detail = @"订单无效(如有疑问,可以询问苹果客服)";
                break;
                
            case SKErrorPaymentNotAllowed:
                NSLog(@"SKErrorPaymentNotAllowed");
                detail = @"当前苹果设备无法购买商品(如有疑问,可以询问苹果客服)";
                break;
                
            case SKErrorStoreProductNotAvailable:
                NSLog(@"SKErrorStoreProductNotAvailable");
                detail = @"当前商品不可用";
                break;
                
            default:
                
                NSLog(@"No Match Found for error");
                detail = @"未知错误";
                break;
        }
        
         NSLog(@"detail == %@",transaction.error.localizedDescription);
    }
    
    if (status == IAPPurchaseSucceeded) {
       /***************此处有坑,需特别注意*****************/
       //由于网络问题等种种原因,即使用户已经付款成功,客户端也可能一时半会收不到苹果API的支付成功通知,也无法主动向苹果API请求查询支付状态,只能被动等待通知。
      //因此有些情况下,客户端会延迟收到支付成功的通知(可能是过了几分钟,也有可能是下次打开App的时候),针对这种情况,需要做好两件事:
      //1. 客户端本地保存所有支付结果未确认的交易信息,并设置一个监听进程,在收到支付成功的信息后,继续处理这笔交易的后续流程。极端情况下,用户在交易结果未确认的情况下删除App,保存在App本地数据库中的交易信息也会丢失,因此,更好的方案是把交易信息存到iOS系统的keychain里面
      //2.当本地存在支付结果未确认的交易信息时,在交互上提示用户可能需要等待支付结果,避免用户重复付款
        //获得交易 凭证
        NSURL *receipturl = [[NSBundle mainBundle] appStoreReceiptURL];
        NSData *receiptData = [NSData dataWithContentsOfURL:receipturl];
        NSLog(@"receiptData == %@",receiptData);
        //获取购买者标识
        NSLog(@"payment.applicationUsername == %@",transaction.payment.applicationUsername);
       //本次交易的唯一标识符
        MyLog(@"transaction.transactionIdentifier == %@",transaction.transactionIdentifier);
       //将 1、交易凭证 2、购买者标识  3、购买的产品类型(ProductID)4、本次交易的唯一标识符 保存到本地(keyChain)
       // [LGJKeyChainTools setObject:receiptStr forService:@"user" account:transaction.transactionIdentifier ];
       
       //向本地服务器发送请求 传送 1、交易凭证 2、购买者标识 3、购买的产品类型(ProductID)4、本次交易的唯一标识符,本地服务器向苹果服务器发送验证验证交易凭证请求。如果凭证有效,则发放产品,删除保存到本地的相应信息,如果无效则提示相应的错误提示,也要删除保存到本地的相应信息。
      
    }
  //无论什么状态都应该将本次交易做结束处理,否则下次购买会出现问题。
   [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    //发送通知
    [[NSNotificationCenter defaultCenter] postNotificationName:IAPPurchaseNotification object:self];

 //在客户端向服务端轮询结果时,为了避免用户在支付结果页面等待过久,交互层面上可以先结束支付流程(经过一定超时时间),同时提示用户需要等待支付结果,避免用户重复付款
            
}
Demo下载地址 https://github.com/234313037/MyINAppPurchasesDemo
最后 感谢 http://bbs.netease.im/read-tid-730
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容