// // KBPaySvipVC.m // keyBoard // // Created by Mac on 2026/2/3. // #import "KBPaySvipVC.h" #import "KBSvipSubscribeCell.h" #import "KBSvipBenefitCell.h" #import "KBSvipFlowLayout.h" #import "PayVM.h" #import "KBPayProductModel.h" #import "KBBizCode.h" #import "keyBoard-Swift.h" #import static NSString * const kKBSvipSubscribeCellId = @"kKBSvipSubscribeCellId"; static NSString * const kKBSvipBenefitCellId = @"kKBSvipBenefitCellId"; static NSString * const kKBSvipBenefitHeaderId = @"kKBSvipBenefitHeaderId"; @interface KBPaySvipVC () @property (nonatomic, copy) void(^scrollCallback)(UIScrollView *scrollView); /// 1:UI 控件 @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) UIButton *payButton; @property (nonatomic, strong) UILabel *agreementLabel; @property (nonatomic, strong) UIButton *agreementButton; /// 2:数据 @property (nonatomic, strong) NSArray *plans; @property (nonatomic, assign) NSInteger selectedIndex; @property (nonatomic, strong) NSArray *benefits; @property (nonatomic, strong) PayVM *payVM; @end @implementation KBPaySvipVC #pragma mark - Life Cycle - (void)viewDidLoad { [super viewDidLoad]; /// 1:控件初始化 [self setupUI]; /// 2:数据初始化 [self setupData]; /// 3:加载数据 [self loadData]; } #pragma mark - 1:控件初始化 - (void)setupUI { self.view.backgroundColor = [UIColor colorWithHex:0xF6F7FB]; [self.view addSubview:self.payButton]; [self.view addSubview:self.agreementLabel]; [self.view addSubview:self.agreementButton]; [self.view addSubview:self.collectionView]; [self.agreementButton mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.view); make.bottom.equalTo(self.view).offset(-KB_SAFE_BOTTOM - 15); }]; [self.agreementLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.view); make.bottom.equalTo(self.agreementButton.mas_top).offset(0); }]; [self.payButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(24); make.right.equalTo(self.view).offset(-24); make.bottom.equalTo(self.agreementLabel.mas_top).offset(-14); make.height.mas_equalTo(58); }]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.top.equalTo(self.view); make.bottom.equalTo(self.payButton.mas_top).offset(-16); }]; } #pragma mark - 2:数据初始化 - (void)setupData { self.payVM = [PayVM new]; self.plans = @[]; self.selectedIndex = NSNotFound; // 权益列表数据(使用现有图标资源) self.benefits = @[ @{@"icon": @"pay_ai_icon", @"title": KBLocalized(@"Wireless Sub-ai Dialogue")}, @{@"icon": @"pay_keyboard_icon", @"title": KBLocalized(@"Personalized Keyboard")}, @{@"icon": @"pay_chat_icon", @"title": KBLocalized(@"Chat Persona")}, @{@"icon": @"pay_emotion_icon", @"title": KBLocalized(@"Emotional Counseling")}, @{@"icon": @"pay_chat_icon", @"title": KBLocalized(@"Longer Chat History")}, @{@"icon": @"pay_chat_icon", @"title": KBLocalized(@"Unlimited Chatting")}, @{@"icon": @"pay_ai_icon", @"title": KBLocalized(@"Chat Without Speed Limits")}, @{@"icon": @"pay_vip_icon", @"title": KBLocalized(@"Coming Soon")}, ]; } #pragma mark - 3:加载数据 - (void)loadData { __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]; return; } self.plans = products ?: @[]; self.selectedIndex = self.plans.count > 0 ? 1 : NSNotFound; // 默认选中第二个(1 Month) [self.collectionView reloadData]; [self selectCurrentPlanAnimated:NO]; [self prepareStoreKitWithPlans:self.plans]; }); }]; } #pragma mark - Private Methods - (void)prepareStoreKitWithPlans:(NSArray *)plans { if (![plans isKindOfClass:NSArray.class] || plans.count == 0) { return; } NSMutableArray *ids = [NSMutableArray array]; for (KBPayProductModel *plan in plans) { if (![plan isKindOfClass:KBPayProductModel.class]) { continue; } if (plan.productId.length) { [ids addObject:plan.productId]; } } if (ids.count == 0) { return; } [[KBStoreKitBridge shared] prepareWithProductIds:ids completion:nil]; } - (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:0]; [self.collectionView selectItemAtIndexPath:ip animated:animated scrollPosition:UICollectionViewScrollPositionNone]; KBSvipSubscribeCell *cell = (KBSvipSubscribeCell *)[self.collectionView cellForItemAtIndexPath:ip]; if ([cell isKindOfClass:KBSvipSubscribeCell.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; } return self.plans[self.selectedIndex]; } - (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 - Actions - (void)onTapPayButton { KBPayProductModel *plan = [self currentSelectedPlan]; if (!plan) { [KBHUD showInfo:KBLocalized(@"Please select a product")]; return; } NSMutableDictionary *extra = [NSMutableDictionary dictionary]; if ([plan.productId isKindOfClass:NSString.class] && plan.productId.length > 0) { extra[@"product_id"] = plan.productId; } [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_svip_pay_btn" pageId:@"svip_pay" elementId:@"pay_btn" extra:extra.copy completion:nil]; NSString *productId = plan.productId; if (productId.length == 0) { [KBHUD showInfo:KBLocalized(@"Product unavailable")]; return; } [KBHUD show]; __weak typeof(self) weakSelf = self; [[KBStoreKitBridge shared] purchaseWithProductId:productId completion:^(BOOL success, NSString * _Nullable message) { __strong typeof(weakSelf) self = weakSelf; [KBHUD dismiss]; [KBHUD showInfo:KBLocalized(message)]; if (!self || !success) { return; } [self selectCurrentPlanAnimated:NO]; }]; } - (void)agreementButtonAction { [KBHUD showInfo:KBLocalized(@"Open agreement")]; } #pragma mark - UICollectionView DataSource - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { // Section 0: 订阅选项(横向) // Section 1: 权益列表 return 2; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (section == 0) { return self.plans.count; } return self.benefits.count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0) { KBSvipSubscribeCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBSvipSubscribeCellId forIndexPath:indexPath]; 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]; } return cell; } else { KBSvipBenefitCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBSvipBenefitCellId forIndexPath:indexPath]; if (indexPath.item < self.benefits.count) { NSDictionary *benefit = self.benefits[indexPath.item]; [cell configWithIcon:benefit[@"icon"] title:benefit[@"title"]]; } return cell; } } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 1 && [kind isEqualToString:UICollectionElementKindSectionHeader]) { UICollectionReusableView *header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kKBSvipBenefitHeaderId forIndexPath:indexPath]; // 清除旧的子视图 for (UIView *subview in header.subviews) { [subview removeFromSuperview]; } // 添加标题 UILabel *titleLabel = [UILabel new]; titleLabel.text = KBLocalized(@"Membership Benefits"); titleLabel.textColor = [UIColor colorWithHex:0x999999]; titleLabel.font = [KBFont medium:13]; titleLabel.textAlignment = NSTextAlignmentCenter; [header addSubview:titleLabel]; [titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(header); }]; // 左右横线 UIView *leftLine = [UIView new]; leftLine.backgroundColor = [UIColor colorWithHex:0xE5E5E5]; [header addSubview:leftLine]; [leftLine mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(titleLabel.mas_left).offset(-12); make.centerY.equalTo(header); make.width.mas_equalTo(40); make.height.mas_equalTo(1); }]; UIView *rightLine = [UIView new]; rightLine.backgroundColor = [UIColor colorWithHex:0xE5E5E5]; [header addSubview:rightLine]; [rightLine mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(titleLabel.mas_right).offset(12); make.centerY.equalTo(header); make.width.mas_equalTo(40); make.height.mas_equalTo(1); }]; return header; } return [UICollectionReusableView new]; } #pragma mark - UICollectionView Delegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section != 0 || indexPath.item >= self.plans.count) { return; } if (self.selectedIndex == indexPath.item) { return; } NSInteger old = self.selectedIndex; self.selectedIndex = indexPath.item; KBPayProductModel *plan = self.plans[indexPath.item]; NSMutableDictionary *extra = [NSMutableDictionary dictionary]; extra[@"index"] = @(indexPath.item); if ([plan.productId isKindOfClass:NSString.class] && plan.productId.length > 0) { extra[@"product_id"] = plan.productId; } [[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_svip_select_plan" pageId:@"svip_pay" elementId:@"plan_item" extra:extra.copy completion:nil]; KBSvipSubscribeCell *newCell = (KBSvipSubscribeCell *)[collectionView cellForItemAtIndexPath:indexPath]; [newCell applySelected:YES animated:YES]; if (old >= 0 && old < self.plans.count) { NSIndexPath *oldIP = [NSIndexPath indexPathForItem:old inSection:0]; KBSvipSubscribeCell *oldCell = (KBSvipSubscribeCell *)[collectionView cellForItemAtIndexPath:oldIP]; [oldCell applySelected:NO animated:YES]; } } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0 && indexPath.item < self.plans.count && [cell isKindOfClass:KBSvipSubscribeCell.class]) { BOOL sel = (indexPath.item == self.selectedIndex); KBSvipSubscribeCell *c = (KBSvipSubscribeCell *)cell; if (sel) { [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; } [c applySelected:sel animated:NO]; } } #pragma mark - FlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGFloat screenW = KB_SCREEN_WIDTH; if (indexPath.section == 0) { // 订阅选项:横向三等分 CGFloat itemW = (screenW - 32 - 20) / 3.0; // 左右各16,间距10*2 return CGSizeMake(itemW, 90); } else { // 权益项 return CGSizeMake(screenW - 32, 56); } } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { if (section == 1) { return CGSizeMake(KB_SCREEN_WIDTH, 50); } return CGSizeZero; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { if (section == 0) { return 10; } return 0; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { if (section == 0) { return 10; } return 0; } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { if (section == 0) { return UIEdgeInsetsMake(16, 16, 10, 16); } return UIEdgeInsetsMake(0, 16, 20, 16); } #pragma mark - UIScrollView Delegate(转发给分页容器) - (void)scrollViewDidScroll:(UIScrollView *)scrollView { !self.scrollCallback ?: self.scrollCallback(scrollView); } #pragma mark - JXPagingViewListViewDelegate - (UIView *)listView { return self.view; } - (UIScrollView *)listScrollView { return self.collectionView; } - (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback { self.scrollCallback = callback; } - (void)listWillAppear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); } - (void)listDidAppear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); } - (void)listWillDisappear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); } - (void)listDidDisappear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); } #pragma mark - Lazy - (UICollectionView *)collectionView { if (!_collectionView) { KBSvipFlowLayout *layout = [KBSvipFlowLayout new]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; layout.decorationSection = 1; // Section 1 添加背景 _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.backgroundColor = [UIColor clearColor]; _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.alwaysBounceVertical = YES; _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; [_collectionView registerClass:KBSvipSubscribeCell.class forCellWithReuseIdentifier:kKBSvipSubscribeCellId]; [_collectionView registerClass:KBSvipBenefitCell.class forCellWithReuseIdentifier:kKBSvipBenefitCellId]; [_collectionView registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kKBSvipBenefitHeaderId]; } return _collectionView; } - (UIButton *)payButton { if (!_payButton) { _payButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_payButton setTitle:KBLocalized(@"Recharge Now") forState:UIControlStateNormal]; [_payButton setTitleColor:[UIColor colorWithHex:KBBlackValue] forState:UIControlStateNormal]; _payButton.titleLabel.font = [KBFont medium:15]; _payButton.layer.cornerRadius = 29; _payButton.clipsToBounds = YES; _payButton.backgroundColor = [UIColor colorWithHex:0x222222]; [_payButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [_payButton addTarget:self action:@selector(onTapPayButton) forControlEvents:UIControlEventTouchUpInside]; } return _payButton; } - (UILabel *)agreementLabel { if (!_agreementLabel) { _agreementLabel = [UILabel new]; _agreementLabel.text = KBLocalized(@"By Clicking \"pay\", You Indicate Your Agreement To The"); _agreementLabel.font = [KBFont regular:12]; _agreementLabel.textColor = [UIColor colorWithHex:KBBlackValue]; } return _agreementLabel; } - (UIButton *)agreementButton { if (!_agreementButton) { _agreementButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_agreementButton setTitle:KBLocalized(@"《Embership Agreement》") forState:UIControlStateNormal]; [_agreementButton setTitleColor:[UIColor colorWithHex:KBColorValue] forState:UIControlStateNormal]; _agreementButton.titleLabel.font = [KBFont regular:12]; [_agreementButton addTarget:self action:@selector(agreementButtonAction) forControlEvents:UIControlEventTouchUpInside]; } return _agreementButton; } @end