diff --git a/Shared/KBAPI.h b/Shared/KBAPI.h index 1c76f07..98db7d3 100644 --- a/Shared/KBAPI.h +++ b/Shared/KBAPI.h @@ -58,6 +58,7 @@ /// pay #define API_VALIDATE_RECEIPT @"/api/apple/validate-receipt" // 排行榜标签列表 #define API_INAPP_PRODUCT_LIST @"/products/inApp/list" // 查询 type=in-app-purchase 的商品列表 +#define API_SUBSCRIPTION_PRODUCT_LIST @"/products/subscription/list" // 查询订阅商品列表 /// AI #define API_AI_TALK @"/chat/talk" // 排行榜标签列表 diff --git a/keyBoard/Class/Pay/VC/KBVipPay.m b/keyBoard/Class/Pay/VC/KBVipPay.m index efc4876..b38a70d 100644 --- a/keyBoard/Class/Pay/VC/KBVipPay.m +++ b/keyBoard/Class/Pay/VC/KBVipPay.m @@ -9,6 +9,11 @@ #import "KBVipPayHeaderView.h" #import "KBVipSubscribeCell.h" #import "KBVipReviewListCell.h" +#import "PayVM.h" +#import "KBPayProductModel.h" +#import "FGIAPProductsFilter.h" +#import "FGIAPManager.h" +#import "KBBizCode.h" static NSString * const kKBVipHeaderId = @"kKBVipHeaderId"; static NSString * const kKBVipSubscribeCellId = @"kKBVipSubscribeCellId"; @@ -16,7 +21,7 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; @interface KBVipPay () @property (nonatomic, strong) UICollectionView *collectionView; // 主列表(竖向滚动) -@property (nonatomic, strong) NSArray *plans; // 订阅方案数组 +@property (nonatomic, strong) NSArray *plans; // 订阅方案数组 @property (nonatomic, assign) NSInteger selectedIndex; // 当前选中的方案索引 @property (nonatomic, strong) UIButton *closeButton; // 当前选中的方案索引 @property (nonatomic, strong) UIImageView *bgImageView; // 全屏背景图 @@ -27,6 +32,8 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; @property (nonatomic, strong) UIButton *payButton; // 支付按钮(背景图) @property (nonatomic, strong) UILabel *agreementLabel; // 协议提示 @property (nonatomic, strong) UIButton *agreementButton; // 《Embership Agreement》 +@property (nonatomic, strong) PayVM *payVM; +@property (nonatomic, strong) FGIAPProductsFilter *filter; @end @@ -46,13 +53,10 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; make.left.top.right.equalTo(self.view); make.height.mas_equalTo(224); }]; - // 初始化数据(简单演示) - self.plans = @[ - @{@"title":@"Monthly Subscription", @"price":@"$4.49", @"strike":@"$4.49"}, - @{@"title":@"Monthly Subscription", @"price":@"$4.49", @"strike":@"$4.49"}, - @{@"title":@"Monthly Subscription", @"price":@"$4.49", @"strike":@"$4.49"}, - ]; - self.selectedIndex = 1; // 默认选中第二项 + self.payVM = [PayVM new]; + self.filter = [[FGIAPProductsFilter alloc] init]; + self.plans = @[]; + self.selectedIndex = NSNotFound; // 组装主列表 [self.view addSubview:self.collectionView]; @@ -89,18 +93,67 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; // 预计算 Header 高度(由内部约束决定) self.headerHeight = [self kb_calcHeaderHeightForWidth:KB_SCREEN_WIDTH]; [self.collectionView reloadData]; + + [self fetchSubscriptionPlans]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - // 首次进入,确保订阅项保持选中态(避免首屏仅显示 Header,待滚动出现时没有选中样式) + [self selectCurrentPlanAnimated:NO]; +} + +#pragma mark - Data +- (void)fetchSubscriptionPlans { + __weak typeof(self) weakSelf = self; + [self.payVM fetchSubscriptionProductsNeedShow: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.plans = @[]; + self.selectedIndex = NSNotFound; + [self.collectionView reloadData]; + NSString *tip = msg.length ? msg : KBLocalized(@"Failed to load products"); + if (tip.length) { [KBHUD showInfo:tip]; } + return; + } + self.plans = products ?: @[]; + self.selectedIndex = self.plans.count > 0 ? 0 : NSNotFound; + [self.collectionView reloadData]; + [self selectCurrentPlanAnimated:NO]; + }); + }]; +} + +- (void)selectCurrentPlanAnimated:(BOOL)animated { + if (self.selectedIndex == NSNotFound) { return; } + if (self.selectedIndex < 0 || self.selectedIndex >= self.plans.count) { return; } NSIndexPath *ip = [NSIndexPath indexPathForItem:self.selectedIndex inSection:1]; if (!ip) { return; } - // 系统层面也置为选中 - [self.collectionView selectItemAtIndexPath:ip animated:NO scrollPosition:UICollectionViewScrollPositionNone]; - // 若此时 cell 不可见,willDisplay 再兜底 + [self.collectionView selectItemAtIndexPath:ip animated:animated scrollPosition:UICollectionViewScrollPositionNone]; KBVipSubscribeCell *cell = (KBVipSubscribeCell *)[self.collectionView cellForItemAtIndexPath:ip]; - if (cell) { [cell applySelected:YES animated:NO]; } + if ([cell isKindOfClass:KBVipSubscribeCell.class]) { + [cell applySelected:YES animated:animated]; + } +} + +- (KBPayProductModel *)currentSelectedPlan { + if (self.selectedIndex == NSNotFound) { return nil; } + if (self.selectedIndex < 0 || self.selectedIndex >= self.plans.count) { return nil; } + id plan = self.plans[self.selectedIndex]; + if (![plan isKindOfClass:KBPayProductModel.class]) { return nil; } + return plan; +} + +- (NSString *)displayTitleForPlan:(KBPayProductModel *)plan { + if (!plan) { return @""; } + if (plan.productDescription.length) { return plan.productDescription; } + NSString *name = plan.name ?: @""; + NSString *unit = plan.unit ?: @""; + if (name.length && unit.length) { return [NSString stringWithFormat:@"%@%@", name, unit]; } + if (name.length) { return name; } + if (unit.length) { return unit; } + return KBLocalized(@"Subscription"); } #pragma mark - Header Height Calc @@ -126,8 +179,41 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; #pragma mark - Bottom Actions - (void)onTapPayButton { - // TODO: 接入支付,这里仅做 UI - [KBHUD showInfo:KBLocalized(@"Pay clicked")]; + KBPayProductModel *plan = [self currentSelectedPlan]; + if (!plan) { + [KBHUD showInfo:KBLocalized(@"Please select a product")]; + return; + } + NSString *productId = plan.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) { + 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]; + }); + }]; + }); + }]; } - (void)agreementButtonAction{ [KBHUD showInfo:KBLocalized(@"Open agreement")]; @@ -149,9 +235,16 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 1) { KBVipSubscribeCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBVipSubscribeCellId forIndexPath:indexPath]; - NSDictionary *plan = self.plans[indexPath.item]; - [cell configTitle:plan[@"title"] price:plan[@"price"] strike:plan[@"strike"]]; - [cell applySelected:(indexPath.item == self.selectedIndex) animated:NO]; + if (indexPath.item < self.plans.count) { + KBPayProductModel *plan = self.plans[indexPath.item]; + NSString *title = [self displayTitleForPlan:plan]; + NSString *price = [plan priceDisplayText]; + [cell configTitle:title price:price strike:nil]; + [cell applySelected:(indexPath.item == self.selectedIndex) animated:NO]; + } else { + [cell configTitle:@"" price:@"" strike:nil]; + [cell applySelected:NO animated:NO]; + } return cell; } else { KBVipReviewListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBVipReviewListCellId forIndexPath:indexPath]; @@ -169,7 +262,7 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; #pragma mark - UICollectionView Delegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section != 1) { return; } + if (indexPath.section != 1 || indexPath.item >= self.plans.count) { return; } if (self.selectedIndex == indexPath.item) { return; } NSInteger old = self.selectedIndex; self.selectedIndex = indexPath.item; @@ -185,7 +278,7 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { // 兜底:当订阅项第一次出现在屏幕上,强制同步选中样式 - if (indexPath.section == 1 && [cell isKindOfClass:KBVipSubscribeCell.class]) { + if (indexPath.section == 1 && indexPath.item < self.plans.count && [cell isKindOfClass:KBVipSubscribeCell.class]) { BOOL sel = (indexPath.item == self.selectedIndex); KBVipSubscribeCell *c = (KBVipSubscribeCell *)cell; if (sel) { diff --git a/keyBoard/Class/Pay/VM/PayVM.h b/keyBoard/Class/Pay/VM/PayVM.h index 2054148..0a1c01c 100644 --- a/keyBoard/Class/Pay/VM/PayVM.h +++ b/keyBoard/Class/Pay/VM/PayVM.h @@ -30,6 +30,10 @@ typedef void(^KBPayProductsCompletion)(NSInteger sta, - (void)fetchInAppProductsNeedShow:(BOOL)needShow completion:(KBPayProductsCompletion)completion; +/// 查询订阅商品列表 +- (void)fetchSubscriptionProductsNeedShow:(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 0b1f0dd..a277901 100644 --- a/keyBoard/Class/Pay/VM/PayVM.m +++ b/keyBoard/Class/Pay/VM/PayVM.m @@ -32,9 +32,30 @@ - (void)fetchInAppProductsNeedShow:(BOOL)needShow completion:(KBPayProductsCompletion)completion { - if (needShow) { [KBHUD show]; } NSDictionary *params = @{ @"type": @"in-app-purchase" }; - [[KBNetworkManager shared] GET:API_INAPP_PRODUCT_LIST + [self fetchProductListWithPath:API_INAPP_PRODUCT_LIST + params:params + needShow:needShow + completion:completion]; +} + +- (void)fetchSubscriptionProductsNeedShow:(BOOL)needShow + completion:(KBPayProductsCompletion)completion { + NSDictionary *params = @{ @"type": @"subscription" }; + [self fetchProductListWithPath:API_SUBSCRIPTION_PRODUCT_LIST + params:params + needShow:needShow + completion:completion]; +} + +#pragma mark - Helpers + +- (void)fetchProductListWithPath:(NSString *)path + params:(NSDictionary *)params + needShow:(BOOL)needShow + completion:(KBPayProductsCompletion)completion { + if (needShow) { [KBHUD show]; } + [[KBNetworkManager shared] GET:path parameters:params headers:nil autoShowBusinessError:NO @@ -64,8 +85,6 @@ }]; } -#pragma mark - Helpers - //+ (NSInteger)extractStatusFromResponseObject:(id)obj response:(NSURLResponse *)resp { // // 优先从 JSON 提取 code/status/success // if ([obj isKindOfClass:NSDictionary.class]) {