iOS 内购

2019 - 03 - 05

Posted by IIronMan

一.协议、税务和银行业务 信息填写

1.1、协议、税务和银行业务 信息填写 的入口

1.2、选择申请合同类型

进入协议、税务和银行业务页面后,会有3种合同类型,如果你之前没有主动申请过去合同,那么一般你现在激活的合同只有iOS Free Application一种。

页面内容分为两块:

Request Contracts    申请合同
Contracts In Effect  已生效合同

合同类型分为3种:

iOS Free Application  免费应用合同
iOS Paid Application  付费应用合同
iAd App NetNetwork    广告合同

这里我们主要主要讲一下付费应用合同的申请流程。

1.3、申请iOS Paid Application合同(协议、税务和银行业务3个都要填写)

2.4、Contact Info(填写联系方式)

如果你没有添加过联系人,你需要通过Add New Contact按钮来添加一个新的联系人。然后指定联系人的职务,

职务如下:

Senior Management  高管
Financial          财务
Technical          技术支持
Legal              法务
Marketing          市场推广

如果你是独立开发者,可以全部填你自己一个人。

1.5、填写银行信息

选择你的银行账户,如果你没有,点击旁边的Add Bank Account添加一个账户。下面是添加一个账户的流程。

1.5.1、选择银行所在的国家

1.5.2、填写银行标识CNAPS Code

如果你不知道CNAPS Code是多少,可以百度搜CNAPS Code来查询,查询时会根据3个关键信息来查询,如下:

Bank Name    银行的英文名称(不能是拼音)
City         银行所在的城市英文名称(中国的城市用拼音)
Postal Code  邮编

然后在下面就会出来备选的银行,选择正确的银行后,点击next,进入下一步。

2.5.3、确认银行信息

1.5.4、填写银行账号信息
Bank Account Number           银行账号
Confirm Bank Account Number   再次输入银行账号
Account Holder Name           持卡人姓名,中文名用拼写,名在前,姓在后
Bank Account Currency         货币类型,一般国内的开发者选择CNY

1.5.5、确认所有信息

1.6.填写税务信息

1.6.1.税务信息这一块了解不是很多,不过因为是国内开发者,可以不用太费心,税务信息分3种:
U.S Tax Forms         美国税务
Australia Tax Forms   澳大利亚税务
Canada Tax Form       加拿大税务

1.6.2.一堆条约

我选择的是U.S Tax Forms,选择后会问你两个问题:

  • 第1个问题如下:询问你是否是美国居民,有没有美国伙伴关系或者美国公司,如果没有直接选择No。

接下来第二个问题如下:询问你有没有在美国的商业性活动,没有也直接选No

1.6.3.然后填写你的税务信息,包括以下几点:
Individual or Organization Name             个人或者组织名称
Country of incorporation                    所在国家
Type of Beneficial Owner                    受益方式,独立开发者选个人
Permanent Residence                         居住地址
Mailing address                             邮寄地址
Name of Person Making this Declaration      声明人
Title                                       头衔

1.6.4.打钩

1.6.5.澳大利亚的不要管了

1.6.6.加拿大的也不用管了

1.7.填写完成

1.8.待审核

你填写完所有资料后,合同状态就会变成Processing,大概24小时内就会有结果。

二.内购商品的添加

2.1.创建内购商品

2.2.选择内购类型

2.2.1.消耗型商品:类似游戏中的钻石,还有现在某些APP中的货币,比如斗鱼里的鱼丸、映客里的映票。会被消耗的,要选择消耗型商品

注意:大多数的消耗型商品都是需要登录的,因为需要在数据库存余额。在登录之前,你最好不要让用户看到商品,有可能会因为这个原因被拒

2.2.2.非消耗型商品:无法被消耗的商品,比如上文提到的视频课程,一次购买,就应该永久可以观看

注意:当你使用非消耗型商品时,你需要添加一个恢复购买的按钮,这个常见于各种游戏中,其实知道这个规定以后还是挺好理解的,非消耗型商品是不可被消耗的,一次购买终身使用的,非消耗型的商品是跟appleId绑定的,就是你平时下载APP让你输入账号密码的内个。你需要一个恢复购买的按钮,来让用户恢复他购买的内容

2.2.3.订阅类型商品:如果你的公司是外包公司,有订阅类型商品的APP一定要用客户的账号提交审核,因为当APP中有过订阅类型商品,注意是有过,创建过再删除也算,这个APP无法被转移账号

注意:使用或曾经使用过订阅型商品的APP无法转移

2.3.创建好的产品

2.4.在上线的时候记得添加内购的商品

三.添加沙盒测试账号

3.1.添加沙盒测试的入口

3.2.添加沙盒测试账号

3.3.具体的测试账号信息填写

四.内购代码的具体实现

我创建了一个购买金币的内购控制器ApplePayCIOViewController在此,我仅仅向大家贴出.m的详细代码

4.1.内购的流程详细讲解

  1. 用户先拿到购买产品的单子
  2. 拿着单子去苹果那里交钱,交完钱让苹果在单子上盖个章
  3. 拿着盖了章的单子传给自己的服务器来验证是否真的支付成功
  4. 根据服务器返回的信息做具体的处理

4.2.代码

  1. 先导入StoreKit.framework库
  2. 创建ApplePayCIOViewController,遵守协议<SKPaymentTransactionObserver,SKProductsRequestDelegate>
// ApplePayCIOViewController.m
 
#import "ApplePayCIOViewController.h"
#import <StoreKit/StoreKit.h>
// 产品的ID
#define ProductID1 @"CIOCourses1"
@interface ApplePayCIOViewController ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>
{
    NSString *selectProductID;
}
@end
@implementation ApplePayCIOViewController
-(void)viewWillAppear:(BOOL)animated{
   [super viewWillAppear:animated];
   // 添加观察者
   [[SKPaymentQueue defaultQueue] addTransactionObserver:self];   
}
-(void)viewWillDisappear:(BOOL)animated{
   [super viewWillDisappear:animated];
   // 移除观察者
  [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

- (void)viewDidLoad {
  [super viewDidLoad];
   self.title = @"内购";

   self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]initWithTitle:@"测试" style:UIBarButtonItemStylePlain target:self action:@selector(test)];

    // 恢复购买的按钮
    UIButton * revert = [[UIButton alloc]initWithFrame:CGRectMake(20, 100, 100, 80)];
    [revert setBackgroundColor:JKRandomColor];
    [revert addTarget:self action:@selector(replyToBuy) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview: revert];
    self.view.backgroundColor =[UIColor redColor];
}

#pragma mark 恢复购买(主要是针对非消耗产品)
-(void)replyToBuy{

   [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
#pragma mark 测试内购
-(void)test{

  if([SKPaymentQueue canMakePayments]){

    // productID就是你在创建购买项目时所填写的产品ID
    selectProductID = [NSString stringWithFormat:@"%@",ProductID1];
    [self requestProductID:selectProductID];

   }else{
              
   // NSLog(@"不允许程序内付费");
   UIAlertView *alertError = [[UIAlertView alloc] initWithTitle:@"温馨提示"
                                                                                                                    message:@"请先开启应用内付费购买功能。"
                                                                                                    delegate:nil
                                                                                                              cancelButtonTitle:@"确定"
                                                                                                        otherButtonTitles: nil];
   [alertError show];
   }
}
#pragma mark 1.请求所有的商品ID
-(void)requestProductID:(NSString *)productID{

   // 1.拿到所有可卖商品的ID数组
   NSArray *productIDArray = [[NSArray alloc]initWithObjects:productID, nil];
   NSSet *sets = [[NSSet alloc]initWithArray:productIDArray];

   // 2.向苹果发送请求,请求所有可买的商品
   // 2.1.创建请求对象
   SKProductsRequest *sKProductsRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:sets];
   // 2.2.设置代理(在代理方法里面获取所有的可卖的商品)
   sKProductsRequest.delegate = self;
   // 2.3.开始请求
   [sKProductsRequest start];

}
#pragma mark 2.苹果那边的内购监听
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{

   NSLog(@"可卖商品的数量=%ld",response.products.count);

   NSArray *product = response.products;
   if([product count] == 0){

     NSLog(@"没有商品");
     return;
   }

 for (SKProduct *sKProduct in product) {

      NSLog(@"pro info");
      NSLog(@"SKProduct 描述信息:%@", sKProduct.description);
      NSLog(@"localizedTitle 产品标题:%@", sKProduct.localizedTitle);
      NSLog(@"localizedDescription 产品描述信息:%@",sKProduct.localizedDescription);
      NSLog(@"price 价格:%@",sKProduct.price);
      NSLog(@"productIdentifier Product id:%@",sKProduct.productIdentifier);

     if([sKProduct.productIdentifier isEqualToString: selectProductID]){
  
        [self buyProduct:sKProduct];
  
        break;
  
     }else{
  
      //NSLog(@"不不不相同");
     }
  }

}

#pragma mark 内购的代码调用
-(void)buyProduct:(SKProduct *)product{

   // 1.创建票据
  SKPayment *skpayment = [SKPayment paymentWithProduct:product];

  // 2.将票据加入到交易队列
  [[SKPaymentQueue defaultQueue] addPayment:skpayment];

  // 3.添加观察者,监听用户是否付钱成功(不在此处添加观察者)
  //[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

}

#pragma mark 4.实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{

  /*
    SKPaymentTransactionStatePurchasing,    正在购买
    SKPaymentTransactionStatePurchased,     已经购买
    SKPaymentTransactionStateFailed,        购买失败
    SKPaymentTransactionStateRestored,      回复购买中
    SKPaymentTransactionStateDeferred       交易还在队列里面,但最终状态还没有决定
  */

  for (SKPaymentTransaction *transaction in transactions) {
     switch (transaction.transactionState) {
           case SKPaymentTransactionStatePurchasing:{
      
                NSLog(@"正在购买");
           }break;
           case SKPaymentTransactionStatePurchased:{
      
              NSLog(@"购买成功");
              // 购买后告诉交易队列,把这个成功的交易移除掉
              [queue finishTransaction:transaction];
              [self buyAppleStoreProductSucceedWithPaymentTransactionp:transaction];
           }break;
           case SKPaymentTransactionStateFailed:{
      
               NSLog(@"购买失败");
               // 购买失败也要把这个交易移除掉
               [queue finishTransaction:transaction];
           }break;
           case SKPaymentTransactionStateRestored:{
               NSLog(@"回复购买中,也叫做已经购买");
               // 回复购买中也要把这个交易移除掉
               [queue finishTransaction:transaction];
           }break;
  case SKPaymentTransactionStateDeferred:{
      
              NSLog(@"交易还在队列里面,但最终状态还没有决定");
           }break;
           default:
           break;
         }
    }
 }

// 苹果内购支付成功
- (void)buyAppleStoreProductSucceedWithPaymentTransactionp:(SKPaymentTransaction *)paymentTransactionp {

  NSString * productIdentifier = paymentTransactionp.payment.productIdentifier;
  // NSLog(@"productIdentifier Product id:%@", productIdentifier);
  NSString *transactionReceiptString= nil;

  //系统IOS7.0以上获取支付验证凭证的方式应该改变,切验证返回的数据结构也不一样了。
   NSString *version = [UIDevice currentDevice].systemVersion;
   if([version intValue] >= 7.0){
       // 验证凭据,获取到苹果返回的交易凭据
       // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
       NSURLRequest * appstoreRequest = [NSURLRequest requestWithURL:[[NSBundle mainBundle]appStoreReceiptURL]];
       NSError *error = nil;
       NSData * receiptData = [NSURLConnection sendSynchronousRequest:appstoreRequest returningResponse:nil error:&error];
       transactionReceiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
   }else{

      NSData * receiptData = paymentTransactionp.transactionReceipt;
        //  transactionReceiptString = [receiptData base64EncodedString];
      transactionReceiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    }
   // 去验证是否真正的支付成功了
   [self checkAppStorePayResultWithBase64String:transactionReceiptString];

}

- (void)checkAppStorePayResultWithBase64String:(NSString *)base64String {

   /* 生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明 */
  /*
   注意:
   自己测试的时候使用的是沙盒购买(测试环境)
   App Store审核的时候也使用的是沙盒购买(测试环境)
   上线以后就不是用的沙盒购买了(正式环境)
   所以此时应该先验证正式环境,在验证测试环境

  正式环境验证成功,说明是线上用户在使用
  正式环境验证不成功返回21007,说明是自己测试或者审核人员在测试
   */
   /*
     苹果AppStore线上的购买凭证地址是: https://buy.itunes.apple.com/verifyReceipt
     测试地址是:https://sandbox.itunes.apple.com/verifyReceipt
    */
   //    NSNumber *sandbox;
   NSString *sandbox;
   #if (defined(APPSTORE_ASK_TO_BUY_IN_SANDBOX) && defined(DEBUG))
  //sandbox = @(0);
  sandbox = @"0";
  #else
  //sandbox = @(1);
  sandbox = @"1";
  #endif

  NSMutableDictionary *prgam = [[NSMutableDictionary alloc] init];;
  [prgam setValue:sandbox forKey:@"sandbox"];
  [prgam setValue:base64String forKey:@"reciept"];

  /*
     请求后台接口,服务器处验证是否支付成功,依据返回结果做相应逻辑处理
     0 代表沙盒  1代表 正式的内购
     最后最验证后的
   */
    /*
      内购验证凭据返回结果状态码说明
      21000 App Store无法读取你提供的JSON数据  
      21002 收据数据不符合格式  
      21003 收据无法被验证  
      21004 你提供的共享密钥和账户的共享密钥不一致  
      21005 收据服务器当前不可用  
      21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中  
      21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证  
      21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
      */

   NSLog(@"字典==%@",prgam);

}

#pragma mark 客户端验证购买凭据
- (void)verifyTransactionResult
{
   // 验证凭据,获取到苹果返回的交易凭据
   // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
   NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
   // 从沙盒中获取到购买凭据
   NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
   // 传输的是BASE64编码的字符串
   /**
      BASE64 常用的编码方案,通常用于数据传输,以及加密算法的基础算法,传输过程中能够保证数据传输的稳定性
      BASE64是可以编码和解码的
    */
   NSDictionary *requestContents = @{
                            @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                            };
   NSError *error;
   // 转换为 JSON 格式
   NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                options:0
                                                  error:&error];
   // 不存在
   if (!requestData) { /* ... Handle error ... */ }

   // 发送网络POST请求,对购买凭据进行验证
   NSString *verifyUrlString;
   #if (defined(APPSTORE_ASK_TO_BUY_IN_SANDBOX) && defined(DEBUG))
   verifyUrlString = @"https://sandbox.itunes.apple.com/verifyReceipt";
   #else
    verifyUrlString = @"https://buy.itunes.apple.com/verifyReceipt";
   #endif
   // 国内访问苹果服务器比较慢,timeoutInterval 需要长一点
   NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[[NSURL alloc] initWithString:verifyUrlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];

   [storeRequest setHTTPMethod:@"POST"];
   [storeRequest setHTTPBody:requestData];

   // 在后台对列中提交验证请求,并获得官方的验证JSON结果
   NSOperationQueue *queue = [[NSOperationQueue alloc] init];
   [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
                 completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                     if (connectionError) {
                         NSLog(@"链接失败");
                     } else {
                         NSError *error;
                         NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                         if (!jsonResponse) {
                             NSLog(@"验证失败");
                         }
                         
                         // 比对 jsonResponse 中以下信息基本上可以保证数据安全
                         /*
                          bundle_id
                          application_version
                          product_id
                          transaction_id
                          */
                         
                         NSLog(@"验证成功");
                     }
                 }];

}
@end

五.内购的注意事项

  1. 一般发生于首次提交app或添加新商品,当你的app通过审核以后,你发现在生产环境下获取不到商品,这是因为app虽然过审核了,但是内购商品还没有正式添加到苹果的服务器里,耐心等待一段时间就可以啦~
  2. 代码中的_currentProId所填写的是你的购买项目的的ID,这个和第二步创建的内购的productID要一致;本例中是 123。
  3. 在监听购买结果后,一定要调用[[SKPaymentQueue defaultQueue] finishTransaction:tran];来允许你从支付队列中移除交易。
  4. 沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。
  5. 请务必使用真机来测试,一切以真机为准。
  6. 项目的Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。
  7. 真机测试的时候,一定要退出原来的账号,才能用沙盒测试账号
  8. 二次验证,请注意区分宏, 测试用沙盒验证,App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的时候需要判断返回Status Code决定是否去沙盒进行二次验证,为了线上用户的使用,验证的顺序肯定是先验证正式环境,此时若返回值为21007,就需要去沙盒二次验证,因为此购买的是在沙盒进行的。
  9. 您的应用是否处于等待开发者发布(Pending Developer Release)状态?等待发布状态的IAP是无法测试的。
  10. 您的内购项目是否是最近才新建的,或者进行了更改?内购项目需要一段时间才能反应到所有服务器上,这个过程一般是一两小时,也可能再长一些达到若干小时。
  11. 您在iTC中Contracts, Tax, and Banking Information项目中是否有还没有设置或者过期了的项目?不完整的财务信息无法进行内购测试。
  12. 您是在越狱设备上进行内购测试么?越狱设备不能用于正常内购,您需要重装或者寻找一台没有越狱的设备。
  13. 您的应用是否是被拒状态(Rejected)或自己拒绝(Developer Rejected)了?被拒绝状态的应用的话对应还未通过的内购项目也会一起被拒,因此您需要重新将IAP项目设为Cleared for Sale。
  14. 您使用的测试账号是否是美国区账号?虽然不是一定需要,但是鉴于其他地区的测试账号经常抽风,加上美国区账号一直很稳定,因此强烈建议使用美国区账号。正常情况下IAP不需要进行信用卡绑定和其他信息填写,如果你遇到了这种情况,可以试试删除这个测试账号再新建一个其他地区的。
  15. 您是否将设备上原来的app删除了,并重新进行了安装?记得在安装前做一下Clean和Clean Build Folder。
  16. 您的plist中的Bundle identifier的内容是否和您的AppID一致?

六.内购审核的注意事项

6.1.项目里面的内购一定要勾选

6.2.协议一定要通过(下载的状态)

6.3.证书的配置文件里面要支持内购

6.4.项目第一次使用内购,内购的产品一定要跟着版本一起提交,如果提交被拒,内购产品要删掉重新建

七、聊一聊内购上线与苹果的沟通

  1. 在2018年2月初,内购准备充分,测试环境测试以及TestFlight开发环境测试,购买流程都是OK的。信心满满的提交给了苹果,第二天,果断被拒,说让加游客购买😅,按照要求加了游客购买,设置了用户设备的唯一标识符IDFV+KeyChain来做游客购买
  2. 当游客购买做完,开开心心的去提交,第二天回家过年,没想到到家后技术总监告诉我又被苹果拒绝,说点击游客购买不了,邮件说ipv6的问题(第2,3,4,5次说是在ipv6 下点击游客内购购买始终没有反应!),因为苹果审核测试人员并非技术人员,他们只能从现象给出理由,所以经常会归为ipv6问题,但不一定是ipv6问题,有时候网络不好,或许他们测试的iPad出问题了。于是我在Mac上搭建ipv6测试App,包括翻墙什么的,录视频,都说明在ipv6下没问题,购买流程很顺畅。然后给苹果回复,请求重新审核。
  3. 请求重新审核的结果是还是点击购买没反应(也可以解释为苹果那审核测试商品一直是无效的),后面开始怀疑审核人员的网络不好或者是测试的地区没换。于是请求了技术的支持以及客服代表的电话协助,技术测试说内购没问题了,原因是是审核人员的设备的问题(大爷的审核人员,白痴啊)。于是再次请求审核人员审核。
  4. 请求审核还是被拒😅,原因是定位和相册请求的描述不正确,修改完之后,打包,提交,第二天早晨起来一看,通过了😆。到公司后测试线上的内购,所有的流程都是OK的。
  5. 当天下午又重新上了一版,第二天起来审核通过了,但是问题来了又提交的五个内购商品无法购买,原因是appstore还没反应过来(商品在2~24小时是生效时间)。
  6. 说个题外话,在app被拒后,如果自己的app没问题,就直接上诉苹果审查委员会,不要再提新包,等着申诉的回复。申诉成功后,苹果审核人员会自己把应用的包重新从二进制被拒->正在审核,等待再次被审核的结果就好。

八、针对与大家交流的一些问题列举如下

  1. 什么叫第一次内购审核?

    简单的理解就是:同类型的商品没有被批准的就是第一次审核,举个例子:我现在要上一个订阅的商品,我的内购列表里面有4个消费型的商品被批准了,但是订阅的商品还没有被批准的,那么上订阅的商品就是第一次,要和ipa一起审核,一起提交,在提交ipa时候在下面勾选添加内购商品,把订阅的商品选择上就好,一起审核

  2. 什么时候提交内购商品可以不与ipa一起审核?

    当同类型商品有被批准的,那么再次提交内购商品就不需要再和ipa一起提交了,单独提交就好,创建完内购商品,存储,直接提交;举例:内购列表里面有4个消费型商品已经被批准,再提交一个消费型商品,就可以单独提交了

  3. 当第一次提交内购审核,内购被拒怎么办?

    把内购商品删掉,创建新的内购商品,再打包,提交ipa,添加新的内购商品,再次审核就好。

Table of Contents