From dde8716262f8b6ef066f4a26e817577ad8e10b68 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Wed, 17 Dec 2025 16:22:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=94=AE=E7=9B=98=E9=87=8C?= =?UTF-8?q?=E7=9A=84=E5=85=85=E5=80=BC=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CustomKeyboard/KeyboardViewController.m | 111 ++- .../Model/KBKeyboardSubscriptionProduct.h | 43 ++ .../Model/KBKeyboardSubscriptionProduct.m | 55 ++ .../View/KBKeyboardSubscriptionView.h | 31 + .../View/KBKeyboardSubscriptionView.m | 719 ++++++++++++++++++ keyBoard.xcodeproj/project.pbxproj | 12 + keyBoard/AppDelegate.m | 29 +- keyBoard/Class/Pay/VC/KBVipPay.h | 4 + keyBoard/Class/Pay/VC/KBVipPay.m | 48 ++ 9 files changed, 1038 insertions(+), 14 deletions(-) create mode 100644 CustomKeyboard/Model/KBKeyboardSubscriptionProduct.h create mode 100644 CustomKeyboard/Model/KBKeyboardSubscriptionProduct.m create mode 100644 CustomKeyboard/View/KBKeyboardSubscriptionView.h create mode 100644 CustomKeyboard/View/KBKeyboardSubscriptionView.m diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index f1473e6..5d9a9f8 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -17,6 +17,8 @@ #import "KBSkinManager.h" #import "KBSkinInstallBridge.h" #import "KBHostAppLauncher.h" +#import "KBKeyboardSubscriptionView.h" +#import "KBKeyboardSubscriptionProduct.h" // 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。 @interface KeyboardViewController (KBSkinShopBridge) @@ -40,12 +42,13 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, }); } -@interface KeyboardViewController () +@interface KeyboardViewController () @property (nonatomic, strong) UIButton *nextKeyboardButton; // 系统“下一个键盘”按钮(可选) @property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 功能面板视图(点击工具栏第0个时显示) @property (nonatomic, strong) KBFunctionView *functionView; // 功能面板视图(点击工具栏第0个时显示) @property (nonatomic, strong) KBSettingView *settingView; // 设置页 @property (nonatomic, strong) UIImageView *bgImageView; // 背景图(在底层) +@property (nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView; @end @implementation KeyboardViewController @@ -138,6 +141,10 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, // 简单显隐切换,复用相同的布局区域 self.functionView.hidden = !show; self.keyBoardMainView.hidden = show; + + if (show) { + [self hideSubscriptionPanel]; + } // 可选:把当前显示的视图置顶,避免层级遮挡 if (show) { @@ -184,6 +191,44 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, } } +- (void)showSubscriptionPanel { + [self showFunctionPanel:NO]; + KBKeyboardSubscriptionView *panel = self.subscriptionView; + if (!panel.superview) { + panel.hidden = YES; + [self.view addSubview:panel]; + [panel mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.keyBoardMainView); + }]; + } + [self.view bringSubviewToFront:panel]; + panel.hidden = NO; + panel.alpha = 0.0; + CGFloat height = CGRectGetHeight(self.view.bounds); + if (height <= 0) { height = 260; } + panel.transform = CGAffineTransformMakeTranslation(0, height); + [panel refreshProductsIfNeeded]; + [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + panel.alpha = 1.0; + panel.transform = CGAffineTransformIdentity; + } completion:nil]; +} + +- (void)hideSubscriptionPanel { + if (!self.subscriptionView || self.subscriptionView.hidden) { return; } + CGFloat height = CGRectGetHeight(self.subscriptionView.bounds); + if (height <= 0) { height = CGRectGetHeight(self.view.bounds); } + KBKeyboardSubscriptionView *panel = self.subscriptionView; + [UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ + panel.alpha = 0.0; + panel.transform = CGAffineTransformMakeTranslation(0, height); + } completion:^(BOOL finished) { + panel.hidden = YES; + panel.alpha = 1.0; + panel.transform = CGAffineTransformIdentity; + }]; +} + // MARK: - KBKeyBoardMainViewDelegate @@ -215,16 +260,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, [self showFunctionPanel:NO]; return; } - - NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME]; - NSURL *scheme = [NSURL URLWithString:schemeStr]; - // 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App - BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view]; - - if (!ok) { - // 失败兜底:给个文案提示 - // 比如:请回到桌面手动打开 XXX App 进行设置/充值 - } + [self showSubscriptionPanel]; } - (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView { @@ -263,6 +299,17 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, } } +#pragma mark - KBKeyboardSubscriptionViewDelegate + +- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view { + [self hideSubscriptionPanel]; +} + +- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product { + [self hideSubscriptionPanel]; + [self kb_openRechargeForProduct:product]; +} + #pragma mark - lazy - (KBKeyBoardMainView *)keyBoardMainView{ if (!_keyBoardMainView) { @@ -287,9 +334,51 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, return _settingView; } +- (KBKeyboardSubscriptionView *)subscriptionView { + if (!_subscriptionView) { + _subscriptionView = [[KBKeyboardSubscriptionView alloc] init]; + _subscriptionView.delegate = self; + _subscriptionView.hidden = YES; + _subscriptionView.alpha = 0.0; + } + return _subscriptionView; +} + #pragma mark - Actions +- (void)kb_openRechargeForProduct:(KBKeyboardSubscriptionProduct *)product { + if (![product isKindOfClass:KBKeyboardSubscriptionProduct.class] || product.productId.length == 0) { + [KBHUD showInfo:KBLocalized(@"Product unavailable")]; + return; + } + NSString *encodedId = [self.class kb_urlEncodedString:product.productId]; + NSString *title = [product displayTitle]; + NSString *encodedTitle = [self.class kb_urlEncodedString:title]; + NSMutableArray *params = [NSMutableArray arrayWithObject:@"autoPay=1"]; + if (encodedId.length) { + [params addObject:[NSString stringWithFormat:@"productId=%@", encodedId]]; + } + if (encodedTitle.length) { + [params addObject:[NSString stringWithFormat:@"productTitle=%@", encodedTitle]]; + } + NSString *query = [params componentsJoinedByString:@"&"]; + NSString *urlString = [NSString stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query]; + NSURL *scheme = [NSURL URLWithString:urlString]; + BOOL success = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view]; + if (!success) { + [KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")]; + } +} + ++ (NSString *)kb_urlEncodedString:(NSString *)value { + if (value.length == 0) { return @""; } + NSString *reserved = @"!*'();:@&=+$,/?%#[]"; + NSMutableCharacterSet *allowed = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; + [allowed removeCharactersInString:reserved]; + return [value stringByAddingPercentEncodingWithAllowedCharacters:allowed] ?: @""; +} + - (void)onTapSettingsBack { [self showSettingView:NO]; } diff --git a/CustomKeyboard/Model/KBKeyboardSubscriptionProduct.h b/CustomKeyboard/Model/KBKeyboardSubscriptionProduct.h new file mode 100644 index 0000000..8e05136 --- /dev/null +++ b/CustomKeyboard/Model/KBKeyboardSubscriptionProduct.h @@ -0,0 +1,43 @@ +// +// KBKeyboardSubscriptionProduct.h +// CustomKeyboard +// +// 订阅商品模型(键盘扩展专用),用于展示与主 App 相同的订阅列表。 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBKeyboardSubscriptionProduct : NSObject +/// 主键 id +@property (nonatomic, assign) NSInteger identifier; +/// Apple 商品编号 +@property (nonatomic, copy, nullable) NSString *productId; +/// 商品名称,如 Monthly +@property (nonatomic, copy, nullable) NSString *name; +/// 单位,如 Subscription +@property (nonatomic, copy, nullable) NSString *unit; +/// 商品描述 +@property (nonatomic, copy, nullable) NSString *productDescription; +/// 货币符号 +@property (nonatomic, copy, nullable) NSString *currency; +/// 现价 +@property (nonatomic, assign) double price; +/// 原价(如接口未返回,则回退为 price 的 1.25 倍) +@property (nonatomic, assign) double originPrice; +/// 有效期数值 +@property (nonatomic, assign) NSInteger durationValue; +/// 有效期单位 +@property (nonatomic, copy, nullable) NSString *durationUnit; + +/// 标题(描述 > name+unit > name > unit) +- (NSString *)displayTitle; +/// 当前价格文本 +- (NSString *)priceDisplayText; +/// 划线价文本 +- (nullable NSString *)strikePriceDisplayText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/Model/KBKeyboardSubscriptionProduct.m b/CustomKeyboard/Model/KBKeyboardSubscriptionProduct.m new file mode 100644 index 0000000..6a35e9a --- /dev/null +++ b/CustomKeyboard/Model/KBKeyboardSubscriptionProduct.m @@ -0,0 +1,55 @@ +// +// KBKeyboardSubscriptionProduct.m +// CustomKeyboard +// + +#import "KBKeyboardSubscriptionProduct.h" +#import +#import "KBLocalizationManager.h" + +@implementation KBKeyboardSubscriptionProduct + ++ (NSDictionary *)mj_replacedKeyFromPropertyName { + return @{ + @"identifier": @"id", + @"productDescription": @"description", + }; +} + +- (NSString *)displayTitle { + if (self.productDescription.length > 0) { + return self.productDescription; + } + 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; } + if (self.durationValue > 0 && self.durationUnit.length > 0) { + return [NSString stringWithFormat:@"%ld %@", (long)self.durationValue, self.durationUnit]; + } + return KBLocalized(@"Subscription"); +} + +- (NSString *)priceDisplayText { + double priceValue = self.price; + if (priceValue <= 0) { + return @"$0.00"; + } + NSString *currency = self.currency.length ? self.currency : @"$"; + return [NSString stringWithFormat:@"%@%.2f", currency, priceValue]; +} + +- (nullable NSString *)strikePriceDisplayText { + double rawValue = self.originPrice; + if (rawValue <= 0 && self.price > 0) { + rawValue = self.price * 1.25; + } + if (rawValue <= 0) { return nil; } + NSString *currency = self.currency.length ? self.currency : @"$"; + return [NSString stringWithFormat:@"%@%.2f", currency, rawValue]; +} + +@end diff --git a/CustomKeyboard/View/KBKeyboardSubscriptionView.h b/CustomKeyboard/View/KBKeyboardSubscriptionView.h new file mode 100644 index 0000000..29cd1c6 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardSubscriptionView.h @@ -0,0 +1,31 @@ +// +// KBKeyboardSubscriptionView.h +// CustomKeyboard +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class KBKeyboardSubscriptionProduct; +@class KBKeyboardSubscriptionView; + +@protocol KBKeyboardSubscriptionViewDelegate +@optional +- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view; +- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product; +@end + +/// 键盘内的订阅弹层 +@interface KBKeyboardSubscriptionView : UIView + +@property (nonatomic, weak) id delegate; + +/// 首次展示时调用,内部会自动请求订阅商品 +- (void)refreshProductsIfNeeded; +/// 外部强制刷新 +- (void)reloadProducts; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/View/KBKeyboardSubscriptionView.m b/CustomKeyboard/View/KBKeyboardSubscriptionView.m new file mode 100644 index 0000000..eeffc04 --- /dev/null +++ b/CustomKeyboard/View/KBKeyboardSubscriptionView.m @@ -0,0 +1,719 @@ +// +// KBKeyboardSubscriptionView.m +// CustomKeyboard +// + +#import "KBKeyboardSubscriptionView.h" +#import "KBKeyboardSubscriptionProduct.h" +#import "KBNetworkManager.h" +#import "KBFullAccessManager.h" +#import + +static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptionCellId"; + +@interface KBKeyboardSubscriptionOptionCell : UICollectionViewCell +- (void)configureWithProduct:(KBKeyboardSubscriptionProduct *)product; +- (void)applySelected:(BOOL)selected animated:(BOOL)animated; +@end + +@interface KBKeyboardSubscriptionView () +@property (nonatomic, strong) UIView *cardView; +@property (nonatomic, strong) UIButton *closeButton; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UIScrollView *featureScrollView; +@property (nonatomic, strong) UIView *featureContentView; +@property (nonatomic, strong) UICollectionView *collectionView; +@property (nonatomic, strong) UIButton *purchaseButton; +@property (nonatomic, strong) UILabel *agreementLabel; +@property (nonatomic, strong) UIButton *agreementButton; +@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator; +@property (nonatomic, strong) UILabel *emptyLabel; +@property (nonatomic, copy) NSArray *featureItems; +@property (nonatomic, copy) NSArray *products; +@property (nonatomic, assign) NSInteger selectedIndex; +@property (nonatomic, assign) BOOL didLoadOnce; +@property (nonatomic, assign, getter=isLoading) BOOL loading; +@property (nonatomic, strong) CADisplayLink *featureDisplayLink; +@property (nonatomic, assign) CGFloat featureLoopWidth; +@property (nonatomic, strong) CAGradientLayer *cardGradientLayer; +@end + +@implementation KBKeyboardSubscriptionView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.backgroundColor = [UIColor clearColor]; + _selectedIndex = NSNotFound; + [self setupCardView]; + [self setupFeatureItems]; + } + return self; +} + +- (void)dealloc { + [self.featureDisplayLink invalidate]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.cardGradientLayer.frame = self.cardView.bounds; + self.featureLoopWidth = self.featureScrollView.contentSize.width * 0.5f; + [self startFeatureTickerIfNeeded]; +} + +- (void)didMoveToWindow { + [super didMoveToWindow]; + if (self.window) { + [self startFeatureTickerIfNeeded]; + } else { + [self stopFeatureTicker]; + } +} + +- (void)setHidden:(BOOL)hidden { + BOOL oldHidden = self.isHidden; + [super setHidden:hidden]; + if (oldHidden == hidden) { return; } + if (hidden) { + [self stopFeatureTicker]; + } else if (self.window) { + [self startFeatureTickerIfNeeded]; + } +} + +#pragma mark - Public + +- (void)refreshProductsIfNeeded { + if (!self.didLoadOnce) { + [self fetchProducts]; + } +} + +- (void)reloadProducts { + [self fetchProducts]; +} + +#pragma mark - UI + +- (void)setupCardView { + [self addSubview:self.cardView]; + [self.cardView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.mas_left).offset(6); + make.right.equalTo(self.mas_right).offset(-6); + make.top.equalTo(self.mas_top).offset(4); + make.bottom.equalTo(self.mas_bottom).offset(-4); + }]; + + [self.cardView addSubview:self.closeButton]; + [self.cardView addSubview:self.titleLabel]; + [self.cardView addSubview:self.featureScrollView]; + [self.cardView addSubview:self.collectionView]; + [self.cardView addSubview:self.purchaseButton]; + [self.cardView addSubview:self.agreementLabel]; + [self.cardView addSubview:self.agreementButton]; + [self.cardView addSubview:self.loadingIndicator]; + [self.cardView addSubview:self.emptyLabel]; + + [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.cardView.mas_left).offset(12); + make.top.equalTo(self.cardView.mas_top).offset(10); + make.width.height.mas_equalTo(28); + }]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerY.equalTo(self.closeButton.mas_centerY); + make.centerX.equalTo(self.cardView.mas_centerX); + }]; + + [self.featureScrollView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.closeButton.mas_bottom).offset(8); + make.left.equalTo(self.cardView.mas_left).offset(12); + make.right.equalTo(self.cardView.mas_right).offset(-12); + make.height.mas_equalTo(54); + }]; + + [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.right.equalTo(self.featureScrollView); + make.top.equalTo(self.featureScrollView.mas_bottom).offset(12); + make.height.mas_equalTo(138); + }]; + + [self.agreementButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.cardView.mas_centerX); + make.bottom.equalTo(self.cardView.mas_bottom).offset(-14); + }]; + + [self.agreementLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.cardView.mas_centerX); + make.bottom.equalTo(self.agreementButton.mas_top).offset(-4); + }]; + + [self.purchaseButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.cardView.mas_left).offset(24); + make.right.equalTo(self.cardView.mas_right).offset(-24); + make.top.equalTo(self.collectionView.mas_bottom).offset(20); + make.bottom.equalTo(self.agreementLabel.mas_top).offset(-16); + make.height.mas_greaterThanOrEqualTo(@48); + }]; + + [self.loadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.collectionView); + }]; + + [self.emptyLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.collectionView); + }]; + + [self.featureScrollView addSubview:self.featureContentView]; + [self.featureContentView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.bottom.equalTo(self.featureScrollView); + make.left.equalTo(self.featureScrollView); + make.height.equalTo(self.featureScrollView); + make.right.equalTo(self.featureScrollView); + }]; + [self updatePurchaseButtonState]; +} + +- (void)setupFeatureItems { + self.featureItems = @[ + @{@"emoji": @"🤖", @"title": KBLocalized(@"Wireless Sub-ai Dialogue")}, + @{@"emoji": @"⌨️", @"title": KBLocalized(@"Personalized Keyboard")}, + @{@"emoji": @"🧠", @"title": KBLocalized(@"Creative Prompt Library")}, + @{@"emoji": @"💬", @"title": KBLocalized(@"Chat Personalized Coach")} + ]; + [self rebuildFeatureBadges]; +} + +- (void)rebuildFeatureBadges { + [self.featureContentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + NSArray *loopData = [self.featureItems arrayByAddingObjectsFromArray:self.featureItems]; + UIView *previous = nil; + CGFloat spacing = 12.0; + for (NSDictionary *info in loopData) { + UIView *badge = [self buildBadgeWithEmoji:info[@"emoji"] title:info[@"title"]]; + [self.featureContentView addSubview:badge]; + CGFloat textWidth = [info[@"title"] boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 24) + options:NSStringDrawingUsesLineFragmentOrigin + attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold]} + context:nil].size.width; + CGFloat width = MIN(MAX(textWidth + 60, 150), 240); + [badge mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.bottom.equalTo(self.featureContentView); + make.width.mas_equalTo(width); + if (previous) { + make.left.equalTo(previous.mas_right).offset(spacing); + } else { + make.left.equalTo(self.featureContentView.mas_left); + } + }]; + previous = badge; + } + __weak typeof(self) weakSelf = self; + [self.featureContentView mas_remakeConstraints:^(MASConstraintMaker *make) { + make.top.bottom.equalTo(weakSelf.featureScrollView); + make.left.equalTo(weakSelf.featureScrollView); + make.height.equalTo(weakSelf.featureScrollView); + if (previous) { + make.right.equalTo(previous.mas_right); + } else { + make.right.equalTo(weakSelf.featureScrollView); + } + }]; +} + +- (UIView *)buildBadgeWithEmoji:(NSString *)emoji title:(NSString *)title { + UIView *container = [[UIView alloc] init]; + container.layer.cornerRadius = 24; + container.layer.masksToBounds = YES; + container.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.85]; + + UILabel *emojiLabel = [[UILabel alloc] init]; + emojiLabel.text = emoji ?: @"✨"; + emojiLabel.font = [UIFont systemFontOfSize:20]; + + UILabel *textLabel = [[UILabel alloc] init]; + textLabel.text = title ?: @""; + textLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold]; + textLabel.textColor = [UIColor colorWithHex:0x4A4A4A]; + + [container addSubview:emojiLabel]; + [container addSubview:textLabel]; + + [emojiLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(container.mas_left).offset(14); + make.centerY.equalTo(container.mas_centerY); + }]; + + [textLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(emojiLabel.mas_right).offset(10); + make.centerY.equalTo(container.mas_centerY); + make.right.equalTo(container.mas_right).offset(-14); + }]; + return container; +} + +#pragma mark - Actions + +- (void)onTapClose { + if ([self.delegate respondsToSelector:@selector(subscriptionViewDidTapClose:)]) { + [self.delegate subscriptionViewDidTapClose:self]; + } +} + +- (void)onTapPurchase { + if (self.selectedIndex == NSNotFound || self.selectedIndex >= self.products.count) { + [KBHUD showInfo:KBLocalized(@"Please select a product")]; + return; + } + KBKeyboardSubscriptionProduct *product = self.products[self.selectedIndex]; + if ([self.delegate respondsToSelector:@selector(subscriptionView:didTapPurchaseForProduct:)]) { + [self.delegate subscriptionView:self didTapPurchaseForProduct:product]; + } +} + +- (void)onTapAgreement { + [KBHUD showInfo:KBLocalized(@"Agreement coming soon")]; +} + +#pragma mark - Data + +- (void)fetchProducts { + if (self.isLoading) { return; } + if (![[KBFullAccessManager shared] hasFullAccess]) { + [KBHUD showInfo:KBLocalized(@"Enable Full Access to continue")]; + return; + } + self.loading = YES; + self.emptyLabel.hidden = YES; + [self.loadingIndicator startAnimating]; + NSDictionary *params = @{@"type": @"subscription"}; + __weak typeof(self) weakSelf = self; + [[KBNetworkManager shared] GET:API_SUBSCRIPTION_PRODUCT_LIST + parameters:params + headers:nil + completion:^(NSDictionary *json, NSURLResponse *response, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) self = weakSelf; + if (!self) { return; } + self.loading = NO; + [self.loadingIndicator stopAnimating]; + if (error) { + NSString *tip = error.localizedDescription ?: KBLocalized(@"Network error"); + [KBHUD showInfo:tip]; + self.products = @[]; + self.selectedIndex = NSNotFound; + [self.collectionView reloadData]; + self.emptyLabel.hidden = NO; + [self updatePurchaseButtonState]; + return; + } + id dataObj = json[@"data"]; + if (![dataObj isKindOfClass:[NSArray class]]) { + dataObj = json[@"list"]; + } + if (![dataObj isKindOfClass:[NSArray class]]) { + self.products = @[]; + self.selectedIndex = NSNotFound; + [self.collectionView reloadData]; + self.emptyLabel.hidden = NO; + [self updatePurchaseButtonState]; + return; + } + NSArray *models = [KBKeyboardSubscriptionProduct mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj]; + self.products = models ?: @[]; + self.selectedIndex = self.products.count > 0 ? 0 : NSNotFound; + self.emptyLabel.hidden = self.products.count > 0; + [self.collectionView reloadData]; + [self selectCurrentProductAnimated:NO]; + [self updatePurchaseButtonState]; + self.didLoadOnce = YES; + }); + }]; +} + +- (void)selectCurrentProductAnimated:(BOOL)animated { + if (self.selectedIndex == NSNotFound || self.selectedIndex >= self.products.count) { return; } + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.selectedIndex inSection:0]; + [self.collectionView selectItemAtIndexPath:indexPath animated:animated scrollPosition:UICollectionViewScrollPositionCenteredHorizontally]; + KBKeyboardSubscriptionOptionCell *cell = (KBKeyboardSubscriptionOptionCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; + if ([cell isKindOfClass:KBKeyboardSubscriptionOptionCell.class]) { + [cell applySelected:YES animated:animated]; + } +} + +- (void)updatePurchaseButtonState { + BOOL enabled = (self.products.count > 0); + self.purchaseButton.enabled = enabled; + self.purchaseButton.alpha = enabled ? 1.0 : 0.5; +} + +#pragma mark - Feature ticker + +- (void)startFeatureTickerIfNeeded { + if (self.featureDisplayLink || self.featureLoopWidth <= 0) { return; } + self.featureDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleFeatureTick)]; + self.featureDisplayLink.preferredFramesPerSecond = 60; + [self.featureDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +} + +- (void)stopFeatureTicker { + [self.featureDisplayLink invalidate]; + self.featureDisplayLink = nil; +} + +- (void)handleFeatureTick { + if (self.featureLoopWidth <= 0) { return; } + CGFloat nextX = self.featureScrollView.contentOffset.x + 0.35f; + if (nextX >= self.featureLoopWidth) { + nextX -= self.featureLoopWidth; + } + self.featureScrollView.contentOffset = CGPointMake(nextX, 0); +} + +#pragma mark - UICollectionView DataSource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.products.count; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + KBKeyboardSubscriptionOptionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBKeyboardSubscriptionCellId forIndexPath:indexPath]; + if (indexPath.item < self.products.count) { + KBKeyboardSubscriptionProduct *product = self.products[indexPath.item]; + [cell configureWithProduct:product]; + BOOL selected = (indexPath.item == self.selectedIndex); + [cell applySelected:selected animated:NO]; + } else { + [cell configureWithProduct:nil]; + [cell applySelected:NO animated:NO]; + } + return cell; +} + +#pragma mark - UICollectionView Delegate + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.item >= self.products.count) { return; } + NSInteger previous = self.selectedIndex; + self.selectedIndex = indexPath.item; + if (previous != NSNotFound && previous != indexPath.item) { + NSIndexPath *prev = [NSIndexPath indexPathForItem:previous inSection:0]; + KBKeyboardSubscriptionOptionCell *prevCell = (KBKeyboardSubscriptionOptionCell *)[collectionView cellForItemAtIndexPath:prev]; + [prevCell applySelected:NO animated:YES]; + } + KBKeyboardSubscriptionOptionCell *cell = (KBKeyboardSubscriptionOptionCell *)[collectionView cellForItemAtIndexPath:indexPath]; + [cell applySelected:YES animated:YES]; +} + +#pragma mark - Layout + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + CGFloat width = MIN(MAX(collectionView.bounds.size.width * 0.56, 150), 220); + return CGSizeMake(width, collectionView.bounds.size.height - 12); +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { + return 12.0; +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { + return UIEdgeInsetsMake(6, 4, 6, 4); +} + +#pragma mark - Lazy + +- (UIView *)cardView { + if (!_cardView) { + _cardView = [[UIView alloc] init]; + _cardView.layer.cornerRadius = 20; + _cardView.layer.masksToBounds = YES; + _cardGradientLayer = [CAGradientLayer layer]; + _cardGradientLayer.colors = @[ (id)[UIColor colorWithRed:0.80 green:0.96 blue:0.91 alpha:1].CGColor, + (id)[UIColor colorWithRed:0.72 green:0.89 blue:0.98 alpha:1].CGColor ]; + _cardGradientLayer.startPoint = CGPointMake(0, 0); + _cardGradientLayer.endPoint = CGPointMake(1, 1); + [_cardView.layer insertSublayer:_cardGradientLayer atIndex:0]; + } + return _cardView; +} + +- (UIButton *)closeButton { + if (!_closeButton) { + _closeButton = [UIButton buttonWithType:UIButtonTypeSystem]; + _closeButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7]; + _closeButton.layer.cornerRadius = 14; + _closeButton.layer.masksToBounds = YES; + [_closeButton setTitle:@"✕" forState:UIControlStateNormal]; + [_closeButton setTitleColor:[UIColor colorWithHex:0x666666] forState:UIControlStateNormal]; + _closeButton.titleLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]; + [_closeButton addTarget:self action:@selector(onTapClose) forControlEvents:UIControlEventTouchUpInside]; + } + return _closeButton; +} + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.text = KBLocalized(@"Premium Subscription"); + _titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + _titleLabel.textColor = [UIColor colorWithHex:0x24556B]; + } + return _titleLabel; +} + +- (UIScrollView *)featureScrollView { + if (!_featureScrollView) { + _featureScrollView = [[UIScrollView alloc] init]; + _featureScrollView.showsHorizontalScrollIndicator = NO; + _featureScrollView.scrollEnabled = NO; + _featureScrollView.clipsToBounds = YES; + _featureScrollView.layer.cornerRadius = 26; + } + return _featureScrollView; +} + +- (UIView *)featureContentView { + if (!_featureContentView) { + _featureContentView = [[UIView alloc] init]; + } + return _featureContentView; +} + +- (UICollectionView *)collectionView { + if (!_collectionView) { + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + _collectionView.backgroundColor = [UIColor clearColor]; + _collectionView.showsHorizontalScrollIndicator = NO; + _collectionView.dataSource = self; + _collectionView.delegate = self; + [_collectionView registerClass:KBKeyboardSubscriptionOptionCell.class forCellWithReuseIdentifier:kKBKeyboardSubscriptionCellId]; + } + return _collectionView; +} + +- (UIButton *)purchaseButton { + if (!_purchaseButton) { + _purchaseButton = [UIButton buttonWithType:UIButtonTypeSystem]; + _purchaseButton.layer.cornerRadius = 26; + _purchaseButton.layer.masksToBounds = YES; + [_purchaseButton setTitle:KBLocalized(@"Recharge Now") forState:UIControlStateNormal]; + _purchaseButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + [_purchaseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [_purchaseButton setBackgroundImage:[self imageWithColor:[UIColor colorWithHex:0x02BEAC]] forState:UIControlStateNormal]; + [_purchaseButton addTarget:self action:@selector(onTapPurchase) forControlEvents:UIControlEventTouchUpInside]; + } + return _purchaseButton; +} + +- (UILabel *)agreementLabel { + if (!_agreementLabel) { + _agreementLabel = [[UILabel alloc] init]; + _agreementLabel.text = KBLocalized(@"By clicking \"pay\", you agree to the"); + _agreementLabel.font = [UIFont systemFontOfSize:11]; + _agreementLabel.textColor = [UIColor colorWithHex:0x4A4A4A]; + } + return _agreementLabel; +} + +- (UIButton *)agreementButton { + if (!_agreementButton) { + _agreementButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_agreementButton setTitle:KBLocalized(@"Membership Agreement") forState:UIControlStateNormal]; + _agreementButton.titleLabel.font = [UIFont systemFontOfSize:11 weight:UIFontWeightSemibold]; + [_agreementButton setTitleColor:[UIColor colorWithHex:0x02BEAC] forState:UIControlStateNormal]; + [_agreementButton addTarget:self action:@selector(onTapAgreement) forControlEvents:UIControlEventTouchUpInside]; + } + return _agreementButton; +} + +- (UIActivityIndicatorView *)loadingIndicator { + if (!_loadingIndicator) { + _loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; + _loadingIndicator.hidesWhenStopped = YES; + } + return _loadingIndicator; +} + +- (UILabel *)emptyLabel { + if (!_emptyLabel) { + _emptyLabel = [[UILabel alloc] init]; + _emptyLabel.text = KBLocalized(@"No products available"); + _emptyLabel.font = [UIFont systemFontOfSize:13]; + _emptyLabel.textColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; + _emptyLabel.textAlignment = NSTextAlignmentCenter; + _emptyLabel.hidden = YES; + } + return _emptyLabel; +} + +- (UIImage *)imageWithColor:(UIColor *)color { + CGSize size = CGSizeMake(1, 1); + UIGraphicsBeginImageContextWithOptions(size, NO, 0); + [color setFill]; + UIRectFill(CGRectMake(0, 0, size.width, size.height)); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +@end + +#pragma mark - Cell + +@interface KBKeyboardSubscriptionOptionCell () +@property (nonatomic, strong) UIView *cardView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *priceLabel; +@property (nonatomic, strong) UILabel *strikeLabel; +@property (nonatomic, strong) UIView *checkBadge; +@property (nonatomic, strong) UILabel *checkLabel; +@end + +@implementation KBKeyboardSubscriptionOptionCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.contentView.backgroundColor = [UIColor clearColor]; + [self.contentView addSubview:self.cardView]; + [self.cardView addSubview:self.titleLabel]; + [self.cardView addSubview:self.priceLabel]; + [self.cardView addSubview:self.strikeLabel]; + [self.cardView addSubview:self.checkBadge]; + [self.checkBadge addSubview:self.checkLabel]; + + [self.cardView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.contentView); + }]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.cardView.mas_top).offset(12); + make.left.equalTo(self.cardView.mas_left).offset(14); + make.right.equalTo(self.cardView.mas_right).offset(-14); + }]; + + [self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.titleLabel.mas_left); + make.top.equalTo(self.titleLabel.mas_bottom).offset(10); + }]; + + [self.strikeLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.priceLabel.mas_left); + make.top.equalTo(self.priceLabel.mas_bottom).offset(4); + }]; + + [self.checkBadge mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerX.equalTo(self.cardView.mas_centerX); + make.bottom.equalTo(self.cardView.mas_bottom).offset(-8); + make.width.height.mas_equalTo(20); + }]; + + [self.checkLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.center.equalTo(self.checkBadge); + }]; + } + return self; +} + +- (void)prepareForReuse { + [super prepareForReuse]; + self.titleLabel.text = @""; + self.priceLabel.text = @""; + self.strikeLabel.attributedText = nil; + self.strikeLabel.hidden = YES; + [self applySelected:NO animated:NO]; +} + +- (void)configureWithProduct:(KBKeyboardSubscriptionProduct *)product { + if (!product) { return; } + self.titleLabel.text = [product displayTitle]; + self.priceLabel.text = [product priceDisplayText]; + NSString *strike = [product strikePriceDisplayText]; + if (strike.length > 0) { + NSDictionary *attr = @{ + NSStrikethroughStyleAttributeName: @(NSUnderlineStyleSingle), + NSForegroundColorAttributeName: [[UIColor blackColor] colorWithAlphaComponent:0.35], + NSFontAttributeName: [UIFont systemFontOfSize:12] + }; + self.strikeLabel.attributedText = [[NSAttributedString alloc] initWithString:strike attributes:attr]; + self.strikeLabel.hidden = NO; + } else { + self.strikeLabel.attributedText = nil; + self.strikeLabel.hidden = YES; + } +} + +- (void)applySelected:(BOOL)selected animated:(BOOL)animated { + void (^changes)(void) = ^{ + self.cardView.layer.borderColor = selected ? [UIColor colorWithHex:0x02BEAC].CGColor : [[UIColor blackColor] colorWithAlphaComponent:0.12].CGColor; + self.cardView.layer.borderWidth = selected ? 2.0 : 1.0; + self.checkBadge.backgroundColor = selected ? [UIColor colorWithHex:0x02BEAC] : [[UIColor blackColor] colorWithAlphaComponent:0.15]; + self.checkLabel.textColor = [UIColor whiteColor]; + }; + if (animated) { + [UIView animateWithDuration:0.18 animations:changes]; + } else { + changes(); + } +} + +- (UIView *)cardView { + if (!_cardView) { + _cardView = [[UIView alloc] init]; + _cardView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.96]; + _cardView.layer.cornerRadius = 20; + _cardView.layer.borderWidth = 1.0; + _cardView.layer.borderColor = [[UIColor blackColor] colorWithAlphaComponent:0.12].CGColor; + } + return _cardView; +} + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold]; + _titleLabel.numberOfLines = 2; + _titleLabel.textColor = [UIColor colorWithHex:0x2F2F2F]; + } + return _titleLabel; +} + +- (UILabel *)priceLabel { + if (!_priceLabel) { + _priceLabel = [[UILabel alloc] init]; + _priceLabel.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold]; + _priceLabel.textColor = [UIColor colorWithHex:0x232323]; + } + return _priceLabel; +} + +- (UILabel *)strikeLabel { + if (!_strikeLabel) { + _strikeLabel = [[UILabel alloc] init]; + _strikeLabel.hidden = YES; + } + return _strikeLabel; +} + +- (UIView *)checkBadge { + if (!_checkBadge) { + _checkBadge = [[UIView alloc] init]; + _checkBadge.layer.cornerRadius = 10; + _checkBadge.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.15]; + } + return _checkBadge; +} + +- (UILabel *)checkLabel { + if (!_checkLabel) { + _checkLabel = [[UILabel alloc] init]; + _checkLabel.text = @"✓"; + _checkLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightBold]; + _checkLabel.textColor = [UIColor whiteColor]; + _checkLabel.textAlignment = NSTextAlignmentCenter; + } + return _checkLabel; +} + +@end diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 943fe87..f71e586 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -214,6 +214,8 @@ A1B2E1022EBC7AAA00000001 /* HomeHotCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0042EBC7AAA00000001 /* HomeHotCell.m */; }; EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */; }; ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; }; + 04FEDC122F00010000999999 /* KBKeyboardSubscriptionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC112F00010000999999 /* KBKeyboardSubscriptionView.m */; }; + 04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -519,6 +521,8 @@ 04FC953A2EAFAE56007BD342 /* KeyBoardPrefixHeader.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyBoardPrefixHeader.pch; sourceTree = ""; }; 04FC95642EB0546C007BD342 /* KBKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKey.h; sourceTree = ""; }; 04FC95652EB0546C007BD342 /* KBKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKey.m; sourceTree = ""; }; + 04FEDC202F00020000999999 /* KBKeyboardSubscriptionProduct.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionProduct.h; sourceTree = ""; }; + 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionProduct.m; sourceTree = ""; }; 04FC95682EB05497007BD342 /* KBKeyButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyButton.h; sourceTree = ""; }; 04FC95692EB05497007BD342 /* KBKeyButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyButton.m; sourceTree = ""; }; 04FC956B2EB054B7007BD342 /* KBKeyboardView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardView.h; sourceTree = ""; }; @@ -533,6 +537,8 @@ 04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyBoardMainView.m; sourceTree = ""; }; 04FC95B02EB0B2CC007BD342 /* KBSettingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSettingView.h; sourceTree = ""; }; 04FC95B12EB0B2CC007BD342 /* KBSettingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSettingView.m; sourceTree = ""; }; + 04FEDC102F00010000999999 /* KBKeyboardSubscriptionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyboardSubscriptionView.h; sourceTree = ""; }; + 04FEDC112F00010000999999 /* KBKeyboardSubscriptionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardSubscriptionView.m; sourceTree = ""; }; 04FC95C72EB1E4C9007BD342 /* BaseNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BaseNavigationController.h; sourceTree = ""; }; 04FC95C82EB1E4C9007BD342 /* BaseNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BaseNavigationController.m; sourceTree = ""; }; 04FC95CA2EB1E780007BD342 /* BaseTabBarController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BaseTabBarController.h; sourceTree = ""; }; @@ -1131,6 +1137,8 @@ A1B2C3F22EB35A9900000001 /* KBFullAccessGuideView.m */, 04FC95B02EB0B2CC007BD342 /* KBSettingView.h */, 04FC95B12EB0B2CC007BD342 /* KBSettingView.m */, + 04FEDC102F00010000999999 /* KBKeyboardSubscriptionView.h */, + 04FEDC112F00010000999999 /* KBKeyboardSubscriptionView.m */, 049FB22D2EC34EB900FAB05D /* KBStreamTextView.h */, 049FB22E2EC34EB900FAB05D /* KBStreamTextView.m */, 049FB23A2EC4766700FAB05D /* Function */, @@ -1152,6 +1160,8 @@ children = ( 04FC95642EB0546C007BD342 /* KBKey.h */, 04FC95652EB0546C007BD342 /* KBKey.m */, + 04FEDC202F00020000999999 /* KBKeyboardSubscriptionProduct.h */, + 04FEDC212F00020000999999 /* KBKeyboardSubscriptionProduct.m */, ); path = Model; sourceTree = ""; @@ -1832,6 +1842,7 @@ 04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */, 04FEDAA12EEDB00100123456 /* KBEmojiDataProvider.m in Sources */, 04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */, + 04FEDC122F00010000999999 /* KBKeyboardSubscriptionView.m in Sources */, 04791F8E2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */, 049FB23B2EC4766700FAB05D /* KBFunctionTagListView.m in Sources */, 049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */, @@ -1843,6 +1854,7 @@ 04FC95672EB0546C007BD342 /* KBKey.m in Sources */, A1B2C3F42EB35A9900000001 /* KBFullAccessGuideView.m in Sources */, 0498BD8F2EE6A3BD006CC1D5 /* KBMyMainModel.m in Sources */, + 04FEDC222F00020000999999 /* KBKeyboardSubscriptionProduct.m in Sources */, 0450AA742EF013D000B6AF06 /* KBEmojiCollectionCell.m in Sources */, 550CB2630FA4A7B4B9782EFA /* KBMyTheme.m in Sources */, 0498BDDA2EE7ECEA006CC1D5 /* WJXEventSource.m in Sources */, diff --git a/keyBoard/AppDelegate.m b/keyBoard/AppDelegate.m index d5a2b03..95abc4f 100644 --- a/keyBoard/AppDelegate.m +++ b/keyBoard/AppDelegate.m @@ -187,10 +187,20 @@ } else if ([host isEqualToString:@"settings"]) { // kbkeyboard://settings [self kb_openAppSettings]; return YES; - }else if ([host isEqualToString:@"recharge"]) { // kbkeyboard://settings -// [self kb_openAppSettings]; -// [KBHUD showInfo:@"去充值"]; + }else if ([host isEqualToString:@"recharge"]) { // kbkeyboard://recharge + NSDictionary *params = [self kb_queryParametersFromURL:url]; + NSString *productId = params[@"productId"]; + BOOL autoPay = NO; + NSString *autoFlag = params[@"autoPay"]; + if ([autoFlag respondsToSelector:@selector(boolValue)]) { + autoPay = autoFlag.boolValue; + } + NSString *action = params[@"action"].lowercaseString; + if ([action isEqualToString:@"autopay"]) { + autoPay = YES; + } KBVipPay *vc = [[KBVipPay alloc] init]; + [vc configureWithProductId:productId autoPurchase:autoPay]; [KB_CURRENT_NAV pushViewController:vc animated:true]; return YES; } @@ -199,6 +209,19 @@ return NO; } +- (NSDictionary *)kb_queryParametersFromURL:(NSURL *)url { + if (!url) { return @{}; } + NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + NSArray *items = components.queryItems ?: @[]; + if (items.count == 0) { return @{}; } + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:items.count]; + for (NSURLQueryItem *item in items) { + if (item.name.length == 0) { continue; } + dict[item.name] = item.value ?: @""; + } + return dict; +} + - (void)kb_presentLoginSheetIfNeeded { // 已登录则不提示 // BOOL loggedIn = ([AppleSignInManager shared].storedUserIdentifier.length > 0); diff --git a/keyBoard/Class/Pay/VC/KBVipPay.h b/keyBoard/Class/Pay/VC/KBVipPay.h index 13e914d..5504032 100644 --- a/keyBoard/Class/Pay/VC/KBVipPay.h +++ b/keyBoard/Class/Pay/VC/KBVipPay.h @@ -11,6 +11,10 @@ NS_ASSUME_NONNULL_BEGIN /// VIP 订阅页(整体使用 UICollectionView,上下滚动) @interface KBVipPay : BaseViewController + +/// 通过键盘深链配置初始商品及是否自动发起购买 +- (void)configureWithProductId:(nullable NSString *)productId + autoPurchase:(BOOL)autoPurchase; @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/VC/KBVipPay.m b/keyBoard/Class/Pay/VC/KBVipPay.m index 1e51b41..e9eb266 100644 --- a/keyBoard/Class/Pay/VC/KBVipPay.m +++ b/keyBoard/Class/Pay/VC/KBVipPay.m @@ -33,6 +33,10 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; @property (nonatomic, strong) UILabel *agreementLabel; // 协议提示 @property (nonatomic, strong) UIButton *agreementButton; // 《Embership Agreement》 @property (nonatomic, strong) PayVM *payVM; +@property (nonatomic, copy, nullable) NSString *pendingProductId; +@property (nonatomic, assign) BOOL pendingAutoPurchase; +@property (nonatomic, assign) BOOL hasTriggeredAutoPurchase; +@property (nonatomic, assign) BOOL viewVisible; @end @@ -104,7 +108,24 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; + self.viewVisible = YES; [self selectCurrentPlanAnimated:NO]; + [self kb_triggerAutoPurchaseIfNeeded]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + self.viewVisible = NO; +} + +#pragma mark - Deep Link Support + +- (void)configureWithProductId:(NSString *)productId autoPurchase:(BOOL)autoPurchase { + self.pendingProductId = productId.length ? [productId copy] : nil; + self.pendingAutoPurchase = autoPurchase; + self.hasTriggeredAutoPurchase = NO; + [self kb_applyPendingPrefillIfNeeded]; + [self kb_triggerAutoPurchaseIfNeeded]; } #pragma mark - Data @@ -126,7 +147,9 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; self.selectedIndex = self.plans.count > 0 ? 0 : NSNotFound; [self.collectionView reloadData]; [self prepareStoreKitWithPlans:self.plans]; + [self kb_applyPendingPrefillIfNeeded]; [self selectCurrentPlanAnimated:NO]; + [self kb_triggerAutoPurchaseIfNeeded]; }); }]; } @@ -164,6 +187,31 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; return plan; } +- (void)kb_applyPendingPrefillIfNeeded { + if (self.pendingProductId.length == 0 || self.plans.count == 0) { return; } + __block NSInteger target = NSNotFound; + [self.plans enumerateObjectsUsingBlock:^(KBPayProductModel *obj, NSUInteger idx, BOOL *stop) { + if (![obj isKindOfClass:KBPayProductModel.class]) { return; } + if ([obj.productId isEqualToString:self.pendingProductId]) { + target = (NSInteger)idx; + *stop = YES; + } + }]; + if (target != NSNotFound) { + self.selectedIndex = target; + } +} + +- (void)kb_triggerAutoPurchaseIfNeeded { + if (!self.pendingAutoPurchase || self.hasTriggeredAutoPurchase || !self.viewVisible) { return; } + if (![self currentSelectedPlan]) { return; } + self.hasTriggeredAutoPurchase = YES; + self.pendingAutoPurchase = NO; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.35 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self onTapPayButton]; + }); +} + - (NSString *)displayTitleForPlan:(KBPayProductModel *)plan { if (!plan) { return @""; } if (plan.productDescription.length) { return plan.productDescription; }