From 65b0120cd72f354979b17d1ee92dd0b06ada867e Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 20 Jan 2026 04:44:51 +0300 Subject: [PATCH] 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 && ( -
-
2
-
-

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

- {selectedApp.deepLink && ( - - )} - + {getLocalizedText(btn.buttonText)} + + ))}
-
- )} + )} +
+ )} - {/* 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 && ( +
+ +
+ )} + ) }