diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 4526e3b18..dbdb45c89 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -2079,9 +2079,14 @@ for (const snippet of [ "code: 'unsupported_capability'", "code: 'cancelled'", 'rejects image import before picker launch when library permission is denied', + 'rejects image import before picker launch when library permission request fails', + 'rejects image capture before camera launch when camera permission request fails', "message: 'photo library permission denied'", + "message: 'photo library permission unavailable'", 'expect(imageLibraryMock).not.toHaveBeenCalled()', "message: 'camera permission denied'", + "message: 'camera permission unavailable'", + 'expect(cameraMock).not.toHaveBeenCalled()', "mediaTypes: ['images']", "options: { encoding: 'base64' }", ]) { diff --git a/apps/mobile-shell/src/host-bridge/files.test.ts b/apps/mobile-shell/src/host-bridge/files.test.ts index 136cccaa0..080c92fd8 100644 --- a/apps/mobile-shell/src/host-bridge/files.test.ts +++ b/apps/mobile-shell/src/host-bridge/files.test.ts @@ -303,6 +303,26 @@ describe('mobile HostBridge file actions', () => { expect(imageLibraryMock).not.toHaveBeenCalled(); }); + test('rejects image import before picker launch when library permission request fails', async () => { + libraryPermissionMock.mockRejectedValueOnce(new Error('native permission failed')); + + await expect(importImageFile()).rejects.toMatchObject({ + code: 'host_error', + message: 'photo library permission unavailable', + }); + expect(imageLibraryMock).not.toHaveBeenCalled(); + }); + + test('rejects image capture before camera launch when camera permission request fails', async () => { + cameraPermissionMock.mockRejectedValueOnce(new Error('native permission failed')); + + await expect(captureImageFile()).rejects.toMatchObject({ + code: 'host_error', + message: 'camera permission unavailable', + }); + expect(cameraMock).not.toHaveBeenCalled(); + }); + test('imports audio and exports binary files through controlled payloads', async () => { fileBase64Data.set('file:///picked/voice.mp3', MP3_BASE64); fileSizes.set('file:///picked/voice.mp3', Buffer.byteLength('ID3\x04\x00\x00\x00\x00\x00\x10')); diff --git a/apps/mobile-shell/src/host-bridge/files.ts b/apps/mobile-shell/src/host-bridge/files.ts index 140a07ff4..acfa93392 100644 --- a/apps/mobile-shell/src/host-bridge/files.ts +++ b/apps/mobile-shell/src/host-bridge/files.ts @@ -270,7 +270,15 @@ export async function exportMobileHostBridgeImageFile( } export async function importImageFile(): Promise { - const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); + let permission: ImagePicker.MediaLibraryPermissionResponse; + try { + permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); + } catch { + throw { + code: 'host_error', + message: 'photo library permission unavailable', + } satisfies HostBridgeError; + } if (permission.status !== ImagePicker.PermissionStatus.GRANTED) { throw { code: 'host_error', @@ -301,7 +309,15 @@ export async function importMobileHostBridgeImageFile( } export async function captureImageFile(): Promise { - const permission = await ImagePicker.requestCameraPermissionsAsync(); + let permission: ImagePicker.CameraPermissionResponse; + try { + permission = await ImagePicker.requestCameraPermissionsAsync(); + } catch { + throw { + code: 'host_error', + message: 'camera permission unavailable', + } satisfies HostBridgeError; + } if (permission.status !== ImagePicker.PermissionStatus.GRANTED) { throw { code: 'host_error',