import { useState, useMemo, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { subscriptionApi } from '../api/subscription' import { useTelegramWebApp } from '../hooks/useTelegramWebApp' import type { AppInfo, AppConfig, LocalizedText } from '../types' interface ConnectionModalProps { onClose: () => void } const CloseIcon = () => ( ) const CopyIcon = () => ( ) const CheckIcon = () => ( ) const LinkIcon = () => ( ) const ChevronIcon = () => ( ) const BackIcon = () => ( ) const HappIcon = () => ( ) const ClashMetaIcon = () => ( ) const ShadowrocketIcon = () => ( ) const StreisandIcon = () => ( ) const getAppIcon = (appName: string): React.ReactNode => { const name = appName.toLowerCase() if (name.includes('happ')) return if (name.includes('shadowrocket') || name.includes('rocket')) return if (name.includes('streisand')) return if (name.includes('clash') || name.includes('meta') || name.includes('verge')) return return 📦 } const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'] const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'] function isValidExternalUrl(url: string | undefined): boolean { if (!url) return false const lowerUrl = url.toLowerCase().trim() if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) return false return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://') } function isValidDeepLink(url: string | undefined): boolean { if (!url) return false const lowerUrl = url.toLowerCase().trim() if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) return false return lowerUrl.includes('://') } 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 } function useIsMobile() { // Initialize synchronously to avoid flash between desktop/mobile layouts const [isMobile, setIsMobile] = useState(() => { if (typeof window === 'undefined') return false return window.innerWidth < 768 }) useEffect(() => { const check = () => setIsMobile(window.innerWidth < 768) window.addEventListener('resize', check) return () => window.removeEventListener('resize', check) }, []) return isMobile } export default function ConnectionModal({ onClose }: ConnectionModalProps) { const { t, i18n } = useTranslation() const [selectedApp, setSelectedApp] = useState(null) const [copied, setCopied] = useState(false) const [detectedPlatform, setDetectedPlatform] = useState(null) const [showAppSelector, setShowAppSelector] = useState(false) const { isTelegramWebApp, isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp() const isMobileScreen = useIsMobile() // Use mobile layout only on small screens, even in Telegram Desktop const isMobile = isMobileScreen const safeBottom = isTelegramWebApp ? Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom) : 0 // In fullscreen mode, add +45px for Telegram native controls (close/menu buttons in corners) const safeTop = isTelegramWebApp ? Math.max(safeAreaInset.top, contentSafeAreaInset.top) + (isFullscreen ? 45 : 0) : 0 const { data: appConfig, isLoading, error } = useQuery({ queryKey: ['appConfig'], queryFn: () => subscriptionApi.getAppConfig(), }) useEffect(() => { setDetectedPlatform(detectPlatform()) }, []) useEffect(() => { if (!appConfig?.platforms || selectedApp) return const platform = detectedPlatform || platformOrder.find(p => appConfig.platforms[p]?.length > 0) if (!platform || !appConfig.platforms[platform]?.length) return const apps = appConfig.platforms[platform] const app = apps.find(a => a.isFeatured) || apps[0] if (app) setSelectedApp(app) }, [appConfig, detectedPlatform, selectedApp]) useEffect(() => { document.body.style.overflow = 'hidden' return () => { document.body.style.overflow = '' } }, []) const getLocalizedText = (text: LocalizedText | undefined): string => { if (!text) return '' const lang = i18n.language || 'en' return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || '' } const availablePlatforms = useMemo(() => { if (!appConfig?.platforms) return [] const available = platformOrder.filter(key => appConfig.platforms[key]?.length > 0) if (detectedPlatform && available.includes(detectedPlatform)) { return [detectedPlatform, ...available.filter(p => p !== detectedPlatform)] } return available }, [appConfig, detectedPlatform]) const copySubscriptionLink = async () => { if (!appConfig?.subscriptionUrl) return try { await navigator.clipboard.writeText(appConfig.subscriptionUrl) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { const textarea = document.createElement('textarea') textarea.value = appConfig.subscriptionUrl document.body.appendChild(textarea) textarea.select() document.execCommand('copy') document.body.removeChild(textarea) setCopied(true) setTimeout(() => setCopied(false), 2000) } } const handleConnect = (app: AppInfo) => { if (!app.deepLink || !isValidDeepLink(app.deepLink)) return const lang = i18n.language?.startsWith('ru') ? 'ru' : 'en' const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(app.deepLink)}&lang=${lang}` const tg = (window as unknown as { Telegram?: { WebApp?: { openLink?: (url: string, options?: object) => void } } }).Telegram?.WebApp if (tg?.openLink) { try { tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true }) return } catch { /* fallback */ } } window.location.href = redirectUrl } // Desktop modal wrapper - compact centered modal with max height const DesktopWrapper = ({ children }: { children: React.ReactNode }) => (
e.stopPropagation()} > {/* Desktop close button */} {children}
) // Mobile fullscreen wrapper - like React Native Modal with animationType="slide" // Use portal to render directly in body, avoiding transform/filter issues with fixed positioning const MobileWrapper = ({ children }: { children: React.ReactNode }) => { const content = ( <> {/* Backdrop */}
{/* Modal - fullscreen overlay */}
{/* Close button */} {children}
) if (typeof document !== 'undefined') { return createPortal(content, document.body) } return content } const Wrapper = isMobile ? MobileWrapper : DesktopWrapper // Loading if (isLoading) { return (
) } // Error if (error || !appConfig) { return (
😕

{t('common.error')}

) } // No subscription if (!appConfig.hasSubscription) { return (
📱

{t('subscription.connection.title')}

{t('subscription.connection.noSubscription')}

) } // App selector if (showAppSelector) { const platformNames: Record = { ios: 'iOS', android: 'Android', windows: 'Windows', macos: 'macOS', linux: 'Linux', androidTV: 'Android TV', appleTV: 'Apple TV' } return ( {/* Header */}

{t('subscription.connection.selectApp')}

{/* Apps grouped by platform */}
{availablePlatforms.map(platform => { const apps = appConfig.platforms[platform] if (!apps?.length) return null const isCurrentPlatform = platform === detectedPlatform return (
{/* Platform header */}
{platformNames[platform] || platform} {isCurrentPlatform && ( {t('subscription.connection.yourDevice')} )}
{/* Apps for this platform */}
{apps.map(app => ( ))}
) })}
) } // Main view return ( {/* Header - app selector */}
{/* Content */}
{/* Step 1 */} {selectedApp?.installationStep && (
1

{t('subscription.connection.installApp')}

{getLocalizedText(selectedApp.installationStep.description)}

{selectedApp.installationStep.buttons && selectedApp.installationStep.buttons.length > 0 && (
{selectedApp.installationStep.buttons.filter(btn => isValidExternalUrl(btn.buttonLink)).map((btn, idx) => ( {getLocalizedText(btn.buttonText)} ))}
)}
)} {/* Step 2 */} {selectedApp?.addSubscriptionStep && (
2

{t('subscription.connection.addSubscription')}

{getLocalizedText(selectedApp.addSubscriptionStep.description)}

{selectedApp.deepLink && ( )}
)} {/* Step 3 */} {selectedApp?.connectAndUseStep && (
3

{t('subscription.connection.connectVpn')}

{getLocalizedText(selectedApp.connectAndUseStep.description)}

)}
) }