移动壳收紧外链打开检查
为移动壳外链打开增加系统可打开性校验 补充 HostBridge 外链失败测试与配置门禁 更新原生壳外链边界文档
This commit is contained in:
@@ -1063,3 +1063,11 @@ if (mobileCapabilitySet.has('app.setBadgeCount')) {
|
||||
if (!dispatchSource.includes('Appearance.getColorScheme()')) {
|
||||
throw new Error('mobile shell HostBridge must read the native color scheme');
|
||||
}
|
||||
|
||||
if (!dispatchSource.includes('Linking.canOpenURL(url)')) {
|
||||
throw new Error('mobile shell HostBridge external URL flow must check Linking.canOpenURL');
|
||||
}
|
||||
|
||||
if (!shellAppSource.includes('Linking.canOpenURL(externalUrl)')) {
|
||||
throw new Error('mobile shell WebView external navigation must check Linking.canOpenURL');
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ vi.mock('expo-image-picker', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('expo-linking', () => ({
|
||||
canOpenURL: vi.fn(),
|
||||
openURL: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -294,6 +295,8 @@ afterEach(() => {
|
||||
expires: 'never',
|
||||
});
|
||||
vi.mocked(Linking.openURL).mockReset();
|
||||
vi.mocked(Linking.canOpenURL).mockReset();
|
||||
vi.mocked(Linking.canOpenURL).mockResolvedValue(true);
|
||||
vi.mocked(Network.getNetworkStateAsync).mockReset();
|
||||
vi.mocked(Network.getNetworkStateAsync).mockResolvedValue({
|
||||
type: Network.NetworkStateType.WIFI,
|
||||
@@ -528,9 +531,26 @@ describe('handleMobileHostBridgeMessage', () => {
|
||||
);
|
||||
|
||||
expectOk(response);
|
||||
expect(Linking.canOpenURL).toHaveBeenCalledWith('https://example.com/path');
|
||||
expect(Linking.openURL).toHaveBeenCalledWith('https://example.com/path');
|
||||
});
|
||||
|
||||
test('app.openExternalUrl 在系统不可打开时返回 host_error', async () => {
|
||||
vi.mocked(Linking.canOpenURL).mockResolvedValue(false);
|
||||
|
||||
const response = await send(
|
||||
request('app.openExternalUrl', {
|
||||
url: 'tel:+12345678',
|
||||
}),
|
||||
);
|
||||
|
||||
const failedResponse = expectFailed(response);
|
||||
|
||||
expect(failedResponse.error.code).toBe('host_error');
|
||||
expect(failedResponse.error.message).toBe('external URL cannot be opened');
|
||||
expect(Linking.openURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('app.openExternalUrl 拒绝危险协议', async () => {
|
||||
const response = await send(
|
||||
request('app.openExternalUrl', {
|
||||
@@ -541,6 +561,7 @@ describe('handleMobileHostBridgeMessage', () => {
|
||||
const failedResponse = expectFailed(response);
|
||||
|
||||
expect(failedResponse.error.code).toBe('invalid_request');
|
||||
expect(Linking.canOpenURL).not.toHaveBeenCalled();
|
||||
expect(Linking.openURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -76,6 +76,13 @@ async function openExternalUrl(payload: unknown) {
|
||||
throw invalidRequest('url must use an allowed external protocol');
|
||||
}
|
||||
|
||||
if (!(await Linking.canOpenURL(url))) {
|
||||
throw {
|
||||
code: 'host_error',
|
||||
message: 'external URL cannot be opened',
|
||||
} satisfies HostBridgeError;
|
||||
}
|
||||
|
||||
await Linking.openURL(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -215,7 +215,9 @@ export default function ShellApp() {
|
||||
|
||||
const externalUrl = resolveMobileShellExternalUrl(request.url);
|
||||
if (externalUrl) {
|
||||
void Linking.openURL(externalUrl).catch(() => undefined);
|
||||
void Linking.canOpenURL(externalUrl)
|
||||
.then((canOpen) => (canOpen ? Linking.openURL(externalUrl) : undefined))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -2336,6 +2336,7 @@
|
||||
|
||||
- 背景:Expo 移动壳和 Tauri 桌面壳都需要一个真实的宿主级刷新入口,供 H5 在检测到资源、登录态或运行态需要重新载入时请求宿主刷新当前容器;该能力不能演变成任意 URL 导航或原生 WebView ref 透传。
|
||||
- 决策:新增 HostBridge method `app.reloadWebView` 和 H5 facade `reloadHostWebView()`。移动端只调用当前 `react-native-webview` 的 `reload()`,桌面端只调用 Tauri 主 `WebviewWindow.reload()`;该 method 不接受 payload,成功只表示宿主已发起刷新,刷新后当前 H5 上下文会卸载。继续把同源跳转留给 `navigation.openNativePage`,外链离开容器留给 `app.openExternalUrl`。
|
||||
- 2026-06-18 追加:Expo 移动壳的外链离开容器路径必须在协议白名单后再调用 `Linking.canOpenURL`;WebView 外域导航只有当前设备确认可打开时才调用 `Linking.openURL`,`app.openExternalUrl` 在系统不可打开时返回 `host_error`。该收紧不新增 HostBridge method,不把危险协议、相对路径或设备不可处理的外链留在带完整 HostBridge 的 WebView 内。
|
||||
- 2026-06-18 追加:`AuthGate` 登录态身份边界刷新改为优先调用 `reloadHostWebView()`,用于登录成功、退出登录或从已登录变为未登录后的主站重新初始化;宿主未声明、返回失败或不可用时再回退浏览器 `window.location.reload()`,普通 token refresh、账号资料更新、主题和音量变化仍不触发整页刷新。
|
||||
- 2026-06-18 追加:Expo 移动壳的 iOS `onContentProcessDidTerminate` 和 Android `onRenderProcessGone` 也复用当前 WebView 的受控 `reload()` 路径,系统回收 WebView 内容 / 渲染进程后只恢复同一 H5 容器,不改写 URL、不注入额外脚本、不新增宿主恢复页面。
|
||||
- 2026-06-18 追加:Expo 移动壳的 `onError` / `onHttpError` 只对同源 H5 主页面展示原生加载失败兜底层,用户重试时仍复用当前 WebView `reload()`;兜底不接管外域、危险协议、`about:blank` 或 favicon 失败,也不向 H5 注入错误事件。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user