iOS应用内支付(IAP)详解

在iOS开发中如果涉及到虚拟物品的购买,就需要使用IAP服务,我们今天来看看如何实现。

在实现代码之前我们先做一些准备工作,一步步来看。


1、IAP流程

IAP流程分为两种,一种是直接使用Apple的服务器进行购买和验证,另一种就是自己假设服务器进行验证。由于国内网络连接Apple服务器验证非常慢,而且也为了防止黑客伪造购买凭证,通用做法是自己架设服务器进行验证。

下面我们通过图来看看两种方式的差别:

1.1、使用Apple服务器

image

1.2、自己架设服务器

image

简单说下第二中情况的流程:

  1. 用户进入购买虚拟物品页面,App从后台服务器获取产品列表然后显示给用户
  2. 用户点击购买购买某一个虚拟物品,APP就发送该虚拟物品的productionIdentifier到Apple服务器
  3. Apple服务器根据APP发送过来的productionIdentifier返回相应的物品的信息(描述,价格等)
  4. 用户点击确认键购买该物品,购买请求发送到Apple服务器
  5. Apple服务器完成购买后,返回用户一个完成购买的凭证
  6. APP发送这个凭证到后台服务器验证
  7. 后台服务器把这个凭证发送到Apple验证,Apple返回一个字段给后台服务器表明该凭证是否有效
  8. 后台服务器把验证结果在发送到APP,APP根据验证结果做相应的处理

2、iTunes Connet操作

搞清楚了自己架设服务器是如何完成IAP购买的流程了之后,我们下一步就是登录到iTunes Connet创建应用和指定虚拟物品价格表

2.1、创建自己的App

如下图所示,我们需要创建一个自己的APP,要注意的是这里的Bundle ID一定要跟你的项目中的info.plist中的Bundle ID保证一致。也就是图中红框部分。

image

2.2、创建虚拟物品价格表
2.2.1、虚拟物品分为如下几种:
  1. 消耗品(Consumable products):比如游戏内金币等。

  2. 不可消耗品(Non-consumable products):简单来说就是一次购买,终身可用(用户可随时从App Store restore)。

  3. 自动更新订阅品(Auto-renewable subscriptions):和不可消耗品的不同点是有失效时间。比如一整年的付费周刊。在这种模式下,开发者定期投递内容,用户在订阅期内随时可以访问这些内容。订阅快要过期时,系统将自动更新订阅(如果用户同意)。

  4. 非自动更新订阅品(Non-renewable subscriptions):一般使用场景是从用户从IAP购买后,购买信息存放在自己的开发者服务器上。失效日期/可用是由开发者服务器自行控制的,而非由App Store控制,这一点与自动更新订阅品有差异。

  5. 免费订阅品(Free subscriptions):在Newsstand中放置免费订阅的一种方式。免费订阅永不过期。只能用于Newsstand-enabled apps。

类型2、3、5都是以Apple ID为粒度的。比如小张有三个iPad,有一个Apple ID购买了不可消耗品,则三个iPad上都可以使用。

类型1、4一般来说则是现买现用。如果开发者自己想做更多控制,一般选4

2.2.2、创建成功后如下所示:

image

其中产品id是字母或者数字,或者两者的组合,用于唯一表示该虚拟物品,app也是通过请求产品id来从apple服务器获取虚拟物品信息的。

2.3、设置税务和银行卡信息

这一步必须设置,不然是无法从apple获取虚拟产品信息。

设置成功后如下所示:

image

更多关于iTunes Connet的操作请才看这篇博文http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/


3、iOS端具体代码实现

完成了上面的准备工作,我们就可以开始着手IAP的代码实现了。

我们假设你已经完成了从后台服务器获取虚拟物品列表这一步操作了,这一步后台服务器还会返回每个虚拟物品所对应的productionIdentifier,假设你也获取到了,并保存在属性self.productIdent中。

需要在工程中引入 storekit.framework。

我们来看看后续如何实现IAP

3.1、确认用户是否允许IAP
//移除监听
-(void)dealloc
{[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}//添加监听
- (void)viewDidLoad{[super viewDidLoad];[self.tableView.mj_header beginRefreshing];[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}- (void)buyProdution:(UIButton *)sender{    if ([SKPaymentQueue canMakePayments]) {[self getProductInfo:self.productIdent];} else {[self showMessage:@"用户禁止应用内付费购买"];}
}
3.2、发起购买操作

如果用户允许IAP,那么就可以发起购买操作了

//从Apple查询用户点击购买的产品的信息
- (void)getProductInfo:(NSString *)productIdentifier {NSArray *product = [[NSArray alloc] initWithObjects:productIdentifier, nil];NSSet *set = [NSSet setWithArray:product];SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];request.delegate = self;[request start];[self showMessageManualHide:@"正在购买,请稍后"];
}// 查询成功后的回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {[self hideHUD];NSArray *myProduct = response.products;if (myProduct.count == 0) {[self showMessage:@"无法获取产品信息,请重试"];return;}SKPayment * payment = [SKPayment paymentWithProduct:myProduct[0]];[[SKPaymentQueue defaultQueue] addPayment:payment];
}//查询失败后的回调
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {[self hideHUD];[self showMessage:[error localizedDescription]];
}
3.3、购买操作后的回调
//购买操作后的回调
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {[self hideHUD];for (SKPaymentTransaction *transaction in transactions){switch (transaction.transactionState){case SKPaymentTransactionStatePurchased://交易完成self.receipt = [GTMBase64 stringByEncodingData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]];[self checkReceiptIsValid];//把self.receipt发送到服务器验证是否有效[self completeTransaction:transaction];break;case SKPaymentTransactionStateFailed://交易失败[self failedTransaction:transaction];break;case SKPaymentTransactionStateRestored://已经购买过该商品[self showMessage:@"恢复购买成功"];[self restoreTransaction:transaction];break;case SKPaymentTransactionStatePurchasing://商品添加进列表[self showMessage:@"正在请求付费信息,请稍后"];break;default:break;}}}- (void)completeTransaction:(SKPaymentTransaction *)transaction {[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}- (void)failedTransaction:(SKPaymentTransaction *)transaction {if(transaction.error.code != SKErrorPaymentCancelled) {UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil];[alertView show];} else {[self showMessage:@"用户取消交易"];}[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}- (void)restoreTransaction:(SKPaymentTransaction *)transaction {[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
3.4、向服务器端验证购买凭证的有效性

在这一步我们需要向服务器验证Apple服务器返回的购买凭证的有效性,然后把验证结果通知用户

- (void)checkReceiptIsValid{AFHTTPSessionManager manager]GET:@"后台服务器地址"  parameters::@"发送的参数(必须包括购买凭证)"success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {if(凭证有效){你要做的事}else{//凭证无效你要做的事}} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"购买失败,请重试"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重试", nil];[alertView show];}}
3.5、发送凭证失败的处理

如果出现网络问题,导致无法验证。我们需要持久化保存购买凭证,在用户下次启动APP的时候在后台向服务器再一次发起验证,直到成功然后移除该凭证。
保证如下define可在全局访问:

#define AppStoreInfoLocalFilePath [NSString stringWithFormat:@"%@/%@/", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject],@"EACEF35FE363A75A"]

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{if (buttonIndex == 0){[self saveReceipt];}else{[self checkReceiptIsValid];}
}//持久化存储用户购买凭证(这里最好还要存储当前日期,用户id等信息,用于区分不同的凭证)
-(void)saveReceipt{NSString *fileName = [AppUtils getUUIDString];NSString *savedPath = [NSString stringWithFormat:@"%@%@.plist", AppStoreInfoLocalFilePath, fileName];NSDictionary *dic =[ NSDictionary dictionaryWithObjectsAndKeys:self.receipt,                           Request_transactionReceipt,self.date                               DATE                        self.userId                             USERIDnil];[dic writeToFile:savedPath atomically:YES];
}
3.6、APP启动后再次发送持久化存储的购买凭证到后台服务器
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{    NSFileManager *fileManager = [NSFileManager defaultManager];//从服务器验证receipt失败之后,在程序再次启动的时候,使用保存的receipt再次到服务器验证if (![fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]) {//如果在改路下不存在文件,说明就没有保存验证失败后的购买凭证,也就是说发送凭证成功。[fileManager createDirectoryAtPath:AppStoreInfoLocalFilePath//创建目录withIntermediateDirectories:YESattributes:nilerror:nil];}else//存在购买凭证,说明发送凭证失败,再次发起验证{[self sendFailedIapFiles];}
}//验证receipt失败,App启动后再次验证
- (void)sendFailedIapFiles{NSFileManager *fileManager = [NSFileManager defaultManager];NSError *error = nil;//搜索该目录下的所有文件和目录NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:AppStoreInfoLocalFilePath error:&error];if (error == nil){for (NSString *name in cacheFileNameArray){if ([name hasSuffix:@".plist"])//如果有plist后缀的文件,说明就是存储的购买凭证{NSString *filePath = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, name];[self sendAppStoreRequestBuyPlist:filePath];}}}else{DebugLog(@"AppStoreInfoLocalFilePath error:%@", [error domain]);}
}-(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath
{NSString *path = [NSString stringWithFormat:@"%@%@", AppStoreInfoLocalFilePath, plistPath];NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:path];//这里的参数请根据自己公司后台服务器接口定制,但是必须发送的是持久化保存购买凭证NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:[dic objectForKey:USERID],                           USERID,                    [dic objectForKey:DATE],                             DATE,                                                                                                         [dic objectForKey:Request_transactionReceipt],      Request_transactionReceipt,nil];AFHTTPSessionManager manager]GET:@"后台服务器地址"  parameters:params  success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {if(凭证有效){[self removeReceipt]}else{//凭证无效你要做的事}} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {}}//验证成功就从plist中移除凭证
-(void)removeReceipt{[AppUtils removeIapFailedPath:AppStoreInfoLocalFilePath];
}//AppUtils类方法,验证成功,移除存储的receipt
+ (void)removeIapFailedPath:(NSString *)plistPath{NSString *path = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, plistPath];NSFileManager *fileManager = [NSFileManager defaultManager];if ([fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]){[fileManager removeItemAtPath:AppStoreInfoLocalFilePath error:nil];}if ([fileManager fileExistsAtPath:path]){[fileManager removeItemAtPath:path error:nil];}
}

至此,整个流程结束,有任何疑问欢迎大家留言


参考:

  1. http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/

  2. http://www.himigame.com/iphone-cocos2d/550.html

  3. http://blog.devtang.com/2012/12/09/in-app-purchase-check-list/

  4. http://yarin.blog.51cto.com/1130898/549141

  5. 更多技术文章,欢迎大家访问我的技术博客:http://blog.ximu.site


转载于:https://www.cnblogs.com/XimuYouzi/p/5401749.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/373233.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php 去除 html 属性,用PHP 去掉所有html标签里的部分属性

用PHP 去掉所有html标签里的部分属性http://zhidao.baidu.com/question/418471924.html用PHP 去掉所有html标签里的部分属性 tppabsset_time_limit(0);function view_dir($dir){$dpopendir($dir); //打开目录句柄//echo "".$dir."";$path2;while ($file r…

在Windows上安装Elasticsearch 5.0

在windows上安装Elasticsearch Elasticsearch可以使用.zip软件包安装在Windows上。 elasticsearch-service.bat命令,它将设置Elasticsearch作为服务运行。 Elasticsearch的最新稳定版在Download Elasticsearch下载,其他的版本在Past Releases page下载。…

Java EE 6示例– Galleria

您是否一直想知道在哪里可以找到使用Java EE 6构建的良好端到端示例? 我有。 您在网上找到的大多数东西都是非常基础的,不能解决现实世界中的问题。 Java EE 6教程就是这样。 所有其他内容,例如Adam Bien所发表的大多数内容,都是范…

如何在Jetty中使用SPDY

SPDY是Google提出的一种新协议,是针对网络的新协议。 SPDY与HTTP兼容,但尝试通过压缩,多路复用和优先级降低网页负载。准确地说,快速的目标是:( http://dev.chromium.org/spdy/spdy-whitepaper &#xff09…

虐杀外星人java,逆天游戏《毁灭全人类2》登PS4 外星人疯狂虐杀地球人

逆天游戏《毁灭全人类2》登PS4 外星人疯狂虐杀地球人2016-10-17 10:45:58来源:游戏下载编辑:小年青评论(0)广大的小伙伴都有看过许多外星人企图入侵毁灭地球的电影,已此为题材而开发的游戏也不在少数。近日泛欧洲游戏信息组织又为一款该种题材…

Android之仿微信图片选择器

先上效果图。第一张图显示的是“相机”文件夹中的所有图片;通过点击多张图片可以到第二张图所示的效果(被选择的图片会变暗,同时选择按钮变亮);点击最下面的那一栏可以到第三张图所示的效果(显示手机中所有…

loss function

什么是loss? loss: loss是我们用来对模型满意程度的指标。loss设计的原则是:模型越好loss越低,模型越差loss越高,但也有过拟合的情况。     loss function: 在分类问题中,输入样本经过含权重矩阵θ的模型后会得出关于各个类别…

oracle查询表的id,oracle 查看所有用户及密码 实现Oracle查询用户所有表

1、oracle 查看所有用户及密码SQL> select username from dba_users;2、 实现Oracle查询用户所有表下面为您介绍的语句用于实现Oracle查询用户所有表,如果您对oracle查询方面感兴趣的话,不妨一看。select * from all_tab_comments-- 查询所有用户的表…

FindBugs和JSR-305

假设那组开发人员在大型项目的各个部分上并行工作–一些开发人员在进行服务实现,而其他开发人员在使用该服务的代码。 考虑到API的假设,两个小组都同意服务API,并开始单独工作。 您认为这个故事会有幸福的结局吗? 好吧&#xff0c…

什么是Akka?

在深入研究什么是Akka之前,让我们退后一步来了解并发编程的概念在应用程序开发世界中是如何演变的。 应用程序已经从大型的整体程序演变为面向对象的模型。 随着Java EE和Spring框架的出现,应用程序设计演变为更多的基于流程或任务的设计模型。 EJB或Poj…

Unity3d 实现顶点动画

在今年GDC上发现一个非常有趣的演讲,叫做Animating With Math,遂实现之,是讲述顶点shader动画的,举了几个经典的例子,但是讲者并没有给代码,而是像虚幻引擎那样的节点,这样更加清楚明了之前博主…

windows2012同步linux时间,Windows server2012时间同步NTP配置

遇到经常服务器时间无法同步,可以自己建立一台时间同步服务器,NTP配置如下:一、服务端配置 (Ntp服务器,客户端将根据这台服务器的时间进行同步)1、微软键R键,进入“运行”,输入“regedit”,进入注册表2、 H…

Java EE 6示例– Galleria第2部分

您可能在最后一篇Java EE 6 Galleria示例帖子中关注了我。 第一个是基本介绍。 第二个是关于在最新的GlassFish上运行它。 有人提到RedHat,我们应该研究将这个示例从GlassFish中移除。 很好;)感谢您的好主意。 这正是我们今天要做的。 我将把Galleria示例…

与reCAPTCHA的Spring集成

有时我们只需要CAPTCHA ,这是一个可悲的事实。 今天,我们将学习如何与reCAPTCHA集成。 因为主题本身并不是特别有趣和高级,所以我们将通过使用Spring Integration处理低级细节来过度设计(?)。 Google决定使…

《机器学习基石》---感知机算法

1 推导感知机模型 基本思想是,把特征的线性加权值作为一个分数,根据这个分数与一个门限值的关系来进行分类: 我们加一个特征x0等于1,门限值就可以放到w里面去,得到更简单的形式: 这就是感知机模型&#xff…

Python之路【第八篇】:堡垒机实例以及数据库操作

Python之路【第八篇】:堡垒机实例以及数据库操作 堡垒机前戏 开发堡垒机之前,先来学习Python的paramiko模块,该模块机遇SSH用于连接远程服务器并执行相关操作 SSHClient 用于连接远程服务器并执行基本命令 基于用户名密码连接: 12…

ADF BC:创建绑定到业务组件的UI表

在此示例中,我们将展示如何创建绑定到业务组件的简单UI表(af:table)。 我再次尝试使用简单的标准在网上进行搜索: “如何创建绑定到业务组件ADF 11g的af:table” 我必须承认我没有得到我想要的答案。 信息…

MyBaits 错误分析

错误原因:在DAO的映射文件中,在映射标签中的type类型写成DAO类了,应该写成javaBean转载于:https://www.cnblogs.com/shuaiandjun/p/5428847.html

斑马打印机linux驱动安装教程,linux-Zebra软件包的基本安装与配置

Zebra是一个路由软件包,提供基于TCP/IP路由服务,支持RIPv1, RIPv2, RIPng, OSPFv2, OSPFv3, BGP- 4,和 BGP-4等众多路由协议。Zebra还支持BGP特性路由反射器(Route Reflector)。除了传统的 IPv4路由协议,Zebra也支持IPv6路由协议。如果运行的…

Java 7对抑制异常的支持

在JDK 7中 ,向Throwable类( Exception和Error类的父类)添加了一个新的构造函数和两个新方法。 添加了新的构造函数和两个新方法以支持“抑制的异常”(不要与吞咽或忽略异常的不良做法相混淆)。 在本文中,我…