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 (