From 1eeeef266bc072045657458dfb852e66a3e7f4ec Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Fri, 12 Dec 2025 14:46:38 +0800 Subject: [PATCH] 1 --- Shared/KBAPI.h | 1 + keyBoard.xcodeproj/project.pbxproj | 6 + .../Class/Pay/M/IAPVerifyTransactionObj.m | 2 + keyBoard/Class/Pay/M/KBPayProductModel.h | 44 ++++++ keyBoard/Class/Pay/M/KBPayProductModel.m | 38 +++++ keyBoard/Class/Pay/VC/KBJfPay.m | 146 +++++++++++------- keyBoard/Class/Pay/VM/PayVM.h | 13 +- keyBoard/Class/Pay/VM/PayVM.m | 36 +++++ 8 files changed, 230 insertions(+), 56 deletions(-) create mode 100644 keyBoard/Class/Pay/M/KBPayProductModel.h create mode 100644 keyBoard/Class/Pay/M/KBPayProductModel.m diff --git a/Shared/KBAPI.h b/Shared/KBAPI.h index a6fbf56..1c76f07 100644 --- a/Shared/KBAPI.h +++ b/Shared/KBAPI.h @@ -57,6 +57,7 @@ /// pay #define API_VALIDATE_RECEIPT @"/api/apple/validate-receipt" // 排行榜标签列表 +#define API_INAPP_PRODUCT_LIST @"/products/inApp/list" // 查询 type=in-app-purchase 的商品列表 /// AI #define API_AI_TALK @"/chat/talk" // 排行榜标签列表 diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 17bb9d3..173e3e4 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 04122F872EC6198C00EF7AB3 /* WMDragView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F852EC6198C00EF7AB3 /* WMDragView.m */; }; 04122F882EC6F07F00EF7AB3 /* KBFullAccessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */; }; 04122F8B2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F8A2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m */; }; + 04B5A1A22EEFA12300AAAAAA /* KBPayProductModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04B5A1A12EEFA12300AAAAAA /* KBPayProductModel.m */; }; 04122F8E2EC6F83F00EF7AB3 /* PayVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F8D2EC6F83F00EF7AB3 /* PayVM.m */; }; 04122F912EC73AF700EF7AB3 /* KBVipPay.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F902EC73AF700EF7AB3 /* KBVipPay.m */; }; 04122FAA2EC73C0100EF7AB3 /* KBVipPayHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122FA92EC73C0100EF7AB3 /* KBVipPayHeaderView.m */; }; @@ -247,6 +248,8 @@ 04122F852EC6198C00EF7AB3 /* WMDragView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WMDragView.m; sourceTree = ""; }; 04122F892EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IAPVerifyTransactionObj.h; sourceTree = ""; }; 04122F8A2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IAPVerifyTransactionObj.m; sourceTree = ""; }; + 04B5A1A02EEFA12300AAAAAA /* KBPayProductModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPayProductModel.h; sourceTree = ""; }; + 04B5A1A12EEFA12300AAAAAA /* KBPayProductModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPayProductModel.m; sourceTree = ""; }; 04122F8C2EC6F83F00EF7AB3 /* PayVM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PayVM.h; sourceTree = ""; }; 04122F8D2EC6F83F00EF7AB3 /* PayVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PayVM.m; sourceTree = ""; }; 04122F8F2EC73AF700EF7AB3 /* KBVipPay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBVipPay.h; sourceTree = ""; }; @@ -625,6 +628,8 @@ children = ( 04122F892EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.h */, 04122F8A2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m */, + 04B5A1A02EEFA12300AAAAAA /* KBPayProductModel.h */, + 04B5A1A12EEFA12300AAAAAA /* KBPayProductModel.m */, ); path = M; sourceTree = ""; @@ -1748,6 +1753,7 @@ 04122F882EC6F07F00EF7AB3 /* KBFullAccessManager.m in Sources */, 04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */, 04122F8B2EC6F7C800EF7AB3 /* IAPVerifyTransactionObj.m in Sources */, + 04B5A1A22EEFA12300AAAAAA /* KBPayProductModel.m in Sources */, 04286A062ECC81B200CE730C /* KBSkinService.m in Sources */, 0479204A2EDDCE25004E8522 /* KBUserSessionManager.m in Sources */, 04122FAD2EC73C0100EF7AB3 /* KBVipSubscribeCell.m in Sources */, diff --git a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m index 66e96e7..3c57687 100644 --- a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m +++ b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m @@ -45,8 +45,10 @@ [KBHUD showInfo:(sta == !KBBizCodeSuccess ? KBLocalized(@"Payment failed") : KBLocalized(@"Payment successful"))]; if (sta == KBBizCodeSuccess) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + [KBHUD showSuccess:@"Success"]; if (handler) handler(KBLocalized(@"Success"), nil); } else { + [KBHUD showError:@"Failed"]; if (handler) handler(KBLocalized(@"Failed"), nil); } (void)weakSelf; // keep self during block life if needed diff --git a/keyBoard/Class/Pay/M/KBPayProductModel.h b/keyBoard/Class/Pay/M/KBPayProductModel.h new file mode 100644 index 0000000..bec536b --- /dev/null +++ b/keyBoard/Class/Pay/M/KBPayProductModel.h @@ -0,0 +1,44 @@ +// +// KBPayProductModel.h +// keyBoard +// +// Created to map /products/inApp/list responses to strongly typed objects. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 内购商品模型 +@interface KBPayProductModel : NSObject +/// 主键 id +@property (nonatomic, assign) NSInteger identifier; +/// Apple IAP 商品编号 +@property (nonatomic, copy, nullable) NSString *productId; +/// 商品类型(例如 in-app-purchase) +@property (nonatomic, copy, nullable) NSString *type; +/// 商品名称(如 100、1000) +@property (nonatomic, copy, nullable) NSString *name; +/// 单位(如 金币) +@property (nonatomic, copy, nullable) NSString *unit; +/// 有效期数值(可能为 0) +@property (nonatomic, assign) NSInteger durationValue; +/// 有效期单位(如 day/week) +@property (nonatomic, copy, nullable) NSString *durationUnit; +/// 有效期天数 +@property (nonatomic, assign) NSInteger durationDays; +/// 价格 +@property (nonatomic, assign) double price; +/// 货币符号 +@property (nonatomic, copy, nullable) NSString *currency; +/// 文案描述 +@property (nonatomic, copy, nullable) NSString *productDescription; + +/// 展示金币(自动拼接单位) +- (NSString *)coinsDisplayText; +/// 展示价格,保留两位小数 +- (NSString *)priceDisplayText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/M/KBPayProductModel.m b/keyBoard/Class/Pay/M/KBPayProductModel.m new file mode 100644 index 0000000..19fd65f --- /dev/null +++ b/keyBoard/Class/Pay/M/KBPayProductModel.m @@ -0,0 +1,38 @@ +// +// KBPayProductModel.m +// keyBoard +// + +#import "KBPayProductModel.h" +#import + +@implementation KBPayProductModel + ++ (NSDictionary *)mj_replacedKeyFromPropertyName { + return @{ + @"identifier": @"id", + @"productDescription": @"description", + }; +} + +- (NSString *)coinsDisplayText { + NSString *name = self.name ?: @""; + NSString *unit = self.unit ?: @""; + if (name.length && unit.length) { + return [NSString stringWithFormat:@"%@ %@", name, unit]; + } + if (name.length) { return name; } + if (unit.length) { return unit; } + return @""; +} + +- (NSString *)priceDisplayText { + NSString *currency = self.currency ?: @""; + double priceValue = self.price; + if (currency.length > 0) { + return [NSString stringWithFormat:@"%@%.2f", currency, priceValue]; + } + return [NSString stringWithFormat:@"%.2f", priceValue]; +} + +@end diff --git a/keyBoard/Class/Pay/VC/KBJfPay.m b/keyBoard/Class/Pay/VC/KBJfPay.m index 07f5ac4..3241ad0 100644 --- a/keyBoard/Class/Pay/VC/KBJfPay.m +++ b/keyBoard/Class/Pay/VC/KBJfPay.m @@ -6,6 +6,9 @@ #import "KBJfPayCell.h" #import "FGIAPProductsFilter.h" #import "FGIAPManager.h" +#import "PayVM.h" +#import "KBPayProductModel.h" +#import "KBBizCode.h" static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; @interface KBJfPay () @@ -32,10 +35,11 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; @property (nonatomic, strong) UIButton *agreementButton; // 数据 -@property (nonatomic, strong) NSArray *data; // 简单演示数据:@{coins, price} +@property (nonatomic, strong) NSArray *data; // In-App 商品展示数据 @property (nonatomic, assign) NSInteger selectedIndex; // 当前选中项 @property (nonatomic, strong) FGIAPProductsFilter *filter; +@property (nonatomic, strong) PayVM *payVM; @end @@ -44,6 +48,9 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; - (void)viewDidLoad { [super viewDidLoad]; self.filter = [[FGIAPProductsFilter alloc] init]; + self.payVM = [PayVM new]; + self.data = @[]; + self.selectedIndex = NSNotFound; self.bgImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"my_bg_icon"]]; self.bgImageView.contentMode = UIViewContentModeScaleAspectFill; self.kb_navView.backgroundColor = [UIColor clearColor]; @@ -53,17 +60,6 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; }]; self.kb_titleLabel.text = KBLocalized(@"Points Recharge"); - // 默认数据(演示) - self.data = @[ - @{ @"coins": @690, @"price": @"$6.90",@"product_id" : @"100_coin" }, - @{ @"coins": @1280, @"price": @"$12.90" ,@"product_id" : @"vip_a_week" }, - @{ @"coins": @3290, @"price": @"$32.90" ,@"product_id" : @"100_coin" }, - @{ @"coins": @4990, @"price": @"$49.90" ,@"product_id" : @"100_coin" }, - @{ @"coins": @9990, @"price": @"$99.90" ,@"product_id" : @"100_coin" }, - @{ @"coins": @19990,@"price": @"$199.90" ,@"product_id" : @"100_coin" }, - ]; - self.selectedIndex = 0; - // 视图组装 [self.view addSubview:self.myPointsTitleLabel]; [self.view addSubview:self.pointsLabel]; @@ -139,34 +135,19 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; // 刷新 [self.collectionView reloadData]; - // 确保首次进入就出现选中态外边框与阴影 - dispatch_async(dispatch_get_main_queue(), ^{ - NSIndexPath *ip = [NSIndexPath indexPathForItem:self.selectedIndex inSection:0]; - // 让系统层面也处于选中态,便于 setSelected 同步 UI - if (ip) { - [self.collectionView selectItemAtIndexPath:ip animated:NO scrollPosition:UICollectionViewScrollPositionNone]; - } - KBJfPayCell *cell = (KBJfPayCell *)[self.collectionView cellForItemAtIndexPath:ip]; - if (cell) { [cell applySelected:YES animated:NO]; } - }); + [self fetchInAppProductList]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - // 再兜底一次(某些布局时机下,首屏 reload 后 cell 还未可见) - NSIndexPath *ip = [NSIndexPath indexPathForItem:self.selectedIndex inSection:0]; - if (ip) { - [self.collectionView selectItemAtIndexPath:ip animated:NO scrollPosition:UICollectionViewScrollPositionNone]; - } - KBJfPayCell *cell = (KBJfPayCell *)[self.collectionView cellForItemAtIndexPath:ip]; - if (cell) { [cell applySelected:YES animated:NO]; } + [self selectItemAtCurrentIndexAnimated:NO]; } #pragma mark - UICollectionView Delegate (ensure first show) - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { if (![cell isKindOfClass:KBJfPayCell.class]) { return; } KBJfPayCell *c = (KBJfPayCell *)cell; - BOOL sel = (indexPath.item == self.selectedIndex); + BOOL sel = (self.selectedIndex != NSNotFound && indexPath.item == self.selectedIndex); if (sel) { [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; } @@ -194,9 +175,9 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBJfPayCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBJfPayCellId forIndexPath:indexPath]; - NSDictionary *item = self.data[indexPath.item]; - NSString *coins = [NSString stringWithFormat:@"%@", item[@"coins"]]; - NSString *price = item[@"price"]; // 形如 "$6.90" + KBPayProductModel *model = self.data[indexPath.item]; + NSString *coins = [model coinsDisplayText]; + NSString *price = [model priceDisplayText]; [cell configCoins:coins price:price]; [cell applySelected:(indexPath.item == self.selectedIndex) animated:NO]; return cell; @@ -235,30 +216,89 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; return 30; } +#pragma mark - Data +- (void)fetchInAppProductList { + __weak typeof(self) weakSelf = self; + [self.payVM fetchInAppProductsNeedShow:YES completion:^(NSInteger sta, NSString * _Nullable msg, NSArray * _Nullable products) { + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) self = weakSelf; + if (!self) { return; } + if (sta != KBBizCodeSuccess || ![products isKindOfClass:NSArray.class]) { + self.data = @[]; + self.selectedIndex = NSNotFound; + [self.collectionView reloadData]; + NSString *tip = msg.length ? msg : KBLocalized(@"Failed to load products"); + [KBHUD showInfo:tip]; + return; + } + self.data = products ?: @[]; + self.selectedIndex = self.data.count > 0 ? 0 : NSNotFound; + [self.collectionView reloadData]; + [self selectItemAtCurrentIndexAnimated:NO]; + }); + }]; +} + +- (KBPayProductModel *)currentSelectedProductItem { + if (self.selectedIndex == NSNotFound) { return nil; } + if (self.selectedIndex < 0 || self.selectedIndex >= self.data.count) { return nil; } + id item = self.data[self.selectedIndex]; + if (![item isKindOfClass:KBPayProductModel.class]) { return nil; } + return item; +} + +- (void)selectItemAtCurrentIndexAnimated:(BOOL)animated { + if (self.selectedIndex == NSNotFound) { return; } + if (self.selectedIndex < 0 || self.selectedIndex >= self.data.count) { return; } + NSIndexPath *ip = [NSIndexPath indexPathForItem:self.selectedIndex inSection:0]; + if (!ip) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.selectedIndex == NSNotFound) { return; } + if ([self.collectionView numberOfItemsInSection:0] <= ip.item) { return; } + [self.collectionView selectItemAtIndexPath:ip animated:animated scrollPosition:UICollectionViewScrollPositionNone]; + KBJfPayCell *cell = (KBJfPayCell *)[self.collectionView cellForItemAtIndexPath:ip]; + if ([cell isKindOfClass:KBJfPayCell.class]) { + [cell applySelected:YES animated:animated]; + } + }); +} + #pragma mark - Actions - (void)onTapPayButton { - // 这里只做 UI,实际支付逻辑由调用方接入 -// if (self.selectedIndex >= 0 && self.selectedIndex < self.data.count) { -// NSDictionary *item = self.data[self.selectedIndex]; -// NSString *msg = [NSString stringWithFormat:@"购买:%@ Coins %@", item[@"coins"], item[@"price"]]; -// [KBHUD showInfo:msg]; -// } - NSString *productId = @"com.loveKey.nyx.1day"; - /// 2.获取商品信息 + KBPayProductModel *selectedItem = [self currentSelectedProductItem]; + if (!selectedItem) { + [KBHUD showInfo:KBLocalized(@"Please select a product")]; + return; + } + NSString *productId = selectedItem.productId; + if (productId.length == 0) { + [KBHUD showInfo:KBLocalized(@"Product unavailable")]; + return; + } [KBHUD show]; + __weak typeof(self) weakSelf = self; [self.filter requestProductsWith:[NSSet setWithObject:productId] completion:^(NSArray * _Nonnull products) { -// NSLog(@"====="); -// if (products.count > 0) { -// SKProduct *pro = productsp[0]; -// } -// [[FGIAPManager shared].iap buyProduct:product onCompletion:^(NSString * _Nonnull message, FGIAPManagerPurchaseRusult result) { }]; - - /// 3.支付购买 - SKProduct *product = products.firstObject; - [[FGIAPManager shared].iap buyProduct:products.firstObject onCompletion:^(NSString * _Nonnull message, FGIAPManagerPurchaseRusult result) { -// [self.view makeToast:message]; - [KBHUD dismiss]; - }]; + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) self = weakSelf; + if (!self) { return; } + SKProduct *match = nil; + for (SKProduct *product in products) { + if ([product.productIdentifier isEqualToString:productId]) { + match = product; + break; + } + } + if (!match) { + [KBHUD dismiss]; + [KBHUD showInfo:KBLocalized(@"Unable to load product information")]; + return; + } + [[FGIAPManager shared].iap buyProduct:match onCompletion:^(NSString * _Nonnull message, FGIAPManagerPurchaseRusult result) { + dispatch_async(dispatch_get_main_queue(), ^{ + [KBHUD dismiss]; + }); + }]; + }); }]; } diff --git a/keyBoard/Class/Pay/VM/PayVM.h b/keyBoard/Class/Pay/VM/PayVM.h index 125669a..2054148 100644 --- a/keyBoard/Class/Pay/VM/PayVM.h +++ b/keyBoard/Class/Pay/VM/PayVM.h @@ -5,13 +5,17 @@ #import +@class KBPayProductModel; + NS_ASSUME_NONNULL_BEGIN /// 统一的支付回调:sta 为状态码(0 成功,非 0 失败),msg 为后端返回的消息 typedef void(^KBPayCompletion)(NSInteger sta, NSString * _Nullable msg); -/// 统一状态码(与原 Swift 代码的 succCode / errorCode 语义一致) - +/// In-App 商品列表回调(返回 data 数组) +typedef void(^KBPayProductsCompletion)(NSInteger sta, + NSString * _Nullable msg, + NSArray * _Nullable products); @interface PayVM : NSObject @@ -22,7 +26,10 @@ typedef void(^KBPayCompletion)(NSInteger sta, NSString * _Nullable msg); needShow:(BOOL)needShow completion:(KBPayCompletion)completion; +/// 查询 type=in-app-purchase 的商品列表 +- (void)fetchInAppProductsNeedShow:(BOOL)needShow + completion:(KBPayProductsCompletion)completion; + @end NS_ASSUME_NONNULL_END - diff --git a/keyBoard/Class/Pay/VM/PayVM.m b/keyBoard/Class/Pay/VM/PayVM.m index ecbd9bc..0b1f0dd 100644 --- a/keyBoard/Class/Pay/VM/PayVM.m +++ b/keyBoard/Class/Pay/VM/PayVM.m @@ -7,6 +7,8 @@ #import "KBAPI.h" #import "KBHUD.h" #import "KBBizCode.h" +#import "KBPayProductModel.h" +#import @implementation PayVM - (void)applePayReqWithParams:(NSDictionary *)params @@ -28,6 +30,40 @@ }]; } +- (void)fetchInAppProductsNeedShow:(BOOL)needShow + completion:(KBPayProductsCompletion)completion { + if (needShow) { [KBHUD show]; } + NSDictionary *params = @{ @"type": @"in-app-purchase" }; + [[KBNetworkManager shared] GET:API_INAPP_PRODUCT_LIST + parameters:params + headers:nil + autoShowBusinessError:NO + completion:^(NSDictionary * _Nullable json, + NSURLResponse * _Nullable response, + NSError * _Nullable error) { + if (needShow) { [KBHUD dismiss]; } + if (error) { + if (completion) { + NSInteger code = (error.code == 0 ? KBBizCodeSystemError : error.code); + completion(code, error.localizedDescription ?: KBLocalized(@"Network error"), nil); + } + return; + } + id dataObj = json[KBData] ?: json[@"data"]; + if (![dataObj isKindOfClass:[NSArray class]]) { + if (completion) { + NSString *msg = [self.class extractMessageFromResponseObject:json] ?: KBLocalized(@"Invalid response"); + completion(KBBizCodeSystemError, msg, nil); + } + return; + } + NSArray *models = [KBPayProductModel mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj]; + if (completion) { + completion(KBBizCodeSuccess, nil, models ?: @[]); + } + }]; +} + #pragma mark - Helpers //+ (NSInteger)extractStatusFromResponseObject:(id)obj response:(NSURLResponse *)resp {