iOS 苹果的内购

一、介绍

苹果规定,凡是虚拟的物品(例如:QQ音乐的乐币)进行交易时,都必须走苹果的内购通道,苹果要收取大约30%的抽成,所以不允许接入第三方的支付方式(微信、支付宝等),当然开发者可以设置后门,在审核时避开审核人员。这个是有风险的,一旦发现,app会被立即下架,还是老老实实接入内购吧。

二、注意

内购接入还是比较简单的,苹果提供了专门的框架<StoreKit/StoreKit.h>,只要按照它提供的api进行开发就行。然而,接入的过程还是有需要注意的地方,分别是:漏单处理、二次验证、移除交易、游客模式。

漏单处理: 这个是一定会存在的,因为用户的一些误操作,造成的漏单基本无法避免,针对这种情况,最终的处理方式就是人工客服。当然,这个过程是可以优化的,开发者可以进行存储订单票据,server存储订单号,本地存储票据。如果用户启动app后,检测到用户上次付了款,但是需要的商品没有给到用户,此时可以自动进行验证并处理,验证通过,就将商品补给用户。

二次验证:这个步骤必不可少,首先正式环境验证,如果验证通过,说明是线上环境,可以正常操作。如果验证不通过,说明是沙盒环境,需要在沙盒环境下再次验证,沙盒环境下的验证结果会有一个统一的弹框标识[Environment : Sandbox],只要内购没有上线,验证时都是沙盒环境弹框。 二次验证的这个过程可以避免在审核app时,因为没有验证通过直接被拒的风险。二次验证放在server端实现,更加安全。

移除交易:用户再次交易时,如果上次的交易没有被移除,那么此次的交易会一直在队列中等候,无法被提交,所以一定要在上次交易完成时移除交易。

游客模式:如果我们的app支持游客使用,那么这个内购就必须要求对游客进行开放,否则审核会被拒绝。

三、使用

1、实现代理

@interface InAppPurchaseViewController ()<SKPaymentTransactionObserver,SKProductsRequestDelegate>
@property (nonatomic, strong)NSMutableArray *products;
@property (nonatomic, strong)Product *currentProduct;
@property (nonatomic, copy)NSString *currentPayNo;
@end

2、添加观察者

-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// 添加观察者
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

3、移除观察者

-(void)viewWillDisappear:(BOOL)animated
{
// 移除观察者
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

4、移除没有关闭的交易并做漏单处理

#pragma mark 先检查之前是否有未关闭的交易并做漏单处理
-(void)checkNotCloseAndFinishedTransaction{ NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
for (SKPaymentTransaction* transaction in transactions) {
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
} NSArray *tickets = [InAppPurchaseTicketService fetchInAppPurchaseTicket];
if (tickets.count > ) {
[MBProgressHUD showMessage:@"正在验证未处理的订单,请稍后"];
self.conchChargeView.userInteractionEnabled = NO;
for (InAppPurchaseTicket *ticket in tickets) {
[self checkAppStorePayResultWithTikect:ticket];
}
}
}

5、用户使用productId进行下单

-(void)loadPayNoData{

    AppWeak(weakSelf, self);
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"productId"] = self.currentProduct.productId;
[InAppPurchaseTicketService getIosOrderWithParams:params success:^(NSArray *items, BOOL isLocalData) {
if (items.count > ) {
InAppPurchaseInfo *inAppPurchaseInfo = items[];
weakSelf.currentPayNo = inAppPurchaseInfo.payNo;
[weakSelf startPayForProduct:weakSelf.currentProduct.productId];
}
} failure:^(id errorInfo) {
[self showErrorInfo:errorInfo];
}];
}

6、开始内购

-(void)startPayForProduct:(NSString *)productID{

    if([SKPaymentQueue canMakePayments]){
[MBProgressHUD showMessage:@"正在请求商品信息,请稍等..."];
self.conchChargeView.userInteractionEnabled = NO; // productID就是你在创建购买项目时所填写的产品ID
[self requestProductID:productID]; }else{
// NSLog(@"不允许程序内付费");
UIAlertView *alertError = [[UIAlertView alloc] initWithTitle:@"温馨提示"
message:@"请先开启应用内付费购买功能。"
delegate:nil
cancelButtonTitle:@"确定"
otherButtonTitles: nil];
[alertError show];
}
}

7、请求所有的商品ID

-(void)requestProductID:(NSString *)productID{

    // 1.拿到所有可卖商品的ID数组
NSMutableArray *productIDArray = [NSMutableArray array];
for (Product *product in self.products) {
[productIDArray addObject:product.productId];
}
NSSet *sets = [[NSSet alloc] initWithArray:productIDArray]; // 2.向苹果发送请求,请求所有可买的商品
// 2.1.创建请求对象
SKProductsRequest *sKProductsRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:sets]; // 2.2.设置代理(在代理方法里面获取所有的可卖的商品)
sKProductsRequest.delegate = self; // 2.3.开始请求
[sKProductsRequest start];
}

8、获取苹果那边的内购监听

//请求成功
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{ NSArray *product = response.products;
if([product count] == ){
[MBProgressHUD hideHUD];
self.conchChargeView.userInteractionEnabled = YES;
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"没有商品" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
[alertView show];
return;
} for (SKProduct *sKProduct in product) { 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:self.currentProduct.productId]){
[self buyProduct:sKProduct];
break;
}
}
} //请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{ [MBProgressHUD hideHUD];
self.conchChargeView.userInteractionEnabled = YES;
UIAlertView *alerView = [[UIAlertView alloc] initWithTitle:@"提示" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil];
[alerView show]; }

9、创建票据,在队列中等候处理

-(void)buyProduct:(SKProduct *)product{

    // 1.创建票据
SKPayment *skpayment = [SKPayment paymentWithProduct:product]; // 2.将票据加入到交易队列
[[SKPaymentQueue defaultQueue] addPayment:skpayment];
}

10、内购回调

#pragma mark 4.实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{ /*
SKPaymentTransactionStatePurchasing, 正在购买
SKPaymentTransactionStatePurchased, 已经购买
SKPaymentTransactionStateFailed, 购买失败
SKPaymentTransactionStateRestored, 回复购买中
SKPaymentTransactionStateDeferred 交易还在队列里面,但最终状态还没有决定
*/ for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:{
[MBProgressHUD hideHUD];
[MBProgressHUD showMessage:@"正在购买中,别走开..."];
NSLog(@"正在购买...");
}
break;
case SKPaymentTransactionStatePurchased:{
// 购买后告诉交易队列,把这个成功的交易移除掉
[queue finishTransaction:transaction];
[MBProgressHUD hideHUD];
[self SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:transaction];
NSLog(@"购买成功");
}
break;
case SKPaymentTransactionStateFailed:{
// 购买失败也要把这个交易移除掉
[queue finishTransaction:transaction];
[MBProgressHUD hideHUD];
self.conchChargeView.userInteractionEnabled = YES;
NSString *errorInfo = @"购买失败,请稍后重新购买";
if (transaction.error) {
NSString *reason = transaction.error.userInfo[NSLocalizedFailureReasonErrorKey];
if ([StringUtility isStringNotEmptyOrNil:reason]) {
errorInfo = reason;
}
}
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
[alertView show];
NSLog(@"购买失败");
}
break;
case SKPaymentTransactionStateRestored:{
// 回复购买中也要把这个交易移除掉
[queue finishTransaction:transaction];
[MBProgressHUD hideHUD];
self.conchChargeView.userInteractionEnabled = YES;
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"重复购买了" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
[alertView show];
NSLog(@"重复购买了");
}
break;
case SKPaymentTransactionStateDeferred:{
NSLog(@"交易还在队列里面,但最终状态还没有决定");
}
break;
default:
break;
}
}
}

11、本地存储票据

// 苹果内购支付成功
- (void)SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:(SKPaymentTransaction *)paymentTransactionp { // 传输的是BASE64编码的字符串
// 验证凭据,获取到苹果返回的交易凭据
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; // 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; // 传输的是BASE64编码的字符串
NSString *reciept = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; // productIdentifier
NSString *productIdentifier = paymentTransactionp.payment.productIdentifier;
if ([productIdentifier length]>) { //本地存储票据
InAppPurchaseTicket *ticket = [[InAppPurchaseTicket alloc] init];
ticket.payNo = self.currentPayNo;
ticket.productId = self.currentProduct.productId;
ticket.reciept = reciept;
ticket.state = ;
[InAppPurchaseTicketService saveLocalTransaction:ticket]; // 去验证是否真正的支付成功了
[MBProgressHUD showMessage:@"购买成功,正在验证订单..."];
[self checkAppStorePayResultWithTikect:ticket];
}
}

12、二次验证

#pragma mark 服务端验证购买凭据
- (void)checkAppStorePayResultWithTikect:(InAppPurchaseTicket *)tikect { /*
生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明 注意:
自己测试的时候使用的是沙盒购买(测试环境)
App Store审核的时候也使用的是沙盒购买(测试环境)
上线以后就不是用的沙盒购买了(正式环境)
所以此时应该先验证正式环境,在验证测试环境 正式环境验证成功,说明是线上用户在使用
正式环境验证不成功返回21007,说明是自己测试或者审核人员在测试 苹果AppStore线上的购买凭证地址是: https://buy.itunes.apple.com/verifyReceipt
测试地址是:https://sandbox.itunes.apple.com/verifyReceipt */ NSString *sandbox;
#ifdef TEST
sandbox = @""; //沙盒测试环境
#else
sandbox = @""; //线上正式环境
#endif if (!tikect || !tikect.payNo || tikect.payNo.length== || !tikect.reciept || tikect.reciept.length==) {
return;
} AppWeak(weakSelf, self);
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"payno"] = tikect.payNo; //订单号
params[@"sandbox"] = sandbox; //使用环境
params[@"receipt"] = tikect.reciept; //票据信息 [InAppPurchaseTicketService doubleIosVerifyWithParams:params success:^(NSArray *items, BOOL isLocalData) { //隐藏loding
[MBProgressHUD hideHUD];
[MBProgressHUD showSuccess:@"恭喜你,购买成功" afterDelay:2.0];
weakSelf.conchChargeView.userInteractionEnabled = YES; //清除本地当前对应订单票据
InAppPurchaseInfo *info = items[];
[InAppPurchaseTicketService clearInAppPurchaseTicketWithPayNo:info.payNo]; //刷新UI
if (self.fromVCType == FromVCTypeCurrentInAppPurchaseVC) {
[weakSelf loadConchData];
}
else{
//回调并跳转页面
if (weakSelf.chargeConchSuccsssBlock) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
weakSelf.chargeConchSuccsssBlock(@(YES));
[weakSelf.navigationController popViewControllerAnimated:YES];
});
}
} } failure:^(id errorInfo) {
[MBProgressHUD hideHUD];
weakSelf.conchChargeView.userInteractionEnabled = YES;
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"验证失败" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
[alertView show];
}];
}

13、懒加载plist中的所有约定的商品productId

-(NSMutableArray *)products{
if (!_products) {
_products = [NSMutableArray array];
NSString *path = [[NSBundle mainBundle] pathForResource:@"Products" ofType:@"plist"];
NSArray *items = [NSArray arrayWithContentsOfFile:path];
if ([ArrayUtility isArrayNotEmptyOrNil:items]) {
for (NSDictionary *dic in items) {
Product *product = [[Product alloc] initWithProperties:dic];
[_products addObject:product];
}
}
}
return _products;
}

三、推荐

最好把监听回调和验证写在单例中,这样app一启动时,就可以监听回调状态。

四、结论

这就是内购的全部流程了,我把主要的流程梳理了一下,具体的细节,开发人员自己去整理。与君共勉。。。。。

iOS:苹果内购实践的更多相关文章

  1. ios 苹果内购订单验证 --- php实现

    验证函数: function appleVerify($receipt_data,$orderId = 0) { /* * 21000 App Store不能读取你提供的JSON对象 * 21002 ...

  2. IOS,苹果内购和添加广告

    内购——应用内购买 通过苹果应用程序商店有三种主要赚钱的方式: 直接收费(与国内大部分用户的消费习惯相悖) 广告(降低用户体验 应用程序名称带Lite可以添加广告) O2O -> Online推 ...

  3. ios 苹果内购订单验证 --- nodejs实现

    实现代码 function IosPlayVerify(data,orderid,cb) { itunesPost(data,function (error,responseData) { if (e ...

  4. iOS开发苹果内购的介绍与实现

    1.iOS开发苹果内购的介绍 1.1 介绍 苹果规定,凡是虚拟的物品(例如:QQ音乐的乐币)进行交易时,都必须走苹果的内购通道,苹果要收取大约30%的抽成,所以不允许接入第三方的支付方式(微信.支付宝 ...

  5. ios IAP 内购验证

    参考我之前的笔记 苹果内购笔记,在客户端向苹果购买成功之后,我们需要进行二次验证. 二次验证 IOS在沙箱环境下购买成功之后,向苹果进行二次验证,确认用户是否购买成功. 当应用向Apple服务器请求购 ...

  6. 苹果内购和 Apple Pay

    作者:CC老师_MissCC链接:http://www.jianshu.com/p/e3bc47e81785來源:简书 苹果内购 1.什么是内购? 如果你购买的商品,是在本app中使用和消耗的,就一定 ...

  7. apicloud含有微信支付。支付宝支付和苹果内购的代码

    apicloud含有微信支付.支付宝支付和苹果内购的代码 <!DOCTYPE html> <html> <head> <meta charset=" ...

  8. Cocos 2d-X Lua 游戏添加苹果内购(二) OC和Lua交互代码详解

    这是第二篇 Cocos 2d-X Lua 游戏添加苹果内购(一) 图文详解准备流程 这是前面的第一篇,详细的说明了怎样添加内购项目以及填写银行信息提交以及沙盒测试员的添加使用以及需要我们注意的东西,结 ...

  9. 苹果内购服务器验证之receipt返回多组in_app思考

    最近有部分用户反映,苹果内购充值失败,经过测试总结有几个关键点出现问题 1.app购买成功苹果没有返回票据,属于票据遗漏(取决于苹果服务器的响应状况),只能客户端进行监听刷新等处理 2.app连续购买 ...

随机推荐

  1. Flink-- 数据输出Data Sinks

    flink在批处理中常见的sink 1.基于本地集合的sink(Collection-based-sink) 2.基于文件的sink(File-based-sink) 基于本地集合的sink(Coll ...

  2. net core体系-3再次认识net core

    1 什么是ASP.NET Core ASP.NET Core 是一个全新的开源.跨平台框架,可以用它来构建基于网络连接的现代云应用程序,比如:Web 应用,IoT(Internet Of Things ...

  3. Python学习(二十七)—— Django和pymysql搭建学员管理系统

    转载自http://www.cnblogs.com/liwenzhou/p/8270250.html 一.学员管理系统 1.项目规划阶段 项目背景 近年来老男孩教育的入学学员数量稳步快速增长,传统的e ...

  4. Python学习(一) —— 基础

    一.计算机的组成 计算机硬件主要由cpu.内存.硬盘组成. cpu:相当于人类的大脑,用于计算 内存:临时加载数据或者程序.缺点:断电即消失. 硬盘:用于永久存放数据或者程序.缺点:运行速度慢. 二. ...

  5. 007 numpy数组文件的存取

    不知道这个有没有用,都整理了一番. 一:数组以二进制格式进行存储 1.说明 np.save与np.load是读写磁盘数组数据的两个重要函数. 默认情况下,数组以压缩的原始二进制格式保存在扩展名为npy ...

  6. 堆排序算法(Java实现)

    将待排序的序列构造成一个大顶堆(从大到小排要构造成小顶堆).此时,整个序列的最大值就是堆顶的根节点,将他和末尾元素交换,然后将剩余的length-1个节点序列重新构造成新的堆.重复执行,便能得到一个有 ...

  7. POJ 2112 Optimal Milking (二分+最短路+最大流)

    <题目链接> 题目大意: 有K台挤奶机和C头奶牛,都被视为物体,这K+C个物体之间存在路径.给出一个 (K+C)x(K+C) 的矩阵A,A[i][j]表示物体i和物体j之间的距离,有些物体 ...

  8. 常见素数筛选方法原理和Python实现

    1. 普通筛选(常用于求解单个素数问题) 自然数中,除了1和它本身以外不再有其他因数. import math def func_get_prime(n): func = lambda x: not ...

  9. JVM 调优-给你的java应用看看病

    目录 java 应用 1 cpu 负载过高 1.1 分析问题 1.2 解决方案 2 内存占用过多 2.1 从内存回收方面 2.2 从代码层面 java 应用 1 cpu 负载过高 1.1 分析问题 首 ...

  10. Django 学习第九天——请求与响应

    一.HttpRequest 对象: 服务器接收到http协议的请求后,会根据报文创建 HttpRequest 对象视图函数的第一个参数是HttpRequest 对象再django.http 模块中定义 ...