Files
peko-ios/YuMi/Modules/YMMine/View/Recharge/IAPManager.m

598 lines
22 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// IAPManager.m
// YuMi
//
// Created by P on 2024/9/24.
//
#import "IAPManager.h"
#import <Bugly/Bugly.h>
#import "Api+Mine.h"
#import "YuMi-swift.h"
#import "RechargeStorage.h"
#define MAX_RETRY_COUNT 10
@interface IAPManager()
@property (nonatomic, assign) BOOL isLogin;
@property (nonatomic, assign) NSInteger recheckInterval;
@property (nonatomic, assign) NSInteger recheckIndex;
@property (nonatomic, strong) NSTimer *recheckTimer;
@property (nonatomic, assign) BOOL isProcessing;
@property (nonatomic, copy) NSString *orderID;
@property (nonatomic, copy) NSString *transactionID;
@property (nonatomic, copy) void(^successPurchase)(NSString *transactionID, NSString *orderID);
@property (nonatomic, copy) void(^successRecheck)(void);
@property (nonatomic, copy) void(^failurePurchase)(NSError *error);
@property (nonatomic, copy) void(^contactCustomerService)(NSString *uid);
// 记录交易重试次数
@property (nonatomic, strong) NSMutableDictionary *retryCountMap;
@end
@implementation IAPManager
+ (instancetype)sharedManager {
static IAPManager *proxy;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
proxy = [[self alloc] init];
proxy.recheckIndex = 0;
proxy.recheckInterval = 1.0;
proxy.retryCountMap = [NSMutableDictionary dictionary];
});
return proxy;
}
- (void)handleLogin {
self.isLogin = YES;
}
- (void)handleLogout {
self.isLogin = NO;
if (self.recheckTimer) {
[self.recheckTimer invalidate];
}
}
- (NSString *)fetchEncodedReceipt {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
if (!receiptData) return nil;
return [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
}
- (BOOL)isValidTransactionID:(NSString *)tID orderID:(NSString *)orderID {
return (tID.length > 0 && orderID.length > 0);
}
// 开始正常内购流程
- (void)purchase:(NSString *)productId
success:(void(^)(NSString *transactionID, NSString *orderID))success
failure:(void(^)(NSError *error))failure
contactCS:(void(^)(NSString *uid))contactCS {
self.successPurchase = success;
self.failurePurchase = failure;
self.contactCustomerService = contactCS;
[self handleIAPState];
[self requestAPPOrderData:productId isFroRecheck:NO];
}
// 定时轮训未完成的订单并进行验单
- (void)retryCheckAllReceipt {
// 先清理旧定时器
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
// 设置最大重试间隔
NSTimeInterval interval = MIN(self.recheckInterval, 300.0);
// 使用传统定时器创建方式避免在旧版iOS上的兼容性问题
self.recheckTimer = [NSTimer scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(handleRetryCheckReceipt)
userInfo:nil
repeats:NO];
}
// 验单逻辑
- (void)handleRetryCheckReceipt {
// NSLog(@"[YuMi IAP] 用户触发补单检查 - Retry checking receipts");
NSArray *array = [RechargeStorage getAllReceiptsWithUid:[AccountInfoStorage instance].getUid];
// NSLog(@" ------------.------------ 尝试:%@", array);
@synchronized (array) {
if (array.count == 0 || self.isProcessing) {
return;
}
if (!self.isLogin) {
return;
}
if (self.recheckIndex >= array.count) {
self.recheckIndex = 0;
}
#if DEBUG
// [self requestAPPOrderData:@"com.hflighting.yumi.gold.1_7000" isFroRecheck:YES];
#endif
self.isProcessing = YES;
NSDictionary *dic = [array xpSafeObjectAtIndex:self.recheckIndex];
NSString *transactionId = dic[@"transactionId"];
[self _logToBugly:transactionId oID:dic[@"orderId"] status:0];
NSInteger retryCount = [self getRetryCountForTransaction:transactionId];
if (retryCount > MAX_RETRY_COUNT) {
// 超过最大重试次数,记录并清理
[self _logToBugly:transactionId oID:dic[@"orderId"] status:4]; // 新状态:重试次数过多
[RechargeStorage delegateTransactionId:transactionId uid:[AccountInfoStorage instance].getUid];
self.isProcessing = NO;
return;
}
// 增加重试计数
[self incrementRetryCountForTransaction:transactionId];
@kWeakify(self);
[self backgroundCheckReceiptWithTransactionID:transactionId
orderID:dic[@"orderId"]
next:^(BOOL isSuccess){
@kStrongify(self);
if (isSuccess) {
[RechargeStorage delegateTransactionId:transactionId
uid:[AccountInfoStorage instance].getUid];
self.recheckInterval = MIN(self.recheckInterval * 2, 300.0);
[self _logToBugly:transactionId oID:dic[@"orderId"] status:1];
// 成功后移除重试记录
[self removeRetryCountForTransaction:transactionId];
} else {
self.recheckInterval = self.recheckInterval * 2;
[self _logToBugly:transactionId oID:dic[@"orderId"] status:2];
}
self.recheckIndex += 1;
self.isProcessing = NO;
[self.recheckTimer invalidate];
[self retryCheckAllReceipt];
}];
}
}
- (void)_logToBugly:(NSString *)tid oID:(NSString *)oid status:(NSInteger)status {
NSMutableDictionary *logDic = [NSMutableDictionary dictionary];
[logDic setObject:tid forKey:@"内购 transactionId"];
[logDic setObject:oid forKey:@"内购 orderId"];
[logDic setObject:[AccountInfoStorage instance].getUid forKey:@"内购 用户id"];
NSString *statusMsg = @"";
NSInteger code = -20000;
switch (status) {
case 0:
statusMsg = [NSString stringWithFormat:@"UID: %@, 尝试验单",
[AccountInfoStorage instance].getUid];
break;
case 1:
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单成功",
[AccountInfoStorage instance].getUid];
code = -20001;
break;
case 2:
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单失败",
[AccountInfoStorage instance].getUid];
code = -20002;
break;
case 3:
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单 id 异常",
[AccountInfoStorage instance].getUid];
code = -20002;
break;
case 4:
statusMsg = [NSString stringWithFormat:@"UID: %@, 重试次数过多",
[AccountInfoStorage instance].getUid];
code = -20003;
break;
default:
break;
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[Bugly reportError:[NSError errorWithDomain:statusMsg
code:code
userInfo:logDic]];
});
}
// 内购成功并通知外部
- (void)handleSuccessPurchase:(NSString *)tID order:(NSString *)orderID {
if (self.successPurchase) {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
self.successPurchase(tID, orderID);
});
}
if (self.successRecheck) {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
self.successRecheck();
});
}
}
// 内购失败并通知外部
- (void)handleFailurePurchase:(NSString *)errorMsg {
if (self.failurePurchase) {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
if (errorMsg.length == 0) {
self.failurePurchase(nil);
} else {
self.failurePurchase([NSError errorWithDomain:errorMsg code:-1 userInfo:nil]);
}
});
}
}
// 获取客服内容
- (void)handleContactCS {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
TTAlertConfig *config = [[TTAlertConfig alloc]init];
config.title = YMLocalizedString(@"XPIAPRechargeViewController7");
config.message = YMLocalizedString(@"XPIAPRechargeViewController8");
TTAlertButtonConfig *confirmButtonConfig = [[TTAlertButtonConfig alloc]init];
confirmButtonConfig.title = YMLocalizedString(@"XPIAPRechargeViewController9");
UIImage *image = [UIImage gradientColorImageFromColors:@[UIColorFromRGB(0x13E2F5),UIColorFromRGB(0x9DB4FF),UIColorFromRGB(0xCC67FF)] gradientType:GradientTypeLeftToRight imgSize:CGSizeMake(200, 200)];
confirmButtonConfig.backgroundColor = [UIColor colorWithPatternImage:image];
confirmButtonConfig.cornerRadius = 38/2;
config.confirmButtonConfig = confirmButtonConfig;
@kWeakify(self);
[TTPopup alertWithConfig:config confirmHandler:^{
@kStrongify(self);
[self loadCSUid];
} cancelHandler:^{}];
});
}
// 获取客服 UID
- (void)loadCSUid {
if (self.contactCustomerService) {
[Api requestContactCustomerServiceCompletion:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
NSString *uid = [NSString stringWithFormat:@"%@",data.data];
self.contactCustomerService(uid);
}
}];
}
}
// 监听内购流程状态
- (void)handleIAPState {
if (@available(iOS 15.0, *)) {
@kWeakify(self);
[[PIIAPRegulate shared] setConditionBlock:^(enum StoreConditionResult state, NSDictionary<NSString *,NSString *> * _Nullable param) {
@kStrongify(self);
switch (state) {
case StoreConditionResultStart:
// NSLog(@"~~~```~~~ Purchase started");
break;
case StoreConditionResultPay:
// NSLog(@"~~~```~~~ Processing payment");
break;
case StoreConditionResultVerifiedServer:
// NSLog(@"~~~```~~~ Verified by server");
[self handleIAPSuccess:param];
break;
case StoreConditionResultUserCancelled:
// NSLog(@"~~~```~~~ User cancelled purchase");
[self handleFailurePurchase:@""];
break;
case StoreConditionResultNoProduct:
// NSLog(@"~~~```~~~ No product found");
[self handleFailurePurchase:[NSString stringWithFormat:@"%@ No product found", YMLocalizedString(@"XPIAPRechargeViewController1")]];
break;
case StoreConditionResultFailedVerification:
// NSLog(@"~~~```~~~ Verification failed");
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController1")];
break;
case StoreConditionResultUnowned:
// NSLog(@"~~~```~~~ Result Unowned");
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController1")];
break;
default:
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController0")];
break;
}
}];
}
}
// 生成后端订单
- (void)requestAPPOrderData:(NSString *)productId isFroRecheck:(BOOL)isFroRecheck {
if (@available(iOS 15.0, *)) {
@kWeakify(self);
[Api requestIAPRecharge:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (isFroRecheck) {
return;
}
@kStrongify(self);
if (code == 200) {
NSString *orderId = (NSString *)data.data[@"recordId"];
NSString *uuid = (NSString *)data.data[@"appAccountToken"];
[self requestIAPOrder:orderId
productID:productId
uuid:uuid];
} else if (code == 50000) {
[self handleContactCS];
[self handleFailurePurchase:@""];
} else {
[self handleFailurePurchase:msg];
}
}
chargeProdId:productId
uid:[AccountInfoStorage instance].getUid
ticket:[AccountInfoStorage instance].getTicket
deviceInfo:[YYUtility deviceID]
clientIp:[YYUtility ipAddress]];
} else {
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController10")];
}
}
// 获取后端订单后,查询线上是否有对应 productID 的物品如果有会自动拉起付费弹窗PIIAPRegulate - purchase
- (void)requestIAPOrder:(NSString *)orderID productID:(NSString *)productID uuid:(NSString *)uuid {
self.orderID = orderID;
if (@available(iOS 15.0, *)) {
// @kWeakify(self);
[[PIIAPRegulate shared] demandCommodityThingWithProductId:productID
uuid:uuid
completionHandler:^(NSError * _Nullable error) {
// @kStrongify(self);
if (error) {
// 已在 ConditionBlock 中回调
}
}];
}
}
// 处理内购付款成功
- (void)handleIAPSuccess:(NSDictionary *)param {
id tid = param[@"transactionId"];
self.transactionID = tid;
if (self.transactionID.length == 0) {
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController1")];
return;
}
[self saveTransactionID];
[self checkReceiptWithTransactionID:self.transactionID
orderID:self.orderID];
}
// 内购付款成功后保存收据 id 到钥匙串
- (void)saveTransactionID {
NSString *encodedReceipt = [self fetchEncodedReceipt];
NSMutableDictionary *receiptInfo = [NSMutableDictionary dictionary];
[self addValueIfNotNil:self.transactionID forKey:@"transactionId" toDictionary:receiptInfo];
[self addValueIfNotNil:encodedReceipt forKey:@"receipt" toDictionary:receiptInfo];
[self addValueIfNotNil:self.orderID forKey:@"orderId" toDictionary:receiptInfo];
// 添加时间戳便于后续判断过期交易
[receiptInfo setObject:[[NSDate date] description] forKey:@"timestamp"];
@synchronized (self.transactionID) {
[RechargeStorage saveTransactionId:self.transactionID
receipt:[receiptInfo toJSONString]
uid:[AccountInfoStorage instance].getUid];
}
}
// 通过苹果收据与后端订单进行验单与发货
// MARK: 必须等待结果
- (void)checkReceiptWithTransactionID:(NSString *)tID
orderID:(NSString *)orderID {
// NSLog(@" ------------.------------ 后端验单:%@ | %@", tID, orderID);
@kWeakify(self);
[Api checkReceipt:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
@kStrongify(self);
if (code == 200) {
[self handleSuccessPurchase:tID order:orderID];
[self handleCheckReceiptSuccess:tID isFromRecheck:NO];
} else {
[self handleFailurePurchase:msg];
if (code == 1444) {
// 参数异常,暂不处理
}
}
// NSLog(@" ------------.------------ 后端验单结果:%@ ",msg);
}
chooseEnv:@"true"
chargeRecordId:orderID
transcationId:tID
uid:[AccountInfoStorage instance].getUid
ticket:[AccountInfoStorage instance].getTicket];
}
// 通过苹果收据与后端订单进行验单与发货,此方法是对本地未删除的订单进行检测
- (void)backgroundCheckReceiptWithTransactionID:(NSString *)tID
orderID:(NSString *)orderID
next:(void(^)(BOOL isSuccess))next {
// 创建后台任务标识
UIBackgroundTaskIdentifier backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"VerifyReceipt"
expirationHandler:^{
// 超时处理
if (next) {
next(NO);
}
[[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
}];
// 原验证代码...
// 完成后务必结束后台任务
@kWeakify(self);
[Api checkReceipt:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
@kStrongify(self);
// 原有验证代码...
// 结束后台任务
[[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
}
chooseEnv:@"true"
chargeRecordId:orderID
transcationId:tID
uid:[AccountInfoStorage instance].getUid
ticket:[AccountInfoStorage instance].getTicket];
}
// 查找在缓存中的 transactionID 并告知 apple 订单已完成
- (void)handleCheckReceiptSuccess:(NSString *)tID
isFromRecheck:(BOOL)isFromRecheck {
if (@available(iOS 15.0, *)) {
@kWeakify(self);
[[PIIAPRegulate shared] verifyBusinessAccomplishWithTransactionID:tID
completionHandler:^(BOOL success, NSError * _Nullable error) {
@kStrongify(self);
if (success) {
// NSLog(@" ------------.------------ apple 验单成功");
// 流程完成,移除本地缓存账单
[RechargeStorage delegateTransactionId:tID
uid:[AccountInfoStorage instance].getUid];
// 成功后移除重试记录
[self removeRetryCountForTransaction:tID];
} else {
// 出现异常
// NSLog(@" ------------.------------ apple 验单成功:%@ ",error);
if (error == nil) {
// 该订单在 appstore 已无法找到,不必再重试
[RechargeStorage delegateTransactionId:tID
uid:[AccountInfoStorage instance].getUid];
// 清理不存在的交易记录
[self removeRetryCountForTransaction:tID];
} else {
[self retryCheckAllReceipt];
}
}
if (!isFromRecheck) {
// 正常购买情况,重置内存订单数据
self.orderID = @"";
self.transactionID = @"";
}
}];
}
}
- (void)addValueIfNotNil:(id)value
forKey:(NSString *)key
toDictionary:(NSMutableDictionary *)dictionary {
if (value != nil && key != nil) {
dictionary[key] = value;
}
}
// 添加 dealloc 方法确保定时器释放
- (void)dealloc {
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
}
// 在应用进入后台时暂停定时器
- (void)applicationDidEnterBackground {
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
}
// 在应用恢复活动时重新启动定时器
- (void)applicationWillEnterForeground {
if (self.isLogin) {
[self retryCheckAllReceipt];
}
}
// 清理长时间未处理的交易记录
- (void)cleanupStaleTransactions {
NSArray *receipts = [RechargeStorage getAllReceiptsWithUid:[AccountInfoStorage instance].getUid];
NSDate *now = [NSDate date];
for (NSDictionary *receipt in receipts) {
// 检查交易时间如果超过7天还未成功则清理
NSString *timestamp = receipt[@"timestamp"];
if (timestamp) {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; // 根据实际时间格式调整
NSDate *transactionDate = [dateFormatter dateFromString:timestamp];
if (transactionDate && [now timeIntervalSinceDate:transactionDate] > 70 * 24 * 60 * 60) {
NSString *tID = receipt[@"transactionId"];
[RechargeStorage delegateTransactionId:tID uid:[AccountInfoStorage instance].getUid];
[self _logToBugly:tID oID:receipt[@"orderId"] status:5]; // 新状态:过期清理
// 清理过期交易的重试记录
[self removeRetryCountForTransaction:tID];
}
}
}
}
// 获取交易的重试次数
- (NSInteger)getRetryCountForTransaction:(NSString *)transactionId {
NSNumber *countNumber = self.retryCountMap[transactionId];
return countNumber ? [countNumber integerValue] : 0;
}
// 增加交易的重试次数
- (void)incrementRetryCountForTransaction:(NSString *)transactionId {
NSInteger currentCount = [self getRetryCountForTransaction:transactionId];
self.retryCountMap[transactionId] = @(currentCount + 1);
}
// 移除交易的重试记录
- (void)removeRetryCountForTransaction:(NSString *)transactionId {
[self.retryCountMap removeObjectForKey:transactionId];
}
// 清理所有重试记录
- (void)cleanAllRetryCount {
[self.retryCountMap removeAllObjects];
}
@end