From 53312d1b4927ea70f08e6cf9f0a406ded6c9ee3b Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 09:56:57 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=A7=BB=E5=8A=A8=E6=89=AB?= =?UTF-8?q?=E7=A0=81=E6=9D=83=E9=99=90=E8=AF=B7=E6=B1=82=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动扫码权限请求异常时记录原始错误 移动扫码 overlay 测试覆盖权限请求失败路径 移动壳配置检查反查扫码权限失败日志边界 共享决策日志补充扫码权限失败边界 --- apps/mobile-shell/scripts/check-config.mjs | 5 +++++ .../src/shell/QrScannerOverlay.test.tsx | 19 +++++++++++++++++++ .../src/shell/QrScannerOverlay.tsx | 7 ++++++- .../shared-memory/decision-log.md | 7 +++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index a7d5f2912..accbfa141 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -2180,6 +2180,8 @@ for (const snippet of [ 'failQrCodeScan', 'cancelQrCodeScan', 'subscribeQrScannerState', + 'logQrScannerPermissionFailure(error)', + "console.warn('mobile QR scanner permission request failed', error)", ]) { if (!qrScannerOverlaySource.includes(snippet)) { throw new Error(`mobile shell QR scanner overlay missing ${snippet}`); @@ -2191,7 +2193,10 @@ for (const snippet of [ 'scanQrCode()', 'requestCameraPermissionsAsync', "message: 'camera permission denied'", + "message: 'camera permission unavailable'", "message: 'qr scan cancelled'", + "test('logs and rejects the active scan when camera permission request fails'", + "'mobile QR scanner permission request failed'", "barcodeTypes: ['qr']", "type: 'qr'", "value: 'https://app.genarrative.world/works/detail?work=PZ-1'", diff --git a/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx b/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx index 5be42ae7d..435e08316 100644 --- a/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx +++ b/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx @@ -118,6 +118,25 @@ describe('QrScannerOverlay', () => { expect(overlayHarness.cameraViewProps.current).toBeNull(); }); + test('logs and rejects the active scan when camera permission request fails', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const error = new Error('camera unavailable'); + cameraPermissionMock.mockRejectedValue(error); + render(); + + const scanPromise = scanQrCode(); + + await expect(scanPromise).rejects.toMatchObject({ + code: 'host_error', + message: 'camera permission unavailable', + }); + expect(warnSpy).toHaveBeenCalledWith( + 'mobile QR scanner permission request failed', + error, + ); + expect(overlayHarness.cameraViewProps.current).toBeNull(); + }); + test('close button cancels the pending scan', async () => { cameraPermissionMock.mockResolvedValue({ granted: true, diff --git a/apps/mobile-shell/src/shell/QrScannerOverlay.tsx b/apps/mobile-shell/src/shell/QrScannerOverlay.tsx index 9afb0a3a5..600913e07 100644 --- a/apps/mobile-shell/src/shell/QrScannerOverlay.tsx +++ b/apps/mobile-shell/src/shell/QrScannerOverlay.tsx @@ -13,6 +13,10 @@ import { subscribeQrScannerState, } from '../host-bridge/scanner'; +function logQrScannerPermissionFailure(error: unknown) { + console.warn('mobile QR scanner permission request failed', error); +} + export function QrScannerOverlay() { const [isActive, setIsActive] = useState(false); const [requestKey, setRequestKey] = useState(0); @@ -45,8 +49,9 @@ export function QrScannerOverlay() { } setHasPermission(true); }) - .catch(() => { + .catch((error: unknown) => { if (!disposed) { + logQrScannerPermissionFailure(error); failQrCodeScan('camera permission unavailable'); } }); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0f783d372..993b25d7e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2951,6 +2951,13 @@ - 影响范围:`apps/mobile-shell/src/shell/lifecycle.ts`、`apps/mobile-shell/src/shell/lifecycle.test.ts`、`apps/mobile-shell/scripts/check-config.mjs`。 - 验证方式:`npm run mobile-shell:test -- src/shell/lifecycle.test.ts`、`npm run mobile-shell:config`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-20 移动扫码权限请求失败不可静默 + +- 背景:Expo 移动壳 `scanner.scanQrCode` 会通过真实 `expo-camera` 权限 API 启动扫码;如果权限请求 API 自身失败后只返回通用 `host_error` 而不记录原始错误,用户会看到扫码不可用但开发侧难以区分系统拒绝、原生模块异常或设备能力问题。 +- 决策:`QrScannerOverlay` 的 `Camera.requestCameraPermissionsAsync()` reject 路径必须调用 `logQrScannerPermissionFailure(...)` 输出 `mobile QR scanner permission request failed` 日志,再通过 `failQrCodeScan('camera permission unavailable')` 结束当前请求;`QrScannerOverlay.test.tsx` 必须覆盖该 reject 路径,移动壳配置检查反查日志 helper 和测试断言。 +- 影响范围:`apps/mobile-shell/src/shell/QrScannerOverlay.tsx`、`apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx`、`apps/mobile-shell/scripts/check-config.mjs`。 +- 验证方式:`npm run mobile-shell:test -- src/shell/QrScannerOverlay.test.tsx`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-20 移动分享单测边界 - 背景:Expo 移动壳 `share.open` / `share.setTarget` 已经由 `share.ts` 承接共享 HostBridge 分享 URL 归一和缓存目标,但关键边界主要压在巨型 `bridge.test.ts` 中,后续拆分桥接 helper 时容易遗漏非法显式 payload 不回退缓存、空分享拒绝和缓存目标保留语义。