import { HOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS } from '../../../../packages/shared/src/contracts/hostBridge'; import { DEFAULT_MOBILE_SHELL_WEB_URL } from './url'; const MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS = new Set( HOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS, ); function mobileWebViewBlockedDownloadProtocolMapScript() { return HOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS.map( (protocol) => ` ${JSON.stringify(protocol)}: true`, ).join(',\n'); } export type MobileWebViewNavigationRequest = { url?: string | null; }; export function shouldBlockMobileWebViewDownloadUrl( rawUrl: string | null | undefined, ) { if (!rawUrl) { return false; } try { return MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS.has( new URL(rawUrl, DEFAULT_MOBILE_SHELL_WEB_URL).protocol, ); } catch { return false; } } export function shouldBlockMobileWebViewNavigationRequest( request: MobileWebViewNavigationRequest, ) { return shouldBlockMobileWebViewDownloadUrl(request.url); } export const BLOCK_WEBVIEW_DOWNLOAD_SCRIPT = ` (function() { var blockedDownloadProtocols = { ${mobileWebViewBlockedDownloadProtocolMapScript()} }; function shouldBlockDownloadUrl(rawUrl) { if (typeof rawUrl !== 'string') { return false; } try { return blockedDownloadProtocols[new URL(rawUrl, window.location.href).protocol] === true; } catch (error) { return false; } } function findDownloadAnchor(target) { if (!target) { return null; } if (typeof target.closest === 'function') { return target.closest('a'); } var element = target; while (element && element !== document) { if (element.tagName === 'A') { return element; } element = element.parentNode; } return null; } function shouldBlockAnchor(anchor) { return Boolean( anchor && (anchor.hasAttribute('download') || shouldBlockDownloadUrl(anchor.href)) ); } function blockDownloadEvent(event) { event.preventDefault(); event.stopImmediatePropagation(); event.stopPropagation(); return false; } document.addEventListener('click', function(event) { if (shouldBlockAnchor(findDownloadAnchor(event.target))) { return blockDownloadEvent(event); } }, true); var originalOpen = window.open; window.open = function(url) { if (shouldBlockDownloadUrl(url)) { return null; } return originalOpen.apply(window, arguments); }; if (window.HTMLAnchorElement && HTMLAnchorElement.prototype.click) { var originalAnchorClick = HTMLAnchorElement.prototype.click; HTMLAnchorElement.prototype.click = function() { if (shouldBlockAnchor(this)) { return undefined; } return originalAnchorClick.apply(this, arguments); }; } })(); true; `; export const TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT = ` (function() { if (window.__GENARRATIVE_MOBILE_HISTORY_TRACKER_INSTALLED__) { if (typeof window.__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__ === 'function') { window.__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__(); } return true; } var historyIndexKey = '__genarrativeMobileHistoryIndex'; var originalPushState = window.history.pushState.bind(window.history); var originalReplaceState = window.history.replaceState.bind(window.history); function readIndex(state) { if (!state || typeof state !== 'object') { return null; } var value = state[historyIndexKey]; return Number.isInteger(value) && value >= 0 ? value : null; } function stateWithIndex(state, index) { if (state && typeof state === 'object' && !Array.isArray(state)) { var nextState = {}; Object.keys(state).forEach(function(key) { nextState[key] = state[key]; }); nextState[historyIndexKey] = index; return nextState; } var indexedState = {}; indexedState[historyIndexKey] = index; return indexedState; } var currentIndex = readIndex(window.history.state) || 0; function postNavigationState() { var bridge = window.ReactNativeWebView; if (!bridge || typeof bridge.postMessage !== 'function') { return; } bridge.postMessage(JSON.stringify({ type: 'genarrative.mobile.historyState', canGoBack: currentIndex > 0 })); } function replaceCurrentState() { try { originalReplaceState( stateWithIndex(window.history.state, currentIndex), '', window.location.href ); } catch (error) { console.warn('mobile navigation state sync failed'); } } window.history.pushState = function(state, title, url) { var nextIndex = currentIndex + 1; var result = originalPushState(stateWithIndex(state, nextIndex), title, url); currentIndex = nextIndex; postNavigationState(); return result; }; window.history.replaceState = function(state, title, url) { var result = originalReplaceState( stateWithIndex(state, currentIndex), title, url ); postNavigationState(); return result; }; window.__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__ = postNavigationState; window.addEventListener('popstate', function(event) { var nextIndex = readIndex(event.state); currentIndex = nextIndex === null ? 0 : nextIndex; if (nextIndex === null) { replaceCurrentState(); } postNavigationState(); }); window.__GENARRATIVE_MOBILE_HISTORY_TRACKER_INSTALLED__ = true; replaceCurrentState(); postNavigationState(); })(); true; `; export const MOBILE_WEBVIEW_BEFORE_CONTENT_SCRIPT = ` ${BLOCK_WEBVIEW_DOWNLOAD_SCRIPT} ${TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT} true; `;