import { useState, useMemo, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import DOMPurify from 'dompurify'; import type { AppConfig, LocalizedText, RemnawaveAppClient, RemnawavePlatformData, RemnawaveButtonClient, } from '@/types'; import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock } from './blocks'; import type { BlockRendererProps } from './blocks'; const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV']; // eslint-disable-next-line no-script-url const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:']; function isValidDeepLink(url: string | undefined): boolean { if (!url) return false; const lowerUrl = url.toLowerCase().trim(); if (dangerousSchemes.some((s) => lowerUrl.startsWith(s))) return false; return lowerUrl.includes('://'); } function isValidExternalUrl(url: string | undefined): boolean { if (!url) return false; const lowerUrl = url.toLowerCase().trim(); if (dangerousSchemes.some((s) => lowerUrl.startsWith(s))) return false; return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://'); } function detectPlatform(): string | null { if (typeof window === 'undefined' || !navigator?.userAgent) return null; const ua = navigator.userAgent.toLowerCase(); if (/iphone|ipad|ipod/.test(ua)) return 'ios'; if (/android/.test(ua)) return /tv|television/.test(ua) ? 'androidTV' : 'android'; if (/macintosh|mac os x/.test(ua)) return 'macos'; if (/windows/.test(ua)) return 'windows'; if (/linux/.test(ua)) return 'linux'; return null; } const RENDERERS: Record> = { cards: CardsBlock, timeline: TimelineBlock, accordion: AccordionBlock, minimal: MinimalBlock, }; // Icons const CopyIcon = () => ( ); const CheckIcon = () => ( ); const BackIcon = () => ( ); interface Props { appConfig: AppConfig; onOpenDeepLink: (url: string) => void; isTelegramWebApp: boolean; onGoBack: () => void; } export default function InstallationGuide({ appConfig, onOpenDeepLink, isTelegramWebApp, onGoBack, }: Props) { const { t, i18n } = useTranslation(); const detectedPlatform = useMemo(() => detectPlatform(), []); const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; const [activePlatformKey, setActivePlatformKey] = useState(null); const [selectedApp, setSelectedApp] = useState(null); const [copied, setCopied] = useState(false); // --- Helpers --- const getLocalizedText = useCallback( (text: LocalizedText | undefined): string => { if (!text) return ''; const lang = i18n.language || 'en'; return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || ''; }, [i18n.language], ); const getBaseTranslation = useCallback( (key: string, i18nKey: string): string => { const bt = appConfig.baseTranslations; if (bt && key in bt) { const text = getLocalizedText(bt[key as keyof typeof bt] as LocalizedText); if (text) return text; } return t(i18nKey); }, [appConfig.baseTranslations, getLocalizedText, t], ); const getSvgHtml = useCallback( (svgKey: string | undefined): string => { if (!svgKey || !appConfig.svgLibrary?.[svgKey]) return ''; const entry = appConfig.svgLibrary[svgKey]; const raw = typeof entry === 'string' ? entry : entry.svgString; if (!raw) return ''; return DOMPurify.sanitize(raw, { USE_PROFILES: { svg: true, svgFilters: true } }); }, [appConfig.svgLibrary], ); // --- Available platforms --- const availablePlatforms = useMemo(() => { if (!appConfig.platforms) return []; const available = platformOrder.filter((key) => { const data = appConfig.platforms[key] as RemnawavePlatformData | undefined; return data && data.apps && data.apps.length > 0; }); if (detectedPlatform && available.includes(detectedPlatform)) { return [detectedPlatform, ...available.filter((p) => p !== detectedPlatform)]; } return available; }, [appConfig.platforms, detectedPlatform]); // --- Auto-select platform & app --- useEffect(() => { if (selectedApp || !availablePlatforms.length) return; const platform = availablePlatforms[0]; const data = appConfig.platforms[platform] as RemnawavePlatformData | undefined; if (!data?.apps?.length) return; const app = data.apps.find((a) => a.featured) || data.apps[0]; if (app) { setSelectedApp(app); setActivePlatformKey(platform); } }, [appConfig.platforms, availablePlatforms, selectedApp]); // --- Copy --- const handleCopy = useCallback(async (url: string) => { try { await navigator.clipboard.writeText(url); } catch { const textarea = document.createElement('textarea'); textarea.value = url; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); } setCopied(true); setTimeout(() => setCopied(false), 2000); }, []); // --- Button renderer (passed to block renderers) --- const renderBlockButtons = useCallback( (buttons: RemnawaveButtonClient[] | undefined, variant: 'light' | 'subtle') => { if (!buttons || buttons.length === 0) return null; const baseClass = variant === 'light' ? 'rounded-xl border border-accent-500/40 px-4 py-2 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/10' : 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/50'; return (
{buttons.map((btn, idx) => { const btnText = getLocalizedText(btn.text); const btnSvg = getSvgHtml(btn.svgIconKey); const btnIcon = btnSvg ? (
) : null; if (btn.type === 'subscriptionLink') { const url = btn.resolvedUrl || btn.url || btn.link || selectedApp?.deepLink || appConfig.subscriptionUrl; if (!url || !isValidDeepLink(url)) return null; return ( ); } if (btn.type === 'copyButton') { if (appConfig.hideLink) return null; const url = btn.resolvedUrl || appConfig.subscriptionUrl; if (!url) return null; return ( ); } // external const href = btn.link || btn.url || ''; if (!isValidExternalUrl(href)) return null; return ( {btnIcon} {btnText} ); })}
); }, [ selectedApp, appConfig, copied, getSvgHtml, getLocalizedText, getBaseTranslation, handleCopy, onOpenDeepLink, t, ], ); // --- Current platform data --- const currentPlatformKey = activePlatformKey || availablePlatforms[0]; const currentPlatformData = currentPlatformKey ? (appConfig.platforms[currentPlatformKey] as RemnawavePlatformData | undefined) : undefined; const currentPlatformApps = currentPlatformData?.apps || []; // Platform display name const getPlatformDisplayName = useCallback( (key: string): string => { const data = appConfig.platforms[key] as RemnawavePlatformData | undefined; if (data?.displayName) { const name = getLocalizedText(data.displayName); if (name) return name; } if (appConfig.platformNames?.[key]) { return getLocalizedText(appConfig.platformNames[key]); } const fallback: Record = { ios: 'iOS', android: 'Android', windows: 'Windows', macos: 'macOS', linux: 'Linux', androidTV: 'Android TV', appleTV: 'Apple TV', }; return fallback[key] || key; }, [appConfig.platforms, appConfig.platformNames, getLocalizedText], ); // Platform SVG icon for dropdown const currentPlatformSvg = getSvgHtml(currentPlatformData?.svgIconKey); // Block renderer const blockType = appConfig.uiConfig?.installationGuidesBlockType || 'cards'; const Renderer = RENDERERS[blockType] || CardsBlock; return (
{/* Header + platform dropdown */}
{!isTelegramWebApp && ( )}

{getBaseTranslation('installationGuideHeader', 'subscription.connection.title')}

{availablePlatforms.length > 1 && (
{currentPlatformSvg && (
)}
)}
{/* App chips */} {currentPlatformApps.length > 0 && (
{currentPlatformApps.map((app, idx) => { const isSelected = selectedApp?.name === app.name; const appIconSvg = getSvgHtml(app.svgIconKey); return ( ); })}
)} {/* Tutorial button */} {appConfig.baseSettings?.isShowTutorialButton && appConfig.baseSettings?.tutorialUrl && ( {getBaseTranslation('tutorial', 'subscription.connection.tutorial')} )} {/* Blocks */} {selectedApp && ( )}
); }