// // IAPManager.m // YuMi // // Created by P on 2024/9/24. // #import "IAPManager.h" #import #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 * _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