import React, { useEffect, useMemo, useState } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { applyStyles, parseSizeOrPercent } from '../../utils/styles'; import { usePaywallContext } from '../../context/PaywallContext'; import { buildSmartTextReplacements, interpolateSmartText } from '../../utils/smartText'; import { initializeVideoControls, subscribeVideoControls } from '../../utils/videoControls'; // Resolve expo-video at module scope so hooks are called unconditionally let ExpoVideoView: React.ComponentType | null = null; let useExpoVideoPlayer: ((source: string, setup?: (player: any) => void) => any) | null = null; try { const expoVideo = require('expo-video'); ExpoVideoView = expoVideo.VideoView; useExpoVideoPlayer = expoVideo.useVideoPlayer; } catch { // expo-video is optional } interface Props { component: any; scaleFactor: number; parentDirection?: string; } /** * Inner component that uses expo-video hooks. * Only rendered when expo-video is available, so the hook call is safe. */ const ExpoVideoPlayer: React.FC<{ url: string; width: any; height: any; containerStyle: any; component: any; playing: boolean; muted: boolean; }> = ({ url, width, height, containerStyle, component, playing, muted }) => { const VideoView = ExpoVideoView as React.ComponentType; const player = useExpoVideoPlayer!(url, (p) => { p.loop = component.loopVideo ?? component.loop ?? false; p.muted = muted; if (playing) { p.play(); } }); useEffect(() => { if (!player) return; if (playing) { player.play(); } else { player.pause(); } }, [player, playing]); useEffect(() => { if (!player) return; player.muted = muted; }, [player, muted]); const contentFit = component.imageCropping === 'fit' ? 'contain' : 'cover'; return ( ); }; export const NamiVideo: React.FC = ({ component, scaleFactor, parentDirection }) => { const ctx = usePaywallContext(); const [playing, setPlaying] = useState(!!component.autoplayVideo); const [muted, setMuted] = useState(!!(component.mute ?? component.muted)); const smartTextSku = component?.smartTextSku ?? component?.sku; const url = useMemo(() => { const raw = component.url ?? component.videoUrl ?? (ctx.state as any).appSuppliedVideoUrl ?? ''; const replacements = buildSmartTextReplacements(ctx.state, ctx.flow, smartTextSku); const resolved = interpolateSmartText(raw, replacements); return resolved == null ? '' : String(resolved); }, [component, ctx.state, ctx.flow, smartTextSku]); useEffect(() => { const playingFromConfig = component.paused != null ? !component.paused : !!component.autoplayVideo; initializeVideoControls({ playing: playingFromConfig, muted: !!(component.mute ?? component.muted), }); }, [component.paused, component.autoplayVideo, component.mute, component.muted]); useEffect(() => subscribeVideoControls((state) => { setPlaying(state.playing); setMuted(state.muted); }), []); const containerStyle = applyStyles(component, scaleFactor, false, parentDirection); const rawW = component.width ?? component.fixedWidth; const rawH = component.height ?? component.fixedHeight; const w = parseSizeOrPercent(rawW, scaleFactor) ?? '100%'; const h = parseSizeOrPercent(rawH, scaleFactor) ?? 200; if (ExpoVideoView && useExpoVideoPlayer && url) { return ( ); } return ( Video ); }; const styles = StyleSheet.create({ placeholder: { backgroundColor: '#000', justifyContent: 'center', alignItems: 'center' }, placeholderText: { color: '#fff', fontSize: 14 }, });