import { useEffect, useState, useCallback } from 'react' import { useSearchParams, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { brandingApi } from '../api/branding' type Status = 'countdown' | 'fallback' | 'error' // App schemes configuration - same as miniapp const appSchemes = [ { scheme: 'happ://', icon: 'H', name: 'Happ' }, { scheme: 'flclash://', icon: 'F', name: 'FlClash' }, { scheme: 'clash://', icon: 'C', name: 'Clash Meta' }, { scheme: 'sing-box://', icon: 'S', name: 'sing-box' }, { scheme: 'v2rayng://', icon: 'V', name: 'v2rayNG' }, { scheme: 'sub://', icon: 'R', name: 'Shadowrocket' }, { scheme: 'shadowrocket://', icon: 'R', name: 'Shadowrocket' }, { scheme: 'hiddify://', icon: 'H', name: 'Hiddify' }, { scheme: 'streisand://', icon: 'S', name: 'Streisand' }, { scheme: 'quantumult://', icon: 'Q', name: 'Quantumult X' }, { scheme: 'surge://', icon: 'S', name: 'Surge' }, { scheme: 'loon://', icon: 'L', name: 'Loon' }, { scheme: 'nekobox://', icon: 'N', name: 'NekoBox' }, { scheme: 'v2box://', icon: 'V', name: 'V2Box' }, ] const COUNTDOWN_SECONDS = 5 export default function DeepLinkRedirect() { const { i18n } = useTranslation() const navigate = useNavigate() const [searchParams] = useSearchParams() const [status, setStatus] = useState('countdown') const [countdown, setCountdown] = useState(COUNTDOWN_SECONDS) const [copied, setCopied] = useState(false) // Get branding const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: brandingApi.getBranding, staleTime: 60000, }) const projectName = branding ? branding.name : (import.meta.env.VITE_APP_NAME || 'VPN') const logoLetter = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V' const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null // Get parameters const deepLink = searchParams.get('url') || searchParams.get('deeplink') || '' const subscriptionUrl = searchParams.get('sub') || '' const appParam = searchParams.get('app') || '' // Detect app from deep link const appInfo = deepLink ? appSchemes.find(a => deepLink.toLowerCase().startsWith(a.scheme)) : null const appName = appInfo?.name || appParam || 'VPN' const appIcon = appInfo?.icon || appName[0]?.toUpperCase() || 'V' // Translations const texts = { en: { connecting: 'Connecting to', redirecting: 'Redirecting in', seconds: 'seconds', manual: 'If nothing happens, click the button below.', openApp: 'Open App', copyLink: 'Copy subscription link', copied: 'Copied!', tryAgain: 'Try again', backToCabinet: 'Back to cabinet', errorTitle: 'Error', errorDesc: 'Connection link is missing', goToSubscription: 'Go to subscription', howToAdd: 'How to add manually:', step1: 'Copy the link using the button above', step2: 'Open the app', step3: 'Find "+" or "Add subscription"', step4: 'Select "From clipboard" or "Paste link"', }, ru: { connecting: 'Подключение к', redirecting: 'Перенаправление через', seconds: 'сек', manual: 'Если ничего не происходит, нажмите кнопку ниже.', openApp: 'Открыть приложение', copyLink: 'Скопировать ссылку подписки', copied: 'Скопировано!', tryAgain: 'Попробовать снова', backToCabinet: 'Вернуться в кабинет', errorTitle: 'Ошибка', errorDesc: 'Ссылка для подключения не найдена', goToSubscription: 'Перейти к подписке', howToAdd: 'Как добавить вручную:', step1: 'Скопируйте ссылку кнопкой выше', step2: 'Откройте приложение', step3: 'Найдите "+" или "Добавить подписку"', step4: 'Выберите "Из буфера" или "Вставить ссылку"', } } const lang = i18n.language?.startsWith('ru') ? 'ru' : 'en' const txt = texts[lang] // Open deep link - same as miniapp, just window.location.href const openDeepLink = useCallback(() => { if (!deepLink) return window.location.href = deepLink }, [deepLink]) // Countdown timer effect useEffect(() => { if (!deepLink) { setStatus('error') return } if (status !== 'countdown') return const timer = setInterval(() => { setCountdown(prev => { if (prev <= 1) { clearInterval(timer) openDeepLink() // Show fallback after a delay setTimeout(() => setStatus('fallback'), 2000) return 0 } return prev - 1 }) }, 1000) return () => clearInterval(timer) }, [deepLink, status, openDeepLink]) const handleCopyLink = async () => { const linkToCopy = subscriptionUrl || deepLink try { await navigator.clipboard.writeText(linkToCopy) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { const textarea = document.createElement('textarea') textarea.value = linkToCopy document.body.appendChild(textarea) textarea.select() document.execCommand('copy') document.body.removeChild(textarea) setCopied(true) setTimeout(() => setCopied(false), 2000) } } // Progress percentage const progress = ((COUNTDOWN_SECONDS - countdown) / COUNTDOWN_SECONDS) * 100 return (
{/* Background */}
{/* Logo with pulse animation */}
{branding?.has_custom_logo && logoUrl ? ( {projectName ) : ( {logoLetter} )}

{projectName || 'VPN'}

{status !== 'error' && (

{txt.connecting} {appName}...

)} {/* Countdown State */} {status === 'countdown' && (
{/* App icon */}
{appIcon}
{/* Spinner */}
{/* Timer */}

{txt.redirecting}

{countdown} {txt.seconds}
{/* Progress bar */}

{txt.manual}

{/* Open now button */}
)} {/* Fallback State - App didn't open */} {status === 'fallback' && (
{/* App icon */}
{appIcon}
{/* Copy subscription link */} {/* Try again button */} {/* Back to cabinet */}
{/* Instructions */}

{txt.howToAdd}

  1. {txt.step1}
  2. {txt.step2} {appName}
  3. {txt.step3}
  4. {txt.step4}
)} {/* Error State */} {status === 'error' && (

{txt.errorTitle}

{txt.errorDesc}

)} {/* Footer */}
VPN Config Redirect
) }