import { getRegisteredAccessToken } from '../tokenAccess'; import { buildPreviewRuntimeUrl, resolvePreviewAppOrigin } from '../previewRuntime'; const CHAT_EXTERNAL_LINK_OPENED_AT_KEY = 'ai-code-app.chat.external-link-opened-at'; const CHAT_EXTERNAL_LINK_TTL_MS = 15_000; const EXTERNAL_WINDOW_TARGET_PREFIX = 'ai-code-app.external-window'; const UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS = 600; type LinkNavigationEvent = { preventDefault?: () => void; stopPropagation?: () => void; }; type OpenExternalLinkOptions = { event?: LinkNavigationEvent; onUnsupportedStandalone?: (url: string) => void; allowSameTabFallback?: boolean; }; function canUseSessionStorage() { return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined'; } function persistExternalLinkOpenTimestamp(openedAt: number) { if (!canUseSessionStorage()) { return; } window.sessionStorage.setItem(CHAT_EXTERNAL_LINK_OPENED_AT_KEY, String(openedAt)); } function clearExternalLinkOpenTimestamp() { if (!canUseSessionStorage()) { return; } window.sessionStorage.removeItem(CHAT_EXTERNAL_LINK_OPENED_AT_KEY); } function isStandaloneDisplayMode() { if (typeof window === 'undefined') { return false; } return ( window.matchMedia?.('(display-mode: standalone)').matches === true || (window.navigator as Navigator & { standalone?: boolean }).standalone === true ); } function isAppleMobileStandaloneMode() { if (typeof window === 'undefined') { return false; } const navigatorValue = window.navigator as Navigator & { standalone?: boolean }; const userAgent = navigatorValue.userAgent ?? ''; const platform = navigatorValue.platform ?? ''; const maxTouchPoints = typeof navigatorValue.maxTouchPoints === 'number' ? navigatorValue.maxTouchPoints : 0; const isAppleMobileUserAgent = /iPhone|iPad|iPod/iu.test(userAgent); const isTouchMac = platform === 'MacIntel' && maxTouchPoints > 1; return isStandaloneDisplayMode() && (isAppleMobileUserAgent || isTouchMac); } function buildExternalWindowTarget() { return `${EXTERNAL_WINDOW_TARGET_PREFIX}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function openExternalWindow(url: string, target: string) { if (typeof window === 'undefined') { return null; } const openedWindow = window.open('', target); if (!openedWindow) { return null; } try { openedWindow.opener = null; } catch { // Ignore opener access failures in restricted runtimes. } try { openedWindow.location.replace(url); } catch { try { openedWindow.location.href = url; } catch { // Leave the popup verification fallback to handle blocked navigations. } } return openedWindow; } function clickExternalAnchor(url: string, target: string) { if (typeof document === 'undefined') { return; } const anchor = document.createElement('a'); anchor.href = url; anchor.target = target; anchor.rel = 'noopener noreferrer'; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); } function buildPreviewRuntimeFallbackUrl(url: string) { if (typeof window === 'undefined') { return null; } try { const targetUrl = new URL(url, window.location.origin); const previewOrigin = resolvePreviewAppOrigin(); if (targetUrl.origin !== window.location.origin && targetUrl.origin !== previewOrigin) { return null; } const previewRuntimeUrl = buildPreviewRuntimeUrl( targetUrl.pathname, targetUrl.search, getRegisteredAccessToken(), null, 'mobile', ); if (!targetUrl.hash) { return previewRuntimeUrl; } const previewUrl = new URL(previewRuntimeUrl); previewUrl.hash = targetUrl.hash; return previewUrl.toString(); } catch { return null; } } function canFallbackToSameTab(url: string) { if (typeof window === 'undefined') { return false; } try { const targetUrl = new URL(url, window.location.origin); const previewOrigin = resolvePreviewAppOrigin(); return targetUrl.origin === window.location.origin || targetUrl.origin === previewOrigin; } catch { return false; } } function openSameTabFallback(url: string) { if (typeof window === 'undefined' || !canFallbackToSameTab(url)) { return false; } window.location.assign(url); return true; } function openPreviewRuntimeFallback(url: string) { if (typeof window === 'undefined') { return false; } const previewRuntimeUrl = buildPreviewRuntimeFallbackUrl(url); if (!previewRuntimeUrl) { return false; } window.location.assign(previewRuntimeUrl); return true; } function hasOpenedWindowNavigated(openedWindow: Window | null) { if (!openedWindow || openedWindow.closed) { return false; } try { return openedWindow.location.href !== 'about:blank'; } catch { return true; } } function scheduleExternalWindowFallback( url: string, openedWindow: Window | null, allowSameTabFallback: boolean, callback?: (url: string) => void, ) { if (typeof window === 'undefined' || typeof document === 'undefined') { return; } window.setTimeout(() => { const pageStillVisible = document.visibilityState !== 'hidden'; const pageStillFocused = typeof document.hasFocus !== 'function' || document.hasFocus(); if (!pageStillVisible || !pageStillFocused) { return; } if (hasOpenedWindowNavigated(openedWindow)) { return; } if (allowSameTabFallback && isAppleMobileStandaloneMode()) { if (openPreviewRuntimeFallback(url)) { return; } } if (allowSameTabFallback && openSameTabFallback(url)) { return; } callback?.(url); }, UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS); } export function shouldSkipForegroundResyncAfterExternalLink() { if (!canUseSessionStorage()) { return false; } const rawOpenedAt = window.sessionStorage.getItem(CHAT_EXTERNAL_LINK_OPENED_AT_KEY); clearExternalLinkOpenTimestamp(); if (!rawOpenedAt) { return false; } const openedAt = Number(rawOpenedAt); return Number.isFinite(openedAt) && Date.now() - openedAt <= CHAT_EXTERNAL_LINK_TTL_MS; } export function openExternalLinkInNewWindow(url: string, options: OpenExternalLinkOptions = {}) { options.event?.preventDefault?.(); options.event?.stopPropagation?.(); if (typeof window === 'undefined') { return; } persistExternalLinkOpenTimestamp(Date.now()); const target = buildExternalWindowTarget(); const allowSameTabFallback = options.allowSameTabFallback ?? true; const openedWindow = openExternalWindow(url, target); if (!openedWindow && allowSameTabFallback) { clickExternalAnchor(url, target); } scheduleExternalWindowFallback(url, openedWindow, allowSameTabFallback, options.onUnsupportedStandalone); } export function openChatExternalLink(url: string, event?: LinkNavigationEvent) { openExternalLinkInNewWindow(url, { event, }); }