Files
keyboard/keyBoard/Class/Pay/VC/KBVipPay.m
2025-12-16 16:09:14 +08:00

407 lines
18 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBVipPay.m
// keyBoard
//
// Created by Mac on 2025/11/14.
//
#import "KBVipPay.h"
#import "KBVipPayHeaderView.h"
#import "KBVipSubscribeCell.h"
#import "KBVipReviewListCell.h"
#import "PayVM.h"
#import "KBPayProductModel.h"
#import "KBBizCode.h"
#import "keyBoard-Swift.h"
static NSString * const kKBVipHeaderId = @"kKBVipHeaderId";
static NSString * const kKBVipSubscribeCellId = @"kKBVipSubscribeCellId";
static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId";
@interface KBVipPay () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) UICollectionView *collectionView; // 主列表(竖向滚动)
@property (nonatomic, strong) NSArray<KBPayProductModel *> *plans; // 订阅方案数组
@property (nonatomic, assign) NSInteger selectedIndex; // 当前选中的方案索引
@property (nonatomic, strong) UIButton *closeButton; // 当前选中的方案索引
@property (nonatomic, strong) UIImageView *bgImageView; // 全屏背景图
// Header 自适应测量
@property (nonatomic, strong) KBVipPayHeaderView *sizingHeader;
@property (nonatomic, assign) CGFloat headerHeight;
// 底部支付与协议
@property (nonatomic, strong) UIButton *payButton; // 支付按钮(背景图)
@property (nonatomic, strong) UILabel *agreementLabel; // 协议提示
@property (nonatomic, strong) UIButton *agreementButton; // 《Embership Agreement》
@property (nonatomic, strong) PayVM *payVM;
@end
@implementation KBVipPay
- (void)viewDidLoad {
[super viewDidLoad];
// 标题与导航样式
// self.kb_titleLabel.text = @"VIP";
// self.kb_navView.backgroundColor = [UIColor clearColor];
self.view.backgroundColor = [UIColor colorWithHex:0xF6F7FB];
self.kb_enableCustomNavBar = NO;
self.bgImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"pay_vipbg_icon"]];
self.bgImageView.contentMode = UIViewContentModeScaleAspectFill;
[self.view addSubview:self.bgImageView];
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.right.equalTo(self.view);
make.height.mas_equalTo(224);
}];
self.payVM = [PayVM new];
self.plans = @[];
self.selectedIndex = NSNotFound;
// 组装主列表
[self.view addSubview:self.collectionView];
// 先添加底部按钮与协议,再让 collectionView 的底部在按钮之上
[self.view addSubview:self.payButton];
[self.view addSubview:self.agreementLabel];
[self.view addSubview:self.agreementButton];
[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.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.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view);
make.top.equalTo(self.view).offset(0);
make.bottom.equalTo(self.payButton.mas_top).offset(-16);
}];
[self.view addSubview:self.closeButton];
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT - 30);
make.left.equalTo(self.view).offset(15);
make.width.height.mas_equalTo(36);
}];
// 预计算 Header 高度(由内部约束决定)
self.headerHeight = [self kb_calcHeaderHeightForWidth:KB_SCREEN_WIDTH];
[self.collectionView reloadData];
[self fetchSubscriptionPlans];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self selectCurrentPlanAnimated:NO];
}
#pragma mark - Data
- (void)fetchSubscriptionPlans {
__weak typeof(self) weakSelf = self;
[self.payVM fetchSubscriptionProductsNeedShow:YES completion:^(NSInteger sta, NSString * _Nullable msg, NSArray<KBPayProductModel *> * _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 prepareStoreKitWithPlans:self.plans];
[self selectCurrentPlanAnimated:NO];
});
}];
}
- (void)prepareStoreKitWithPlans:(NSArray<KBPayProductModel *> *)plans {
if (![plans isKindOfClass:NSArray.class] || plans.count == 0) { return; }
NSMutableArray<NSString *> *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:1];
if (!ip) { return; }
[self.collectionView selectItemAtIndexPath:ip animated:animated scrollPosition:UICollectionViewScrollPositionNone];
KBVipSubscribeCell *cell = (KBVipSubscribeCell *)[self.collectionView cellForItemAtIndexPath:ip];
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
- (CGFloat)kb_calcHeaderHeightForWidth:(CGFloat)width {
if (width <= 0) { width = KB_SCREEN_WIDTH; }
if (!self.sizingHeader) {
self.sizingHeader = [[KBVipPayHeaderView alloc] initWithFrame:CGRectMake(0, 0, width, 1)];
}
// 更新目标宽度并触发布局
self.sizingHeader.bounds = CGRectMake(0, 0, width, self.sizingHeader.bounds.size.height);
[self.sizingHeader setNeedsLayout];
[self.sizingHeader layoutIfNeeded];
CGSize size = [self.sizingHeader systemLayoutSizeFittingSize:CGSizeMake(width, UILayoutFittingCompressedSize.height)
withHorizontalFittingPriority:UILayoutPriorityRequired
verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
return MAX(1, ceil(size.height));
}
#pragma mark - Action
- (void)onTapClose{
[self.navigationController popViewControllerAnimated:true];
}
#pragma mark - Bottom Actions
- (void)onTapPayButton {
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;
[[KBStoreKitBridge shared] purchaseWithProductId:productId completion:^(BOOL success, NSString * _Nullable message) {
__strong typeof(weakSelf) self = weakSelf;
// NSString *tip = message.length ? message : (success ? KBLocalized(@"Payment successful") : KBLocalized(@"Payment failed"));
[KBHUD dismiss];
// [KBHUD showInfo:tip];
[KBHUD showInfo:KBLocalized(message)];
if (!self || !success) { return; }
// 刷新 UI 或数据
[self selectCurrentPlanAnimated:NO];
}];
}
- (void)agreementButtonAction{
[KBHUD showInfo:KBLocalized(@"Open agreement")];
}
#pragma mark - UICollectionView DataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
// 0头部1订阅选项2底部横滑好评
return 3;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (section == 1) { return self.plans.count; }
if (section == 2) { return 1; }
return 0; // 头部仅使用 header
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 1) {
KBVipSubscribeCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBVipSubscribeCellId 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];
} else {
[cell configTitle:@"" price:@"" strike:nil];
[cell applySelected:NO animated:NO];
}
return cell;
} else {
KBVipReviewListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBVipReviewListCellId forIndexPath:indexPath];
return cell;
}
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0 && [kind isEqualToString:UICollectionElementKindSectionHeader]) {
KBVipPayHeaderView *header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kKBVipHeaderId forIndexPath:indexPath];
return header;
}
return [UICollectionReusableView new];
}
#pragma mark - UICollectionView Delegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section != 1 || indexPath.item >= self.plans.count) { return; }
if (self.selectedIndex == indexPath.item) { return; }
NSInteger old = self.selectedIndex;
self.selectedIndex = indexPath.item;
KBVipSubscribeCell *newCell = (KBVipSubscribeCell *)[collectionView cellForItemAtIndexPath:indexPath];
[newCell applySelected:YES animated:YES];
if (old >= 0 && old < self.plans.count) {
NSIndexPath *oldIP = [NSIndexPath indexPathForItem:old inSection:1];
KBVipSubscribeCell *oldCell = (KBVipSubscribeCell *)[collectionView cellForItemAtIndexPath:oldIP];
[oldCell applySelected:NO animated:YES];
}
}
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
// 兜底:当订阅项第一次出现在屏幕上,强制同步选中样式
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) {
[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 w = KB_SCREEN_WIDTH - 32;
if (indexPath.section == 1) {
return CGSizeMake(w, KBFit(75 + 6));
} else {
return CGSizeMake(w, 140);
}
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
if (section == 0) {
// 动态返回测量好的 Header 高度
CGFloat w = collectionView.bounds.size.width ?: KB_SCREEN_WIDTH;
if (self.headerHeight <= 1) { self.headerHeight = [self kb_calcHeaderHeightForWidth:w]; }
return CGSizeMake(w, self.headerHeight);
}
return CGSizeZero;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
if (section == 1) { return 14; }
return 0;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
if (section == 1) {
// 留出顶部间距,避免第一个订阅 cell 的边框被 header 盖住
return UIEdgeInsetsMake(16, 16, 10, 16);
} else if (section == 2) {
return UIEdgeInsetsMake(10, 16, 20, 16);
}
return UIEdgeInsetsZero;
}
#pragma mark - Lazy
- (UICollectionView *)collectionView {
if (!_collectionView) {
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
// 每次宽度变化时让布局失效,便于 header 重算高度
layout.sectionHeadersPinToVisibleBounds = NO;
_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:KBVipPayHeaderView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kKBVipHeaderId];
[_collectionView registerClass:KBVipSubscribeCell.class forCellWithReuseIdentifier:kKBVipSubscribeCellId];
[_collectionView registerClass:KBVipReviewListCell.class forCellWithReuseIdentifier:kKBVipReviewListCellId];
}
return _collectionView;
}
- (UIButton *)closeButton {
if (!_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_closeButton setImage:[UIImage imageNamed:@"close_white2_icon"] forState:UIControlStateNormal];
[_closeButton addTarget:self action:@selector(onTapClose) forControlEvents:UIControlEventTouchUpInside];
}
return _closeButton;
}
- (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 = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
_payButton.titleLabel.font = [KBFont medium:15];
UIImage *bg = [UIImage imageNamed:@"recharge_now_icon"];
if (bg) {
[_payButton setBackgroundImage:bg forState:UIControlStateNormal];
} else {
// 兜底:纯代码渐变
UIImage *fallback = [UIImage kb_gradientImageWithColors:@[[UIColor colorWithHex:0xC7F8F0], [UIColor colorWithHex:0xE8FFF6]] size:CGSizeMake(10, 58) direction:KBGradientDirectionLeftToRight];
[_payButton setBackgroundImage:[fallback resizableImageWithCapInsets:UIEdgeInsetsMake(29, 29, 29, 29) resizingMode:UIImageResizingModeStretch] forState:UIControlStateNormal];
}
[_payButton addTarget:self action:@selector(onTapPayButton) forControlEvents:UIControlEventTouchUpInside];
}
return _payButton;
}
- (UILabel *)agreementLabel {
if (!_agreementLabel) {
_agreementLabel = [UILabel new];
_agreementLabel.text = @"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:@"《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;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// 宽度变化时重算 Header 高度并刷新布局
CGFloat w = self.collectionView.bounds.size.width ?: KB_SCREEN_WIDTH;
CGFloat newH = [self kb_calcHeaderHeightForWidth:w];
if (fabs(newH - self.headerHeight) > 0.5) {
self.headerHeight = newH;
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
[layout invalidateLayout];
}
}
@end