// // IAPManager.m // YuMi // // Created by P on 2024/9/24. // #import "IAPManager.h" #import "Api+Mine.h" #import "YuMi-swift.h" #import "RechargeStorage.h" @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); @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; }); return proxy; } - (void)handleLogin { self.isLogin = YES; } - (void)handleLogout { self.isLogin = NO; if (self.recheckTimer) { [self.recheckTimer invalidate]; } } // 开始正常内购流程 - (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 { self.recheckTimer = [NSTimer scheduledTimerWithTimeInterval:self.recheckInterval target:self selector:@selector(handleRetryCheckReceipt) userInfo:nil repeats:NO]; } // 验单逻辑 - (void)handleRetryCheckReceipt { 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; } // 尝试请求一下订单号,看看能否触发后端检查用户是否有未下发内容 [self requestAPPOrderData:@"com.hflighting.yumi.gold.1_7000" isFroRecheck:YES]; self.isProcessing = YES; NSDictionary *dic = [array xpSafeObjectAtIndex:self.recheckIndex]; NSString *transactionId = dic[@"transactionId"]; [self backgroundCheckReceiptWithTransactionID:transactionId orderID:dic[@"orderId"] next:^(BOOL isSuccess){ if (isSuccess) { [RechargeStorage delegateTransactionId:transactionId uid:[AccountInfoStorage instance].getUid]; self.recheckInterval = 1.0; } else { self.recheckInterval = self.recheckInterval * 2; } self.recheckIndex += 1; self.isProcessing = NO; [self.recheckTimer invalidate]; [self retryCheckAllReceipt]; }]; } } // 内购成功并通知外部 - (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 { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; NSString *encodedReceipt = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; // 初始化字典并逐步填充 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]; // 如果字典为空,直接返回 if (receiptInfo.count == 0) { return; } // 保存数据到 RechargeStorage @synchronized (self.transactionID) { [RechargeStorage saveTransactionId:self.transactionID receipt:[receiptInfo toJSONString] uid:[AccountInfoStorage instance].getUid]; } NSLog(@" ------------.------------ 保存账单内容:%@ ", receiptInfo); } // 通过苹果收据与后端订单进行验单与发货 // 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 { NSLog(@" ------------.------------ 尝试后端验单:%@ | %@", tID, orderID); if (tID.length == 0 || orderID.length == 0) { // 本地数据异常,不重试 [self handleCheckReceiptSuccess:tID isFromRecheck:YES]; next(NO); return; } @kWeakify(self); [Api checkReceipt:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) { @kStrongify(self); if (code == 200) { [self handleCheckReceiptSuccess:tID isFromRecheck:YES]; } else if (code == 1701) { // 交易已完成,無需重複請求 [self handleCheckReceiptSuccess:tID isFromRecheck:YES]; } NSLog(@" ------------.------------ 尝试后端验单结果:%ld, %@ ", (long)code, msg); if (next) { next(code == 200 || code == 1701); } } 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]; } else { // 出现异常 NSLog(@" ------------.------------ apple 验单成功:%@ ",error); if (error == nil) { // 该订单在 appstore 已无法找到,不必再重试 [RechargeStorage delegateTransactionId:tID uid:[AccountInfoStorage instance].getUid]; } 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; } } @end