保持移动壳主动导航宿主上下文
HostBridge 主动导航同源页面时重新附加 native_app 宿主参数 补充移动壳导航测试覆盖平台版本和能力清单 同步 Expo/Tauri 宿主壳方案和团队决策记录
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
type HostBridgeRequest,
|
||||
type HostBridgeResponse,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import type { MobileShellUrlOptions } from '../shell/url';
|
||||
import {
|
||||
configureMobileHostBridgeNavigation,
|
||||
handleMobileHostBridgeMessage,
|
||||
@@ -46,6 +47,12 @@ const DENIED_NOTIFICATION_PERMISSION = {
|
||||
|
||||
let requestSequence = 0;
|
||||
|
||||
const TEST_MOBILE_URL_OPTIONS: MobileShellUrlOptions = {
|
||||
platform: 'ios',
|
||||
hostVersion: '0.1.0',
|
||||
capabilities: ['host.getRuntime', 'share.open'],
|
||||
};
|
||||
|
||||
vi.mock('expo-clipboard', () => ({
|
||||
getStringAsync: vi.fn(),
|
||||
setStringAsync: vi.fn(),
|
||||
@@ -388,6 +395,7 @@ describe('handleMobileHostBridgeMessage', () => {
|
||||
const openWebViewUrl = vi.fn();
|
||||
configureMobileHostBridgeNavigation({
|
||||
allowedOrigin: 'https://app.genarrative.world',
|
||||
urlOptions: TEST_MOBILE_URL_OPTIONS,
|
||||
openWebViewUrl,
|
||||
reloadWebView: vi.fn(),
|
||||
});
|
||||
@@ -399,14 +407,24 @@ describe('handleMobileHostBridgeMessage', () => {
|
||||
);
|
||||
|
||||
expectOk(response);
|
||||
expect(openWebViewUrl).toHaveBeenCalledWith(
|
||||
'https://app.genarrative.world/works/detail?work=PZ-1',
|
||||
const openedUrl = new URL(openWebViewUrl.mock.calls[0]?.[0] as string);
|
||||
expect(openedUrl.origin).toBe('https://app.genarrative.world');
|
||||
expect(openedUrl.pathname).toBe('/works/detail');
|
||||
expect(openedUrl.searchParams.get('work')).toBe('PZ-1');
|
||||
expect(openedUrl.searchParams.get('clientRuntime')).toBe('native_app');
|
||||
expect(openedUrl.searchParams.get('hostShell')).toBe('expo_mobile');
|
||||
expect(openedUrl.searchParams.get('hostPlatform')).toBe('ios');
|
||||
expect(openedUrl.searchParams.get('hostVersion')).toBe('0.1.0');
|
||||
expect(openedUrl.searchParams.get('bridgeVersion')).toBe('1');
|
||||
expect(openedUrl.searchParams.get('hostCapabilities')).toBe(
|
||||
'host.getRuntime,share.open',
|
||||
);
|
||||
});
|
||||
|
||||
test('navigation.openNativePage 拒绝外域目标', async () => {
|
||||
configureMobileHostBridgeNavigation({
|
||||
allowedOrigin: 'https://app.genarrative.world',
|
||||
urlOptions: TEST_MOBILE_URL_OPTIONS,
|
||||
openWebViewUrl: vi.fn(),
|
||||
reloadWebView: vi.fn(),
|
||||
});
|
||||
@@ -475,6 +493,7 @@ describe('handleMobileHostBridgeMessage', () => {
|
||||
const reloadWebView = vi.fn();
|
||||
configureMobileHostBridgeNavigation({
|
||||
allowedOrigin: 'https://app.genarrative.world',
|
||||
urlOptions: TEST_MOBILE_URL_OPTIONS,
|
||||
openWebViewUrl: vi.fn(),
|
||||
reloadWebView,
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
unsupported,
|
||||
} from './protocol';
|
||||
import { openShare } from './share';
|
||||
import { buildMobileShellUrl } from '../shell/url';
|
||||
|
||||
const LOCAL_NOTIFICATION_CHANNEL_ID = 'genarrative-local';
|
||||
|
||||
@@ -222,7 +223,9 @@ function openNativePage(payload: unknown) {
|
||||
throw invalidRequest('url must be an allowed same-origin web path');
|
||||
}
|
||||
|
||||
navigation.openWebViewUrl(webViewUrl);
|
||||
navigation.openWebViewUrl(
|
||||
buildMobileShellUrl(webViewUrl, navigation.urlOptions),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
isHostBridgeMethod,
|
||||
normalizeHostBridgeRequestId,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import type { MobileShellUrlOptions } from '../shell/url';
|
||||
|
||||
export const HOST_BRIDGE_RESPONSE_CACHE_MAX = 128;
|
||||
|
||||
export type MobileHostBridgeNavigation = {
|
||||
allowedOrigin: string;
|
||||
urlOptions: MobileShellUrlOptions;
|
||||
openWebViewUrl: (url: string) => void;
|
||||
reloadWebView: () => void;
|
||||
};
|
||||
|
||||
@@ -89,6 +89,7 @@ export default function ShellApp() {
|
||||
useEffect(() => {
|
||||
configureMobileHostBridgeNavigation({
|
||||
allowedOrigin: allowedWebOrigin,
|
||||
urlOptions,
|
||||
openWebViewUrl(url) {
|
||||
setWebUrl(url);
|
||||
},
|
||||
@@ -96,7 +97,7 @@ export default function ShellApp() {
|
||||
});
|
||||
|
||||
return () => configureMobileHostBridgeNavigation(null);
|
||||
}, [allowedWebOrigin, reloadCurrentWebView]);
|
||||
}, [allowedWebOrigin, reloadCurrentWebView, urlOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
|
||||
@@ -2485,3 +2485,10 @@
|
||||
- 决策:`SquareHoleResultView` 图片槽位弹窗在 `native_app` 且宿主声明 `file.importImage` 时优先调用 `importHostImageFile()`,把宿主返回的图片内容副本转换为 `data:<mime>;base64,<data>` 并写回当前槽位 `imageSrc`。该动作继续走现有 result edit state、自动保存、试玩和发布链路,不新增后端上传路径,不暴露设备 URI、本机绝对路径或通用文件系统;普通浏览器、小程序和未声明能力的裁剪壳继续使用原浏览器文件输入。
|
||||
- 影响范围:`src/components/square-hole-result/SquareHoleResultView.tsx`、`src/services/host-bridge/hostBridge.ts`、方洞玩法链路文档与 Expo / Tauri HostBridge 方案文档。
|
||||
- 验证方式:`npm run test -- src/components/square-hole-result/SquareHoleResultView.test.tsx`、`npm run typecheck -- --pretty false`、`npm run check:encoding`、`npm run check:native-shells`、`git diff --check`。
|
||||
|
||||
## 2026-06-18 移动壳主动导航保留宿主上下文
|
||||
|
||||
- 背景:Expo 移动壳启动 URL 和 deep link 已经会给 H5 追加 `native_app`、`expo_mobile`、平台、版本和 capability query;但 H5 通过 HostBridge 调用 `navigation.openNativePage` 主动跳转同源 route 时,壳层只把裸同源 URL 交给 WebView,目标页首屏可能短暂或持续按普通浏览器运行态识别。
|
||||
- 决策:`navigation.openNativePage` 仍只接受同源 H5 route,不新增真实原生页面、不放宽外域导航;通过校验后的目标 URL 在进入 WebView 前统一调用 `buildMobileShellUrl(...)` 重新附加当前 `MobileShellUrlOptions`,确保主动导航后的页面继续带 `clientRuntime=native_app`、`hostShell=expo_mobile`、真实平台、宿主版本和当前 capability 清单。
|
||||
- 影响范围:`apps/mobile-shell/src/host-bridge/`、`apps/mobile-shell/src/shell/ShellApp.tsx`、Expo / Tauri HostBridge 方案文档。
|
||||
- 验证方式:`npm run mobile-shell:test`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run typecheck -- --pretty false`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user