前言

本文主要集中于代码实现,关于创建商品网上已经有很多了,就不说了,比较简单。

之前做过 消耗性 和 非续订型 内购,代码里一直都是这两种。最近有个新需求,需要续订型 VIP。现在项目里有三种内购类型的产品了,嗯...

流程与代码

流程图

下面这句代码应该在程序入口写,这样写的好处是,如果有未完成的payment,进入程序后会继续走下去。而如果是在特定页面写,只有进入到这个页面才会继续走内购流程。

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

按照流程图

第一步:请求产品列表

是在进入内购页面后,去我们服务器请求

  [self requestProductInfo];

第二步:服务器返回 产品ID 列表

  成功拿到一系列产品ID

第三步:去苹果后台请求详细的产品信息(也可以使用我们服务器的信息,不去请求苹果上的信息)

[[IAPManager getInstance] requestProductsInfo:array];//请求商品信息的代码
- (void)requestProductsInfo:(NSArray*)prodIds
{SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:prodIds]];productsRequest.delegate = self;[productsRequest start];
}

第四步:苹果后台返回详细的产品信息

//在代理方法中,返回详细的商品信息#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveProductsResponse:)])[self.delegate didReceiveProductsResponse:response.products];
}

第五步:展示产品

- (void)didReceiveProductsResponse:(NSArray *)array {if (array != nil && array.count > 0){//可以先按价格排个序NSSortDescriptor* sortDes = [[NSSortDescriptor alloc] initWithKey:@"price.doubleValue" ascending:YES];NSArray *sortArray = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDes]];_dataArray = [sortArray mutableCopy];[tableview reloadData];}}

这里有一个产品价格本地化

使用 NSNumberFormatter *_numberFormatter;

_numberFormatter = [[NSNumberFormatter alloc] init];
[_numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[_numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];- (void)updatePrice {[_numberFormatter setLocale:product.priceLocale];NSString *priceStr = [_numberFormatter stringFromNumber:price];//priceStr 就是正确的当地价格(例如 Rs290,¥10 等)

}

第六步:用户点击购买

点击某一产品购买时,应先判断

  [SKPaymentQueue canMakePayments]   如果是 YES,继续

第六点五步:这里我们的流程略有差异,我们先去自己服务器下单,拿到一个订单号

第七步:发送 payment 请求

下单后,这里把订单号设置进来;苹果会透传过来,确认订单时,需要使用到订单号。

SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = count;
payment.applicationUsername = orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];

这时,会有弹框出现,需要用户输入账户密码购买产品,将从这个账户绑定的卡上扣钱,我们无需做什么。

第八步:代理方法中返回结果

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{for (SKPaymentTransaction *transaction in transactions){[self onTransactionCompleted:transaction];}
}//根据状态做相应的操作- (void)onTransactionCompleted:(SKPaymentTransaction *)transaction
{switch (transaction.transactionState){case SKPaymentTransactionStatePurchased:[self completeTransaction:transaction];break;case SKPaymentTransactionStateFailed:[self failedTransaction:transaction];break;case SKPaymentTransactionStateRestored:[self restoreTransaction:transaction];     //订阅型和非消耗型的商品才有恢复状态break;default:break;}
}

第九步:成功支付的,会有收据,transaction 的状态是 SKPaymentTransactionStatePurchased

需要把必要信息发送给我们服务器

- (void)completeTransaction:(SKPaymentTransaction *)transaction {//这里就是第七步中设置进去的订单号,正常情况下,苹果会透传过来;偶尔的没有透传嘛,就是个坑了。填坑中会有解决方法。
NSString* orderId = transaction.payment.applicationUsername;  NSString* transactionId = transaction.transactionIdentifier;NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];    //收据NSInteger count = transaction.payment.quantity;//把这些信息传给后台 ...//[self sendToServer:(id)data];}

之后 APP端就等着服务器返回即可

第十四步:成功返回

服务器成功返回后,要关掉这次交易,这一步非常重要。

[self finishTransaction:transaction];

没有成功返回的,不要关掉。有可能是我们的服务器在某个时刻,没有响应,或者返回了错误,这时候交易还在队列中,下次打开APP,

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];      //所以这句写在了程序入口处

之后,会重新触发代理方法:即从第八步再走一遍

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions


下面开始填坑

坑一:确定订单时,没有订单号

经过线上运营一段时间,发现会出现少量的掉单情况。对于这类情况,能提供收据的,基本都是手动补发产品了。之后针对这类情况,优化了代码,就很少有掉单情况了。

经研究发现,我们的掉单,发生在第九步,去我们服务器确认时,苹果没有把 订单号 发过来。订单号为空,自然找不到对应的订单了。

于是,在下单成功后,先把订单保存在本地。去确认订单时,如果没有订单号,就从本地拿一下,再去确认;确认成功后,删除对应订单号。

- (void)purchaseProduct:(SKProduct*)product count:(int)count order:(NSString*)orderId{    {        // 暂存最后一次支付订单的数据        [self saveDataWithProductIdentifier:product.productIdentifier orderId:orderId];    }

    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];    payment.quantity = count;    payment.applicationUsername = orderId;    [[SKPaymentQueue defaultQueue] addPayment:payment];}

- (void)completeTransaction:(SKPaymentTransaction *)transaction{    NSString* orderId = transaction.payment.applicationUsername;

    if (orderId == nil) {     //如果没有订单号,本地取一下订单号        orderId = [self getOrderIdWithProductIdentifier:transaction.payment.productIdentifier];    }

    BOOL bVIP = [self isVipTransaction:transaction];    NSString* transactionId = transaction.transactionIdentifier;    NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];    NSInteger count = transaction.payment.quantity;    ...}

- (void)finishTransaction:(SKPaymentTransaction *)transaction{//移除订单号    [self removeOrderIdWithProductIdentifier:transaction.payment.productIdentifier];    // remove the transaction from the payment queue.    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];}
//对应产品ID,保存订单号
- (void)saveDataWithProductIdentifier:(NSString *)identifier orderId:(NSString *)orderId {if (!identifier || !orderId) {return;}NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *path = [pathArray objectAtIndex:0];NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path];NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];if (!dic) {dic = [NSMutableDictionary dictionary];}[dic setValue:orderId forKey:identifier];BOOL flag = [dic writeToFile:filePath atomically:YES];if(!flag) {NSLog(@"orderId保存失败");}
}//获取某一订单号
- (NSString *)getOrderIdWithProductIdentifier:(NSString *)productIdentifier {if (!productIdentifier) {return nil;}NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *path = [pathArray objectAtIndex:0];NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path];NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];return [dic valueForKey:productIdentifier];
}//成功后删除对应订单号
- (void)removeOrderIdWithProductIdentifier:(NSString *)productIdentifier {if (!productIdentifier) {return;}NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);NSString *path = [pathArray objectAtIndex:0];NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path];NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];[dic removeObjectForKey:productIdentifier];BOOL flag = [dic writeToFile:filePath atomically:YES];if(!flag) {NSLog(@"orderId重新保存失败");}
}

其他多为业务逻辑,欢迎各位留言交流

代码地址:https://github.com/lionwhitcher/InAppPurchase

转载于:https://www.cnblogs.com/lion-witcher/p/11169992.html

iOS 内购详解及遇到的坑相关推荐

  1. iOS 内购详解-代码篇

    内购项目-代码篇 一.分步骤说明 1.获取商品列表 2.苹果服务器返回的可购买商品 3.下单购买商品 4.购买队列状态变化,判断购买状态是否成功 5.交易验证 6.拿到的收据信息是,此App所有购买的 ...

  2. 苹果服务器维护时间表2019,ios 内购详解(2019)

    #import "EInAppPurchasing.h" #import #if TARGET_IPHONE_SIMULATOR // 开发时模拟器使用的验证服务器地址 #defi ...

  3. IOS内购对接成功回调中的坑

    1. 项目中服务器的流程如下,和代码请求逻辑相同 . 2.内购校验IOS/Cash 和device/IOS/cash接口使用沙箱和正式两种校验的目的 3.老demo中沙箱返回状态码21002,需要改为 ...

  4. iOS 内购(In-App Purchase)详解

    iOS 内购(In-App Purchase)详解 概述 IAP 全称:In-App Purchase,是指苹果 App Store 的应用内购买,是苹果为 App 内购买虚拟商品或服务提供的一套交易 ...

  5. IOS 多线程04-GCD详解 底层并发 API

    IOS 多线程04-GCD详解 底层并发 API 注:本人是翻译过来,并且加上本人的一点见解. 前言 想要揭示出表面之下深层次的一些可利用的方面.这些底层的 API 提供了大量的灵活性,随之而来的是大 ...

  6. iOS 2D绘图详解(Quartz 2D)之路径(点,直线,虚线,曲线,圆弧,椭圆,矩形)

    前言:一个路径可以包含由一个或者多个shape以及子路径subpath,quartz提供了很多方便的shape可以直接调用.例如:point,line,Arc(圆弧),Curves(曲线),Ellip ...

  7. IOS内购流程从0-1手把手教会

    苹果掌握着可能是全球最重要的APP分发渠道,然而30%的抽成近年来也被人批评,现在苹果似乎也看到反对意见了,从2021年1月1日开始,部分小型企业的分成费用降低到15%. 据报道,苹果将于2021年1 ...

  8. iOS核心动画详解swift版----基础动画

    2019独角兽企业重金招聘Python工程师标准>>> iOS核心动画详解swift版---基础动画 创建工程,添加2个ViewController,通过rootViewContro ...

  9. iOS内购-防越狱破解刷单

    ---------------------------2018.10.16更新--------------------------- 最近我们公司丢单率上涨,尤其是10月份比9月份来说丢单率翻了3倍, ...

  10. IOS内购验证 (Java版)

    此处给各位贴出apple官方文档 App 内购买项目配置流程 apple 收据文档 apple 收据responseBody字段释义 IOS内购逻辑图 IOS内购验证相关代码 package xxxx ...

最新文章

  1. 信息服务器已停止工作,游戏服务器已停止工作
  2. Opencv中的FaceRecognizer类
  3. 调试JDK源码-HashSet实现原理
  4. python花钱培训值吗-Python培训班学生刚学到70%就拿到月薪万元的Offer
  5. [转]ASP.Net缓存总结
  6. Python入门字符串
  7. 互联网日报 | 6月4日 星期五 | 蚂蚁消费金融获批开业;腾讯云四个国际数据中心同步开服;滴滴App上线“老人打车”模式...
  8. 企业即时通讯设计理念及实现代码
  9. 【毕业答辩】论文答辩过不了?做好这几点,再也不用担心自己被“仍论文”
  10. NRF51822基于蓝牙协议栈SDK+FDS-Flash data storage 的使用及应用例程
  11. 微信支付接口 java服务器,JAVA微信支付接口开发——支付(示例代码)
  12. 德保罗大学计算机专业,德保罗大学计算机、信息与网络安全研究生语言及申请要求-费用-课程设置...
  13. 数学建模:回归分析——regress 函数
  14. 机械革命计算机配置,单品:机械革命X6Ti-S_机械革命笔记本电脑_笔记本评测-中关村在线...
  15. 华为5G模块MH5000-31资料全集5G技术论坛
  16. Redis如何存储热点数据
  17. 为chromium增加底部工具栏
  18. echarts+vue——散点图+折线图——技能提升
  19. Linux文件系统的压缩
  20. Keil uVision5新建工程模板

热门文章

  1. JavaScript 是如何工作的:WebRTC和对等网络的机制!
  2. Netty中有哪些自带的ChannelHandler?
  3. java使用jeids实现redis2.6的HyperLogLog数据结构的操作
  4. [javaSE] 集合框架(迭代器)
  5. 陶哲轩实分析 习题 12.5.4,12.5.5
  6. winform datagridview 自定义tooltip
  7. csv数据源的创建(一)
  8. 常用的一些注入命令!!!
  9. ORM 革命 —— 复兴 | ORM Revolution -- Revived
  10. php成长之路--1.composer下载依赖