import React, { useMemo, useState } from 'react'; import { ActivityIndicator, Modal, Platform, Pressable, Text, View } from 'react-native'; import { X } from 'lucide-react-native'; import { WebView } from 'react-native-webview'; import { editorShellStyles } from '../../features/editor/editorShellStyles'; import { themedColor } from '../../theme'; import { createArtifactNavigationGuard } from '../artifactPreviewUtils'; import { toolWidgetStyles } from '../primitives/toolWidgetStyles'; type FrameProps = { html: string; onFail: () => void; scrollEnabled: boolean; }; /** * The sandboxed frame, mirroring the web's iframe posture (sandbox * `allow-scripts`, no `allow-same-origin`; see ArtifactInlineCard). The * sandbox invariants, as a set: * 1. The CSP baked into the html shell blocks programmatic egress * (`connect-src 'none'`) — but only for THAT document, so * 2. the platform-aware navigation guard admits at most one document per * WebView: on iOS the initial `source={{ html }}` load (reported as * about:blank); on Android the initial load never consults the guard, so * it denies every callback. Every artifact-initiated navigation, top or * sub frame (http(s), data:, about:blank re-navs), is cancelled on both * platforms, so artifact JS can never mint a replacement document * without the CSP. * 3. `originWhitelist={['*']}` so every request reaches that guard — * react-native-webview requires `['*']` for static HTML sources, and * non-whitelisted URLs are NOT blocked, they are handed to the OS * (`Linking.openURL`), which would itself be an escape. * 4. Popup windows are discarded natively (multiple windows ON, no * `onOpenWindow`) and `window.open`/`target="_blank"` are frozen inside * the document by the shell's POPUP_BLOCK. * 5. No shared state or device access: `incognito`, no DOM storage, no * file/geolocation access. */ function ArtifactFrame(props: FrameProps) { // Remount per document: an artifact rewrite gets a fresh loading overlay // and a fresh navigation guard instead of stale ones from the previous html. return ; } function ArtifactDocumentFrame({ html, onFail, scrollEnabled }: FrameProps) { const [loading, setLoading] = useState(true); // Fresh guard per WebView document (the keyed remount above pins one html // per mount; on iOS the initial document must be admitted again). const guardNavigation = useMemo(() => createArtifactNavigationGuard(Platform.OS), []); return ( setLoading(false)} onShouldStartLoadWithRequest={(request) => guardNavigation(request.url)} // '*' is the documented requirement for `source={{ html }}` documents // (and their subresources); the guard above is the real gate — see // invariant 3. originWhitelist={['*']} scrollEnabled={scrollEnabled} // Deliberately ON: with multiple windows DISABLED, Android loads // `target="_blank"` in the SAME WebView without invoking // onShouldStartLoadWithRequest (documented react-native-webview // bypass). Enabled with no onOpenWindow handler, popup requests are // discarded on both platforms; the shell's POPUP_BLOCK script // neutralizes window.open/_blank inside the document too. setSupportMultipleWindows source={{ html }} style={toolWidgetStyles.artifactWebView} /> {loading ? ( ) : null} ); } /** Fixed-height inline preview for the artifact card body. */ export function ArtifactInlinePreview({ html, onFail }: { html: string; onFail: () => void }) { return ( ); } /** Full-screen viewer — same modal chrome as EditorDrawer (top bar + close). */ export function ArtifactFullscreenModal({ html, onClose, onFail, title, visible, }: { html: string; onClose: () => void; onFail: () => void; title: string; visible: boolean; }) { return ( {title} {visible ? : null} ); }