封装跨应用拉起,

This commit is contained in:
2025-11-21 18:26:02 +08:00
parent 0f4ca89060
commit fc87c545a0
11 changed files with 251 additions and 295 deletions

View File

@@ -16,6 +16,7 @@
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
#import "KBSkinInstallBridge.h"
#import "KBExtensionAppLauncher.h"
// 使 static kb_consumePendingShopSkin
@interface KeyboardViewController (KBSkinShopBridge)
@@ -206,13 +207,25 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
}
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
if (index == 0) {
///
[KBHUD showInfo:KBLocalized(@"Recharge Now")];
// [self showFunctionPanel:YES];
} else {
if (index != 0) {
[self showFunctionPanel:NO];
return;
}
// 1. schemekbkeyboardAppExtension://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完成充值")];
}
}];
}
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {

View File

@@ -0,0 +1,36 @@
//
// KBExtensionAppLauncher.h
// CustomKeyboard
//
// 封装:在键盘扩展中拉起主 AppScheme / Universal Link + 响应链兜底)。
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBExtensionAppLauncher : NSObject
/// 通用入口:优先尝试 primaryURL失败后尝试 fallbackURL
/// 两者都失败时再通过响应链openURL:)做兜底。
/// - Parameters:
/// - primaryURL: 第一优先尝试的 URL可为 Scheme 或 UL
/// - fallbackURL: 失败时的备用 URL可为 nil
/// - ivc: 当前的 UIInputViewController用于 extensionContext openURL
/// - source: 兜底时用作起点的 responder通常传 self 或 self.view
/// - completion: 最终是否“看起来已成功发起”打开动作(不保证一定跳转到 App
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
fallbackURL:(NSURL * _Nullable)fallbackURL
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion;
/// 简化版:只针对单一 Scheme 做尝试 + 响应链兜底。
+ (void)openScheme:(NSURL *)scheme
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,121 @@
//
// KBExtensionAppLauncher.m
// CustomKeyboard
//
#import "KBExtensionAppLauncher.h"
#import <objc/message.h>
@implementation KBExtensionAppLauncher
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
fallbackURL:(NSURL * _Nullable)fallbackURL
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion {
if (!ivc || (!primaryURL && !fallbackURL)) {
if (completion) { completion(NO); }
return;
}
// 线 dispatch
void (^finish)(BOOL) = ^(BOOL ok){
if (!completion) return;
if ([NSThread isMainThread]) {
completion(ok);
} else {
dispatch_async(dispatch_get_main_queue(), ^{ completion(ok); });
}
};
NSURL *first = primaryURL ?: fallbackURL;
NSURL *second = (first == primaryURL) ? fallbackURL : nil;
if (!first) {
finish(NO);
return;
}
[ivc.extensionContext openURL:first completionHandler:^(BOOL ok) {
if (ok) {
finish(YES);
return;
}
if (second) {
[ivc.extensionContext openURL:second completionHandler:^(BOOL ok2) {
if (ok2) {
finish(YES);
return;
}
BOOL bridged = [self p_bridgeFirst:first second:second from:source];
finish(bridged);
}];
} else {
BOOL bridged = [self p_bridgeFirst:first second:nil from:source];
finish(bridged);
}
}];
}
+ (void)openScheme:(NSURL *)scheme
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion {
[self openPrimaryURL:scheme
fallbackURL:nil
usingInputController:ivc
source:source
completion:completion];
}
#pragma mark - Private
// openURL: KBURLOpenBridge
+ (BOOL)p_openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
#if KB_URL_BRIDGE_ENABLE
if (!url || !start) return NO;
SEL sel = NSSelectorFromString(@"openURL:");
UIResponder *responder = start;
while (responder) {
@try {
if ([responder respondsToSelector:sel]) {
BOOL handled = NO;
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
if (funcBool) {
handled = funcBool(responder, sel, url);
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[responder performSelector:sel withObject:url];
handled = YES;
#pragma clang diagnostic pop
}
return handled;
}
} @catch (__unused NSException *e) {
// ignore and continue
}
responder = responder.nextResponder;
}
return NO;
#else
(void)url; (void)start;
return NO;
#endif
}
+ (BOOL)p_bridgeFirst:(NSURL * _Nullable)first
second:(NSURL * _Nullable)second
from:(UIResponder *)source {
BOOL bridged = NO;
if (first) {
bridged = [self p_openURLViaResponder:first from:source];
}
if (!bridged && second) {
bridged = [self p_openURLViaResponder:second from:source];
}
return bridged;
}
@end

View File

@@ -1,30 +0,0 @@
//
// KBURLOpenBridge.h
// 非公开:通过响应链查找 `openURL:` 选择器,尝试在扩展环境中打开自定义 scheme。
// 警告:存在审核风险。默认仅 Debug 启用(见 KB_URL_BRIDGE_ENABLE
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#ifndef KB_URL_BRIDGE_ENABLE
#if DEBUG
#define KB_URL_BRIDGE_ENABLE 1
#else
#define KB_URL_BRIDGE_ENABLE 0
#endif
#endif
@interface KBURLOpenBridge : NSObject
/// 尝试通过响应链调用 openURL:(仅在 KB_URL_BRIDGE_ENABLE 为 1 时执行)。
/// @param url 自定义 scheme如 kbkeyboard://settings
/// @param start 起始 responder传 self 或任意视图)
/// @return 是否看起来已发起打开动作(不保证一定成功)
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,46 +0,0 @@
//
// KBURLOpenBridge.m
//
#import "KBURLOpenBridge.h"
#import <objc/message.h>
@implementation KBURLOpenBridge
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
#if KB_URL_BRIDGE_ENABLE
if (!url || !start) return NO;
SEL sel = NSSelectorFromString(@"openURL:");
UIResponder *responder = start;
while (responder) {
@try {
if ([responder respondsToSelector:sel]) {
// 退 performSelector
BOOL handled = NO;
// (BOOL)openURL:(NSURL *)
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
if (funcBool) {
handled = funcBool(responder, sel, url);
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[responder performSelector:sel withObject:url];
handled = YES;
#pragma clang diagnostic pop
}
return handled;
}
} @catch (__unused NSException *e) {
// ignore and continue
}
responder = responder.nextResponder;
}
return NO;
#else
(void)url; (void)start;
return NO;
#endif
}
@end

View File

@@ -7,7 +7,7 @@
#import "Masonry.h"
#import "KBResponderUtils.h" // UIInputViewController
#import "KBHUD.h"
#import "KBURLOpenBridge.h"
#import "KBExtensionAppLauncher.h"
@interface KBFullAccessGuideView ()
@property (nonatomic, strong) UIControl *backdrop;
@@ -168,63 +168,20 @@
// Universal Link AASA/Associated Domains KB_UL_BASE
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
void (^finish)(BOOL) = ^(BOOL ok){
if (ok) { [self dismiss]; }
else {
__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];
}
};
// Scheme宿 App
if (scheme) {
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok) {
if (ok) { finish(YES); return; }
if (ul) {
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok2) {
if (ok2) { finish(YES); return; }
// openURL:
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
if (!bridged && ul) {
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
}
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}];
} else {
// UL Scheme
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}
}];
return;
}
// scheme UL
if (ul) {
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) { finish(YES); return; }
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}];
} else {
finish(NO);
}
}];
}
@end

View File

@@ -16,8 +16,8 @@
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
#import "KBAuthManager.h" //
#import "KBULBridge.h" // Darwin UL
#import "KBURLOpenBridge.h" // openURL:
#import "KBULBridgeNotification.h" // Darwin UL
#import "KBExtensionAppLauncher.h"
#import "KBStreamTextView.h" //
#import "KBStreamOverlayView.h" //
#import "KBFunctionTagListView.h"
@@ -329,16 +329,11 @@ 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;
[ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) {
if (ok2) return;
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
if (!bridged) {
[KBExtensionAppLauncher openScheme:scheme
usingInputController:ivc
source:self
completion:^(BOOL success) {
if (!success) {
[KBHUD showInfo:KBLocalized(@"请切换到主App完成登录")];
}
}];
@@ -383,29 +378,18 @@ 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(), ^{
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) return; // Universal Link
// 使 App Scheme
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
if (ok2) return;
// openURL:
// 宿
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
if (!bridged) {
// 访宿 Manager
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
}
}];
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) {
// 访宿 Manager
dispatch_async(dispatch_get_main_queue(), ^{
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self];
});
}
}];
});
}