From 7029209a4d8ad8f3bcebe401f88d37cf70586d4c Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Wed, 4 Mar 2026 19:49:07 +0800 Subject: [PATCH] 1 --- Shared/KBAPI.h | 2 + Shared/KBLocalizationManager.h | 4 +- Shared/KBLocalizationManager.m | 14 +- keyBoard/AppDelegate.m | 226 +++++++++++++++++++++++++--- keyBoard/Class/Login/VM/KBLoginVM.h | 4 + keyBoard/Class/Login/VM/KBLoginVM.m | 68 +++++++++ 6 files changed, 293 insertions(+), 25 deletions(-) diff --git a/Shared/KBAPI.h b/Shared/KBAPI.h index 23ed4d3..120230d 100644 --- a/Shared/KBAPI.h +++ b/Shared/KBAPI.h @@ -25,6 +25,8 @@ #define API_RESET_PWD @"/user/resetPassWord" // 重置密码 #define API_USER_FEEDBACK @"/user/feedback" // 提交反馈 +#define API_APP_CHECK_UPDATE @"/appVersions/checkUpdate" // 检查更新 + #define API_LOGOUT @"/user/logout" // 退出登录 diff --git a/Shared/KBLocalizationManager.h b/Shared/KBLocalizationManager.h index c8c39c6..356511b 100644 --- a/Shared/KBLocalizationManager.h +++ b/Shared/KBLocalizationManager.h @@ -19,10 +19,10 @@ FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeEnglish; // @"en" FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeSimplifiedChinese; // @"zh-Hans" FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeTraditionalChinese; // @"zh-Hant" FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeSpanish; // @"es" -FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodePortuguese; // @"pt" +FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodePortuguese; // @"pt-PT" FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeIndonesian; // @"id" -/// 默认支持的语言列表(当前:en / zh-Hans / zh-Hant / es / pt / id) +/// 默认支持的语言列表(当前:en / zh-Hans / zh-Hant / es / pt-PT / id) FOUNDATION_EXPORT NSArray *KBDefaultSupportedLanguageCodes(void); /// 当前语言变更通知(不附带 userInfo) diff --git a/Shared/KBLocalizationManager.m b/Shared/KBLocalizationManager.m index 7572faa..99dad17 100644 --- a/Shared/KBLocalizationManager.m +++ b/Shared/KBLocalizationManager.m @@ -12,7 +12,7 @@ KBLanguageCode const KBLanguageCodeEnglish = @"en"; KBLanguageCode const KBLanguageCodeSimplifiedChinese = @"zh-Hans"; KBLanguageCode const KBLanguageCodeTraditionalChinese = @"zh-Hant"; KBLanguageCode const KBLanguageCodeSpanish = @"es"; -KBLanguageCode const KBLanguageCodePortuguese = @"pt"; +KBLanguageCode const KBLanguageCodePortuguese = @"pt-PT"; KBLanguageCode const KBLanguageCodeIndonesian = @"id"; /// 默认支持语言列表(集中配置) @@ -94,7 +94,7 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) { if (code.length == 0) return; // 忽略空值 if ([code isEqualToString:self.currentLanguageCode]) return; // 无变更 [self applyLanguage:code]; - if (persist) { [[self class] kc_write:code]; } // 需同步到 App/扩展 + if (persist) { [[self class] kc_write:self.currentLanguageCode]; } // 需同步到 App/扩展 [[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil]; } @@ -160,12 +160,16 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) { #pragma mark - 内部实现 - (void)applyLanguage:(NSString *)code { - _currentLanguageCode = [code copy]; + NSString *normalizedCode = code; + if ([normalizedCode.lowercaseString isEqualToString:@"pt"]) { + normalizedCode = @"pt-PT"; + } + _currentLanguageCode = [normalizedCode copy]; // 基于当前 Target(App 或扩展)的主 bundle 加载 .lproj 资源 - NSString *path = [NSBundle.mainBundle pathForResource:code ofType:@"lproj"]; + NSString *path = [NSBundle.mainBundle pathForResource:normalizedCode ofType:@"lproj"]; if (!path) { // 尝试去区域后缀:如 en-GB -> en - NSString *shortCode = [[code componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"-_"]] firstObject]; + NSString *shortCode = [[normalizedCode componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"-_"]] firstObject]; if (shortCode.length > 0) { path = [NSBundle.mainBundle pathForResource:shortCode ofType:@"lproj"]; } diff --git a/keyBoard/AppDelegate.m b/keyBoard/AppDelegate.m index 1a1ee66..2075ba5 100644 --- a/keyBoard/AppDelegate.m +++ b/keyBoard/AppDelegate.m @@ -37,6 +37,7 @@ static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0; @interface AppDelegate () @property (nonatomic, strong) LSTPopView *appUpdatePopView; +@property (nonatomic, copy) NSString *appUpdateURLString; @end @implementation AppDelegate @@ -87,31 +88,220 @@ static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0; } - (void)kb_requestAppUpdateAndPresentIfNeeded { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - if (self.appUpdatePopView) { return; } - CGFloat width = KBFit(323.0); - CGFloat height = KBFit(390.0); - KBAppUpdateView *view = [[KBAppUpdateView alloc] initWithFrame:CGRectMake(0, 0, width, height)]; - view.backgroundImageName = @"app_update_bg"; - view.delegate = self; - LSTPopView *pop = [LSTPopView initWithCustomView:view - parentView:nil - popStyle:LSTPopStyleScale - dismissStyle:LSTDismissStyleScale]; - pop.hemStyle = LSTHemStyleCenter; - pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; - pop.isClickBgDismiss = NO; - pop.cornerRadius = 0; - self.appUpdatePopView = pop; - [pop pop]; - }); + __weak typeof(self) weakSelf = self; + [[KBLoginVM shared] checkAppUpdateWithCompletion:^(NSDictionary * _Nullable data, NSError * _Nullable error) { + if (error) { return; } + if (![data isKindOfClass:NSDictionary.class]) { return; } + NSDictionary *payload = data[@"data"]; + if (![payload isKindOfClass:NSDictionary.class]) { + payload = (NSDictionary *)data; + } + if (![payload isKindOfClass:NSDictionary.class]) { return; } + if (![weakSelf kb_shouldPresentAppUpdateWithData:payload]) { return; } + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf kb_presentAppUpdateWithData:payload]; + }); + }]; +} + +- (void)kb_presentAppUpdateWithData:(NSDictionary *)data { + if (self.appUpdatePopView) { return; } + CGFloat width = KBFit(323.0); + CGFloat height = KBFit(390.0); + KBAppUpdateView *view = [[KBAppUpdateView alloc] initWithFrame:CGRectMake(0, 0, width, height)]; + view.backgroundImageName = @"app_update_bg"; + view.delegate = self; + NSString *title = [self kb_stringForKeys:@[@"title", @"updateTitle", @"headline", @"appTitle"] inDictionary:data]; + if (title.length > 0) { + view.titleText = title; + } + NSString *versionName = [self kb_stringForKeys:@[@"versionName", @"latestVersionName", @"newVersionName", @"version", @"latestVersion"] inDictionary:data]; + if (versionName.length > 0) { + if (![versionName hasPrefix:@"V"] && ![versionName hasPrefix:@"v"]) { + versionName = [NSString stringWithFormat:@"V%@", versionName]; + } + view.versionText = versionName; + } + NSString *contentTitle = [self kb_stringForKeys:@[@"contentTitle", @"updateContentTitle", @"descTitle", @"updateDescTitle"] inDictionary:data]; + if (contentTitle.length > 0) { + view.contentTitleText = contentTitle; + } + id contentObj = [self kb_objectForKeys:@[@"contentItems", @"updateContent", @"updateDesc", @"description", @"desc", @"content"] inDictionary:data]; + NSArray *items = [self kb_contentItemsFromObject:contentObj]; + if (items.count > 0) { + view.contentItems = items; + } + NSString *buttonTitle = [self kb_stringForKeys:@[@"upgradeButtonTitle", @"upgradeButtonText", @"buttonText", @"buttonTitle"] inDictionary:data]; + if (buttonTitle.length > 0) { + view.upgradeButtonTitle = buttonTitle; + } + self.appUpdateURLString = [self kb_stringForKeys:@[@"downloadUrl", @"downloadURL", @"url", @"appStoreUrl", @"appStoreURL", @"installUrl", @"upgradeUrl"] inDictionary:data]; + + LSTPopView *pop = [LSTPopView initWithCustomView:view + parentView:nil + popStyle:LSTPopStyleScale + dismissStyle:LSTDismissStyleScale]; + pop.hemStyle = LSTHemStyleCenter; + pop.bgColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; + pop.isClickBgDismiss = NO; + pop.cornerRadius = 0; + self.appUpdatePopView = pop; + [pop pop]; +} + +- (BOOL)kb_shouldPresentAppUpdateWithData:(NSDictionary *)data { + NSArray *flagKeys = @[@"needUpdate", @"hasUpdate", @"shouldUpdate", @"needUpgrade", @"hasUpgrade", @"update", @"isUpdate"]; + for (NSString *key in flagKeys) { + id obj = data[key]; + if (obj && ![obj isKindOfClass:NSNull.class] && + ([obj isKindOfClass:NSNumber.class] || [obj isKindOfClass:NSString.class])) { + return [self kb_boolValueFromObject:obj]; + } + } + NSArray *typeKeys = @[@"updateType", @"upgradeType", @"updateMode"]; + for (NSString *key in typeKeys) { + id obj = data[key]; + if (obj && ![obj isKindOfClass:NSNull.class] && + ([obj isKindOfClass:NSNumber.class] || [obj isKindOfClass:NSString.class])) { + NSInteger typeValue = [self kb_integerValueFromObject:obj]; + return typeValue > 0; + } + } + NSInteger localCode = [self kb_localVersionCode]; + NSArray *codeKeys = @[@"versionCode", @"latestVersionCode", @"newVersionCode", @"latestCode", @"buildNumber"]; + for (NSString *key in codeKeys) { + id obj = data[key]; + if (obj && ![obj isKindOfClass:NSNull.class]) { + NSInteger remoteCode = [self kb_versionCodeFromObject:obj]; + if (remoteCode > 0 && localCode > 0) { + return remoteCode > localCode; + } + } + } + return NO; +} + +- (nullable id)kb_objectForKeys:(NSArray *)keys inDictionary:(NSDictionary *)dict { + for (NSString *key in keys) { + id obj = dict[key]; + if (obj && ![obj isKindOfClass:NSNull.class]) { + return obj; + } + } + return nil; +} + +- (nullable NSString *)kb_stringForKeys:(NSArray *)keys inDictionary:(NSDictionary *)dict { + id obj = [self kb_objectForKeys:keys inDictionary:dict]; + return [self kb_stringFromObject:obj]; +} + +- (nullable NSString *)kb_stringFromObject:(id)obj { + if ([obj isKindOfClass:NSString.class]) { return (NSString *)obj; } + if ([obj respondsToSelector:@selector(stringValue)]) { return [obj stringValue]; } + return nil; +} + +- (BOOL)kb_boolValueFromObject:(id)obj { + if ([obj isKindOfClass:NSNumber.class]) { return ((NSNumber *)obj).boolValue; } + if ([obj isKindOfClass:NSString.class]) { + NSString *lower = [(NSString *)obj lowercaseString]; + if ([lower isEqualToString:@"true"] || [lower isEqualToString:@"yes"]) { return YES; } + if ([lower isEqualToString:@"false"] || [lower isEqualToString:@"no"]) { return NO; } + return lower.integerValue != 0; + } + return NO; +} + +- (NSInteger)kb_integerValueFromObject:(id)obj { + if ([obj respondsToSelector:@selector(integerValue)]) { return [obj integerValue]; } + return 0; +} + +- (NSInteger)kb_versionCodeFromObject:(id)obj { + if ([obj isKindOfClass:NSString.class]) { + NSString *digits = [self kb_digitsOnlyStringFromString:obj]; + if (digits.length > 0) { return digits.longLongValue; } + return 0; + } + if ([obj respondsToSelector:@selector(longLongValue)]) { + return (NSInteger)[obj longLongValue]; + } + return 0; +} + +- (NSArray *)kb_contentItemsFromObject:(id)obj { + if ([obj isKindOfClass:NSArray.class]) { + NSMutableArray *items = [NSMutableArray array]; + for (id item in (NSArray *)obj) { + NSString *text = [self kb_stringFromObject:item]; + if (text.length > 0) { + [items addObject:text]; + } + } + return items; + } + if ([obj isKindOfClass:NSString.class]) { + NSString *text = [(NSString *)obj stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (text.length == 0) { return @[]; } + NSCharacterSet *splitSet = [NSCharacterSet characterSetWithCharactersInString:@"\n;"]; + NSArray *parts = [text componentsSeparatedByCharactersInSet:splitSet]; + NSMutableArray *items = [NSMutableArray array]; + for (NSString *part in parts) { + NSString *trimmed = [part stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (trimmed.length > 0) { + [items addObject:trimmed]; + } + } + return items.count > 0 ? items : @[text]; + } + return @[]; +} + +- (NSInteger)kb_localVersionCode { + NSString *buildNumber = [self kb_stringFromObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]; + NSString *shortVersion = [self kb_stringFromObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]; + NSString *digits = [self kb_digitsOnlyStringFromString:buildNumber]; + if (digits.length == 0) { + digits = [self kb_digitsOnlyStringFromString:shortVersion]; + } + if (digits.length == 0) { return 0; } + return digits.longLongValue; +} + +- (nullable NSString *)kb_digitsOnlyStringFromString:(nullable NSString *)string { + if (![string isKindOfClass:NSString.class] || string.length == 0) { return nil; } + NSCharacterSet *set = [NSCharacterSet decimalDigitCharacterSet]; + NSMutableString *digits = [NSMutableString string]; + for (NSUInteger i = 0; i < string.length; i++) { + unichar c = [string characterAtIndex:i]; + if ([set characterIsMember:c]) { + [digits appendFormat:@"%C", c]; + } + } + return digits; } #pragma mark - KBAppUpdateViewDelegate - (void)appUpdateViewDidTapUpgrade:(KBAppUpdateView *)view { + NSString *urlString = self.appUpdateURLString; + if (urlString.length > 0) { + NSURL *url = [NSURL URLWithString:urlString]; + if (url) { + UIApplication *app = [UIApplication sharedApplication]; + if ([app canOpenURL:url]) { + if (@available(iOS 10.0, *)) { + [app openURL:url options:@{} completionHandler:nil]; + } else { + [app openURL:url]; + } + } + } + } [self.appUpdatePopView dismiss]; self.appUpdatePopView = nil; + self.appUpdateURLString = nil; } - (void)applicationDidBecomeActive:(UIApplication *)application{ diff --git a/keyBoard/Class/Login/VM/KBLoginVM.h b/keyBoard/Class/Login/VM/KBLoginVM.h index a5a1de8..387532d 100644 --- a/keyBoard/Class/Login/VM/KBLoginVM.h +++ b/keyBoard/Class/Login/VM/KBLoginVM.h @@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN typedef void(^KBLoginCompletion)(BOOL success, NSError * _Nullable error); typedef void(^KBRegisterCompletion)(BOOL success, NSError * _Nullable error); typedef void(^KBVerifyMailCompletion)(BOOL success, NSError * _Nullable error); +typedef void(^KBAppUpdateCompletion)(NSDictionary * _Nullable data, NSError * _Nullable error); @interface KBLoginVM : NSObject @@ -42,6 +43,9 @@ typedef void(^KBVerifyMailCompletion)(BOOL success, NSError * _Nullable error); /// 是否已登录:由 KBAuthManager 判断(是否存在有效 token) - (BOOL)isLoggedIn; +/// 检查是否需要更新 +- (void)checkAppUpdateWithCompletion:(KBAppUpdateCompletion)completion; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Login/VM/KBLoginVM.m b/keyBoard/Class/Login/VM/KBLoginVM.m index dbf3729..95f7e2f 100644 --- a/keyBoard/Class/Login/VM/KBLoginVM.m +++ b/keyBoard/Class/Login/VM/KBLoginVM.m @@ -164,6 +164,15 @@ }]; } +/// 检查更新 +- (void)checkAppUpdateWithCompletion:(KBAppUpdateCompletion)completion { + NSDictionary *params = [self kb_appUpdateParams]; + [[KBNetworkManager shared] POST:API_APP_CHECK_UPDATE jsonBody:params headers:nil autoShowBusinessError:NO completion:^(NSDictionary * _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (error) { if (completion) completion(nil, error); return; } + if (completion) completion(jsonOrData, nil); + }]; +} + #pragma mark - Private - (void)kb_syncKeyboardCharactersAfterLogin { @@ -228,4 +237,63 @@ return @(value); } +- (NSDictionary *)kb_appUpdateParams { + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + params[@"appId"] = @"main"; + params[@"platform"] = @"ios"; + params[@"channel"] = @"AppStore"; + NSString *shortVersion = [self kb_bundleShortVersion]; + if (shortVersion.length) { + params[@"clientVersionName"] = shortVersion; + } + NSString *buildNumber = [self kb_bundleBuildNumber]; + if (buildNumber.length) { + params[@"buildNumber"] = buildNumber; + } + NSNumber *versionCode = [self kb_versionCodeFromBuildNumber:buildNumber shortVersion:shortVersion]; +// if (versionCode) { + params[@"clientVersionCode"] = @1; +// } + return params; +} + +- (nullable NSString *)kb_bundleShortVersion { + id obj = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if ([obj isKindOfClass:NSString.class]) { return (NSString *)obj; } + if ([obj respondsToSelector:@selector(stringValue)]) { return [obj stringValue]; } + return nil; +} + +- (nullable NSString *)kb_bundleBuildNumber { + id obj = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + if ([obj isKindOfClass:NSString.class]) { return (NSString *)obj; } + if ([obj respondsToSelector:@selector(stringValue)]) { return [obj stringValue]; } + return nil; +} + +- (nullable NSNumber *)kb_versionCodeFromBuildNumber:(nullable NSString *)buildNumber + shortVersion:(nullable NSString *)shortVersion { + NSString *digits = [self kb_digitsOnlyStringFromString:buildNumber]; + if (digits.length == 0) { + digits = [self kb_digitsOnlyStringFromString:shortVersion]; + } + if (digits.length == 0) { return nil; } + long long value = digits.longLongValue; + if (value <= 0) { return nil; } + return @(value); +} + +- (nullable NSString *)kb_digitsOnlyStringFromString:(nullable NSString *)string { + if (![string isKindOfClass:NSString.class] || string.length == 0) { return nil; } + NSCharacterSet *set = [NSCharacterSet decimalDigitCharacterSet]; + NSMutableString *digits = [NSMutableString string]; + for (NSUInteger i = 0; i < string.length; i++) { + unichar c = [string characterAtIndex:i]; + if ([set characterIsMember:c]) { + [digits appendFormat:@"%C", c]; + } + } + return digits; +} + @end