iOS in_App Purchases(内购)

首先我们知道什么是内购?
- 内购就是在我们的App中能够购买东西
- 当然购买东西包括手机,毛绒玩具等日常用品,也包括购买虚拟的例如双倍经验卡,大血瓶等.
- 那么什么时候使用内购呢?

我们还需要知道苹果公司的盈利模式:

通过苹果应用程序商店有三种主要赚钱的方式:

1. 直接收费(与国内大部分用户 的消费习惯相悖)

 1.1 广告
 1.2 O2O -> Online推广 & Offline交易,闭环

2. 内购:应用程序本身的增值产品,游戏装备,应用程序中增值功能同样可以内购

  内购:三(苹果)七(开发商)开

3.第三方支付:跟应用程序无关的

苹果不能从中获利
所以.当我们在应用中有销售行为的话,能不走内购就不走内购,但是增值业务的话,必须是走内购流程,否则在上架App Store的时候是会被拒绝的.
但是,我们如果是做电商类App,那么久不要走内购了,因为你的产品销量越大,你就亏的越多,因为App开发商还要给苹果30%的分红,哦?不,是苹果先拿到钱,然后再给你70%的分红.

so,内购主要是针对APP内置的增值服务的,就是虚拟商品,一把屠龙宝刀,不就是一张图片吗?所以苹果要分红.那么App做内购的步骤有哪些呢?

1.首先你要有一个开发者账号 需要99$.

2.你要有一部 iOS 真机,未越狱版本,越狱版不能测试,只能购买

3.你需要在 苹果开发者网站 配置相关证书 .cer 和 .mobileprovision证书 配置App相关内购信息

3.1 --  配置App(必须要有一些消耗品或者非消耗品), AppID 不能使用统配符. (越狱手机不能测试内购,但是能购买内购商品).
  3.1.1  --设置方式: 在 iTunes Connect 网站里,  ->"App Store" 添加内购APP,  然后再进入”功能"(设置 内购产品,:非消耗型项目,    一次性消费型项目)
3.2 --  测试账号:  不用真的花钱.
  3.2.1  -- 设置方式: “功能”—>” “在用户和职能”—> “沙箱测试员” —>添加测试账号" .这个邮箱瞎写,但是密码要记住
3.3 --  配置账户信息  借记卡/信用卡都行  
  3.3.1--设置方式: “功能”—>”协议\税务和银行业务”  (都是老板的信息,钱由苹果公司打给老板)
3.4 -- 在回到 App Store 里将刚刚那些 设置的Appde内购项目添加到 你的App里

很重要却很容易忽视的细节

>>> 在使用未越狱的真机测试内购的时候,因为使用的是测试账号,是真机.
>>>所以需要在真机的 “设置”—> “iTunes Store 和 App Store” —>注销真实的账号(否则就真扣钱了) —>换成 刚刚在develop网站里填写的 内购测试账号

>代码是敲不完的,我们学的是思路.

>代码源于生活,又高于生活.

那么就让我们结合实际商场购物来理一下我们写代码思路:
1> App 想苹果服务器请求可销售商品
2> 代理方法获得可销售商品
3> 展示商品(tableView等)
4>用户选择商品
5> 根据用户选择的商品,给用户小票(商品转payment类型),那小票去排队
6> 监听 交易队列变化
7> 用户去交钱,我们监听用户付款状态
8> 付款成功, 我们给用户相应的增值服务
9> 结束交易 (否则用户下一次进来还是在交易)
App内购图示
官方内购配图

下面我们来上代码:

 //内购需要导入框架
#import <StoreKit/StoreKit.h>  
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //需要在 APP中 设置 bundel ID 和 在 网站注册时候一样.
    // 有内购需求的App 需要 安装在App里按钮 苹果develop 网站里面配置好 ios_development.cer 和 xxx.mobileprovision(配置文件),只要双击一下,一闪就算安装好了
}

0>首先:我们需要读取JSON文件

  //0 需要获得可销售商品(就是我们的APP内购商品的 productsID)
  NSString * productPath = [[NSBundle mainBundle] pathForResource:@"products.json" ofType:nil];
  
  //把JSON 转为 data
  NSData *jsonData = [NSData dataWithContentsOfFile:productPath];
  
  //反序列化
  NSArray *productArr = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
  NSLog(@"%@",productArr);
  
  //我们需要取出 [{productId:"xxxx"}]  数组里的字典里面的 key 对应的Value值
 
  //KVC 取值
  NSArray *tempArr2 = [productArr valueForKey:@"productId"];
  NSLog(@"%@",tempArr2);
  

1>我们想苹果发送销售App内购商品的请求

    //1>App向苹果请求可销售商品
    SKProductsRequest * proRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:[NSSet setWithArray:tempArr2]];
 

2>通过代理获得 苹果返回的课销售商品

    //2 >获取可销售的 商品 (通过代理)
    //2.1
    proRequest.delegate = self;
    
    //2.2 需要开始请求
    [proRequest start];
    
/** 2.3
 *  当获取到苹果允许销售的商品 时候调用
 *
 *  @param request  销售内购商品的请求
 *  @param response 苹果对我的请求的返回的响应 (里面有产品数组,以及不允许销售商品的 ID)
 */
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    
    //遍历,获得每一个可销售商品的信息
    for (SKProduct *product in response.products) {
        
        NSLog(@"产品价格%@---产品ID %@---产品标题 %@----产品描述 %@",product.price,product.productIdentifier,product.localizedTitle,product.localizedDescription);
    }
    
    //给全局数组赋值,里面装可销售 商品
    self.productsArray = response.products;
    
    //赋值后,需要刷新数据源
    [self.tableView reloadData];
}
//2.4 还要在上面遵守代理
@interface ViewController ()<SKProductsRequestDelegate>

//2.5 并且需要赋值一个数组,所以我们上面定义一个数组
/**
 *  装所有 可销售内购商品的数组
 */
@property (nonatomic, strong) NSArray * productsArray;

3>展示数据(可销售的内购商品)

//3 通过tableview展示数据  可销售商品的数据
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.productsArray.count;
}
//3.1
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
    static NSString *ID = @"storeCell";
    UITableViewCell *cell  = [tableView dequeueReusableCellWithIdentifier:ID];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
    }
    
    SKProduct *product = self.productsArray[indexPath.row];
    
    cell.textLabel.text = product.localizedTitle;
    //    cell.detailTextLabel.text = product.price.stringValue;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"%@",[product.price description]];  
    return cell;
}

3 补充 :上面给cell赋值时候,不注意引起Crash.分享如下:

  /*引起BUG源代码:
     cell.detailTextLabel.text = product.price;--->引起Crash.报错如下 是因为price类型是 NSDecimalNumber.
     报错如下:
     reason: '-[NSDecimalNumber isEqualToString:]: unrecognized selector sent to instance 0x7fa11ad28f10'
     更改方式1:    cell.detailTextLabel.text = product.price.stringValue;
     更改方式2:    cell.detailTextLabel.text = [NSString stringWithFormat:@"%@",[product.price description]];
     (更改方式1/2 留下一个就行.)
     */
```

> 4> 用户选择购买那些增值服务

//4 用户选择商品(屠龙刀 ,大药瓶等), 代理方法
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

// 4.1 取出用户选择的产品
SKProduct *product = self.productsArray[indexPath.row];

}


> 5> 用户拿小票排队等待付款

````
    //5.1 拿商品转换类型 (根据商品是什么样的,商家给用户小票,让用户拿着小票去排队付款)
    SKPayment * payment = [SKPayment paymentWithProduct:product];
    
    //5 .  排队,等付款
    [[SKPaymentQueue defaultQueue] addPayment:payment];
````
>  6> 监听付款队列的变化

````
    //6 监听 付款队列的变化,增加一个监听者,遵守SKPaymentTransactionObserver 协议就行
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    
  //6.1在上面遵守协议
   @interface ViewController ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>
````

>7  监听付款队列的变化 轮到自己付款.

````
/**
 *  7. 监听付款队列发生了变化时候调用   直白:更新付款队列的情报
 *
 *  @param queue        付款队列 交易队列
 *  @param transactions 所有的交易(别人也在交易,存在许多交易在队列里,放进一个数组里)
 */


-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{}

````

>8 > 根据付款的状态,提供对应的增值服务
>9 > 如果交易结束,关闭交易

````
/*
 SKPaymentTransactionStatePurchasing,    // 交易正在被加入队列
 SKPaymentTransactionStatePurchased,     // 交易在队列中, 用户已经付款.客户端需要完成交易
 SKPaymentTransactionStateFailed,        // 交易失败
 SKPaymentTransactionStateRestored,      // 交易从交易历史中被恢复.  客户端需要完成交易.
 SKPaymentTransactionStateDeferred       // 交易暂时不确定. 加入交易队列了,在犹豫要不要付款. iOS8新增
 */
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    
    //遍历所有交易,得出没有交易的状态
    for (SKPaymentTransaction * payTran in transactions) {
        
                if(payTran.transactionState == SKPaymentTransactionStatePurchased){
        
        //8. 用户付款成功,我们提供相应增值服务.
        NSLog(@"用户已经付款,我方App给他提供增值服务");
        
        //9 结束交易.  操作队列结束本次 交易.
        [[SKPaymentQueue defaultQueue] finishTransaction:payTran];
                }
    
                if(payTran.transactionState == SKPaymentTransactionStateRestored){
        
        //8. 用户付款成功,我们提供相应增值服务.
                    NSLog(@"用户已经付款,我方App给他提供增值服务");
        
        //9 结束交易.  操作队列结束本次 交易.
                    [[SKPaymentQueue defaultQueue] finishTransaction:payTran];
                }
        
               //增加一个判断.如果失败,失败的原因是什么(不是用户主动取消的情况)
                 if (payTran.transactionState == SKPaymentTransactionStateFailed) {
                    if (payTran.error.code != SKErrorPaymentCancelled) {
                        NSLog(@"交易失败: %@", payTran.error.localizedDescription);
                    }
                }
    }   
}
````

> 10 用户如果换手机了,怎么办?之前购买过的可以设置一键恢复

````
// 10 :一键 恢复购买装备 (例如换手机了)
//在viewdidLoad里增加一个复位按钮
- (void)viewDidLoad {
    [super viewDidLoad];
   self.navigationItem.title = @"测试";
    UIBarButtonItem *restoreButton = [[UIBarButtonItem alloc]initWithTitle:@"恢复购买" style:UIBarButtonItemStylePlain target:self action:@selector(restoreButtonClick:)];
        self.navigationItem.leftBarButtonItem = restoreButton;
}

-(void)restoreButtonClick:(UIButton *)sender{
    // 付款队列 恢复 已经完成的 所有交易
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
    
    // 付款队列 恢复 已经完成的 所有交易 需要应用程序的 用户名. (用户可能有3个号)(用户填写用户名去后台验证密码)
    //    [[SKPaymentQueue defaultQueue] restoreCompletedTransactionsWithApplicationUsername:<#(nullable NSString *)#>];
}

````

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

推荐阅读更多精彩内容