import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import DOMPurify from 'dompurify'; import { adminAppsApi, LocalizedText, RemnawaveApp, RemnawaveSvgItem, RemnawaveBlock, RemnawaveButton, RemnawaveBaseSettings, RemnawaveBaseTranslations, } from '../api/adminApps'; import { useBackButton } from '../platform/hooks/useBackButton'; import { usePlatform } from '../platform/hooks/usePlatform'; // Icons const BackIcon = () => ( ); const AppsIcon = () => ( ); const StarIcon = ({ filled }: { filled: boolean }) => ( ); const CloudSyncIcon = () => ( ); const RefreshIcon = () => ( ); const SettingsIcon = () => ( ); const ChevronDownIcon = () => ( ); const PLATFORM_LABELS: Record = { ios: 'iOS', android: 'Android', macos: 'macOS', windows: 'Windows', linux: 'Linux', androidTV: 'Android TV', appleTV: 'Apple TV', }; const PLATFORMS = ['ios', 'android', 'macos', 'windows', 'linux', 'androidTV', 'appleTV']; function getLocalizedText(text: LocalizedText | undefined, lang: string): string { if (!text) return ''; return ( text[lang as keyof LocalizedText] || text['en'] || text['ru'] || Object.values(text).find((v) => v) || '' ); } function renderSvgIcon(svgLibrary: Record, key?: string, color?: string) { if (!key || !svgLibrary[key]?.svgString) return null; const sanitized = DOMPurify.sanitize(svgLibrary[key].svgString, { USE_PROFILES: { svg: true, svgFilters: true }, }); return (
); } function BaseSettingsPanel({ settings }: { settings: RemnawaveBaseSettings }) { const { t } = useTranslation(); return (

{t('admin.apps.baseSettings', 'Base Settings')}

{t('admin.apps.tutorialButton', 'Tutorial button')}: {settings.isShowTutorialButton ? 'ON' : 'OFF'}
{settings.tutorialUrl && (
URL: {settings.tutorialUrl}
)}
); } function BaseTranslationsPanel({ translations, lang, }: { translations: RemnawaveBaseTranslations; lang: string; }) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); const keys: (keyof RemnawaveBaseTranslations)[] = [ 'installApp', 'addSubscription', 'connectAndUse', 'copyLink', 'openApp', 'tutorial', 'close', ]; return (
{expanded && (
{keys.map((key) => ( ))}
Key Value
{key} {getLocalizedText(translations[key], lang)}
)}
); } function AppCard({ app, svgLibrary, lang, }: { app: RemnawaveApp; svgLibrary: Record; lang: string; }) { const [expanded, setExpanded] = useState(false); const firstBlock = app.blocks?.[0]; return (
{/* Expanded blocks */} {expanded && (
{app.blocks?.map((block: RemnawaveBlock, idx: number) => ( ))} {(!app.blocks || app.blocks.length === 0) && (

No blocks

)}
)}
); } function BlockCard({ block, index, svgLibrary, lang, }: { block: RemnawaveBlock; index: number; svgLibrary: Record; lang: string; }) { const title = getLocalizedText(block.title, lang); const description = getLocalizedText(block.description, lang); const iconColor = block.svgIconColor || '#8B5CF6'; return (
{renderSvgIcon(svgLibrary, block.svgIconKey, block.svgIconColor) || ( {index + 1} )}
{title &&
{title}
} {description &&
{description}
} {block.buttons && block.buttons.length > 0 && ( )}
); } export default function AdminApps() { const { t, i18n } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); const lang = i18n.language || 'en'; useBackButton(() => navigate('/admin')); const [selectedPlatform, setSelectedPlatform] = useState('ios'); const [showRemnaWaveSettings, setShowRemnaWaveSettings] = useState(false); const [remnaWaveUuidInput, setRemnaWaveUuidInput] = useState(''); // RemnaWave status const { data: remnaWaveStatus, refetch: refetchStatus } = useQuery({ queryKey: ['remnawave-status'], queryFn: adminAppsApi.getRemnaWaveStatus, staleTime: 60000, }); // List available RemnaWave configs (for settings modal) const { data: availableConfigs, isLoading: isLoadingConfigs } = useQuery({ queryKey: ['remnawave-configs-list'], queryFn: adminAppsApi.listRemnaWaveConfigs, enabled: showRemnaWaveSettings, staleTime: 30000, }); // Mutation for setting UUID const setUuidMutation = useMutation({ mutationFn: adminAppsApi.setRemnaWaveUuid, onSuccess: () => { refetchStatus(); queryClient.invalidateQueries({ queryKey: ['remnawave-config'] }); setShowRemnaWaveSettings(false); }, }); // Fetch RemnaWave config const { data: remnaWaveConfig, isLoading, refetch: refetchRemnaWave, } = useQuery({ queryKey: ['remnawave-config'], queryFn: adminAppsApi.getRemnaWaveConfig, enabled: !!remnaWaveStatus?.enabled, staleTime: 0, }); // Extract data from raw RemnaWave config const currentPlatformApps = remnaWaveConfig?.platforms?.[selectedPlatform]?.apps || []; const svgLibrary = remnaWaveConfig?.svgLibrary || {}; const baseSettings = remnaWaveConfig?.baseSettings; const baseTranslations = remnaWaveConfig?.baseTranslations; return (
{/* Header */}
{!capabilities.hasBackButton && ( )}

{t('admin.apps.title')}

{remnaWaveStatus?.enabled && ( )}
{/* RemnaWave Info Banner */} {remnaWaveStatus?.enabled && (
{t('admin.apps.remnaWaveMode', 'Showing apps from RemnaWave panel. Read-only mode.')}
)} {/* Base Settings */} {baseSettings && } {/* Platform Tabs */}
{PLATFORMS.map((platform) => ( ))}
{/* Apps List */} {isLoading ? (
) : currentPlatformApps.length > 0 ? (
{currentPlatformApps.map((app: RemnawaveApp, index: number) => ( ))}
) : (

{t('admin.apps.noApps')}

)} {/* Base Translations */} {baseTranslations && } {/* RemnaWave Settings Modal */} {showRemnaWaveSettings && (

{t('admin.apps.remnaWaveSettings', 'RemnaWave Settings')}

setRemnaWaveUuidInput(e.target.value)} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" className="input w-full font-mono text-sm" />

{t( 'admin.apps.remnaWaveConfigUuidHint', 'UUID of subscription page config from RemnaWave panel', )}

{/* Available configs from RemnaWave */} {isLoadingConfigs ? (
) : ( availableConfigs && availableConfigs.length > 0 && (
{availableConfigs.map((config) => ( ))}
) )}
)}
); }