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}
);
}