From f90bfc167d8c2ff6e7ff01485e6488153c20104d Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 20 Jan 2026 04:34:49 +0300 Subject: [PATCH 1/6] Update ConnectionModal.tsx --- src/components/ConnectionModal.tsx | 627 +++++++++-------------------- 1 file changed, 187 insertions(+), 440 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index a7a2687..2ad4d28 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -9,56 +9,13 @@ interface ConnectionModalProps { onClose: () => void } -// Platform SVG Icons -const IosIcon = () => ( - - - -) - -const AndroidIcon = () => ( - - - -) - -const WindowsIcon = () => ( - - - -) - -const MacosIcon = () => ( - - - -) - -const LinuxIcon = () => ( - - - -) - -const TvIcon = () => ( - - - - -) - +// Icons const CloseIcon = () => ( ) -const BackIcon = () => ( - - - -) - const CopyIcon = () => ( @@ -77,26 +34,15 @@ const LinkIcon = () => ( ) -const ChevronIcon = () => ( - - +const ChevronDownIcon = () => ( + + ) -// Platform icon components map -const platformIconComponents: Record = { - ios: IosIcon, - android: AndroidIcon, - macos: MacosIcon, - windows: WindowsIcon, - linux: LinuxIcon, - androidTV: TvIcon, - appleTV: TvIcon, -} - // App icons const HappIcon = () => ( - + @@ -107,117 +53,79 @@ const HappIcon = () => ( ) const ClashMetaIcon = () => ( - + - - - - - - -) - -const ClashVergeIcon = () => ( - - - - - ) const ShadowrocketIcon = () => ( - + ) const StreisandIcon = () => ( - - + + ) -// App icon mapping by name (case-insensitive) -const getAppIcon = (appName: string, isFeatured: boolean): React.ReactNode => { +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('verge')) { - return - } - if (name.includes('clash') || name.includes('meta')) { - return - } - // Default icons - return isFeatured ? '⭐' : '📦' + 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 📦 } -// Platform order for display const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'] -// Dangerous schemes that should never be allowed 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 - } + 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 - } + if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) return false return lowerUrl.includes('://') } function detectPlatform(): string | null { - if (typeof window === 'undefined' || !navigator?.userAgent) { - return 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)) { - if (/tv|television|smart-tv|smarttv/.test(ua)) return 'androidTV' - return 'android' - } + 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 isMobile(): boolean { + if (typeof window === 'undefined') return false + return window.innerWidth < 768 +} + 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 modalContentRef = useRef(null) + const modalRef = useRef(null) - // Telegram Mini App support const { isTelegramWebApp, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp() - // Calculate safe area - prefer Telegram values, fallback to CSS env - const safeTop = isTelegramWebApp - ? Math.max(safeAreaInset.top, contentSafeAreaInset.top) - : 0 const safeBottom = isTelegramWebApp - ? Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom) - : 0 + ? Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom, 16) + : 16 const { data: appConfig, isLoading, error } = useQuery({ queryKey: ['appConfig'], @@ -228,54 +136,18 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { setDetectedPlatform(detectPlatform()) }, []) - // Auto-select platform and app when data is loaded 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] - // Prefer featured app, otherwise first app const app = apps.find(a => a.isFeatured) || apps[0] - - if (app) { - setSelectedApp(app) - } + if (app) setSelectedApp(app) }, [appConfig, detectedPlatform, selectedApp]) - // Scroll modal content to top when switching views useEffect(() => { - modalContentRef.current?.scrollTo({ top: 0, behavior: 'instant' }) - }, [showAppSelector]) - - // Scroll lock when modal is open - useEffect(() => { - const scrollY = window.scrollY - - // Prevent all touch/wheel scroll on backdrop - const preventScroll = (e: TouchEvent) => { - const target = e.target as HTMLElement - if (target.closest('[data-modal-content]')) return - e.preventDefault() - } - - const preventWheel = (e: WheelEvent) => { - const target = e.target as HTMLElement - if (target.closest('[data-modal-content]')) return - e.preventDefault() - } - - document.addEventListener('touchmove', preventScroll, { passive: false }) - document.addEventListener('wheel', preventWheel, { passive: false }) document.body.style.overflow = 'hidden' - - return () => { - document.removeEventListener('touchmove', preventScroll) - document.removeEventListener('wheel', preventWheel) - document.body.style.overflow = '' - window.scrollTo(0, scrollY) - } + return () => { document.body.style.overflow = '' } }, []) const getLocalizedText = (text: LocalizedText | undefined): string => { @@ -284,34 +156,21 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || '' } - const getPlatformName = (platformKey: string): string => { - if (!appConfig?.platformNames?.[platformKey]) { - return platformKey - } - return getLocalizedText(appConfig.platformNames[platformKey]) - } - const availablePlatforms = useMemo(() => { if (!appConfig?.platforms) return [] - const available = platformOrder.filter( - (key) => appConfig.platforms[key] && appConfig.platforms[key].length > 0 - ) + const available = platformOrder.filter(key => appConfig.platforms[key]?.length > 0) if (detectedPlatform && available.includes(detectedPlatform)) { - const filtered = available.filter(p => p !== detectedPlatform) - return [detectedPlatform, ...filtered] + return [detectedPlatform, ...available.filter(p => p !== detectedPlatform)] } return available }, [appConfig, detectedPlatform]) - // Get all apps for selector (must be before any conditional returns) - const allAppsForSelector = useMemo(() => { + const allApps = useMemo(() => { if (!appConfig?.platforms) return [] - const result: { platform: string; apps: AppInfo[] }[] = [] + const result: AppInfo[] = [] for (const platform of availablePlatforms) { const apps = appConfig.platforms[platform] - if (apps?.length) { - result.push({ platform, apps }) - } + if (apps?.length) result.push(...apps) } return result }, [appConfig, availablePlatforms]) @@ -335,256 +194,163 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { } const handleConnect = (app: AppInfo) => { - if (!app.deepLink || !isValidDeepLink(app.deepLink)) { - console.warn('Invalid or missing deep link:', app.deepLink) - return - } + 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 isCustomScheme = !/^https?:\/\//i.test(app.deepLink) const tg = (window as unknown as { Telegram?: { WebApp?: { openLink?: (url: string, options?: object) => void } } }).Telegram?.WebApp - if (isCustomScheme && tg?.openLink) { + if (tg?.openLink) { try { tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true }) return - } catch (e) { - console.warn('tg.openLink failed:', e) - } + } catch { /* fallback */ } } window.location.href = redirectUrl } - // Modal wrapper - fullscreen on mobile, centered on desktop - const ModalWrapper = ({ children }: { children: React.ReactNode }) => { - // For Telegram, use JS values; for browser, use CSS env() - const closeButtonTop = isTelegramWebApp - ? `${12 + safeTop}px` - : `calc(0.75rem + env(safe-area-inset-top, 0px))` - - const safeTopStyle = isTelegramWebApp - ? `${safeTop}px` - : 'env(safe-area-inset-top, 0px)' - - const safeBottomStyle = isTelegramWebApp - ? `${safeBottom}px` - : 'env(safe-area-inset-bottom, 0px)' + const mobile = isMobile() || isTelegramWebApp + // Loading + if (isLoading) { return ( -
- {/* Mobile: fullscreen */} -
e.stopPropagation()} - > - {/* Mobile close button - fixed top right */} - - - {/* Mobile safe area spacer - top */} -
- - {children} - - {/* Mobile safe area spacer - bottom */} -
+
+
e.stopPropagation()}> +
) } - // Loading state - if (isLoading) { - return ( - -
-
-
- - ) - } - - // Error state + // Error or no config if (error || !appConfig) { return ( - -
-
- 😕 -
-

{t('common.error')}

- +
+
e.stopPropagation()}> +
😕
+

{t('common.error')}

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

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

-

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

- +
+
e.stopPropagation()}> +
📱
+

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

+

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

+
- - ) - } - - // App selector view - if (showAppSelector || !selectedApp) { - return ( - - {/* Header */} -
-
- {selectedApp && ( - - )} -
-

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

-

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

-
-
- -
- - {/* Apps by platform */} -
- {allAppsForSelector.map(({ platform, apps }) => { - const IconComponent = platformIconComponents[platform] - const isCurrentPlatform = platform === detectedPlatform - return ( -
-
-
- {IconComponent && } -
- - {getPlatformName(platform)} - {isCurrentPlatform && ({t('subscription.connection.yourDevice')})} - -
-
- {apps.map((app) => ( - - ))} -
-
- ) - })} -
- - {/* Copy link */} -
- -
-
- ) - } - - // App instructions (main view) - return ( - - {/* Header with app info and change button */} -
- -
+ ) + } - {/* Instructions */} -
- {/* Step 1: Install */} - {selectedApp.installationStep && ( -
-
-
1
+ // App selector dropdown + if (showAppSelector) { + return ( +
+
e.stopPropagation()} + > + {/* Handle bar for mobile */} + {mobile &&
} + + {/* Header */} +
+

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

+ +
+ + {/* Apps list */} +
+ {allApps.map(app => ( + + ))} +
+
+
+ ) + } + + // Main view - Instructions + return ( +
+
e.stopPropagation()} + > + {/* Handle bar for mobile */} + {mobile &&
} + + {/* Header with app selector */} +
+ + {!mobile && ( + + )} +
+ + {/* Content */} +
+ {/* Step 1: Install (compact) */} + {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) => ( + {selectedApp.installationStep.buttons?.length > 0 && ( +
+ {selectedApp.installationStep.buttons.filter(btn => isValidExternalUrl(btn.buttonLink)).map((btn, idx) => ( {getLocalizedText(btn.buttonText)} @@ -594,68 +360,49 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { )}
-
- )} + )} - {/* Step 2: Add subscription */} - {selectedApp.addSubscriptionStep && ( -
-
-
2
-
-

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

-

+ {/* Step 2: Connect (main action) */} + {selectedApp?.addSubscriptionStep && ( +

+
2
+
+

{getLocalizedText(selectedApp.addSubscriptionStep.description)}

-
- {selectedApp.deepLink && ( - - )} + {selectedApp.deepLink && ( -
+ )} +
-
- )} + )} - {/* Step 3: Connect */} - {selectedApp.connectAndUseStep && ( -
-
-
3
-
-

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

-

- {getLocalizedText(selectedApp.connectAndUseStep.description)} -

-
+ {/* Step 3: Ready (minimal) */} + {selectedApp?.connectAndUseStep && ( +
+
3
+

{getLocalizedText(selectedApp.connectAndUseStep.description)}

-
- )} + )} +
- - {/* Footer - hidden on mobile since we have close button on top */} -
- -
- +
) } From 7dd340217fddf8c62f35844b6e7e01fb0e44bde3 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 20 Jan 2026 04:35:56 +0300 Subject: [PATCH 2/6] Update ConnectionModal.tsx --- src/components/ConnectionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 2ad4d28..5d27d18 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -342,7 +342,7 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {

{getLocalizedText(selectedApp.installationStep.description)}

- {selectedApp.installationStep.buttons?.length > 0 && ( + {selectedApp.installationStep.buttons && selectedApp.installationStep.buttons.length > 0 && (
{selectedApp.installationStep.buttons.filter(btn => isValidExternalUrl(btn.buttonLink)).map((btn, idx) => ( Date: Tue, 20 Jan 2026 04:44:51 +0300 Subject: [PATCH 3/6] Update ConnectionModal.tsx --- src/components/ConnectionModal.tsx | 395 ++++++++++++++++------------- 1 file changed, 212 insertions(+), 183 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 5d27d18..4e44848 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useRef } from 'react' +import { useState, useMemo, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { subscriptionApi } from '../api/subscription' @@ -9,40 +9,44 @@ interface ConnectionModalProps { onClose: () => void } -// Icons const CloseIcon = () => ( - + ) const CopyIcon = () => ( - + ) const CheckIcon = () => ( - + ) const LinkIcon = () => ( - + ) -const ChevronDownIcon = () => ( +const ChevronIcon = () => ( ) -// App icons +const BackIcon = () => ( + + + +) + const HappIcon = () => ( - + @@ -53,20 +57,20 @@ const HappIcon = () => ( ) const ClashMetaIcon = () => ( - + ) const ShadowrocketIcon = () => ( - + ) const StreisandIcon = () => ( - - + + ) @@ -76,11 +80,10 @@ const getAppIcon = (appName: string): React.ReactNode => { 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 📦 + return 📦 } const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'] - const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'] function isValidExternalUrl(url: string | undefined): boolean { @@ -108,9 +111,15 @@ function detectPlatform(): string | null { return null } -function isMobile(): boolean { - if (typeof window === 'undefined') return false - return window.innerWidth < 768 +function useIsMobile() { + const [isMobile, setIsMobile] = useState(false) + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < 768) + check() + window.addEventListener('resize', check) + return () => window.removeEventListener('resize', check) + }, []) + return isMobile } export default function ConnectionModal({ onClose }: ConnectionModalProps) { @@ -119,13 +128,13 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { const [copied, setCopied] = useState(false) const [detectedPlatform, setDetectedPlatform] = useState(null) const [showAppSelector, setShowAppSelector] = useState(false) - const modalRef = useRef(null) const { isTelegramWebApp, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp() + const isMobileScreen = useIsMobile() + const isMobile = isMobileScreen || isTelegramWebApp - const safeBottom = isTelegramWebApp - ? Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom, 16) - : 16 + const safeTop = isTelegramWebApp ? Math.max(safeAreaInset.top, contentSafeAreaInset.top) : 0 + const safeBottom = isTelegramWebApp ? Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom) : 0 const { data: appConfig, isLoading, error } = useQuery({ queryKey: ['appConfig'], @@ -207,202 +216,222 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { window.location.href = redirectUrl } - const mobile = isMobile() || isTelegramWebApp + // Desktop modal wrapper + const DesktopWrapper = ({ children }: { children: React.ReactNode }) => ( +
+
e.stopPropagation()}> + {children} +
+
+ ) + + // Mobile fullscreen wrapper - like React Native Modal with animationType="slide" + const MobileWrapper = ({ children }: { children: React.ReactNode }) => ( + <> + {/* Backdrop */} +
+ {/* Modal - slides from bottom */} +
+ {/* Close button */} + + {children} +
+ + ) + + const Wrapper = isMobile ? MobileWrapper : DesktopWrapper // Loading if (isLoading) { return ( -
-
e.stopPropagation()}> -
+ +
+
-
+
) } - // Error or no config + // Error if (error || !appConfig) { return ( -
-
e.stopPropagation()}> -
😕
-

{t('common.error')}

- + +
+
😕
+

{t('common.error')}

+
-
+ ) } // No subscription if (!appConfig.hasSubscription) { return ( -
-
e.stopPropagation()}> -
📱
-

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

-

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

- + +
+
📱
+

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

+

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

+
-
+ ) } - // App selector dropdown + // App selector if (showAppSelector) { return ( -
-
e.stopPropagation()} - > - {/* Handle bar for mobile */} - {mobile &&
} - - {/* Header */} -
-

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

- -
- - {/* Apps list */} -
- {allApps.map(app => ( - - ))} -
+ + {/* Header */} +
+ +

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

-
+ + {/* Apps list */} +
+ {allApps.map(app => ( + + ))} +
+ ) } - // Main view - Instructions + // Main view return ( -
-
e.stopPropagation()} - > - {/* Handle bar for mobile */} - {mobile &&
} + + {/* Header - app selector */} +
+ +
- {/* Header with app selector */} -
- - {!mobile && ( - - )} -
- - {/* Content */} -
- {/* Step 1: Install (compact) */} - {selectedApp?.installationStep && ( -
-
1
-
-

- {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: Connect (main action) */} - {selectedApp?.addSubscriptionStep && ( -
- )} + )} +
+ )} - {/* Step 3: Ready (minimal) */} - {selectedApp?.connectAndUseStep && ( -
-
3
-

{getLocalizedText(selectedApp.connectAndUseStep.description)}

+ {/* 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)}

+
+ )}
-
+ + {/* Desktop footer */} + {!isMobile && ( +
+ +
+ )} + ) } From 0054e674fd9ae5207e10bb4e94103a7e855959a4 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 20 Jan 2026 04:47:45 +0300 Subject: [PATCH 4/6] Update ConnectionModal.tsx --- src/components/ConnectionModal.tsx | 79 +++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 4e44848..63850d6 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -293,6 +293,16 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // 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 */} @@ -303,29 +313,54 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {

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

- {/* Apps list */} -
- {allApps.map(app => ( - + ))} +
- {app.name} - {app.isFeatured && ( - - {t('subscription.connection.featured')} - - )} - - ))} + ) + })}
) From 706b2b34f7ebba25830dab77492546095a9fd30e Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 20 Jan 2026 04:48:35 +0300 Subject: [PATCH 5/6] Update ConnectionModal.tsx --- src/components/ConnectionModal.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 63850d6..a5a0b45 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -174,16 +174,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { return available }, [appConfig, detectedPlatform]) - const allApps = useMemo(() => { - if (!appConfig?.platforms) return [] - const result: AppInfo[] = [] - for (const platform of availablePlatforms) { - const apps = appConfig.platforms[platform] - if (apps?.length) result.push(...apps) - } - return result - }, [appConfig, availablePlatforms]) - const copySubscriptionLink = async () => { if (!appConfig?.subscriptionUrl) return try { From 1403a63df58c5cf9e5d927035ae9a355bbb5f7f0 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 20 Jan 2026 04:52:54 +0300 Subject: [PATCH 6/6] Update AnimatedBackground.tsx --- src/components/AnimatedBackground.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/AnimatedBackground.tsx b/src/components/AnimatedBackground.tsx index fe54b1b..0557e2a 100644 --- a/src/components/AnimatedBackground.tsx +++ b/src/components/AnimatedBackground.tsx @@ -4,16 +4,11 @@ import { brandingApi } from '../api/branding' const ANIMATION_CACHE_KEY = 'cabinet_animation_enabled' -// Detect low-performance device (mobile in Telegram WebApp) +// Detect if user prefers reduced motion const isLowPerformance = (): boolean => { - // Check if running in Telegram WebApp - const isTelegramWebApp = !!(window as unknown as { Telegram?: { WebApp?: unknown } }).Telegram?.WebApp - // Check if mobile - const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) - // Check for reduced motion preference + // Only check for reduced motion preference - let animation run everywhere else const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches - - return prefersReducedMotion || (isTelegramWebApp && isMobile) + return prefersReducedMotion } // Get cached value from localStorage