diff --git a/CustomKeyboard/CustomKeyboard.entitlements b/CustomKeyboard/CustomKeyboard.entitlements index 5ea4409..1c1e84a 100644 --- a/CustomKeyboard/CustomKeyboard.entitlements +++ b/CustomKeyboard/CustomKeyboard.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.associated-domains + + applinks:app.tknb.net + com.apple.security.application-groups group.com.loveKey.nyx diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index 553aed6..e9a7d9f 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -16,7 +16,7 @@ #import "KBFullAccessManager.h" #import "KBSkinManager.h" #import "KBSkinInstallBridge.h" -#import "KBExtensionAppLauncher.h" +#import "KBHostAppLauncher.h" // 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。 @interface KeyboardViewController (KBSkinShopBridge) @@ -213,21 +213,26 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, [self showFunctionPanel:NO]; return; } + +// // 1. 构造充值入口的通用链接(UL)和自定义 Scheme: +// // - 当前复用 /login 路径,通过 entry=recharge 区分场景: +// // https://app.tknb.net/ul/login?src=keyboard&entry=recharge +// // - 若宿主拒绝或 UL 配置异常,则回退到 kbkeyboardAppExtension://recharge?src=keyboard +// NSString *ulStr = [NSString stringWithFormat:@"%@?src=keyboard&entry=recharge", KB_UL_RECHARGE]; +// NSURL *ul = [NSURL URLWithString:ulStr]; +// + NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME]; + NSURL *scheme = [NSURL URLWithString:schemeStr]; +// +// if (!ul && !scheme) { return; } +// + // 从当前视图作为起点,通过响应链找到 UIApplication 再调起主 App + BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view]; - // 1. 构造自定义 scheme:kbkeyboardAppExtension://recharge?src=keyboard - NSString *urlStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME]; - NSURL *scheme = [NSURL URLWithString:urlStr]; - if (!scheme) return; - - // 2. 通过统一工具封装:extensionContext + 响应链兜底 - [KBExtensionAppLauncher openScheme:scheme - usingInputController:self - source:self.view - completion:^(BOOL success) { - if (!success) { - [KBHUD showInfo:KBLocalized(@"请切换到主App完成充值")]; - } - }]; + if (!ok) { + // 失败兜底:给个文案提示 + // 比如:请回到桌面手动打开 XXX App 进行设置/充值 + } } - (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView { diff --git a/CustomKeyboard/View/KBFullAccessGuideView.m b/CustomKeyboard/View/KBFullAccessGuideView.m index 571a95f..4e8a7e8 100644 --- a/CustomKeyboard/View/KBFullAccessGuideView.m +++ b/CustomKeyboard/View/KBFullAccessGuideView.m @@ -7,7 +7,7 @@ #import "Masonry.h" #import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具 #import "KBHUD.h" -#import "KBExtensionAppLauncher.h" +#import "KBHostAppLauncher.h" @interface KBFullAccessGuideView () @property (nonatomic, strong) UIControl *backdrop; @@ -157,31 +157,20 @@ #pragma mark - Actions // 工具方法已提取到 KBResponderUtils.h -// 打开主 App,引导用户去系统设置开启完全访问:优先 Scheme,失败再试 UL;仍失败则提示手动路径。 +// 打开主 App,引导用户去系统设置开启完全访问:通过宿主 UIApplication + 自定义 Scheme 拉起。 - (void)onTapGoEnable { UIInputViewController *ivc = KBFindInputViewController(self); - if (!ivc) { [self dismiss]; return; } + // 找不到键盘控制器也可以尝试从自身 responder 链出发 + UIResponder *start = ivc.view ?: (UIResponder *)self; - // 自定义 Scheme(App 里在 openURL 中转到设置页) - // 统一使用主 App 的自定义 Scheme + // 自定义 Scheme(AppDelegate 中处理 kbkeyboardAppExtension://settings) NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]]; - // Universal Link(需 AASA/Associated Domains 配置且 KB_UL_BASE 与域名一致) - NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]]; - - __weak typeof(self) weakSelf = self; - [KBExtensionAppLauncher openPrimaryURL:scheme - fallbackURL:ul - usingInputController:ivc - source:self - completion:^(BOOL ok) { - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) return; - if (ok) { - [strongSelf dismiss]; - } else { - NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings → General → Keyboard → Keyboards → %@ → Allow Full Access"),AppName]; - [KBHUD showInfo:showInfo]; - } - }]; + BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start]; + if (ok) { + [self dismiss]; + } else { + NSString *showInfo = [NSString stringWithFormat:KBLocalized(@"Follow: Settings → General → Keyboard → Keyboards → %@ → Allow Full Access"),AppName]; + [KBHUD showInfo:showInfo]; + } } @end diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index cf9a339..aa90d38 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -17,7 +17,7 @@ #import "KBSkinManager.h" #import "KBAuthManager.h" // 登录态判断(共享钥匙串) #import "KBULBridgeNotification.h" // Darwin 通知常量(UL 已处理) -#import "KBExtensionAppLauncher.h" +#import "KBHostAppLauncher.h" #import "KBStreamTextView.h" // 流式文本视图 #import "KBStreamOverlayView.h" // 带关闭按钮的流式层 #import "KBFunctionTagListView.h" @@ -321,7 +321,7 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [ivc.extensionContext openURL:ul completionHandler:^(__unused BOOL ok) {}]; }); - // 双路兜底:500ms 内未收到主 App 确认,则回退到自定义 Scheme + // 双路兜底:500ms 内未收到主 App 确认,则回退到自定义 Scheme(通过宿主 UIApplication 打开) self.kb_ulHandledFlag = NO; NSUInteger token = ++self.kb_ulSeq; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ @@ -329,14 +329,15 @@ static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/ if (self.kb_ulHandledFlag) return; // 主 App 已确认处理 NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@://login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]]; if (!scheme) return; - [KBExtensionAppLauncher openScheme:scheme - usingInputController:ivc - source:self - completion:^(BOOL success) { - if (!success) { - [KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")]; - } - }]; + UIResponder *start = ivc.view ?: (UIResponder *)self; + // 让键盘失去焦点 + [ivc dismissKeyboard]; + BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start]; + if (!ok) { + [KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")]; + }else{ + + } }); return; } @@ -378,13 +379,16 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C if (!ul) return; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]]; - [KBExtensionAppLauncher openPrimaryURL:ul - fallbackURL:scheme - usingInputController:ivc - source:self - completion:^(BOOL success) { - if (!success) { + // 先尝试通过 extensionContext 打开 UL + [ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) { + if (ok) { + return; + } + // UL 失败时,再通过宿主 UIApplication + 自定义 Scheme 兜底 + NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]]; + UIResponder *start = ivc.view ?: (UIResponder *)self; + BOOL ok2 = [KBHostAppLauncher openHostAppURL:scheme fromResponder:start]; + if (!ok2) { // 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。 dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; @@ -402,6 +406,12 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C // - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗; // - iOS15 及以下不会弹窗,直接返回内容; // 注意:不要在非用户触发的时机主动读取(如 viewDidLoad),否则会造成“立刻弹窗”的体验。 + // 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。 + if (![[KBFullAccessManager shared] hasFullAccess]) { + // 未开启完全访问:保持原有引导路径 + [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; + return; + } UIPasteboard *pb = [UIPasteboard generalPasteboard]; NSString *text = pb.string; // 读取纯文本(可能触发系统粘贴权限弹窗) diff --git a/Shared/KBConfig.h b/Shared/KBConfig.h index ed3f722..28424a8 100644 --- a/Shared/KBConfig.h +++ b/Shared/KBConfig.h @@ -38,8 +38,10 @@ #define KB_UL_BASE @"https://app.tknb.net/ul" #endif -#define KB_UL_LOGIN KB_UL_BASE @"/login" -#define KB_UL_SETTINGS KB_UL_BASE @"/settings" +#define KB_UL_LOGIN KB_UL_BASE @"/login" +#define KB_UL_SETTINGS KB_UL_BASE @"/settings" +// 充值入口的通用链接:当前复用 /login 路径,通过 query 区分(避免额外配置 AASA 路径) +#define KB_UL_RECHARGE KB_UL_BASE @"/recharge" #endif /* KBConfig_h */ diff --git a/Shared/KBHostAppLauncher.h b/Shared/KBHostAppLauncher.h new file mode 100644 index 0000000..7fc6935 --- /dev/null +++ b/Shared/KBHostAppLauncher.h @@ -0,0 +1,18 @@ +// +// KBHostAppLauncher.h +// keyBoard +// +// Created by Mac on 2025/11/24. +// 从扩展拉起主app + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBHostAppLauncher : NSObject +/// 从某个 responder 出发,尝试通过 UIApplication 打开宿主 app 的 URL ++ (BOOL)openHostAppURL:(NSURL *)url fromResponder:(UIResponder *)startResponder; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Shared/KBHostAppLauncher.m b/Shared/KBHostAppLauncher.m new file mode 100644 index 0000000..a6fa1f9 --- /dev/null +++ b/Shared/KBHostAppLauncher.m @@ -0,0 +1,52 @@ +// +// KBHostAppLauncher.m +// keyBoard +// +// Created by Mac on 2025/11/24. +// + +// KBHostAppLauncher.m +#import "KBHostAppLauncher.h" +#import + +@implementation KBHostAppLauncher + ++ (BOOL)openHostAppURL:(NSURL *)url fromResponder:(UIResponder *)startResponder { + if (!url || !startResponder) return NO; + + UIResponder *responder = startResponder; + while (responder) { + if ([responder isKindOfClass:[UIApplication class]]) { + UIApplication *app = (UIApplication *)responder; + + if (@available(iOS 18.0, *)) { + // iOS 18+:用新的 open:options:completionHandler: + SEL sel = @selector(openURL:options:completionHandler:); + if ([app respondsToSelector:sel]) { + // 等价于 [app openURL:url options:@{} completionHandler:nil]; + void (*func)(id, SEL, NSURL *, NSDictionary *, void(^)(BOOL)) = (void *)objc_msgSend; + if (func) { + func(app, sel, url, @{}, nil); + return YES; + } + } + return NO; + } else { + // iOS 17-:兼容老的 openURL: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if ([app respondsToSelector:@selector(openURL:)]) { + return [app openURL:url]; + } +#pragma clang diagnostic pop + return NO; + } + } + responder = responder.nextResponder; + } + + return NO; +} + +@end + diff --git a/Shared/KBKeyboardPermissionManager.h b/Shared/KBKeyboardPermissionManager.h index 5699d56..44d65f5 100644 --- a/Shared/KBKeyboardPermissionManager.h +++ b/Shared/KBKeyboardPermissionManager.h @@ -25,6 +25,9 @@ typedef NS_ENUM(NSInteger, KBFARecord) { /// 最后一次由扩展上报的“完全访问”状态(来源:扩展运行后写入共享钥匙串) - (KBFARecord)lastKnownFullAccess; +/// 重置“完全访问”记录为 Unknown(删除共享钥匙串中的记录,用于 App 首次安装/升级时清理旧状态) +- (void)resetFullAccessRecord; + /// 扩展侧:上报“完全访问”状态(写入共享钥匙串,以便 App 读取) - (void)reportFullAccessFromExtension:(BOOL)granted; @@ -34,4 +37,3 @@ typedef NS_ENUM(NSInteger, KBFARecord) { @end NS_ASSUME_NONNULL_END - diff --git a/Shared/KBKeyboardPermissionManager.m b/Shared/KBKeyboardPermissionManager.m index 19ece74..587012f 100644 --- a/Shared/KBKeyboardPermissionManager.m +++ b/Shared/KBKeyboardPermissionManager.m @@ -20,7 +20,7 @@ static NSString * const kKBPermAccount = @"full_access"; // 保存一个字节/ #pragma mark - App side - (BOOL)isKeyboardEnabled { - // 与 AppDelegate 中同思路:遍历 activeInputModes,匹配自家扩展 bundle id + // 与 AppDelegate 中同思路:遍历 activeInputModes,匹配自家扩展 bundle id 方法2:网上看也有判断Bundle拿到keyboards的方法 for (UITextInputMode *mode in [UITextInputMode activeInputModes]) { NSString *identifier = nil; @try { identifier = [mode valueForKey:@"identifier"]; } @catch (__unused NSException *e) { identifier = nil; } @@ -63,6 +63,14 @@ static NSString * const kKBPermAccount = @"full_access"; // 保存一个字节/ [self keychainWrite:data]; } +#pragma mark - Reset + +- (void)resetFullAccessRecord { + // 删除共享钥匙串中的记录,让 lastKnownFullAccess 回退为 Unknown + NSMutableDictionary *query = [self baseKCQuery]; + SecItemDelete((__bridge CFDictionaryRef)query); +} + #pragma mark - Keychain shared blob - (NSMutableDictionary *)baseKCQuery { diff --git a/Shared/Localization/en.lproj/Localizable.strings b/Shared/Localization/en.lproj/Localizable.strings index bb37890..7708622 100644 --- a/Shared/Localization/en.lproj/Localizable.strings +++ b/Shared/Localization/en.lproj/Localizable.strings @@ -116,6 +116,13 @@ "Apply failed" = "Apply failed"; "Open agreement" = "Open agreement"; "Shop Mall" = "Shop Mall"; +"My skin" = "My skin"; +"my_skin_selected_count" = "Selected: %ld Skins"; +"Editor" = "Editor"; +"Cancel" = "Cancel"; +"Delete" = "Delete"; + + // Skin sample names "极光" = "Aurora"; diff --git a/Shared/Localization/zh-Hans.lproj/Localizable.strings b/Shared/Localization/zh-Hans.lproj/Localizable.strings index 2bca979..1fad9ef 100644 --- a/Shared/Localization/zh-Hans.lproj/Localizable.strings +++ b/Shared/Localization/zh-Hans.lproj/Localizable.strings @@ -117,6 +117,11 @@ "Apply failed" = "应用失败"; "Open agreement" = "跳转协议"; "Shop Mall" = "购物商城"; +"My skin" = "我的皮肤"; +"my_skin_selected_count" = "已选择:%ld 个皮肤"; +"Editor" = "编辑"; +"Cancel" = "取消"; +"Delete" = "删除"; // 皮肤示例名称 "极光" = "极光"; diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 9eb0ef1..5558b09 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -38,7 +38,6 @@ 0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; 046131112ECF3A6E00A6FADF /* fense.zip in Resources */ = {isa = PBXBuildFile; fileRef = 046131102ECF3A6E00A6FADF /* fense.zip */; }; 046131142ECF454500A6FADF /* KBKeyPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = 046131132ECF454500A6FADF /* KBKeyPreviewView.m */; }; - 046131172ED06FC800A6FADF /* KBExtensionAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 046131162ED06FC800A6FADF /* KBExtensionAppLauncher.m */; }; 0477BDF02EBB76E30055D639 /* HomeSheetVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */; }; 0477BDF32EBB7B850055D639 /* KBDirectionIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDF22EBB7B850055D639 /* KBDirectionIndicatorView.m */; }; 0477BDF72EBC63A80055D639 /* KBTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDF62EBC63A80055D639 /* KBTestVC.m */; }; @@ -47,6 +46,8 @@ 0477BE002EBC6A330055D639 /* HomeRankVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BDFF2EBC6A330055D639 /* HomeRankVC.m */; }; 0477BE042EBC83130055D639 /* HomeMainVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BE032EBC83130055D639 /* HomeMainVC.m */; }; 0477BEA22EBCF0000055D639 /* KBTopImageButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 0477BEA12EBCF0000055D639 /* KBTopImageButton.m */; }; + 04791F8E2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */; }; + 04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */; }; 047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650C2EBC8A840035E841 /* KBPanModalView.m */; }; 047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C650F2EBCA8DD0035E841 /* HomeRankContentVC.m */; }; 047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C654F2EBCBA9E0035E841 /* KBShopVC.m */; }; @@ -233,8 +234,6 @@ 046131102ECF3A6E00A6FADF /* fense.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = fense.zip; sourceTree = ""; }; 046131122ECF454500A6FADF /* KBKeyPreviewView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBKeyPreviewView.h; sourceTree = ""; }; 046131132ECF454500A6FADF /* KBKeyPreviewView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyPreviewView.m; sourceTree = ""; }; - 046131152ED06FC800A6FADF /* KBExtensionAppLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBExtensionAppLauncher.h; sourceTree = ""; }; - 046131162ED06FC800A6FADF /* KBExtensionAppLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBExtensionAppLauncher.m; sourceTree = ""; }; 0477BDEE2EBB76E30055D639 /* HomeSheetVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeSheetVC.h; sourceTree = ""; }; 0477BDEF2EBB76E30055D639 /* HomeSheetVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HomeSheetVC.m; sourceTree = ""; }; 0477BDF12EBB7B850055D639 /* KBDirectionIndicatorView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBDirectionIndicatorView.h; sourceTree = ""; }; @@ -251,6 +250,8 @@ 0477BE032EBC83130055D639 /* HomeMainVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HomeMainVC.m; sourceTree = ""; }; 0477BEA02EBCF0000055D639 /* KBTopImageButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTopImageButton.h; sourceTree = ""; }; 0477BEA12EBCF0000055D639 /* KBTopImageButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTopImageButton.m; sourceTree = ""; }; + 04791F8C2ED469C0004E8522 /* KBHostAppLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBHostAppLauncher.h; sourceTree = ""; }; + 04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBHostAppLauncher.m; sourceTree = ""; }; 047C650B2EBC8A840035E841 /* KBPanModalView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBPanModalView.h; sourceTree = ""; }; 047C650C2EBC8A840035E841 /* KBPanModalView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBPanModalView.m; sourceTree = ""; }; 047C650E2EBCA8DD0035E841 /* HomeRankContentVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeRankContentVC.h; sourceTree = ""; }; @@ -587,8 +588,6 @@ 0477BD942EBAFF4E0055D639 /* Utils */ = { isa = PBXGroup; children = ( - 046131152ED06FC800A6FADF /* KBExtensionAppLauncher.h */, - 046131162ED06FC800A6FADF /* KBExtensionAppLauncher.m */, ); path = Utils; sourceTree = ""; @@ -1276,6 +1275,8 @@ 049FB23E2EC4B6EF00FAB05D /* KBULBridgeNotification.m */, 04D1F6B02EDFF10A00B12345 /* KBSkinInstallBridge.h */, 04D1F6B12EDFF10A00B12345 /* KBSkinInstallBridge.m */, + 04791F8C2ED469C0004E8522 /* KBHostAppLauncher.h */, + 04791F8D2ED469C0004E8522 /* KBHostAppLauncher.m */, ); path = Shared; sourceTree = ""; @@ -1540,7 +1541,7 @@ 049FB2352EC45C6A00FAB05D /* NetworkStreamHandler.m in Sources */, 04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */, 04FC95B22EB0B2CC007BD342 /* KBSettingView.m in Sources */, - 046131172ED06FC800A6FADF /* KBExtensionAppLauncher.m in Sources */, + 04791F8E2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */, 049FB23B2EC4766700FAB05D /* KBFunctionTagListView.m in Sources */, 049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */, 049FB22F2EC34EB900FAB05D /* KBStreamTextView.m in Sources */, @@ -1573,6 +1574,7 @@ 04FC95E92EB23B67007BD342 /* KBNetworkManager.m in Sources */, 04FC95D22EB1E7AE007BD342 /* MyVC.m in Sources */, 04286A032ECB0A1600CE730C /* KBSexSelVC.m in Sources */, + 04791F8F2ED469C0004E8522 /* KBHostAppLauncher.m in Sources */, 047C65582EBCC06D0035E841 /* HomeRankCardCell.m in Sources */, 04D1F6B32EDFF10A00B12345 /* KBSkinInstallBridge.m in Sources */, 04122F912EC73AF700EF7AB3 /* KBVipPay.m in Sources */, @@ -1710,6 +1712,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = CustomKeyboard/CustomKeyboard.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -1743,6 +1746,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B12EC429812407B9F0E67565 /* Pods-CustomKeyboard.release.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = CustomKeyboard/CustomKeyboard.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; diff --git a/keyBoard/AppDelegate.m b/keyBoard/AppDelegate.m index dba7ade..e7340ef 100644 --- a/keyBoard/AppDelegate.m +++ b/keyBoard/AppDelegate.m @@ -22,6 +22,7 @@ #import "IAPVerifyTransactionObj.h" #import "FGIAPManager.h" #import "KBSexSelVC.h" +#import "KBKeyboardPermissionManager.h" // 注意:用于判断系统已启用本输入法扩展的 bundle id 需与扩展 target 的 // PRODUCT_BUNDLE_IDENTIFIER 完全一致。 @@ -37,6 +38,15 @@ /// 2:配置国际化 [KBLocalizationManager shared].supportedLanguageCodes = @[ @"en", @"zh-Hans"]; + // 首次安装/升级:重置“完全访问”记录,避免继承旧安装遗留在 Keychain 中的状态 + static NSString *const kKBFullAccessRecordInitializedKey = @"KBFullAccessRecordInitialized"; + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + if (![ud boolForKey:kKBFullAccessRecordInitializedKey]) { + [[KBKeyboardPermissionManager shared] resetFullAccessRecord]; + [ud setBool:YES forKey:kKBFullAccessRecordInitializedKey]; + [ud synchronize]; + } + // 首次安装先进入性别选择页;点击 Skip 或确认后再进入主 TabBar BOOL hasShownSexVC = [[NSUserDefaults standardUserDefaults] boolForKey:KBSexSelectShownKey]; if (hasShownSexVC) { @@ -110,13 +120,31 @@ NSString *host = url.host.lowercaseString ?: @""; if ([host hasSuffix:@"app.tknb.net"]) { NSString *path = url.path.lowercaseString ?: @""; - if ([path hasPrefix:@"/ul/settings"]) { [self kb_openAppSettings]; return YES; } + if ([path hasPrefix:@"/ul/settings"]) { + [self kb_openAppSettings]; + return YES; + } if ([path hasPrefix:@"/ul/login"]) { + // 区分普通登录与“充值”场景:通过 query 中的 entry=recharge 来判断 + NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + NSString *entry = nil; + for (NSURLQueryItem *item in components.queryItems ?: @[]) { + if ([item.name isEqualToString:@"entry"]) { + entry = item.value; + break; + } + } // 通知扩展:UL 已被主 App 接收,扩展可取消回退到 Scheme CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFStringRef)KBDarwinULHandled, NULL, NULL, true); - [self kb_presentLoginSheetIfNeeded]; + if ([entry isEqualToString:@"recharge"]) { + // 充值入口:拉起主 App 后进入充值相关页面(目前先做占位提示) + [KBHUD showInfo:@"去充值"]; + } else { + // 默认逻辑:登录 + [self kb_presentLoginSheetIfNeeded]; + } return YES; } } diff --git a/keyBoard/Class/Base/VC/BaseTabBarController.m b/keyBoard/Class/Base/VC/BaseTabBarController.m index 85bac38..ce5b9e2 100644 --- a/keyBoard/Class/Base/VC/BaseTabBarController.m +++ b/keyBoard/Class/Base/VC/BaseTabBarController.m @@ -61,7 +61,7 @@ // 测试储存Token // [[KBAuthManager shared] saveAccessToken:@"TEST" refreshToken:nil expiryDate:[NSDate dateWithTimeIntervalSinceNow:3600] userIdentifier:nil]; - [[KBAuthManager shared] signOut]; +// [[KBAuthManager shared] signOut]; } - (void)setupTabbarAppearance{ diff --git a/keyBoard/Class/Guard/V/KBGuideTopCell.m b/keyBoard/Class/Guard/V/KBGuideTopCell.m index 25f5c2b..a9dd428 100644 --- a/keyBoard/Class/Guard/V/KBGuideTopCell.m +++ b/keyBoard/Class/Guard/V/KBGuideTopCell.m @@ -138,6 +138,10 @@ _q1Label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; _q1Label.textColor = [UIColor blackColor]; _q1Label.text = KBLocalized(@"What are you doing?"); + // 支持点击复制 + _q1Label.userInteractionEnabled = YES; + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(kb_onTapQ1)]; + [_q1Label addGestureRecognizer:tap]; } return _q1Label; } @@ -148,8 +152,29 @@ _q2Label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; _q2Label.textColor = [UIColor blackColor]; _q2Label.text = KBLocalized(@"I'm going to take a shower."); + // 支持点击复制 + _q2Label.userInteractionEnabled = YES; + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(kb_onTapQ2)]; + [_q2Label addGestureRecognizer:tap]; } return _q2Label; } +/// 复制统一处理 +- (void)kb_copyTextToPasteboard:(NSString *)text { + if (text.length == 0) { return; } + UIPasteboard.generalPasteboard.string = text; + [KBHUD showInfo:KBLocalized(@"Copy Success")]; +} + +/// 点击第一条示例文案 +- (void)kb_onTapQ1 { + [self kb_copyTextToPasteboard:self.q1Label.text]; +} + +/// 点击第二条示例文案 +- (void)kb_onTapQ2 { + [self kb_copyTextToPasteboard:self.q2Label.text]; +} + @end diff --git a/keyBoard/Class/Home/V/KBTopThreeView.m b/keyBoard/Class/Home/V/KBTopThreeView.m index be5fe95..524028a 100644 --- a/keyBoard/Class/Home/V/KBTopThreeView.m +++ b/keyBoard/Class/Home/V/KBTopThreeView.m @@ -50,7 +50,7 @@ [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.cardImageView); - make.top.equalTo(self.cardImageView).offset(84); + make.top.equalTo(self.cardImageView).offset(KBFit(95)); }]; } diff --git a/keyBoard/Class/Me/V/KBMyHeaderView.m b/keyBoard/Class/Me/V/KBMyHeaderView.m index ba28307..481b7b9 100644 --- a/keyBoard/Class/Me/V/KBMyHeaderView.m +++ b/keyBoard/Class/Me/V/KBMyHeaderView.m @@ -44,7 +44,7 @@ make.centerY.equalTo(self.titleLabel); make.right.equalTo(self).offset(-20); make.height.mas_equalTo(34); - make.width.mas_greaterThanOrEqualTo(115); + make.width.mas_greaterThanOrEqualTo(110); }]; [self.avatarView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self).offset(26); @@ -118,7 +118,7 @@ if (!_keyboardBtn) { _keyboardBtn = [UIButton buttonWithType:UIButtonTypeCustom]; [_keyboardBtn setTitle:KBLocalized(@"My Keyboard") forState:UIControlStateNormal]; - _keyboardBtn.titleLabel.font = [UIFont systemFontOfSize:10 weight:UIFontWeightSemibold]; + _keyboardBtn.titleLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold]; [_keyboardBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; _keyboardBtn.backgroundColor = [UIColor colorWithHex:KBColorValue]; _keyboardBtn.layer.cornerRadius = 17; diff --git a/keyBoard/Class/Me/VC/MySkinVC.m b/keyBoard/Class/Me/VC/MySkinVC.m index 5354c6b..d9091ea 100644 --- a/keyBoard/Class/Me/VC/MySkinVC.m +++ b/keyBoard/Class/Me/VC/MySkinVC.m @@ -32,10 +32,10 @@ static NSString * const kMySkinCellId = @"kMySkinCellId"; self.view.backgroundColor = [UIColor whiteColor]; // self.title = @"My Skin"; // 标题 - + self.kb_titleLabel.text = KBLocalized(@"My skin"); // 右上角 Editor/Cancel 使用 BaseViewController 自定义导航栏的 kb_rightButton self.kb_rightButton.hidden = NO; - [self.kb_rightButton setTitle:@"Editor" forState:UIControlStateNormal]; + [self.kb_rightButton setTitle:KBLocalized(@"Editor") forState:UIControlStateNormal]; [self.kb_rightButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; [self.kb_rightButton addTarget:self action:@selector(onToggleEdit) forControlEvents:UIControlEventTouchUpInside]; @@ -49,13 +49,14 @@ static NSString * const kMySkinCellId = @"kMySkinCellId"; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT); make.left.right.equalTo(self.view); - make.bottom.equalTo(self.view.mas_bottom); + make.bottom.equalTo(self.bottomView.mas_top); }]; [self.bottomView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); - make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom); - make.height.mas_equalTo(64); + make.bottom.equalTo(self.view.mas_bottom); + // 初始未编辑状态:高度为 0,不占据空间 + make.height.mas_equalTo(0); }]; // 空态视图(LYEmptyView)统一样式 + 重试按钮 @@ -108,11 +109,21 @@ static NSString * const kMySkinCellId = @"kMySkinCellId"; self.editingMode = !self.editingMode; // 更新顶部按钮 - [self.kb_rightButton setTitle:(self.isEditingMode ? @"Cancel" : @"Editor") + [self.kb_rightButton setTitle:(self.isEditingMode ? KBLocalized(@"Cancel") : KBLocalized(@"Editor")) forState:UIControlStateNormal]; - // 控制底部栏显隐 - self.bottomView.hidden = !self.isEditingMode; + // 根据编辑态更新底部栏高度,并保持列表底部始终贴着 bottomView 顶部 + CGFloat targetHeight = self.isEditingMode ? (KB_SAFE_BOTTOM + 44.0) : 0.0; + // 展开前先确保可见,动画结束后再根据状态隐藏 + self.bottomView.hidden = NO; + [self.bottomView mas_updateConstraints:^(MASConstraintMaker *make) { + make.height.mas_equalTo(targetHeight); + }]; + [UIView animateWithDuration:0.25 animations:^{ + [self.view layoutIfNeeded]; + } completion:^(BOOL finished) { + self.bottomView.hidden = !self.isEditingMode; + }]; // 列表进入/退出多选 self.collectionView.allowsMultipleSelection = self.isEditingMode; @@ -155,7 +166,8 @@ static NSString * const kMySkinCellId = @"kMySkinCellId"; - (void)updateBottomUI { NSInteger count = self.collectionView.indexPathsForSelectedItems.count; - self.selectedLabel.text = [NSString stringWithFormat:@"Selected: %ld Skins", (long)count]; + NSString *format = KBLocalized(@"my_skin_selected_count"); + self.selectedLabel.text = [NSString stringWithFormat:format, (long)count]; BOOL enable = count > 0; self.deleteButton.enabled = enable; @@ -253,11 +265,12 @@ static NSString * const kMySkinCellId = @"kMySkinCellId"; [self.selectedLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(_bottomView).offset(16); - make.centerY.equalTo(_bottomView); + // 固定在安全区内的 64 高度中居中(顶部向下 32) + make.centerY.equalTo(_bottomView.mas_top).offset(32); }]; [self.deleteButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(_bottomView).offset(-16); - make.centerY.equalTo(_bottomView); + make.centerY.equalTo(_bottomView.mas_top).offset(32); make.width.mas_equalTo(92); make.height.mas_equalTo(36); }]; @@ -270,7 +283,7 @@ static NSString * const kMySkinCellId = @"kMySkinCellId"; _selectedLabel = [UILabel new]; _selectedLabel.textColor = [UIColor colorWithHex:0x666666]; _selectedLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; - _selectedLabel.text = @"Selected: 0 Skins"; +// _selectedLabel.text = @"Selected: 0 Skins"; } return _selectedLabel; } @@ -278,7 +291,7 @@ static NSString * const kMySkinCellId = @"kMySkinCellId"; - (UIButton *)deleteButton { if (!_deleteButton) { _deleteButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [_deleteButton setTitle:@"Delete" forState:UIControlStateNormal]; + [_deleteButton setTitle:KBLocalized(@"Delete") forState:UIControlStateNormal]; _deleteButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; _deleteButton.layer.cornerRadius = 18; _deleteButton.layer.borderWidth = 1; diff --git a/keyBoard/Class/Shared/KBConfig.h b/keyBoard/Class/Shared/KBConfig.h index 88ccf09..962b76e 100644 --- a/keyBoard/Class/Shared/KBConfig.h +++ b/keyBoard/Class/Shared/KBConfig.h @@ -18,8 +18,10 @@ #define KB_UL_BASE @"https://your.domain/ul" #endif -#define KB_UL_LOGIN KB_UL_BASE @"/login" -#define KB_UL_SETTINGS KB_UL_BASE @"/settings" +#define KB_UL_LOGIN KB_UL_BASE @"/login" +#define KB_UL_SETTINGS KB_UL_BASE @"/settings" +// 充值入口的通用链接:当前复用 /login 路径,通过 query 区分(避免额外配置 AASA 路径) +#define KB_UL_RECHARGE KB_UL_LOGIN #endif /* KBConfig_h */ diff --git a/keyBoard/Shared/KBConfig.h b/keyBoard/Shared/KBConfig.h index 88ccf09..962b76e 100644 --- a/keyBoard/Shared/KBConfig.h +++ b/keyBoard/Shared/KBConfig.h @@ -18,8 +18,10 @@ #define KB_UL_BASE @"https://your.domain/ul" #endif -#define KB_UL_LOGIN KB_UL_BASE @"/login" -#define KB_UL_SETTINGS KB_UL_BASE @"/settings" +#define KB_UL_LOGIN KB_UL_BASE @"/login" +#define KB_UL_SETTINGS KB_UL_BASE @"/settings" +// 充值入口的通用链接:当前复用 /login 路径,通过 query 区分(避免额外配置 AASA 路径) +#define KB_UL_RECHARGE KB_UL_LOGIN #endif /* KBConfig_h */