OC 技术 苹果内购

一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络,希望未来技术之巅上有你们也有我。

OC 技术 苹果内购视频解说

苹果内购(视频讲解的封装)代码下载地址

前言

之前做过内购的需求,然后把整个过程记录下来,以防将来忘记之后回忆起来。

内购流程

在这里插入图片描述

内购代码逻辑

在这里插入图片描述

代码解析

下面的图片是整个苹果内购封装好的方法,可以直接拖过去用的,如果需要修改的就只有ValidationVoucherModel这个类,需要更改为对应公司的接口,里面的代码看多几篇就熟悉了。下面的图片里面,所有类的封装文件都是为IPAPurchase这个类服务的,再开发调用主要就是使用IPAPurchase就可以了
在这里插入图片描述
下面我会根据交易的整个流程解析代码的逻辑

启动监听

首先App启动的时候需要使用单例,初始化内购的监听方法,两个作用:1.每当app进行内购支付的时候,支付成功失败都会走代理的方法。2.app每次启动都回去调用该代理看看有没有没有走完内购的流程的订单。没有就会继续走一遍完成它

class AppDelegate: UIResponder, UIApplicationDelegate {var window: UIWindow?func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {//创建内购监听IPAPurchase.manager()?.startManager()return true}
}

结束监听

app回到后台就移除监听

func applicationDidEnterBackground(_ application: UIApplication) {IPAPurchase.manager()?.stopManager()
}

购买商品的方法

在商品购买的按键方法里面,通过单例直接调用购买的封装方法就可以了

IPAPurchase.manager()?.buyProduct(withProductID: "201912040101", currentBaseUrl: "http://192.168.1.20:8082/", currentAccountID: String(User.default.accountVO.id), currentOrderID: String(), currentVC: self, payResult: { (isSuccess, result, errorMsg) inprint("result = \(String(describing: result))")if isSuccess {print("购买成功")}else{print("购买失败 - \(String(describing: errorMsg))")}
})

接下来下面的代码解析就是根据把支付的方法一个一个点进去深入解析内购的流程,解析整个代码逻辑,拆解代码详细解析

发起购买的内部方法

#pragma mark -- 发起购买的方法
-(void)buyProductWithProductID:(NSString *)productID CurrentBaseUrl:(NSString *)baseUrl CurrentAccountID:(NSString *)accountID currentOrderID:(NSString *)orderID currentVC:(UIViewController *)vc payResult:(PayResult)payResult{//保存产品IDself.productId = productID;//保存baseUrlself.baseUrl = baseUrl;//保存用户IDself.accountID = accountID;//保存订单IDself.orderID = orderID;//保存当前控制器self.vc = vc;//结束上次未完成的交易[self removeAllUncompleteTransactionsBeforeNewPurchase];//绑定闭包self.payResultBlock = payResult;//提示框购买中//如果产品ID为空if (!self.productId.length) {//显示产品ID为空UIAlertController*alet=[UIAlertController alertControllerWithTitle:nil message:@"没有相应的产品" preferredStyle:UIAlertControllerStyleAlert];UIAlertAction*sure=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];[alet addAction:sure];[vc presentViewController:alet animated:YES completion:nil];}//检测是否允许内购if ([SKPaymentQueue canMakePayments]) {//向苹果发起内购产品列表[self requestProductInfo:self.productId];}else{//请打开应用程序付费购买功能UIAlertController*alet=[UIAlertController alertControllerWithTitle:nil message:@"请打开应用程序付费购买功能" preferredStyle:UIAlertControllerStyleAlert];UIAlertAction*sure=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];[alet addAction:sure];[vc presentViewController:alet animated:YES completion:nil];}}

检测未完成的订单

在购买之前,先看看有没有上一个没有完成的订单,如果有就结束上一次未完成的订单。

//结束上次未完成的交易
[self removeAllUncompleteTransactionsBeforeNewPurchase];#pragma mark -- 结束上次未完成的交易
-(void)removeAllUncompleteTransactionsBeforeNewPurchase{//查看数组中是否有未完成的订单NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;if (transactions.count >= 1) {for (NSInteger count = transactions.count; count > 0; count--) {SKPaymentTransaction* transaction = [transactions objectAtIndex:count-1];if (transaction.transactionState == SKPaymentTransactionStatePurchased||transaction.transactionState == SKPaymentTransactionStateRestored) {[[SKPaymentQueue defaultQueue]finishTransaction:transaction];}}}else{NSLog(@"没有历史未消耗订单");}
}

检测是否允许内购

结束上一次未完成的订单后,检测是否允许内购,向苹果发起获取内购产品列表

//检测是否允许内购
if ([SKPaymentQueue canMakePayments]) {//向苹果发起内购产品列表[self requestProductInfo:self.productId];
}else{//请打开应用程序付费购买功能UIAlertController*alet=[UIAlertController alertControllerWithTitle:nil message:@"请打开应用程序付费购买功能" preferredStyle:UIAlertControllerStyleAlert];UIAlertAction*sure=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];[alet addAction:sure];[vc presentViewController:alet animated:YES completion:nil];
}

发起请求

获取代码的列表发送请求的详细代码就是主要是多请求对象准守一下代理

#pragma mark -- 发起购买请求   检索产品  去苹果开发网站查看这个商品是否存在  是否存在通过代理SKProductsRequestDelegate  返回结果
-(void)requestProductInfo:(NSString *)productID{NSArray * productArray = [[NSArray alloc]initWithObjects:productID,nil];NSSet * IDSet = [NSSet setWithArray:productArray];//把产品ID封装成一个SKProductsRequest请求对象request = [[SKProductsRequest alloc] initWithProductIdentifiers:IDSet];//对响应c方法request.delegate = self;//发送商品请求[request start];}

获取商品列表

发起请求之后,产品列表是通过代理的形式返回的,拿到商品id之后就发起支付

#pragma mark -- SKProductsRequestDelegate 查询成功后的回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{//把所有的产品检索回来NSArray *myProduct = response.products;    if (myProduct.count == 0) {//提示框,没有商品if (self.payResultBlock) {self.payResultBlock(NO, nil, @"无法获取产品信息,购买失败");}return;}//用于保存产品对象(其中产品ID就在里面)SKProduct * product = nil;//打印所有列表的相关信息for(SKProduct * pro in myProduct){NSLog(@"SKProduct 描述信息%@", [pro description]);NSLog(@"产品标题 %@" , pro.localizedTitle);NSLog(@"产品描述信息: %@" , pro.localizedDescription);NSLog(@"价格: %@" , pro.price);NSLog(@"Product id: %@" , pro.productIdentifier);if ([pro.productIdentifier isEqualToString:self.productId]) {product = pro;break;}}//如果产品不为空if (product) {//把产品添加到支付对象里面SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];//使用苹果提供的属性,将平台订单号复制给这个属性作为透传参数payment.applicationUsername = self.orderID;//保存用户ID  目的是防止漏单后,用户登录App登录了别的用户,然后App有漏单,一启动走补单流程,就充错人[[NSUserDefaults standardUserDefaults] setObject:self.accountID forKey:@"unlock_iap_userId"];//调用购买产品接口  购买成功之后会去到updatedTransactions回调[[SKPaymentQueue defaultQueue] addPayment:payment];}else{NSLog(@"没有此商品信息");}
}

支付结果代理回调

支付成功失败都会通过代理的形式在枚举中反应。

#pragma mark -- 监听结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{//当用户购买的操作有结果时,就会触发下面的回调函数,for (SKPaymentTransaction * transaction in transactions) {//根据返回的状态进行处理switch (transaction.transactionState) {//交易完成case SKPaymentTransactionStatePurchased:{[self completeTransaction:transaction];}break;//交易失败case SKPaymentTransactionStateFailed:{[self failedTransaction:transaction];}break;//已经购买过该商品case SKPaymentTransactionStateRestored:{[self restoreTransaction:transaction];}break;//正在购买中...case SKPaymentTransactionStatePurchasing:{NSLog(@"正在购买中...");//[[SKPaymentQueue defaultQueue]finishTransaction:transaction];(会蹦)}break;//最终状态未确定case SKPaymentTransactionStateDeferred:{NSLog(@"最终状态未确定");}break;default:break;}}
}

然后调用成交交易的方法

#pragma mark -- 交易完成的回调
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{//获取交易成功后的购买凭证[self getAndSaveReceipt:transaction];  
}

保存凭证,发送服务器验证

获取交易成功后的购买凭证
1.获取到凭证之后首先保存起来
2.使用后台提供的接口给后台去苹果服务器去进行验证

#pragma mark -- 获取购买凭证  并保存到Tmp文件里面去
-(void)getAndSaveReceipt:(SKPaymentTransaction *)transaction{//存储  向服务器验证凭证前的 凭证 订单号 用户ID 交易时间(简单:向服务器验证前保存一下)NSMutableDictionary *dict = [self saveServeCheskWithSaveReceipt:transaction];//向服务器发送订单ID 凭证验证  购买  然后发货,发货的动作其实就是服务器在订单的某一个属性里面修改状态。[self sendAppStoreRequestBuyWithReceipt:dict[@"receipt_key"] userId:dict[@"user_id"] paltFormOrder:dict[@"order"] trans:transaction];
}

下面的方法就是保存整个内购的过程

#pragma mark -- 存储  向服务器验证凭证前的 凭证 订单号 用户ID 交易时间(简单:向服务器验证前保存一下)
-(NSMutableDictionary *)saveServeCheskWithSaveReceipt:(SKPaymentTransaction *)transaction{//初始化字典NSMutableDictionary * dic = [[NSMutableDictionary alloc]init];//保存凭证到字典里面//获取交易凭证NSURL * receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];NSData * receiptData = [NSData dataWithContentsOfURL:receiptUrl];NSString * base64String = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];NSLog(@"base64String - %@",base64String);[dic setValue: base64String forKey:@"receipt_key"];//保存订单号到字典里面//透传参数  把刚刚购买前 transaction.payment.applicationUsername   里面存储的订单号那回来/*为什么不从上面的self.order获取你,如果这样获取就不会出现空的问题?这样想是错的。如果这样想的话,这个applicationUsername的参数就没有意义了。这个参数的设定是为了下面的坑而设定的。如果用户付款的之后,由于用户网络或者其他各种原因的问题,在没有拿到凭证之前(也就是没有进来updatedTransactions这个回调方法)把App退出。并且删了。这时候,用户重新下载App下.重新登录。由于订单没有结束(因为没有执行到)[[SKPaymentQueue defaultQueue]finishTransaction:transaction];这个方法。所以下次启动Appc注册监听之后,App马上会执行updatedTransactions的方法。方法的transaction对象会返回凭证给你,而applicationUsername属性里面存的就是你刚刚删除前的订单号。如果没有用applicationUsername设置对应凭证的订单号。就算给了凭证给你,你也不知道是哪张订单。到时候向公司服务器是验证不了的。*/NSString *order = @"";if (transaction.payment.applicationUsername != nil) {order = transaction.payment.applicationUsername;}else{order = self.orderID;}//如果这个返回为nilNSLog(@"后台订单号 -- %@",order);[dic setValue: order forKey:@"order"];//保存交易时间到字典里面[dic setValue:[self getCurrentZoneTime] forKey:@"time"];NSString * userId;//NSUserDefaults 值得注意的是对相同的key赋值约等于一次覆盖/*我觉得这里有问题。我决得。保存userID应该放在[[SKPaymentQueue defaultQueue] addPayment:payment];前面保存userid,你放在这里的话要订单凭证成功返回才保存,如果用户付钱后,拿到凭证前删除的话就GG了,反正NSUserDefaults是覆盖的形式的。这个问题有待验证。因为你不存不成功,杀死APP后下次进来就没有UserID了。为了确保成功。应该在用户付钱前就要保存*///  //下面userid不为0的情况是用户成功交易,一直留在app才能走到这里,这时候就拿上保存用户ID。
//  if (self.accountID) {//如果用户ID不为空
//    userId = self.accountID;
//    //把用户ID存起来
//    [[NSUserDefaults standardUserDefaults] setObject:userId forKey:@"unlock_iap_userId"];
//  }else{//如果用户ID为空  用户进来userid为空的原因是,用户交易未完成的情况下,下次重新打开App上会执行updatedTransactions的方法。这时候userid未空的。为空的话就拿会App关掉前保存的userid
//    //从沙盒中获取用户ID
//    userId = [[NSUserDefaults standardUserDefaults] objectForKey:@"unlock_iap_userId"];
//  }//从沙盒中获取用户IDuserId = [[NSUserDefaults standardUserDefaults] objectForKey:@"unlock_iap_userId"];//保存用户ID到字典里面[dic setValue: userId forKey:@"user_id"];//命名plist文件 生成唯一的字符串NSString *fileName = [NSString UUID];//沙盒Tmp 保存方式NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];//这个存储成功与否其实无关紧要BOOL ifWriteSuccess = [dic writeToFile:savedPath atomically:YES];if (ifWriteSuccess){NSLog(@"购买凭据存储成功!");}else{NSLog(@"购买凭据存储失败");}return dic;}

使用后台提供的接口给后台去苹果服务器去进行验证

#pragma mark -- 去服务器验证购买
-(void)sendAppStoreRequestBuyWithReceipt:(NSString *)receipt userId:(NSString *)userId paltFormOrder:(NSString * )order trans:(SKPaymentTransaction *)transaction{ValidationVoucherModel *model = [[ValidationVoucherModel alloc] init];[model getValidationVoucherRequestCurrentBaseUrl:self.baseUrl CurrentAccountID:userId currentOrderID:order currentReceipt:receipt DataSuccess:^(id _Nullable result) {NSLog(@"result - %@",result);BOOL changeOrder = result[@"data"][@"changeOrder"];if (changeOrder  == YES) {//删除  向服务器验证凭证后的 凭证 订单号 用户ID 交易时间(简单:验证成功后可以把凭证删除)[self delectedConsumptionOfGoodsWithReceipt:receipt];//存储  向服务器验证凭证前的 凭证 订单号 用户ID 交易时间(简单:向服务器验证前保存一下)[self saveServeCheskWithSaveReceipt:transaction];self.payResultBlock(YES, @"已经发货", @"");//结束订单[[SKPaymentQueue defaultQueue]finishTransaction:transaction];}else{NSLog(@"未发货");}} failedBlock:^(NSError * _Nonnull error) {NSLog(@"error - %@",error);}];}

删除本地凭证

#pragma mark -- 删除  向服务器验证凭证后的 凭证 订单号 用户ID 交易时间(简单:验证成功后可以把凭证删除)
-(void)delectedConsumptionOfGoodsWithReceipt:(NSString * )receipt{NSFileManager *fileManager = [NSFileManager defaultManager];NSError * error;if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];if (error == nil) {for (NSString * name in cacheFileNameArray) {NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];NSFileManager *fileManager = [NSFileManager defaultManager];NSError * error;NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];NSString * localReceipt = [dic objectForKey:@"receipt_key"];//通过凭证进行对比if ([receipt isEqualToString:localReceipt]) {BOOL ifRemove = [fileManager removeItemAtPath:filePath error:&error];if (ifRemove) {NSLog(@"验证凭证成功后 移除成功");}else{NSLog(@"验证凭证成功后 移除失败");}}else{NSLog(@"本地无与之匹配的订单");}}}}
}

结束交易(关键代码)

//结束订单
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];

经验

1.关于支付丢单的问题

问题1:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App退出,重新启动,账号A登录,但是这个时候 订单A丢失了缓存没了,如何解决该丢单的问题。

解答:支付是可以把订单保存到payment.applicationUsername = self.orderID;的属性中,登录原来的账号,在未完成支付(即调用:[[SKPaymentQueue defaultQueue]finishTransaction:transaction];支付会仍然继续拿到刚支付的凭证)原来的账号,原来的订单id,原来的凭证就可以继续上存后台验证登录。
其实可以把订单id做一个本地保存,安全一定,防止payment.applicationUsername = self.orderID;获取回来的订单id为空。

问题2:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App删除,重新下载,账号A登录,但是这个时候 订单A丢失了(即使保存在本地,app删除也会把订单号删除),如何解决该丢单的问题

解答:支付是可以把订单保存到payment.applicationUsername = self.orderID;的属性中,登录原来的账号,在未完成支付(即调用:[[SKPaymentQueue defaultQueue]finishTransaction:transaction];支付会仍然继续拿到刚支付的凭证)原来的账号,原来的订单id,原来的凭证就可以继续上存后台验证登录。

问题3:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App退出,重新启动,账号B登录,但是这个时候刚刚账号A付款会变成账号B吗,上存后台服务器成功后

解答:会

问题3:账号A: 订单A;支付成功了(拿到凭证),应该拿着(凭证,账号id,订单号)上存到后台验证,这个时候把App退出,换另一台手机登录账号A,账号A还能够继续完成支付吗?

解答: 我没有似过,分两种情况:如果把之前的手机苹果id退出,在另一台手机登录苹果id,app登录账号A,如果能拿到凭证就能继续完成支付,如果手机苹果id登录拿不到凭证就无法继续完成支付。

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

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

相关文章

对谈Concured首席技术官:利用AI和MongoDB打造个性化内容推荐系统

Built with MongoDB 栏目采访了AI初创企业Concured在成立约一年后加入的首席技术官 Tom Wilson&#xff0c;围绕 Concured 的人工智能使用情况、Wilson 加入团队的过程、坚持选择MongoDB的原因以及公司未来发展展开讨论。 关于Concured 内容无处不在。无论消费者寻找什么或所处…

【MATLAB源码-第16期】基于matlab的MSK定是同步仿真,采用gardner算法和锁相环。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 **锁相环&#xff08;PLL&#xff09;** 是一种控制系统&#xff0c;用于将一个参考信号的相位与一个输入信号的相位同步。它在许多领域中都有应用&#xff0c;如通信、无线电、音频、视频和计算机系统。锁相环通常由以下几个…

【Java】LinkedList vs. ArrayList:Java中的数据结构选择

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

图像处理与视觉感知---期末复习重点(4)

文章目录 一、图像复原与图像增强1.1 概述1.2 异同点 二、图像复原/退化模型2.1 模型图简介2.2 线性复原法 三、彩色基础四、彩色模型五、彩色图像处理 一、图像复原与图像增强 1.1 概述 1. 图像增强技术一般要利用人的视觉系统特性&#xff0c;目的是取得较好的视觉效果&…

《仙剑7》登陆Xbox主机平台年末大作空窗期

首发一年后&#xff0c;《仙剑奇侠传7》终于登陆Xbox主机平台&#xff0c;而这也恰逢Xbox平台年末大作的窗口期。 随着年底大作的稀缺&#xff0c;以及海外3A RPG《星空》的延期&#xff0c;2022年底的这段时间给Xbox玩家体验《刀剑7》留下了一段空白。 可以说是因祸得福。 《仙…

Web CSS笔记2

目录 1、背景 ①、背景图片(image) ②、背景平铺&#xff08;repeat&#xff09; ③、背景位置(position) ④、背景附着&#xff08;attachment&#xff09; ⑤、背景透明(CSS3) ⑥、背景图片缩放大小&#xff08;size&#xff09;&#xff1a; ⑦、背景简写 2、标签显…

并发编程之的ArrayList安全性的详细解析

ArrayList不安全 ArrayList中的add方法没有synchronized修饰&#xff0c;是不安全的 下面代码运行结果&#xff08;异常 java.util.ConcurrentModificationException&#xff09;&#xff1a; ​import java.util.ArrayList; import java.util.List; import java.util.UUID; ​…

[CISCN2019 华东北赛区]Web2

[CISCN2019 华东北赛区]Web2 随便注册一个登录&#xff0c;发现 还有反馈页面&#xff0c;一看就知道大概率是xss&#xff0c;应该是为了得到管理员cookie扫描了一下&#xff0c;果然有admin.php后台登录 buu可以连接访问外网了&#xff0c;所以内部的xss平台关闭了&#xff0…

elasticsearch _cat/indices docs.count is different than <index>/_count

今天遇到一个问题&#xff0c;kibana中看到文档数与下面语句查询到的不同 GET /_cat/count/jiankunking_xxxxx_product_expand_test?v GET /jiankunking_xxxxx_product_expand_test/_search?track_total_hitstrue语句查询结果 epoch timestamp count 1711433785 06:16…

上位机图像处理和嵌入式模块部署(qmacvisual区域提取)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在图像处理中&#xff0c;由两部分比较重要&#xff0c;一个是区域分割&#xff0c;一个是区域提取。区域分割&#xff0c;比较好理解&#xff0c;…

Spring Cloud 八:微服务架构中的数据管理

Spring Cloud 一&#xff1a;Spring Cloud 简介 Spring Cloud 二&#xff1a;核心组件解析 Spring Cloud 三&#xff1a;API网关深入探索与实战应用 Spring Cloud 四&#xff1a;微服务治理与安全 Spring Cloud 五&#xff1a;Spring Cloud与持续集成/持续部署&#xff08;CI/C…

office办公技能|word中的常见使用问题解决方案2.0

一、设置多级列表将表注从0开始&#xff0c;设置为从1开始 问题描述&#xff1a;word中插入题注&#xff0c;出来的是表0-1&#xff0c;不是1-1&#xff0c;怎么办&#xff1f; 写论文时&#xff0c;虽然我设置了“第一章”为一级标题&#xff0c;但是这三个字并不是自动插入的…