diff --git a/Shared/KBAPI.h b/Shared/KBAPI.h index 70b747e..509f3c3 100644 --- a/Shared/KBAPI.h +++ b/Shared/KBAPI.h @@ -51,6 +51,8 @@ #define API_THEME_PURCHASED @"/themes/purchased" // 查询已购买主题 #define API_WALLET_BALANCE @"/wallet/balance" // 查询钱包余额 #define API_THEME_DETAIL @"/themes/detail" // 查询主题详情 +#define API_THEME_PURCHASE @"/themes/purchase" // 购买主题 +#define API_THEME_DOWNLOAD @"/themes/download" // 主题下载信息 /// pay #define API_VALIDATE_RECEIPT @"/api/apple/validate-receipt" // 排行榜标签列表 diff --git a/keyBoard/Class/Me/V/KBSkinBottomActionView.h b/keyBoard/Class/Me/V/KBSkinBottomActionView.h index d80f4c9..33e5226 100644 --- a/keyBoard/Class/Me/V/KBSkinBottomActionView.h +++ b/keyBoard/Class/Me/V/KBSkinBottomActionView.h @@ -22,6 +22,9 @@ NS_ASSUME_NONNULL_BEGIN /// 图标(可选),例如金币图 @property (nonatomic, strong, nullable) UIImage *iconImage; +/// 是否展示价格区域(隐藏后仅显示标题) +@property (nonatomic, assign) BOOL showsPrice; + /// 点击回调(也可直接 addTarget 使用) @property (nonatomic, copy, nullable) void (^tapHandler)(void); @@ -34,4 +37,3 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END - diff --git a/keyBoard/Class/Me/V/KBSkinBottomActionView.m b/keyBoard/Class/Me/V/KBSkinBottomActionView.m index ba2c283..ad76656 100644 --- a/keyBoard/Class/Me/V/KBSkinBottomActionView.m +++ b/keyBoard/Class/Me/V/KBSkinBottomActionView.m @@ -7,10 +7,11 @@ #import "KBSkinBottomActionView.h" @interface KBSkinBottomActionView () -@property (nonatomic, strong) UIView *contentView; // 内部容器,使三项整体居中 -@property (nonatomic, strong) UILabel *titleLabel; // 左侧标题 +@property (nonatomic, strong) UIView *contentView; // 内部容器,使内容整体居中 +@property (nonatomic, strong) UIStackView *stackView; // 水平排列 Title/Icon/Price +@property (nonatomic, strong) UILabel *titleLabel; // 左侧标题 @property (nonatomic, strong) UIImageView *coinImageView; // 中间图标(可选) -@property (nonatomic, strong) UILabel *priceLabel; // 右侧价格 +@property (nonatomic, strong) UILabel *priceLabel; // 右侧价格 @end @implementation KBSkinBottomActionView @@ -37,24 +38,20 @@ }]; // 三个元素放进容器,左右顺序:Title - Icon - Price - [self.contentView addSubview:self.titleLabel]; - [self.contentView addSubview:self.coinImageView]; - [self.contentView addSubview:self.priceLabel]; - - [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.contentView); - make.centerY.equalTo(self.contentView); + [self.contentView addSubview:self.stackView]; + [self.stackView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.contentView); }]; + [self.stackView addArrangedSubview:self.titleLabel]; + [self.stackView addArrangedSubview:self.coinImageView]; + [self.stackView addArrangedSubview:self.priceLabel]; + if (@available(iOS 11.0, *)) { + [self.stackView setCustomSpacing:8 afterView:self.titleLabel]; + [self.stackView setCustomSpacing:6 afterView:self.coinImageView]; + } [self.coinImageView mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.titleLabel.mas_right).offset(8); - make.centerY.equalTo(self.contentView); make.width.height.mas_equalTo(18); }]; - [self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.coinImageView.mas_right).offset(6); - make.right.equalTo(self.contentView); - make.centerY.equalTo(self.contentView); - }]; // 默认文案 self.titleText = @"Download"; @@ -62,6 +59,7 @@ UIImage *img = [UIImage systemImageNamed:@"circle.fill"]; self.iconImage = img; // 若项目没有金币图标,用系统占位(黄色) self.coinImageView.tintColor = [UIColor colorWithRed:1.0 green:0.85 blue:0.2 alpha:1.0]; + self.showsPrice = YES; // 点击回调(可选) [self addTarget:self action:@selector(handleTap) forControlEvents:UIControlEventTouchUpInside]; } @@ -100,7 +98,9 @@ - (void)setPriceText:(NSString *)priceText { _priceText = [priceText copy]; - self.priceLabel.text = _priceText; + if (self.showsPrice) { + self.priceLabel.text = _priceText; + } } - (void)setIconImage:(UIImage *)iconImage { @@ -108,6 +108,17 @@ self.coinImageView.image = _iconImage; } +- (void)setShowsPrice:(BOOL)showsPrice { + if (_showsPrice == showsPrice) { return; } + _showsPrice = showsPrice; + self.coinImageView.hidden = !_showsPrice; + self.priceLabel.hidden = !_showsPrice; + self.priceLabel.text = _showsPrice ? self.priceText : @""; + if (@available(iOS 11.0, *)) { + [self.stackView setCustomSpacing:_showsPrice ? 8 : 0 afterView:self.titleLabel]; + } +} + #pragma mark - Lazy - (UILabel *)titleLabel { @@ -131,6 +142,16 @@ return _contentView; } +- (UIStackView *)stackView { + if (!_stackView) { + _stackView = [[UIStackView alloc] init]; + _stackView.axis = UILayoutConstraintAxisHorizontal; + _stackView.alignment = UIStackViewAlignmentCenter; + _stackView.spacing = 6; + } + return _stackView; +} + - (UIImageView *)coinImageView { if (!_coinImageView) { _coinImageView = [[UIImageView alloc] init]; diff --git a/keyBoard/Class/Shop/VC/KBSkinDetailVC.m b/keyBoard/Class/Shop/VC/KBSkinDetailVC.m index 562fafe..889e98a 100644 --- a/keyBoard/Class/Shop/VC/KBSkinDetailVC.m +++ b/keyBoard/Class/Shop/VC/KBSkinDetailVC.m @@ -13,6 +13,8 @@ #import "KBSkinBottomActionView.h" #import "KBShopVM.h" #import "KBShopThemeTagModel.h" +#import "KBHUD.h" +#import "KBSkinService.h" static NSString * const kHeaderCellId = @"kHeaderCellId"; static NSString * const kTagsContainerCellId = @"kTagsContainerCellId"; @@ -34,6 +36,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) { @property (nonatomic, copy) NSArray *gridData; // 底部网格数据 @property (nonatomic, strong) KBShopVM *shopVM; @property (nonatomic, strong, nullable) KBShopThemeDetailModel *detailModel; +@property (nonatomic, assign) BOOL isProcessingAction; @end @implementation KBSkinDetailVC @@ -69,6 +72,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) { make.bottom.equalTo(self.bottomBar.mas_top).offset(-10); }]; + [self updateBottomBarAppearance]; [self fetchThemeDetailIfNeeded]; } @@ -208,7 +212,87 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) { #pragma mark - Actions - (void)handleDownloadAction { - // 预留:下载/购买动作 + if (self.isProcessingAction) { return; } + if (self.themeId.length == 0) { + [KBHUD showInfo:KBLocalized(@"主题信息缺失")]; + return; + } + if (!self.detailModel) { + [KBHUD showInfo:KBLocalized(@"正在加载主题详情")]; + return; + } + if (self.detailModel.isPurchased) { + [self requestDownload]; + } else { + [self purchaseCurrentTheme]; + } +} + +- (void)purchaseCurrentTheme { + if (self.isProcessingAction) { return; } + self.isProcessingAction = YES; + [KBHUD show]; + __weak typeof(self) weakSelf = self; + [self.shopVM purchaseThemeWithId:self.themeId completion:^(BOOL success, NSError * _Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + weakSelf.isProcessingAction = NO; + [KBHUD dismiss]; + if (error || !success) { + NSString *msg = error.localizedDescription ?: KBLocalized(@"购买失败"); + [KBHUD showInfo:msg]; + return; + } + weakSelf.detailModel.isPurchased = YES; + [weakSelf updateBottomBarAppearance]; + [weakSelf requestDownload]; + }); + }]; +} + +- (void)requestDownload { + if (self.isProcessingAction) { return; } + self.isProcessingAction = YES; + [KBHUD show]; + NSMutableDictionary *skin = [NSMutableDictionary dictionary]; + if (!skin[@"id"] && self.detailModel.themeId) { + skin[@"id"] = self.detailModel.themeId; + } + if (!skin[@"name"] && self.detailModel.themeName) { + skin[@"name"] = self.detailModel.themeName; + } + if (!skin[@"themeDownloadUrl"]) { + [KBHUD showInfo:KBLocalized(@"缺少下载地址")]; + return; + } + skin[@"themeDownloadUrl"] = self.detailModel.themeDownloadUrl; + + [[KBSkinService shared] applySkinWithJSON:skin + fromViewController:self + mode:KBSkinSourceModeRemoteZip + completion:^(BOOL success) { + if (success) { + [KBHUD showSuccess:KBLocalized(@"已开始下载")]; + } else { + [KBHUD showInfo:KBLocalized(@"下载失败")]; + } + }]; +} + +- (void)updateBottomBarAppearance { + BOOL purchased = self.detailModel.isPurchased; + if (purchased) { + self.bottomBar.titleText = KBLocalized(@"Download again"); + self.bottomBar.showsPrice = NO; + } else { + NSString *price = self.detailModel ? [NSString stringWithFormat:@"%.2f", self.detailModel.themePrice] : @"0"; + self.bottomBar.titleText = KBLocalized(@"Download"); + self.bottomBar.priceText = price; + self.bottomBar.showsPrice = YES; + UIImage *coin = [UIImage imageNamed:@"shop_jb_icon"]; + if (coin) { + self.bottomBar.iconImage = coin; + } + } } - (void)fetchThemeDetailIfNeeded { @@ -235,6 +319,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) { } } weakSelf.tags = tagNames.copy; + [weakSelf updateBottomBarAppearance]; [weakSelf.collectionView reloadData]; }); }]; diff --git a/keyBoard/Class/Shop/VM/KBShopVM.h b/keyBoard/Class/Shop/VM/KBShopVM.h index 05a2e27..a2afa1a 100644 --- a/keyBoard/Class/Shop/VM/KBShopVM.h +++ b/keyBoard/Class/Shop/VM/KBShopVM.h @@ -23,6 +23,10 @@ typedef void(^KBShopBalanceCompletion)(NSNumber *_Nullable balance, NSError *_Nullable error); typedef void(^KBShopDetailCompletion)(KBShopThemeDetailModel *_Nullable detail, NSError *_Nullable error); +typedef void(^KBShopPurchaseCompletion)(BOOL success, + NSError *_Nullable error); +typedef void(^KBShopDownloadInfoCompletion)(NSDictionary *_Nullable info, + NSError *_Nullable error); @interface KBShopVM : NSObject @property (nonatomic, copy, readonly, nullable) NSArray *styles; @@ -41,6 +45,14 @@ typedef void(^KBShopDetailCompletion)(KBShopThemeDetailModel *_Nullable detail, - (void)fetchThemeDetailWithId:(nullable NSString *)themeId completion:(KBShopDetailCompletion)completion; +/// 购买主题 +- (void)purchaseThemeWithId:(nullable NSString *)themeId + completion:(KBShopPurchaseCompletion)completion; + +/// 获取主题下载信息 +- (void)fetchThemeDownloadInfoWithId:(nullable NSString *)themeId + completion:(KBShopDownloadInfoCompletion)completion; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Shop/VM/KBShopVM.m b/keyBoard/Class/Shop/VM/KBShopVM.m index e556ef3..b9d072a 100644 --- a/keyBoard/Class/Shop/VM/KBShopVM.m +++ b/keyBoard/Class/Shop/VM/KBShopVM.m @@ -13,6 +13,7 @@ @interface KBShopVM () @property (nonatomic, copy, readwrite, nullable) NSArray *styles; +- (id)kb_themeIdParamFromString:(NSString *)themeId; @end @@ -99,17 +100,17 @@ if (completion) completion(nil, [self kb_invalidResponseError]); return; } - NSString *balanceValue = dataObj[@"balanceDisplay"]; -// NSNumber *balanceNumber = nil; -// if ([balanceValue isKindOfClass:[NSNumber class]]) { -// balanceNumber = balanceValue; -// } else if ([balanceValue isKindOfClass:[NSString class]]) { -// balanceNumber = @([(NSString *)balanceValue doubleValue]); -// } -// if (!balanceNumber) { -// balanceNumber = @(0); -// } - if (completion) completion(balanceValue, nil); + id balanceValue = dataObj[@"balance"]; + NSNumber *balanceNumber = nil; + if ([balanceValue isKindOfClass:[NSNumber class]]) { + balanceNumber = balanceValue; + } else if ([balanceValue isKindOfClass:[NSString class]]) { + balanceNumber = @([(NSString *)balanceValue doubleValue]); + } + if (!balanceNumber) { + balanceNumber = @(0); + } + if (completion) completion(balanceNumber, nil); }]; } @@ -119,7 +120,7 @@ if (completion) completion(nil, [self kb_invalidParameterError]); return; } - NSDictionary *params = @{@"themeId": themeId}; + NSDictionary *params = @{@"themeId": [self kb_themeIdParamFromString:themeId]}; [[KBNetworkManager shared] GET:API_THEME_DETAIL parameters:params headers:nil @@ -141,4 +142,56 @@ }]; } +- (void)purchaseThemeWithId:(nullable NSString *)themeId + completion:(KBShopPurchaseCompletion)completion { + if (themeId.length == 0) { + if (completion) completion(NO, [self kb_invalidParameterError]); + return; + } + NSDictionary *body = @{@"themeId": [self kb_themeIdParamFromString:themeId]}; + [[KBNetworkManager shared] POST:API_THEME_PURCHASE + jsonBody:body + headers:nil + autoShowBusinessError:NO + completion:^(NSDictionary * _Nullable json, + NSURLResponse * _Nullable response, + NSError * _Nullable error) { + if (completion) completion(error == nil, error); + }]; +} + +- (void)fetchThemeDownloadInfoWithId:(nullable NSString *)themeId + completion:(KBShopDownloadInfoCompletion)completion { + if (themeId.length == 0) { + if (completion) completion(nil, [self kb_invalidParameterError]); + return; + } + NSDictionary *params = @{@"themeId": [self kb_themeIdParamFromString:themeId]}; + [[KBNetworkManager shared] GET:API_THEME_DOWNLOAD + parameters:params + headers:nil + autoShowBusinessError:NO + completion:^(NSDictionary * _Nullable json, + NSURLResponse * _Nullable response, + NSError * _Nullable error) { + if (error) { + if (completion) completion(nil, error); + return; + } + id dataObj = json[KBData] ?: json[@"data"]; + if (![dataObj isKindOfClass:[NSDictionary class]]) { + if (completion) completion(nil, [self kb_invalidResponseError]); + return; + } + if (completion) completion((NSDictionary *)dataObj, nil); + }]; +} + +- (id)kb_themeIdParamFromString:(NSString *)themeId { + if (themeId.length == 0) { return @""; } + NSNumberFormatter *formatter = [NSNumberFormatter new]; + NSNumber *number = [formatter numberFromString:themeId]; + return number ?: themeId; +} + @end