// // KBJfPay.m // keyBoard #import "KBJfPay.h" #import "KBJfPayCell.h" #import "FGIAPProductsFilter.h" #import "FGIAPManager.h" #import "PayVM.h" #import "KBPayProductModel.h" #import "KBBizCode.h" #import "KBShopVM.h" #import "IAPVerifyTransactionObj.h" static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; @interface KBJfPay () @property (nonatomic, strong) UIImageView *bgImageView; // 全屏背景图 // 顶部信息 @property (nonatomic, strong) UILabel *myPointsTitleLabel; // “My Points” @property (nonatomic, strong) UILabel *pointsLabel; // 积分数值 @property (nonatomic, strong) UIImageView *bigCoinImageView; // 右上装饰大金币 pay_big_icon // “Recharge Now” 小标题 @property (nonatomic, strong) UIImageView *smallLeftIcon; // 左侧小金币 shop_jb_icon @property (nonatomic, strong) UILabel *rechargeLabel; // “Recharge Now” // 列表容器(因为需要只有左上/右上圆角) @property (nonatomic, strong) UIView *listContainerView; // 承载 collectionView @property (nonatomic, strong) UICollectionView *collectionView; // 底部按钮/协议 @property (nonatomic, strong) UIButton *payButton; // 充值按钮 @property (nonatomic, strong) UILabel *agreementLabel; // 协议提示 @property (nonatomic, strong) UIButton *agreementButton; // 数据 @property (nonatomic, strong) NSArray *data; // In-App 商品展示数据 @property (nonatomic, assign) NSInteger selectedIndex; // 当前选中项 @property (nonatomic, strong) FGIAPProductsFilter *filter; @property (nonatomic, strong) PayVM *payVM; @property (nonatomic, strong) KBShopVM *shopVM; @end @implementation KBJfPay - (void)viewDidLoad { [super viewDidLoad]; self.filter = [[FGIAPProductsFilter alloc] init]; self.payVM = [PayVM new]; self.shopVM = [KBShopVM new]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIAPPurchaseSuccess) name:KBIAPDidCompletePurchaseNotification object:nil]; 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]; [self.view insertSubview:self.bgImageView belowSubview:self.kb_navView]; [self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; self.kb_titleLabel.text = KBLocalized(@"Points Recharge"); // 视图组装 [self.view addSubview:self.myPointsTitleLabel]; [self.view addSubview:self.pointsLabel]; [self.view addSubview:self.listContainerView]; [self.view addSubview:self.bigCoinImageView]; [self.listContainerView addSubview:self.smallLeftIcon]; [self.listContainerView addSubview:self.rechargeLabel]; [self.listContainerView addSubview:self.collectionView]; [self.view addSubview:self.payButton]; [self.view addSubview:self.agreementLabel]; [self.view addSubview:self.agreementButton]; // 布局(mas) [self.myPointsTitleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(16); make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 40); }]; [self.pointsLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.myPointsTitleLabel); make.top.equalTo(self.myPointsTitleLabel.mas_bottom).offset(4); }]; [self.bigCoinImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.view).offset(-12); make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 10); make.width.mas_equalTo(131); make.height.mas_equalTo(144); }]; // 列表容器 + 圆角(仅左上/右上) [self.listContainerView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.mas_equalTo(KB_NAV_TOTAL_HEIGHT + 123); make.bottom.equalTo(self.payButton.mas_top).offset(-16); make.height.greaterThanOrEqualTo(@220); }]; [self.smallLeftIcon mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.listContainerView).offset(16); make.top.equalTo(self.listContainerView).offset(16); make.width.height.mas_equalTo(20); }]; [self.rechargeLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self.smallLeftIcon); make.left.equalTo(self.smallLeftIcon.mas_right).offset(8); }]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.bottom.left.right.equalTo(self.listContainerView).inset(16); make.top.equalTo(self.smallLeftIcon.mas_bottom).offset(19); }]; [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 reloadData]; [self fetchInAppProductList]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self fetchWalletBalance]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self selectItemAtCurrentIndexAnimated:NO]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #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 = (self.selectedIndex != NSNotFound && indexPath.item == self.selectedIndex); if (sel) { [collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; } [c applySelected:sel animated:NO]; } #pragma mark - 圆角蒙版 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // 仅左上、右上圆角 UIRectCorner corners = UIRectCornerTopLeft | UIRectCornerTopRight; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.listContainerView.bounds byRoundingCorners:corners cornerRadii:CGSizeMake(20, 20)]; CAShapeLayer *mask = [CAShapeLayer layer]; mask.frame = self.listContainerView.bounds; mask.path = path.CGPath; self.listContainerView.layer.mask = mask; } #pragma mark - UICollectionView - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.data.count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBJfPayCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBJfPayCellId forIndexPath:indexPath]; 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; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (self.selectedIndex == indexPath.item) { return; } NSInteger old = self.selectedIndex; self.selectedIndex = indexPath.item; KBJfPayCell *newCell = (KBJfPayCell *)[collectionView cellForItemAtIndexPath:indexPath]; [newCell applySelected:YES animated:YES]; if (old >= 0 && old < self.data.count) { NSIndexPath *oldIP = [NSIndexPath indexPathForItem:old inSection:0]; KBJfPayCell *oldCell = (KBJfPayCell *)[collectionView cellForItemAtIndexPath:oldIP]; [oldCell applySelected:NO animated:YES]; } } // 三列网格 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGFloat totalW = collectionView.bounds.size.width; CGFloat spacing = 10.0; // 列间距 CGFloat columns = 3.0; CGFloat insets = 0; // 已在 mas 中留了左右 16,这里内部 cell 不额外 inset CGFloat w = floor((totalW - insets - spacing * (columns - 1)) / columns); CGFloat h = KBFit(116); return CGSizeMake(MAX(0, w), h); } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return 10.0; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 30; } #pragma mark - Data - (void)fetchWalletBalance { __weak typeof(self) weakSelf = self; [self.shopVM fetchWalletBalanceWithCompletion:^(NSString * _Nullable balance, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } if (error) { NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error"); [KBHUD showInfo:msg]; return; } NSString *displayValue = balance.length ? balance : @"0"; self.pointsLabel.text = displayValue; }); }]; } - (void)handleIAPPurchaseSuccess { [self fetchWalletBalance]; } - (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 { 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) { 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")]; } #pragma mark - Lazy UI - (UILabel *)myPointsTitleLabel { if (!_myPointsTitleLabel) { _myPointsTitleLabel = [UILabel new]; _myPointsTitleLabel.text = KBLocalized(@"My Points"); _myPointsTitleLabel.font = [KBFont medium:14]; _myPointsTitleLabel.textColor = [UIColor colorWithHex:KBBlackValue]; } return _myPointsTitleLabel; } - (UILabel *)pointsLabel { if (!_pointsLabel) { _pointsLabel = [UILabel new]; _pointsLabel.text = @"--"; _pointsLabel.font = [KBFont bold:30]; _pointsLabel.textColor = [UIColor colorWithHex:KBColorValue]; } return _pointsLabel; } - (UIImageView *)bigCoinImageView { if (!_bigCoinImageView) { _bigCoinImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"pay_big_icon"]]; _bigCoinImageView.contentMode = UIViewContentModeScaleAspectFit; } return _bigCoinImageView; } - (UIImageView *)smallLeftIcon { if (!_smallLeftIcon) { _smallLeftIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"shop_jb_icon"]]; _smallLeftIcon.contentMode = UIViewContentModeScaleAspectFit; } return _smallLeftIcon; } - (UILabel *)rechargeLabel { if (!_rechargeLabel) { _rechargeLabel = [UILabel new]; _rechargeLabel.text = KBLocalized(@"Recharge Now"); // _rechargeLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; _rechargeLabel.font = [KBFont medium:14]; _rechargeLabel.textColor = [UIColor colorWithHex:KBBlackValue]; } return _rechargeLabel; } - (UIView *)listContainerView { if (!_listContainerView) { _listContainerView = [UIView new]; // 轻微底色,突出圆角区域(也可用渐变,按需) _listContainerView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.43]; } return _listContainerView; } - (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.backgroundColor = UIColor.clearColor; _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.alwaysBounceVertical = YES; [_collectionView registerClass:KBJfPayCell.class forCellWithReuseIdentifier:kKBJfPayCellId]; _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } 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 = [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 = KBLocalized(@"By clicking Pay, you indicate your agreement to the"); // 简化文案 // _agreementLabel.font = [UIFont systemFontOfSize:11 weight:UIFontWeightRegular]; _agreementLabel.font = [KBFont regular:12]; _agreementLabel.textColor = [UIColor colorWithWhite:0.45 alpha:1.0]; } 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 = [UIFont systemFontOfSize:10 weight:UIFontWeightSemibold]; _agreementButton.titleLabel.font = [KBFont regular:12]; [_agreementButton addTarget:self action:@selector(agreementButtonAction) forControlEvents:UIControlEventTouchUpInside]; } return _agreementButton; } @end