使用框架
- 【StoreKit】不用多说了
- 【MBProgressHUD】购买过程中禁止其他操作、展示购买流程信息
- 【SAMKeychain】缓存订单等信息,防止删除app或其他原因导致的数据丢失
流程:
- 【初始化】所有代码都封装在一个单例中,app启动时初始化,初始化时会执行检测是否有未完成的交易并处理、开启监听
- 【发起购买】
[self StartToPayWithProductId:@"" orderID:@"" applicationUsername:@"" completionHandler:^(NSInteger payResult) {}];,会检测手机是否开启内购权限,未开启的话会弹框提示 - 【购买前检测】检测是否有未完成的交易并处理,无未完成的交易时,开始查询商品信息
- 【正式发起购买请求】获取到商品信息,正式发起购买请求,存储交易信息
- 【监听购买结果】
- 【获取交易凭证】
- 【验证交易凭证】
- 【交易完成】关闭交易,移除缓存的交易信息
代码如下:
注意点:
#import "AppConfig.h"是我向服务器验证凭证使用的,无需在意相关代码- (void)removeAll;此方法慎用applicationUsername由于我这里每次购买都会创建新订单,所以我用订单号拼接,保证这个参数的唯一性,以便于在处理未完成交易时匹配订单号等信息#define KCM_Key xxx是我在SAMKeychain缓存的key,最好不要写死,避免在同一设备上登录不同账号 导致的 内购数据错乱completionHandler是支付流程结束后 回调支付结果的,以便于进行其他操作- 0-未成功发起支付 1-成功 2-取消 3-失败 4-恢复购买 5-其他
- 开始购买后,如果有未完成的交易,会结束当前的交易流程:回调值为0 ,然后去处理未完成的交易,未完成的交易处理完后不会自动开始当前的交易
// KWIAPManager.h
// Runner
//
// Created by 渴望 on 2024/1/30.
//
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#import <SAMKeychain/SAMKeychain.h>
#import "MBProgressHUD.h"
#import "AppConfig.h"
NS_ASSUME_NONNULL_BEGIN
///最终支付结果回调 0-未成功发起支付 1-成功 2-取消 3-失败 4-恢复购买 5-其他
typedef void (^PaymentCompletionHandler)(NSInteger payResult);
@interface KWIAPManager : NSObject
@property (nonatomic, copy) PaymentCompletionHandler completionHandler;
- (void)callCompletionHandlerWithResult:(NSInteger)payResult;
///初始化
+ (instancetype)sharedInstance;
///开始支付
- (void)StartToPayWithProductId:(NSString *)productId
orderID:(NSString *)orderID
applicationUsername:(NSString *)applicationUsername
completionHandler:(PaymentCompletionHandler)completionHandler;
///是否有未完成交易
- (void)checkUnFinishedOrders;
///删除所有未完成的交易缓存 慎用! 特殊情况下调用清理
///目前在未发现有未完成交易时也会调用,用于清理交易取消关闭时存储的数据
- (void)removeAll
@property (nonatomic, strong) AppConfig * appconfig; //校验凭证方法类
@end
NS_ASSUME_NONNULL_END
————————————————
// KWIAPManager.m
// Runner
//
// Created by 渴望 on 2024/1/30.
//
// 由于 StoreKit2 仅支持iOS15之后的版本
// 当前仍然使用 StoreKit1
#define KCM_Key [NSString stringWithFormat:@"%@dxjdApplePay",[ZZPUtils readUserDefultsDataForKey:@"UserPhone"]]
#import "KWIAPManager.h"
**@interface** KWIAPManager () <SKPaymentTransactionObserver, SKProductsRequestDelegate>
**@property** (**nonatomic**, **copy**) NSString * productId;
**@property** (**nonatomic**, **copy**) NSString * orderID;
**@property** (**nonatomic**, **copy**) NSString * applicationUsername;
**@property** (**nonatomic**, **copy**) NSString * receipt;
**@property** (**nonatomic**, **strong**) MBProgressHUD * hud;
**@property** (**nonatomic**, **assign**) **BOOL** isRegain; //是否正在恢复未完成的交易
**@property** (**nonatomic**, **assign**) **BOOL** isRecept; //是否是验证未完成的交易
**@end**
- (**void**)callCompletionHandlerWithResult:(NSInteger)payResult {
**if** (**self**.completionHandler) {
**self**.completionHandler(payResult);
}
}
#pragma mark **- 1 单例模式初始化**
**static** KWIAPManager *sharedInstance = **nil**;
+ (**instancetype**)sharedInstance {
**static** dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[**self** alloc] init];
});
**return** sharedInstance;
}
- (**instancetype**)init {
**self** = [**super** init];
**if** (**self**) {
// 初始化操作
[[SKPaymentQueue defaultQueue] addTransactionObserver:**self**];
//[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
**self**.productId = @"";
**self**.orderID = @"";
**self**.applicationUsername = @"";
**self**.receipt = @"";
NSLog(@"🐯🐯 内购初始化 开启监听");
}
**return** **self**;
}
#pragma mark **- 2 发起购买请求**
- (**void**)StartToPayWithProductId:(NSString *)productId
orderID:(NSString *)orderID
applicationUsername:(NSString *)applicationUsername
completionHandler:(PaymentCompletionHandler)completionHandler {
**self**.completionHandler = completionHandler;
**self**.productId = productId;
**self**.orderID = orderID;
**self**.applicationUsername = applicationUsername;
NSLog(@"🐯🐯 点击开始购买");
**if** ([SKPaymentQueue canMakePayments]) {
[**self** checkUnFinishedOrders];
} **else** {
NSLog(@"🐯🐯 未开启内购权限");
[**self** callCompletionHandlerWithResult:0];
[**self** showAlert];
}
}
#pragma mark **3 处理未完成交易**
- (**void**)checkUnFinishedOrders {
NSArray * transactions = [SKPaymentQueue defaultQueue].transactions;
**if** (transactions.count > 0) {
NSLog(@"🐯🐯 有未完成的交易 %ld 条",transactions.count);
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"检测到有未完成的交易" message:@"请继续处理" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction * action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {
[**self** showMBProgressHUD:@"交易处理中..."];
**self**.isRegain = **YES**;
[**self** changeMBProgressHUDMessage:@"检测到有未完成的交易" Detail:@"正在处理..."];
[**self** dealPayResult:transactions];
[**self** callCompletionHandlerWithResult:0];
}];
[alert addAction:action];
UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
[rootViewController presentViewController:alert animated:**YES** completion:**nil**];
**return**;
}
//有未验证的交易凭证
NSMutableArray * payArr = [[NSMutableArray alloc]init];
NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
**if** (jsonString.length > 0) {
NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
**if** (error) {
payArr = [[NSMutableArray alloc]init];
}
}
NSMutableArray * dealArr = [[NSMutableArray alloc]init];
**for** (NSDictionary * dic **in** payArr) {
NSString * receipt = dic[@"receipt"];
**if** (receipt != **nil** || receipt != **NULL** || receipt.length > 0) {
[dealArr addObject:dic];
}
}
**if** (dealArr.count > 0) {
NSLog(@"🐯🐯 有未验证的交易 %ld 条",transactions.count);
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"检测到有未验证的交易" message:@"请继续验证" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction * action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {
[**self** showMBProgressHUD:@"交易处理中..."];
**self**.isRecept = **YES**;
[**self** changeMBProgressHUDMessage:@"检测到有未验证的交易" Detail:@"正在验证..."];
NSDictionary * dic = dealArr.firstObject;
**self**.applicationUsername = dic[@"applicationUsername"];
**self**.orderID = dic[@"orderID"];
**self**.productId = dic[@"productId"];
**self**.receipt = dic[@"receipt"];
[**self** verifyReceipt:[SKPaymentTransaction new]];
[**self** callCompletionHandlerWithResult:0];
}];
[alert addAction:action];
UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
[rootViewController presentViewController:alert animated:**YES** completion:**nil**];
**return**;
}
//会存在交易完成,关闭交易,但是未清理缓存的情况
//无未关闭的交易信息时,清理缓存内的交易信息
[**self** removeAll];
NSLog(@"🐯🐯 无未完成的交易");
[**self** startProductsRequest];
}
- (**void**)startProductsRequest {
**self**.isRegain = **NO**;
//直接检测未完成交易时不会调用
**if** (**self**.productId.length > 0) {
[**self** showMBProgressHUD:@"支付中..."];
NSLog(@"🐯🐯 开始查询商品信息");
NSSet *set = [NSSet setWithArray:@[**self**.productId]];
SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
productRequest.delegate = **self**;
[productRequest start];
}
}
#pragma mark **- 4 查询商品信息回调**
- (**void**)productsRequest:(**nonnull** SKProductsRequest *)request didReceiveResponse:(**nonnull** SKProductsResponse *)response {
NSArray *product = response.products;
**if** (product.count == 0) {
NSLog(@"🐯🐯 无法获取商品信息,请重试");
[**self** hideMBProgressHUD:@"支付失败" Detail:@"无法获取商品信息,请重试"];
[**self** callCompletionHandlerWithResult:0];
} **else** {
NSLog(@"🐯🐯 发起购买请求");
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product[0]];
payment.applicationUsername = **self**.applicationUsername;
payment.productIdentifier = **self**.productId;
//存储交易信息
NSDictionary * dic = @{@"applicationUsername":**self**.applicationUsername,
@"orderID": **self**.orderID,
@"productId": **self**.productId,
@"receipt": @""
};
[**self** save:dic];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
}
#pragma mark **5 购买结果回调**
- (**void**)paymentQueue:(**nonnull** SKPaymentQueue *)queue updatedTransactions:(**nonnull** NSArray<SKPaymentTransaction *> *)transactions {
[**self** dealPayResult:transactions];
}
- (**void**)dealPayResult:(NSArray<SKPaymentTransaction *>*)transactions{
**for** (SKPaymentTransaction * tran **in** transactions){
**switch** (tran.transactionState) {
**case** SKPaymentTransactionStatePurchasing:
{
NSLog(@"🐯🐯 正在支付");
}
**break**;
**case** SKPaymentTransactionStatePurchased:
{
NSLog(@"🐯🐯 支付完成");
[**self** getReceipt:tran];
}
**break**;
**case** SKPaymentTransactionStateRestored:
{
NSLog(@"🐯🐯 已经购买过商品,恢复购买");
[**self** finish:tran];
}
**break**;
**case** SKPaymentTransactionStateFailed:
{
NSLog(@"🐯🐯 支付失败%@",tran.error);
[**self** finish:tran];
}
**break**;
**default**:
{
NSLog(@"🐯🐯 --");
[**self** finish:tran];
}
**break**;
}
}
}
#pragma mark **6 获取交易凭证**
- (**void**)getReceipt:(SKPaymentTransaction *)tran {
NSLog(@"🐯🐯 开始获取交易凭证");
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
**if** ([[NSFileManager defaultManager] fileExistsAtPath:[receiptUrl path]]) {
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
**self**.receipt = [receiptData base64EncodedStringWithOptions:0];
NSLog(@"🐯🐯 交易凭证:%@",**self**.receipt);
//存储交易信息
NSDictionary * dic = @{@"applicationUsername":**self**.applicationUsername,
@"orderID": **self**.orderID,
@"productId": **self**.productId,
@"receipt": **self**.receipt
};
[**self** save:dic];
//验证票据
[**self** verifyReceipt:tran];
} **else** {
NSLog(@"Receipt request done but there is no receipt");
}
}
#pragma mark **- 7 验证票据**
- (**void**)verifyReceipt:(SKPaymentTransaction *)tran{
**if** (**self**.isRegain) {
NSLog(@"🐯🐯 处理未完成的交易时 恢复订单号等信息");
NSMutableArray * payArr = [[NSMutableArray alloc]init];
NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
**if** (jsonString.length > 0) {
NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
**if** (error) {
payArr = [[NSMutableArray alloc]init];
}
}
**if** (payArr.count > 0) {
**for** (NSDictionary * paydic **in** payArr) {
**if** ([paydic[@"applicationUsername"] isEqualToString:tran.payment.applicationUsername]) {
**self**.productId = paydic[@"productId"];
**self**.orderID = paydic[@"orderID"];
**self**.applicationUsername = paydic[@"applicationUsername"];
**break**;
}
}
}
}
NSLog(@"🐯🐯 开始验证票据");
[**self** changeMBProgressHUDMessage:@"支付成功" Detail:@"正在验证交易内容..."];
//测试验证失败
// NSLog(@"🐯🐯 验证失败 %@",KCM_Key);
// [self hideMBProgressHUD:@"验证失败" Detail:@""];
// [self callCompletionHandlerWithResult:5];
[**self**.appconfig verifyReceipt:**self**.receipt orderId:**self**.orderID resultHandler:^(**BOOL** result) {
**if** (result) {
NSLog(@"🐯🐯 验证成功");
[**self** hideMBProgressHUD:@"验证成功" Detail:@""];
[**self** callCompletionHandlerWithResult:1];
//关闭交易
[**self** finish:tran];
}**else**{
NSLog(@"🐯🐯 验证失败 %@",KCM_Key);
[**self** hideMBProgressHUD:@"验证失败" Detail:@""];
[**self** callCompletionHandlerWithResult:5];
}
}];
}
#pragma mark **- 8 关闭当前交易**
- (**void**)finish:(SKPaymentTransaction *)tran {
NSLog(@"🐯🐯 关闭交易");
**if** (**self**.isRecept || **self**.isRegain) {
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"为确保您的购买内容准确发放,\n请重启应用!" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction * action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {
}];
[alert addAction:action];
UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
[rootViewController presentViewController:alert animated:**YES** completion:**nil**];
}
//删除缓存
[**self** remove:**self**.applicationUsername];
//重置数据
**self**.productId = @"";
**self**.orderID = @"";
**self**.applicationUsername = @"";
**self**.receipt = @"";
**self**.isRegain = **NO**;
**self**.isRecept = **NO**;
NSLog(@"%ld",[SKPaymentQueue defaultQueue].transactions.count);
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
**if** (tran.transactionState == SKPaymentTransactionStatePurchased) {
//支付完成状态
} **else** **if** (tran.transactionState == SKPaymentTransactionStateRestored) {
[**self** hideMBProgressHUD:@"已恢复购买" Detail:@""];
[**self** callCompletionHandlerWithResult:4];
} **else** **if** (tran.transactionState == SKPaymentTransactionStateFailed) {
**if** (tran.error.code == SKErrorPaymentCancelled) {
[**self** hideMBProgressHUD:@"已取消支付" Detail:@""];
[**self** callCompletionHandlerWithResult:2];
} **else** {
[**self** hideMBProgressHUD:@"支付失败" Detail:tran.error.localizedDescription];
[**self** callCompletionHandlerWithResult:3];
}
} **else** {
[**self** hideMBProgressHUD:@"" Detail:@""];
[**self** callCompletionHandlerWithResult:5];
}
}
#pragma mark **- 存储交易信息**
- (**void**)save:(NSDictionary *)dic{
NSMutableArray * payArr = [[NSMutableArray alloc]init];
NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
**if** (jsonString.length > 0) {
NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
**if** (error) {
payArr = [[NSMutableArray alloc]init];
}
}
NSLog(@"🐯🐯 存储交易信息 原:%ld条",payArr.count);
NSMutableArray *itemsToRemove = [NSMutableArray array];
**for** (NSDictionary * paydic **in** payArr) {
**if** ([paydic[@"applicationUsername"] isEqualToString:dic[@"applicationUsername"]]) {
[itemsToRemove addObject:paydic];
}
}
[payArr removeObjectsInArray:itemsToRemove];
[payArr addObject:dic];
NSLog(@"🐯🐯 存储交易信息 现:%ld条",payArr.count);
NSError *error1;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payArr options:NSJSONWritingPrettyPrinted error:&error1];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[SAMKeychain setPassword:jsonString1 forService:KCM_Key account:KCM_Key];
}
#pragma mark **- 删除交易信息**
- (**void**)remove:(NSString *)applicationUsername {
NSMutableArray * payArr = [[NSMutableArray alloc]init];
NSString * jsonString = [SAMKeychain passwordForService:KCM_Key account:KCM_Key];
**if** (jsonString.length > 0) {
NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
payArr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
**if** (error) {
payArr = [[NSMutableArray alloc]init];
}
}
NSLog(@"🐯🐯 删除交易信息 原:%ld条",payArr.count);
**for** (NSDictionary * paydic **in** payArr) {
**if** ([paydic[@"applicationUsername"] isEqualToString:applicationUsername]) {
[payArr removeObject:paydic];
}
}
NSLog(@"🐯🐯 删除交易信息 现:%ld条",payArr.count);
NSError *error1;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payArr options:NSJSONWritingPrettyPrinted error:&error1];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[SAMKeychain setPassword:jsonString1 forService:KCM_Key account:KCM_Key];
}
#pragma mark **- 删除所有交易信息**
- (**void**)removeAll {
[SAMKeychain setPassword:@"" forService:KCM_Key account:KCM_Key];
}
#pragma mark **- 加载框**
- (**void**)showMBProgressHUD:(NSString *)text {
dispatch_async(dispatch_get_main_queue(), ^{
// 在主线程上访问 MBProgressHUD
**self**.hud = [MBProgressHUD showHUDAddedTo:[UIApplication sharedApplication].keyWindow animated:**YES**];
**self**.hud.mode = MBProgressHUDModeIndeterminate;
**self**.hud.labelText = text; //
**self**.hud.detailsLabelText = @"";
**self**.hud.labelFont = [UIFont systemFontOfSize:15 weight:UIFontWeightBold];
**self**.hud.labelFont = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
[**self**.hud show:**YES**];
});
}
- (**void**)changeMBProgressHUDMessage:(NSString *)message Detail:(NSString *)detail {
dispatch_async(dispatch_get_main_queue(), ^{
**self**.hud.labelText = message;
**self**.hud.detailsLabelText = detail;
});
}
- (**void**)hideMBProgressHUD:(NSString *)message Detail:(NSString *)detail {
dispatch_async(dispatch_get_main_queue(), ^{
**if** (message.length == 0 && detail.length == 0) {
[MBProgressHUD hideAllHUDsForView:[UIApplication sharedApplication].keyWindow animated:**YES**];
//[self.hud hide:YES];
}**else**{
[MBProgressHUD hideAllHUDsForView:[UIApplication sharedApplication].keyWindow animated:**YES**];
//[self.hud hide:YES];
**self**.hud = [MBProgressHUD showHUDAddedTo:[UIApplication sharedApplication].keyWindow animated:**YES**];
**self**.hud.mode = MBProgressHUDModeText;
**self**.hud.labelText = message;
**self**.hud.detailsLabelText = detail;
[**self**.hud hide:**YES** afterDelay:2];
}
});
}
#pragma mark **- 权限Alert**
- (**void**)showAlert {
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"您未开启内购权限\n开启方法:在【手机设置】中搜索【内容与隐私限制】,点击【iTunes Store与App Store购买项目】,将【App内购买项目】设置为【允许】" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction * action = [UIAlertAction actionWithTitle:@"我知道了" style:UIAlertActionStyleDefault handler:^(UIAlertAction * **_Nonnull** action) {
}];
[alert addAction:action];
UIViewController *rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
[rootViewController presentViewController:alert animated:**YES** completion:**nil**];
}
**@end**