diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 302338f2a..4bca38ea6 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1497,6 +1497,8 @@ for (const snippet of [ 'source: window', 'MOBILE_WEBVIEW_BEFORE_CONTENT_SCRIPT', 'shouldBlockMobileWebViewNavigationRequest', + 'logMobileShellDownloadBlocked', + "console.warn('mobile shell blocked WebView file download', event)", 'SafeAreaProvider', 'SafeAreaView', 'MOBILE_SHELL_SAFE_AREA_EDGES', @@ -1515,6 +1517,7 @@ for (const snippet of [ 'webviewDebuggingEnabled={false}', 'injectedJavaScriptBeforeContentLoaded={MOBILE_WEBVIEW_BEFORE_CONTENT_SCRIPT}', 'handleBlockedFileDownload', + 'logMobileShellDownloadBlocked(event)', 'onFileDownload={handleBlockedFileDownload}', 'setSupportMultipleWindows={false}', 'logMobileShellNavigationFailure', @@ -1530,6 +1533,10 @@ if (shellAppSource.includes('catch(() => undefined)')) { throw new Error('mobile shell ShellApp must not hide async navigation failures'); } +if (!shellAppTestSource.includes('blocked WebView file downloads are logged for host diagnostics')) { + throw new Error('mobile shell tests must cover blocked WebView file download diagnostics'); +} + for (const snippet of [ "describe('mobile shell safe area'", "test('protects the WebView from every device edge'", diff --git a/apps/mobile-shell/src/shell/ShellApp.test.tsx b/apps/mobile-shell/src/shell/ShellApp.test.tsx index 0b88f00a1..5596908d4 100644 --- a/apps/mobile-shell/src/shell/ShellApp.test.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.test.tsx @@ -630,6 +630,30 @@ describe('ShellApp HostBridge event injection', () => { warnSpy.mockRestore(); }); + test('blocked WebView file downloads are logged for host diagnostics', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const ShellApp = await importShellApp(); + render(); + + const webViewProps = shellHarness.webViewProps.current as { + onFileDownload?: (event: unknown) => void; + }; + const downloadEvent = { + nativeEvent: { + downloadUrl: 'blob:https://app.genarrative.world/download-id', + }, + }; + + webViewProps.onFileDownload?.(downloadEvent); + + expect(warnSpy).toHaveBeenCalledWith( + 'mobile shell blocked WebView file download', + downloadEvent, + ); + + warnSpy.mockRestore(); + }); + test('initial deep link read failures are logged without replacing the current WebView URL', async () => { const { Linking } = await import('react-native'); const initialUrlError = new Error('initial URL unavailable'); diff --git a/apps/mobile-shell/src/shell/ShellApp.tsx b/apps/mobile-shell/src/shell/ShellApp.tsx index 563cf0cc3..5e0c9fb09 100644 --- a/apps/mobile-shell/src/shell/ShellApp.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.tsx @@ -71,6 +71,10 @@ function logMobileShellNavigationFailure(label: string, error: unknown) { console.warn(`mobile shell navigation failed for ${label}`, error); } +function logMobileShellDownloadBlocked(event: unknown) { + console.warn('mobile shell blocked WebView file download', event); +} + function logMobileShellDeepLinkFailure(label: string, error: unknown) { console.warn(`mobile shell deep link failed for ${label}`, error); } @@ -445,7 +449,9 @@ export default function ShellApp() { ), ); }; - const handleBlockedFileDownload = () => undefined; + const handleBlockedFileDownload = (event: unknown) => { + logMobileShellDownloadBlocked(event); + }; return (