接入移动壳安全区布局
Expo 移动壳使用 SafeAreaProvider 和 SafeAreaView 包裹 WebView 新增安全区边界配置和测试 补充移动壳安全区依赖检查和架构文档
This commit is contained in:
@@ -7,8 +7,8 @@ import {
|
|||||||
Linking,
|
Linking,
|
||||||
Platform,
|
Platform,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import type { WebViewMessageEvent } from 'react-native-webview';
|
import type { WebViewMessageEvent } from 'react-native-webview';
|
||||||
import { WebView } from 'react-native-webview';
|
import { WebView } from 'react-native-webview';
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
shouldOpenInMobileShellWebView,
|
shouldOpenInMobileShellWebView,
|
||||||
} from './src/mobileShellNavigation';
|
} from './src/mobileShellNavigation';
|
||||||
import { subscribeMobileNetworkStatus } from './src/mobileShellNetwork';
|
import { subscribeMobileNetworkStatus } from './src/mobileShellNetwork';
|
||||||
|
import { MOBILE_SHELL_SAFE_AREA_EDGES } from './src/mobileShellSafeArea';
|
||||||
import { buildMobileShellUrl } from './src/mobileShellUrl';
|
import { buildMobileShellUrl } from './src/mobileShellUrl';
|
||||||
|
|
||||||
const defaultWebUrl = 'http://127.0.0.1:3000/';
|
const defaultWebUrl = 'http://127.0.0.1:3000/';
|
||||||
@@ -165,7 +166,8 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<SafeAreaProvider>
|
||||||
|
<SafeAreaView style={styles.root} edges={MOBILE_SHELL_SAFE_AREA_EDGES}>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
@@ -190,7 +192,8 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
setSupportMultipleWindows={false}
|
setSupportMultipleWindows={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"expo-status-bar": "^56.0.4",
|
"expo-status-bar": "^56.0.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-native": "^0.86.0",
|
"react-native": "^0.86.0",
|
||||||
|
"react-native-safe-area-context": "^5.8.0",
|
||||||
"react-native-webview": "^13.16.1"
|
"react-native-webview": "^13.16.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ for (const snippet of [
|
|||||||
'subscribeMobileNetworkStatus',
|
'subscribeMobileNetworkStatus',
|
||||||
'navigation.canGoBack',
|
'navigation.canGoBack',
|
||||||
'buildHostBridgeMessageScript',
|
'buildHostBridgeMessageScript',
|
||||||
|
'SafeAreaProvider',
|
||||||
|
'SafeAreaView',
|
||||||
|
'MOBILE_SHELL_SAFE_AREA_EDGES',
|
||||||
]) {
|
]) {
|
||||||
if (!appSource.includes(snippet)) {
|
if (!appSource.includes(snippet)) {
|
||||||
throw new Error(`mobile shell App missing ${snippet}`);
|
throw new Error(`mobile shell App missing ${snippet}`);
|
||||||
@@ -137,6 +140,7 @@ for (const dependency of [
|
|||||||
'expo-network',
|
'expo-network',
|
||||||
'expo-notifications',
|
'expo-notifications',
|
||||||
'expo-sharing',
|
'expo-sharing',
|
||||||
|
'react-native-safe-area-context',
|
||||||
]) {
|
]) {
|
||||||
if (!packageConfig.dependencies?.[dependency]) {
|
if (!packageConfig.dependencies?.[dependency]) {
|
||||||
throw new Error(`mobile shell package missing ${dependency}`);
|
throw new Error(`mobile shell package missing ${dependency}`);
|
||||||
|
|||||||
14
apps/mobile-shell/src/mobileShellSafeArea.test.ts
Normal file
14
apps/mobile-shell/src/mobileShellSafeArea.test.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { MOBILE_SHELL_SAFE_AREA_EDGES } from './mobileShellSafeArea';
|
||||||
|
|
||||||
|
describe('mobile shell safe area', () => {
|
||||||
|
test('protects the WebView from every device edge', () => {
|
||||||
|
expect(MOBILE_SHELL_SAFE_AREA_EDGES).toEqual([
|
||||||
|
'top',
|
||||||
|
'right',
|
||||||
|
'bottom',
|
||||||
|
'left',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
6
apps/mobile-shell/src/mobileShellSafeArea.ts
Normal file
6
apps/mobile-shell/src/mobileShellSafeArea.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const MOBILE_SHELL_SAFE_AREA_EDGES = [
|
||||||
|
'top',
|
||||||
|
'right',
|
||||||
|
'bottom',
|
||||||
|
'left',
|
||||||
|
] as const;
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
- 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。
|
- 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。
|
||||||
- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则,成功回传 `action=captured`,不暴露设备本地 URI,也不请求麦克风权限;Tauri 壳不声明该能力,不伪造桌面拍摄。
|
- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则,成功回传 `action=captured`,不暴露设备本地 URI,也不请求麦克风权限;Tauri 壳不声明该能力,不伪造桌面拍摄。
|
||||||
- 2026-06-18 H5 图片上传接入宿主导入:`CreativeImageInputPanel` 在 `native_app` 且声明 `file.importImage` / `file.captureImage` 时,主图上传和描述参考图上传可分别调用 `importHostImageFile()` / `captureHostImageFile()`,并把宿主返回的 base64 图片转换为现有 `File` 回调;浏览器、小程序和未声明能力的裁剪壳继续走原生 `<input type="file">` 路径,不新增玩法侧上传分叉。
|
- 2026-06-18 H5 图片上传接入宿主导入:`CreativeImageInputPanel` 在 `native_app` 且声明 `file.importImage` / `file.captureImage` 时,主图上传和描述参考图上传可分别调用 `importHostImageFile()` / `captureHostImageFile()`,并把宿主返回的 base64 图片转换为现有 `File` 回调;浏览器、小程序和未声明能力的裁剪壳继续走原生 `<input type="file">` 路径,不新增玩法侧上传分叉。
|
||||||
|
- 2026-06-18 移动壳安全区:Expo 壳根布局使用 `react-native-safe-area-context` 的 `SafeAreaProvider` 与四边 `SafeAreaView` 保护 WebView,避免 H5 主站内容贴进 iOS 刘海、底部 Home Indicator、Android 状态栏或横屏边缘;该能力属于宿主壳布局保护,不新增 H5 占位 UI,不改变玩法 runtime 或 HostBridge capability。
|
||||||
- 2026-06-18 桌面图片拖入接入主图槽位:`CreativeImageInputPanel` 在桌面壳声明 `file.imageDropped` 时订阅宿主拖入事件,只在拖入坐标命中当前主图卡片且未被上层元素遮挡时消费事件,避免窗口级拖入被多个创作面板同时接收;成功后仍转换为现有 `File` 上传回调。
|
- 2026-06-18 桌面图片拖入接入主图槽位:`CreativeImageInputPanel` 在桌面壳声明 `file.imageDropped` 时订阅宿主拖入事件,只在拖入坐标命中当前主图卡片且未被上层元素遮挡时消费事件,避免窗口级拖入被多个创作面板同时接收;成功后仍转换为现有 `File` 上传回调。
|
||||||
- 2026-06-18 H5 背景音乐接入宿主生命周期:`useBackgroundMusic` 通过 `useHostLifecycleActive()` 消费 `subscribeHostAppLifecycle()` 的归一结果,宿主进入后台、inactive 或桌面窗口失焦时降低音量并暂停音频循环,同时 `suspend` WebAudio context;回到 `active + focused` 且用户原本开启音乐时再恢复播放,不改变用户音量设置。
|
- 2026-06-18 H5 背景音乐接入宿主生命周期:`useBackgroundMusic` 通过 `useHostLifecycleActive()` 消费 `subscribeHostAppLifecycle()` 的归一结果,宿主进入后台、inactive 或桌面窗口失焦时降低音量并暂停音频循环,同时 `suspend` WebAudio context;回到 `active + focused` 且用户原本开启音乐时再恢复播放,不改变用户音量设置。
|
||||||
- 2026-06-18 固定玩法音频接入宿主生命周期:前端新增 `useHostLifecycleActive()` 统一消费 `subscribeHostAppLifecycle()`,`useBackgroundMusic`、拼图运行态和抓大鹅运行态都只依赖该归一状态判断音频可播放性;宿主 inactive、background 或窗口失焦时暂停 `<audio>` / WebAudio,回到 `active + focused` 后仅在运行态仍在播放、音源存在且用户音乐音量大于 0 时恢复,不改变用户音量设置。
|
- 2026-06-18 固定玩法音频接入宿主生命周期:前端新增 `useHostLifecycleActive()` 统一消费 `subscribeHostAppLifecycle()`,`useBackgroundMusic`、拼图运行态和抓大鹅运行态都只依赖该归一状态判断音频可播放性;宿主 inactive、background 或窗口失焦时暂停 `<audio>` / WebAudio,回到 `active + focused` 后仅在运行态仍在播放、音源存在且用户音乐音量大于 0 时恢复,不改变用户音量设置。
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ GameBridge 禁止:
|
|||||||
|
|
||||||
2026-06-18 追加:H5 账号状态刷新开始消费 `app.reloadWebView`。用户登录成功、退出登录或其它身份边界变化需要整页重新初始化时,`AuthGate` 会优先请求 Expo 壳刷新当前 WebView;宿主未声明或刷新失败时再回退浏览器刷新,避免在移动壳内绕过受控容器刷新入口。
|
2026-06-18 追加:H5 账号状态刷新开始消费 `app.reloadWebView`。用户登录成功、退出登录或其它身份边界变化需要整页重新初始化时,`AuthGate` 会优先请求 Expo 壳刷新当前 WebView;宿主未声明或刷新失败时再回退浏览器刷新,避免在移动壳内绕过受控容器刷新入口。
|
||||||
|
|
||||||
|
2026-06-18 追加:移动壳根布局接入 `react-native-safe-area-context`,用 `SafeAreaProvider` 和四边 `SafeAreaView` 承接 iOS 刘海、底部 Home Indicator、Android 状态栏和横屏边缘安全区;WebView 仍加载同一 H5 主站,不改变 H5 路由、玩法 runtime 或 HostBridge capability。该能力属于宿主壳布局保护,不在 H5 内补额外占位 UI。
|
||||||
|
|
||||||
### Phase 3:Tauri 桌面壳 MVP
|
### Phase 3:Tauri 桌面壳 MVP
|
||||||
|
|
||||||
- 新增 `apps/desktop-shell/`。
|
- 新增 `apps/desktop-shell/`。
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -32,6 +32,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-native": "^0.86.0",
|
"react-native": "^0.86.0",
|
||||||
|
"react-native-safe-area-context": "^5.8.0",
|
||||||
"react-native-webview": "^13.16.1",
|
"react-native-webview": "^13.16.1",
|
||||||
"three": "^0.184.0",
|
"three": "^0.184.0",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
@@ -9975,6 +9976,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-safe-area-context": {
|
||||||
|
"version": "5.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.8.0.tgz",
|
||||||
|
"integrity": "sha512-t+ZsAVzY/wWzzx34vqGbo3/as9EEESJdbyZNL7Yg5EYX+toYMtMqFoDDCvqZUi35eeGVsXc6pAaEk4edMwbuCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-webview": {
|
"node_modules/react-native-webview": {
|
||||||
"version": "13.16.1",
|
"version": "13.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz",
|
||||||
@@ -19870,6 +19881,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-native-safe-area-context": {
|
||||||
|
"version": "5.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.8.0.tgz",
|
||||||
|
"integrity": "sha512-t+ZsAVzY/wWzzx34vqGbo3/as9EEESJdbyZNL7Yg5EYX+toYMtMqFoDDCvqZUi35eeGVsXc6pAaEk4edMwbuCQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-native-webview": {
|
"react-native-webview": {
|
||||||
"version": "13.16.1",
|
"version": "13.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz",
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-native": "^0.86.0",
|
"react-native": "^0.86.0",
|
||||||
|
"react-native-safe-area-context": "^5.8.0",
|
||||||
"react-native-webview": "^13.16.1",
|
"react-native-webview": "^13.16.1",
|
||||||
"three": "^0.184.0",
|
"three": "^0.184.0",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user