diff --git a/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/Contents.json b/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/Contents.json new file mode 100644 index 0000000..dde21ca --- /dev/null +++ b/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "buy_sel_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "buy_sel_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@2x.png b/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@2x.png new file mode 100644 index 0000000..15d8416 Binary files /dev/null and b/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@2x.png differ diff --git a/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@3x.png b/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@3x.png new file mode 100644 index 0000000..e21756e Binary files /dev/null and b/CustomKeyboard/KeyboardAssets.xcassets/buy_sel_icon.imageset/buy_sel_icon@3x.png differ diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index eb0235d..dd148ec 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -359,7 +359,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, 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"]; + NSMutableArray *params = [NSMutableArray arrayWithObjects:@"autoPay=1", @"prefill=1", nil]; if (encodedId.length) { [params addObject:[NSString stringWithFormat:@"productId=%@", encodedId]]; } diff --git a/CustomKeyboard/View/KBKeyboardSubscriptionOptionCell.m b/CustomKeyboard/View/KBKeyboardSubscriptionOptionCell.m index f205121..c16116a 100644 --- a/CustomKeyboard/View/KBKeyboardSubscriptionOptionCell.m +++ b/CustomKeyboard/View/KBKeyboardSubscriptionOptionCell.m @@ -11,8 +11,7 @@ @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; +@property (nonatomic, strong) UIImageView *selectedImageView; @end @implementation KBKeyboardSubscriptionOptionCell - (instancetype)initWithFrame:(CGRect)frame { @@ -22,8 +21,7 @@ [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 addSubview:self.selectedImageView]; [self.cardView mas_makeConstraints:^(MASConstraintMaker *make) { // make.edges.equalTo(self.contentView); @@ -32,7 +30,7 @@ }]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(self.cardView.mas_top).offset(4); + make.top.equalTo(self.cardView.mas_top).offset(8); make.left.equalTo(self.cardView.mas_left).offset(10); make.right.equalTo(self.cardView.mas_right).offset(-10); }]; @@ -47,14 +45,11 @@ make.centerY.equalTo(self.priceLabel); }]; - [self.checkBadge mas_makeConstraints:^(MASConstraintMaker *make) { + [self.selectedImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.cardView.mas_centerX); make.bottom.equalTo(self.cardView.mas_bottom).offset(10); - make.width.height.mas_equalTo(20); - }]; - - [self.checkLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.center.equalTo(self.checkBadge); + make.width.mas_equalTo(16); + make.height.mas_equalTo(17); }]; } return self; @@ -92,13 +87,16 @@ 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]; + self.selectedImageView.alpha = selected ? 1.0 : 0.0; }; if (animated) { - [UIView animateWithDuration:0.18 animations:changes]; + self.selectedImageView.hidden = NO; + [UIView animateWithDuration:0.18 animations:changes completion:^(BOOL finished) { + self.selectedImageView.hidden = !selected; + }]; } else { changes(); + self.selectedImageView.hidden = !selected; } } @@ -141,23 +139,13 @@ return _strikeLabel; } -- (UIView *)checkBadge { - if (!_checkBadge) { - _checkBadge = [[UIView alloc] init]; - _checkBadge.layer.cornerRadius = 10; - _checkBadge.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.15]; +- (UIImageView *)selectedImageView { + if (!_selectedImageView) { + _selectedImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"buy_sel_icon"]]; + _selectedImageView.contentMode = UIViewContentModeScaleAspectFit; + _selectedImageView.hidden = YES; + _selectedImageView.alpha = 0.0; } - 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; + return _selectedImageView; } @end diff --git a/CustomKeyboard/View/KBKeyboardSubscriptionView.m b/CustomKeyboard/View/KBKeyboardSubscriptionView.m index cd877f1..d8add92 100644 --- a/CustomKeyboard/View/KBKeyboardSubscriptionView.m +++ b/CustomKeyboard/View/KBKeyboardSubscriptionView.m @@ -9,11 +9,39 @@ #import "KBFullAccessManager.h" #import "KBKeyboardSubscriptionFeatureMarqueeView.h" #import "KBKeyboardSubscriptionOptionCell.h" +#import "KBConfig.h" #import static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptionCellId"; +static id KBKeyboardSubscriptionSanitizeJSON(id obj) { + if (!obj || obj == (id)kCFNull) { return nil; } + if ([obj isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = (NSDictionary *)obj; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:dict.count]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + (void)stop; + if (![key isKindOfClass:[NSString class]]) { return; } + id sanitized = KBKeyboardSubscriptionSanitizeJSON(value); + if (!sanitized) { return; } + result[key] = sanitized; + }]; + return result; + } + if ([obj isKindOfClass:[NSArray class]]) { + NSArray *arr = (NSArray *)obj; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:arr.count]; + for (id item in arr) { + id sanitized = KBKeyboardSubscriptionSanitizeJSON(item); + if (!sanitized) { continue; } + [result addObject:sanitized]; + } + return result; + } + return obj; +} + @interface KBKeyboardSubscriptionView () @property (nonatomic, strong) UIImageView *cardView; @property (nonatomic, strong) UIButton *closeButton; @@ -25,6 +53,7 @@ static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptio @property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator; @property (nonatomic, strong) UILabel *emptyLabel; @property (nonatomic, copy) NSArray *products; +@property (nonatomic, copy, nullable) NSArray *productsRawJSON; @property (nonatomic, assign) NSInteger selectedIndex; @property (nonatomic, assign) BOOL didLoadOnce; @property (nonatomic, assign, getter=isLoading) BOOL loading; @@ -156,6 +185,7 @@ static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptio return; } KBKeyboardSubscriptionProduct *product = self.products[self.selectedIndex]; + [self kb_persistPrefillPayloadForProduct:product]; if ([self.delegate respondsToSelector:@selector(subscriptionView:didTapPurchaseForProduct:)]) { [self.delegate subscriptionView:self didTapPurchaseForProduct:product]; } @@ -191,6 +221,7 @@ static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptio NSString *tip = error.localizedDescription ?: KBLocalized(@"Network error"); [KBHUD showInfo:tip]; self.products = @[]; + self.productsRawJSON = nil; self.selectedIndex = NSNotFound; [self.collectionView reloadData]; self.emptyLabel.hidden = NO; @@ -203,12 +234,15 @@ static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptio } if (![dataObj isKindOfClass:[NSArray class]]) { self.products = @[]; + self.productsRawJSON = nil; self.selectedIndex = NSNotFound; [self.collectionView reloadData]; self.emptyLabel.hidden = NO; [self updatePurchaseButtonState]; return; } + id sanitized = KBKeyboardSubscriptionSanitizeJSON(dataObj); + self.productsRawJSON = [sanitized isKindOfClass:NSArray.class] ? (NSArray *)sanitized : nil; NSArray *models = [KBKeyboardSubscriptionProduct mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj]; self.products = models ?: @[]; self.selectedIndex = self.products.count > 0 ? 0 : NSNotFound; @@ -221,6 +255,25 @@ static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptio }]; } +- (void)kb_persistPrefillPayloadForProduct:(KBKeyboardSubscriptionProduct *)product { + if (![product isKindOfClass:KBKeyboardSubscriptionProduct.class]) { return; } + if (![self.productsRawJSON isKindOfClass:NSArray.class] || self.productsRawJSON.count == 0) { return; } + NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup]; + if (!ud) { return; } + NSMutableDictionary *payload = [NSMutableDictionary dictionary]; + payload[@"ts"] = @((long long)floor([NSDate date].timeIntervalSince1970)); + payload[@"src"] = @"keyboard"; + if (product.productId.length) { + payload[@"productId"] = product.productId; + } + if (self.selectedIndex != NSNotFound) { + payload[@"selectedIndex"] = @(self.selectedIndex); + } + payload[@"products"] = self.productsRawJSON; + [ud setObject:payload forKey:AppGroup_SubscriptionPrefillPayload]; + [ud synchronize]; +} + - (void)selectCurrentProductAnimated:(BOOL)animated { if (self.selectedIndex == NSNotFound || self.selectedIndex >= self.products.count) { return; } NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.selectedIndex inSection:0]; @@ -401,4 +454,3 @@ static NSString * const kKBKeyboardSubscriptionCellId = @"kKBKeyboardSubscriptio } @end - diff --git a/Shared/KBConfig.h b/Shared/KBConfig.h index 63c85d4..79ab795 100644 --- a/Shared/KBConfig.h +++ b/Shared/KBConfig.h @@ -24,6 +24,9 @@ /// 键盘JSON数据 #define AppGroup_MyKbJson @"AppGroup_MyKbJson" +/// 键盘 -> 主 App 订阅页预填充数据(用于免二次请求) +#define AppGroup_SubscriptionPrefillPayload @"AppGroup_SubscriptionPrefillPayload" + /// 皮肤图标加载模式: /// 0 = 使用本地 Assets 图片名(key_icons 的 value 写成图片名,例如 "kb_q_melon") /// 1 = 使用远程 Zip 皮肤包(skinJSON 中提供 zip_url;key_icons 的 value 写成 Zip 内图标文件名,例如 "key_a") diff --git a/keyBoard/AppDelegate.m b/keyBoard/AppDelegate.m index 95abc4f..0af4a55 100644 --- a/keyBoard/AppDelegate.m +++ b/keyBoard/AppDelegate.m @@ -24,6 +24,9 @@ #import "KBVipPay.h" #import "KBUserSessionManager.h" #import "KBLoginVC.h" +#import "KBConfig.h" + +static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0; // 注意:用于判断系统已启用本输入法扩展的 bundle id 需与扩展 target 的 // PRODUCT_BUNDLE_IDENTIFIER 完全一致。 @@ -199,8 +202,36 @@ if ([action isEqualToString:@"autopay"]) { autoPay = YES; } + + BOOL wantsPrefill = NO; + NSString *prefillFlag = params[@"prefill"]; + if ([prefillFlag respondsToSelector:@selector(boolValue)] && prefillFlag.boolValue) { + wantsPrefill = YES; + } + NSString *src = params[@"src"]; + if ([src isKindOfClass:NSString.class] && [src.lowercaseString isEqualToString:@"keyboard"]) { + wantsPrefill = YES; + } + NSDictionary *prefillPayload = wantsPrefill ? [self kb_consumeSubscriptionPrefillPayloadIfValid] : nil; + if ([prefillPayload isKindOfClass:NSDictionary.class]) { + NSString *payloadProductId = prefillPayload[@"productId"]; + if (productId.length == 0 && [payloadProductId isKindOfClass:NSString.class]) { + productId = payloadProductId; + } + } + KBVipPay *vc = [[KBVipPay alloc] init]; - [vc configureWithProductId:productId autoPurchase:autoPay]; + if ([prefillPayload isKindOfClass:NSDictionary.class]) { + NSArray *productsJSON = prefillPayload[@"products"]; + NSNumber *selectedIndexNumber = prefillPayload[@"selectedIndex"]; + NSInteger selectedIndex = [selectedIndexNumber respondsToSelector:@selector(integerValue)] ? selectedIndexNumber.integerValue : NSNotFound; + [vc configureWithProductId:productId + autoPurchase:autoPay + prefillProductsJSON:productsJSON + selectedIndex:selectedIndex]; + } else { + [vc configureWithProductId:productId autoPurchase:autoPay]; + } [KB_CURRENT_NAV pushViewController:vc animated:true]; return YES; } @@ -209,6 +240,23 @@ return NO; } +- (nullable NSDictionary *)kb_consumeSubscriptionPrefillPayloadIfValid { + NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup]; + if (!ud) { return nil; } + id obj = [ud objectForKey:AppGroup_SubscriptionPrefillPayload]; + [ud removeObjectForKey:AppGroup_SubscriptionPrefillPayload]; + [ud synchronize]; + if (![obj isKindOfClass:NSDictionary.class]) { return nil; } + NSDictionary *payload = (NSDictionary *)obj; + NSNumber *ts = payload[@"ts"]; + if (![ts respondsToSelector:@selector(doubleValue)]) { return nil; } + NSTimeInterval age = [NSDate date].timeIntervalSince1970 - ts.doubleValue; + if (age < 0 || age > kKBSubscriptionPrefillTTL) { return nil; } + id products = payload[@"products"]; + if (![products isKindOfClass:NSArray.class] || ((NSArray *)products).count == 0) { return nil; } + return payload; +} + - (NSDictionary *)kb_queryParametersFromURL:(NSURL *)url { if (!url) { return @{}; } NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; diff --git a/keyBoard/Class/Pay/VC/KBVipPay.h b/keyBoard/Class/Pay/VC/KBVipPay.h index 5504032..c5d254e 100644 --- a/keyBoard/Class/Pay/VC/KBVipPay.h +++ b/keyBoard/Class/Pay/VC/KBVipPay.h @@ -15,6 +15,12 @@ NS_ASSUME_NONNULL_BEGIN /// 通过键盘深链配置初始商品及是否自动发起购买 - (void)configureWithProductId:(nullable NSString *)productId autoPurchase:(BOOL)autoPurchase; + +/// 通过键盘扩展预填充商品列表(免二次请求) +- (void)configureWithProductId:(nullable NSString *)productId + autoPurchase:(BOOL)autoPurchase + prefillProductsJSON:(nullable NSArray *)productsJSON + selectedIndex:(NSInteger)selectedIndex; @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/VC/KBVipPay.m b/keyBoard/Class/Pay/VC/KBVipPay.m index e9eb266..b43f65c 100644 --- a/keyBoard/Class/Pay/VC/KBVipPay.m +++ b/keyBoard/Class/Pay/VC/KBVipPay.m @@ -13,6 +13,7 @@ #import "KBPayProductModel.h" #import "KBBizCode.h" #import "keyBoard-Swift.h" +#import static NSString * const kKBVipHeaderId = @"kKBVipHeaderId"; static NSString * const kKBVipSubscribeCellId = @"kKBVipSubscribeCellId"; @@ -37,6 +38,9 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; @property (nonatomic, assign) BOOL pendingAutoPurchase; @property (nonatomic, assign) BOOL hasTriggeredAutoPurchase; @property (nonatomic, assign) BOOL viewVisible; +@property (nonatomic, copy, nullable) NSArray *prefillProductsJSON; +@property (nonatomic, assign) NSInteger prefillSelectedIndex; +@property (nonatomic, assign) BOOL didApplyPrefillPlans; @end @@ -59,6 +63,7 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; self.payVM = [PayVM new]; self.plans = @[]; self.selectedIndex = NSNotFound; + self.prefillSelectedIndex = NSNotFound; // 组装主列表 [self.view addSubview:self.collectionView]; @@ -101,9 +106,12 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; // 预计算 Header 高度(由内部约束决定) self.headerHeight = [self kb_calcHeaderHeightForWidth:KB_SCREEN_WIDTH]; + BOOL appliedPrefill = [self kb_applyPrefillPlansIfPossible]; [self.collectionView reloadData]; - - [self fetchSubscriptionPlans]; + [self selectCurrentPlanAnimated:NO]; + if (!appliedPrefill) { + [self fetchSubscriptionPlans]; + } } - (void)viewDidAppear:(BOOL)animated { @@ -128,6 +136,26 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; [self kb_triggerAutoPurchaseIfNeeded]; } +- (void)configureWithProductId:(nullable NSString *)productId + autoPurchase:(BOOL)autoPurchase + prefillProductsJSON:(nullable NSArray *)productsJSON + selectedIndex:(NSInteger)selectedIndex { + self.pendingProductId = productId.length ? [productId copy] : nil; + self.pendingAutoPurchase = autoPurchase; + self.hasTriggeredAutoPurchase = NO; + self.prefillProductsJSON = [productsJSON isKindOfClass:NSArray.class] ? [productsJSON copy] : nil; + self.prefillSelectedIndex = selectedIndex; + self.didApplyPrefillPlans = NO; + if (self.isViewLoaded) { + BOOL ok = [self kb_applyPrefillPlansIfPossible]; + if (ok) { + [self.collectionView reloadData]; + [self selectCurrentPlanAnimated:NO]; + } + } + [self kb_triggerAutoPurchaseIfNeeded]; +} + #pragma mark - Data - (void)fetchSubscriptionPlans { __weak typeof(self) weakSelf = self; @@ -154,6 +182,30 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; }]; } +- (BOOL)kb_applyPrefillPlansIfPossible { + if (self.didApplyPrefillPlans) { + return (self.plans.count > 0); + } + if (![self.prefillProductsJSON isKindOfClass:NSArray.class] || self.prefillProductsJSON.count == 0) { + return NO; + } + NSArray *models = [KBPayProductModel mj_objectArrayWithKeyValuesArray:self.prefillProductsJSON]; + if (![models isKindOfClass:NSArray.class] || models.count == 0) { + return NO; + } + self.didApplyPrefillPlans = YES; + self.plans = models; + NSInteger idx = self.prefillSelectedIndex; + if (idx != NSNotFound && idx >= 0 && idx < (NSInteger)self.plans.count) { + self.selectedIndex = idx; + } else { + self.selectedIndex = (self.plans.count > 0) ? 0 : NSNotFound; + } + [self prepareStoreKitWithPlans:self.plans]; + [self kb_applyPendingPrefillIfNeeded]; + return YES; +} + - (void)prepareStoreKitWithPlans:(NSArray *)plans { if (![plans isKindOfClass:NSArray.class] || plans.count == 0) { return; } NSMutableArray *ids = [NSMutableArray array];