苹果内购

持续更新~~~

很早之前就做了有关苹果内购的项目,中间也遇到很多的坑,一直也没有写关于这块儿的文章。今天得空,总结一下苹果内购这块需要注意的地方,希望能够帮助到朋友们~废话不多说,开始!!!

苹果内购:顾名思义,苹果手机的一种支付方式。那么什么情况下使用苹果内购?什么情况下使用微信支付和支付宝支付?应用内购买虚拟商品都需要使用苹果内购,其内购流程就是用户付钱给苹果,苹果扣除一定费用后给开发商
IAP:支付渠道+商品管理平台

一、简述苹果内购购买的流程

1.用户在应用内发起购买流程
2.APP客户端调用苹果内购流程
3.用户登录AppleID并付费
4.苹果服务器告知APP客户端支付结果并给出支付凭证
5.APP客户端或者APP服务端拿着苹果给到的支付凭证向苹果服务器发起支付成功的校验(校验目的就是向苹果确认是否支付成功了)
6.APP客户端或者APP服务端收到苹果服务器的校验结果,处理业务逻辑。
7.若为服务端校验,则校验完毕后服务端告知APP客户端校验结果。
我们注意到,步骤5,校验有两种方式,一种是APP客户端校验支付凭证,另一种是服务端校验支付凭证,两种都可以,只是服务端校验相对更加安全

二、苹果内购的使用详解

1.登录苹果开发者网站,在其内部填写公司财务等相关信息
2.依然是苹果开发网站,在应用当中创建虚拟商品(包含名称、价格等)。还有就是要申请沙盒测试账号,用来测试。

内购虚拟商品分为四个类别:
a.消耗性项目:每次要用这类项目时,都需要重新购买,可以多次购买。(例如:游戏道具、游戏币等)
b.非消耗性项目:单次购买永久有效,不会随着使用或者时间而减少,不可以多次购买。(例如:书籍、游戏关卡等)特别注意:这类项目是可以免费重新下载已经购买过的内容,即同一Apple ID只需购买一次,便可以在不同设备上同步该项目,不管在应用内是否是同一个账号。使用该类项目,必须要在应用内有明确的“恢复购买”功能的入口。
c.自动续费订阅型项目:根据时间提供产品或服务(例如视频会员、音频会员等各种VIP)这类项目支持跨设备同步。
d.不自动续费订阅型项目:提供特定时间段的产品或服务,不会自动续费,可以多次购买。(例如,谋视频会员我只买一个月、三个月等)

内购虚拟商品的价格:
不可以随意定价,只能从苹果提供的众多报价中选择

审核:
在你创建好虚拟商品后会进入到苹果的审核流程,首次提审跟着APP审核一起走,后续单独提交审核。

3.苹果内购逻辑代码(有些大神针对苹果内购以及漏单处理进行了封装,大家也可以去使用,这里是自己集成苹果内购的方案)

1.引入头文件
#import<StoreKit/StoreKit.h>

2.设置代理
@interface AppDelegate () <SKProductsRequestDelegate,SKPaymentTransactionObserver>

3.添加内购检测
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

4.购买按钮点击事件
-(void)buyWithProductId:(NSString *)productId {
  self.productId = productId;
  if ([SKPaymentQueue canMakePayments]) {
      [self requestProductData:self.productId];
  }else{
      NSLog(@"不允许程序内付费");
      [MBProgressHUD showGameAQHUDAddto:self.window text:@"不允许程序内付费"];
  }
}

5.请求商品、获取商品信息、以及购买失败、内购请求完成等相关代理
-(void)requestProductData:(NSString *)productId{
  NSLog(@"--------请求对应的产品信息------------");
  [MBProgressHUD showGameAQHUDAddto:self.window text:@"请求对应的产品信息"];
  NSSet *nsset = [NSSet setWithObjects:productId, nil];
  _request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
  _request.delegate = self;
  [_request start];
}

-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
  NSLog(@"-------收到产品反馈消息----------");
  [MBProgressHUD showGameAQHUDAddto:self.window text:@"收到产品反馈消息"];
  NSArray *product = response.products;
  if ([product count] == 0) {
      NSLog(@"-----没有商品-------");
      [MBProgressHUD showGameAQHUDAddto:self.window text:@"没有商品"];
      return;
  }
  NSLog(@"productID:%@",response.invalidProductIdentifiers);
  NSLog(@"产品付费数量:%lu",(unsigned long)product.count);
  SKProduct *prod = nil;
  for (SKProduct *pro in product) {
      NSLog(@"%@",pro.description);
      NSLog(@"%@",pro.localizedTitle);
      NSLog(@"%@",pro.localizedDescription);
      NSLog(@"%@",pro.price);
      NSLog(@"%@",pro.productIdentifier);
      if ([pro.productIdentifier isEqualToString:self.productId]) {
          prod = pro;
      }
  }
  if (prod != nil) {
      SKPayment *payment = [SKPayment paymentWithProduct:prod];
      NSLog(@"-------发送购买请求-------");
      [[SKPaymentQueue defaultQueue] addPayment:payment];
  }
}

-(void)request:(SKRequest *)request didFailWithError:(NSError *)error{
  NSLog(@"购买失败");
}

- (void)requestDidFinish:(SKRequest *)request{
  NSLog(@"反馈信息结束");
}

6.沙盒测试环境校验、正是环境校验
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt" //沙盒测试环境验证
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt" // 正式环境验证

-(void)verifyPurchaseWithPaymentTransaction{
// 从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
// 创建请求到苹果官方进行购买验证
NSURL *url=[NSURL URLWithString:AppStore];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody=bodyData;
requestM.HTTPMethod=@"POST";
// 创建连接并发送同步请求
NSError *error=nil;
NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
    NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
    return;
}
NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
NSLog(@"%@",dic);
if([dic[@"status"] intValue]==0){
    NSLog(@"购买成功!");
    NSDictionary *dicReceipt= dic[@"receipt"];
    NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
    NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识
    //如果是消耗品则记录购买数量,非消耗品则记录是否购买过
    NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
   if ([productIdentifier isEqualToString:@"123"]) {
     int purchasedCount=[defaults integerForKey:productIdentifier];//已购买数量
    [[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier];
   } else {
     [defaults setBool:YES forKey:productIdentifier];
   }

   // 在此处对购买记录进行存储,可以存储到开发商的服务器端
    if ([productIdentifier isEqualToString:@"111111"]) {
        [self chongzhi:@"50"];
    }else if ([productIdentifier isEqualToString:@"22222"]) {
        [self chongzhi:@"108"];
    }else if ([productIdentifier isEqualToString:@"33333"]) {
        [self chongzhi:@"158"];
    }else if ([productIdentifier isEqualToString:@"44444"]) {
        [self chongzhi:@"208"];
    }
}else if([dic[@"status"] intValue]==21007){
    [self verifyPurchaseWithPaymentTransactionSANDBOX];
}else{
    NSLog(@"购买失败,未通过验证!");
}
}

-(void)verifyPurchaseWithPaymentTransactionSANDBOX{
// 从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
//创建请求到苹果官方进行购买验证
NSURL *url=[NSURL URLWithString:SANDBOX];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody=bodyData;
requestM.HTTPMethod=@"POST";
//创建连接并发送同步请求
NSError *error=nil;
NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
    NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);
    return;
}
NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
NSLog(@"%@",dic);
if([dic[@"status"] intValue]==0){
    NSLog(@"购买成功!");
    NSDictionary *dicReceipt= dic[@"receipt"];
    NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
    NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识
    //如果是消耗品则记录购买数量,非消耗品则记录是否购买过
    NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
    if ([productIdentifier isEqualToString:@"123"]) {
      int purchasedCount=[defaults integerForKey:productIdentifier];//已购买数量
      [[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier];
    } else {
      [defaults setBool:YES forKey:productIdentifier];
    }

    //在此处对购买记录进行存储,可以存储到开发商的服务器端
    if ([productIdentifier isEqualToString:@"111111"]) {
        [self chongzhi:@"50"];
    }else if ([productIdentifier isEqualToString:@"22222"]) {
        [self chongzhi:@"108"];
    }else if ([productIdentifier isEqualToString:@"33333"]) {
        [self chongzhi:@"158"];
    }else if ([productIdentifier isEqualToString:@"44444"]) {
        [self chongzhi:@"208"];
    }
}else if([dic[@"status"] intValue]==21007){
    
}else{
    NSLog(@"购买失败,未通过验证!");
}
}

三、使用苹果内购时常遇到的一些问题总结(含:苹果内购漏单的处理)

1.校验失败的问题
因为校验的行为其实还是APP客户端主动发起的,偶尔会出现网络情况不好、用户将应用退出等情况,导致APP客户端接受不到校验结果,最终就会出现用户实际已经发生扣款支付成功了,校验失败,APP客户端的虚拟商品却没有收到,这类问题称之为漏单~我也是在这摸爬滚打了一阵。出现该问题的原因还是因为苹果内购支付成功后支付凭证给到了APP客户端,而不是服务端,微信、支付宝都是支付成功同时告知了APP的服务端,并且会分时段多次告知避免出现以上错误,苹果也是为了避免越狱软件模拟苹果请求达到非法购买的问题。说那么多有啥用?问题来了还不得解决,哈哈哈,解决方案如下:

1.APP服务端进行校验失败时,自动进行二次校验,减少由于网络问题导致的失败。
2.APP客户端规律性的针对校验失败的订单进行再次校验(例如启动APP,再次下单,或者弄一个恢复购买按钮来进行再次校验)因为只有支付成功,苹果才会下发支付凭证给APP客户端,这时候,我们可以将所有支付凭证存储下来,然后发起校验,如果校验成功删除对应凭证,如果失败,则下次启动APP自动再次发起校验。

2.AppleID不同设备同步的问题
APP本身会做用户购买内容的账号同步,但还是必须要支持AppleID的同步,最终导致多个APP用户使用同一个AppleID购买内容只需支付一次。目前没有解决办法,不支持AppleID同步就无法上架AppStore了。

3.APP客户端接受苹果凭证退出了APP,导致无法接受凭证,用户再把APP客户端打开,导致购买的项目丢失,这种问题目前只能人工的方式来处理了。APP调用苹果内购前会得到对应的订单号,通过订单号在苹果服务器查询是否有对应的支付交易。

参考文章:
苹果内购IAP 简单总结
iOS苹果内购详细步骤

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