一直觉得自己写的不是技术,而是情怀,一个个的教程是自己这一路走来的痕迹。靠专业技能的成功是最具可复制性的,希望我的这条路能让你们少走弯路,希望我能帮你们抹去知识的蒙尘,希望我能帮你们理清知识的脉络,希望未来技术之巅上有你们也有我。
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登录拿不到凭证就无法继续完成支付。