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. BZOJ3286 Fibonacci矩阵 矩阵 快速幂 卡常

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ3286 题意概括 n,m,a,b,c,d,e,f<=10^1000000 题解 神奇的卡常题目 ...

  2. oracle中查找某用户执行某张表的操作操作记录

    转载:http://www.cnblogs.com/nizuimeiabc1/p/9441937.html 1,首先查找表的操作记录 select * from v$sqlarea a where a ...

  3. java生成Excel文件,下载

    pom引入poi的maven依赖 <dependency> <groupId>org.apache.poi</groupId> <artifactId> ...

  4. java获取iPhone手机图片旋转角度处理

    参考原博客地址:https://www.cnblogs.com/zhao1949/p/6473882.html 这里需要的jar文件: https://files.cnblogs.com/files/ ...

  5. P2158 [SDOI2008]仪仗队

    P2158 [SDOI2008]仪仗队图是关于y=x对称的,横纵坐标一定是互质的否则在之前就被扫过了,所以就可以用欧拉函数再*2就完了. #include<iostream> #inclu ...

  6. 003.Heartbeat MySQL双主复制

    一 基础环境 节点 系统版本 MySQL版本 业务IP 心跳IP Master01 CentOS 7.5 MySQL 5.6 192.168.88.100 192.168.77.100 Master0 ...

  7. Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)

    Android XML shape 标签使用详解   一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...

  8. spring_AOP

    例子代码 理解AOP AOP为Aspect Oriented Programming的缩写,意为:面向切面编程.大概意思就是在原有源代码的基础上,增加功能,而又不修改原有的代码. 术语 切面(Aspe ...

  9. vue 工作学习总结

    配置ESlint yarn 初始化 yarn init yes 添加依赖 yarn add [package] 升级依赖 yarn upgrade [package] 移出依赖 yarn remove ...

  10. notepad++ 去空行

    Press Ctrl+H (Replace) Select Extended from SearchMode Put \r\n\r\n in Find What Put \r\n in Replace ...