From 64acfbee72a57a3635635166d6de6788834bbe96 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Tue, 20 Jan 2026 02:17:22 +0300 Subject: [PATCH 1/8] Add fullscreen functionality: introduce API methods for getting and updating fullscreen settings, implement caching for fullscreen state, and integrate fullscreen toggle in AdminSettings. Remove unused fullscreen icons from Layout component. --- src/api/branding.ts | 21 +++++++++++++++++ src/components/layout/Layout.tsx | 28 +---------------------- src/hooks/useTelegramWebApp.ts | 30 ++++++++++++++++++++++++ src/pages/AdminSettings.tsx | 39 ++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 27 deletions(-) diff --git a/src/api/branding.ts b/src/api/branding.ts index 8d21793..9bfe29e 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -11,6 +11,10 @@ export interface AnimationEnabled { enabled: boolean } +export interface FullscreenEnabled { + enabled: boolean +} + const BRANDING_CACHE_KEY = 'cabinet_branding' const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded' @@ -121,4 +125,21 @@ export const brandingApi = { const response = await apiClient.patch('/cabinet/branding/animation', { enabled }) return response.data }, + + // Get fullscreen enabled (public, no auth required) + getFullscreenEnabled: async (): Promise => { + try { + const response = await apiClient.get('/cabinet/branding/fullscreen') + return response.data + } catch { + // If endpoint doesn't exist, default to disabled + return { enabled: false } + } + }, + + // Update fullscreen enabled (admin only) + updateFullscreenEnabled: async (enabled: boolean): Promise => { + const response = await apiClient.patch('/cabinet/branding/fullscreen', { enabled }) + return response.data + }, } diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 0c82aee..876f537 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -123,18 +123,6 @@ const WheelIcon = () => ( ) -const FullscreenIcon = () => ( - - - -) - -const ExitFullscreenIcon = () => ( - - - -) - export default function Layout({ children }: LayoutProps) { const { t } = useTranslation() const location = useLocation() @@ -142,7 +130,7 @@ export default function Layout({ children }: LayoutProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const { toggleTheme, isDark } = useTheme() const [userPhotoUrl, setUserPhotoUrl] = useState(null) - const { isTelegramWebApp, isFullscreen, isFullscreenSupported, toggleFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp() + const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp() // Fetch enabled themes from API - same source of truth as AdminSettings const { data: enabledThemes } = useQuery({ @@ -372,20 +360,6 @@ export default function Layout({ children }: LayoutProps) { {/* Right side */}
- {/* Fullscreen toggle - only show in Telegram WebApp */} - {isTelegramWebApp && isFullscreenSupported && ( - - )} - {/* Theme toggle button - only show if both themes are enabled */} {canToggle && (
+ + {/* Fullscreen Toggle */} +
+
+
+

Авто-Fullscreen

+

+ Автоматически открывать на полный экран в Telegram +

+
+ +
+
{/* Theme Colors Card */} From dd85a465953f97793e65b7725b9cbfb514a68b71 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Tue, 20 Jan 2026 02:26:25 +0300 Subject: [PATCH 2/8] Refactor ConnectionModal component: update platform icons to a consistent size, enhance loading and error states with improved UI, and streamline platform detection logic. Add new icons for close, back, copy, check, link, and chevron actions. Optimize layout and styling for better user experience. --- src/components/ConnectionModal.tsx | 520 +++++++++++++---------------- 1 file changed, 233 insertions(+), 287 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 473022d..11c299a 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -10,40 +10,75 @@ interface ConnectionModalProps { // Platform SVG Icons const IosIcon = () => ( - + ) const AndroidIcon = () => ( - + ) const WindowsIcon = () => ( - + ) const MacosIcon = () => ( - - + + ) const LinuxIcon = () => ( - + ) const TvIcon = () => ( - + - + +) + +const CloseIcon = () => ( + + + +) + +const BackIcon = () => ( + + + +) + +const CopyIcon = () => ( + + + +) + +const CheckIcon = () => ( + + + +) + +const LinkIcon = () => ( + + + +) + +const ChevronIcon = () => ( + + ) @@ -64,72 +99,37 @@ const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV // Dangerous schemes that should never be allowed const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'] -/** - * Validate URL to prevent XSS via javascript: and other dangerous schemes - * Only allows http, https, and known app store URLs - */ function isValidExternalUrl(url: string | undefined): boolean { if (!url) return false const lowerUrl = url.toLowerCase().trim() - - // Block dangerous schemes - const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'] if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) { return false } - - // Allow only http/https URLs return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://') } -/** - * Validate deep link URL - blocks dangerous schemes only - * Allows any custom app scheme (clash://, hiddify://, etc.) - */ function isValidDeepLink(url: string | undefined): boolean { if (!url) return false const lowerUrl = url.toLowerCase().trim() - - // Block dangerous schemes if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) { return false } - - // Allow any URL that has a scheme (contains ://) return lowerUrl.includes('://') } -// Detect user's platform from user agent function detectPlatform(): string | null { if (typeof window === 'undefined' || !navigator?.userAgent) { return null } - const ua = navigator.userAgent.toLowerCase() - - // Check for mobile devices first - if (/iphone|ipad|ipod/.test(ua)) { - return 'ios' - } + if (/iphone|ipad|ipod/.test(ua)) return 'ios' if (/android/.test(ua)) { - // Check if it's Android TV - if (/tv|television|smart-tv|smarttv/.test(ua)) { - return 'androidTV' - } + if (/tv|television|smart-tv|smarttv/.test(ua)) return 'androidTV' return 'android' } - - // Desktop platforms - if (/macintosh|mac os x/.test(ua)) { - return 'macos' - } - if (/windows/.test(ua)) { - return 'windows' - } - if (/linux/.test(ua)) { - return 'linux' - } - + if (/macintosh|mac os x/.test(ua)) return 'macos' + if (/windows/.test(ua)) return 'windows' + if (/linux/.test(ua)) return 'linux' return null } @@ -145,20 +145,16 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { queryFn: () => subscriptionApi.getAppConfig(), }) - // Auto-detect platform on mount useEffect(() => { - const detected = detectPlatform() - setDetectedPlatform(detected) + setDetectedPlatform(detectPlatform()) }, []) - // Helper to get localized text 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] || '' } - // Get platform name const getPlatformName = (platformKey: string): string => { if (!appConfig?.platformNames?.[platformKey]) { return platformKey @@ -166,13 +162,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { return getLocalizedText(appConfig.platformNames[platformKey]) } - // Get available platforms sorted (detected platform first) const availablePlatforms = useMemo(() => { if (!appConfig?.platforms) return [] const available = platformOrder.filter( (key) => appConfig.platforms[key] && appConfig.platforms[key].length > 0 ) - // Move detected platform to the front if (detectedPlatform && available.includes(detectedPlatform)) { const filtered = available.filter(p => p !== detectedPlatform) return [detectedPlatform, ...filtered] @@ -180,13 +174,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { return available }, [appConfig, detectedPlatform]) - // Get apps for selected platform const platformApps = useMemo(() => { if (!selectedPlatform || !appConfig?.platforms?.[selectedPlatform]) return [] return appConfig.platforms[selectedPlatform] }, [selectedPlatform, appConfig]) - // Copy subscription link const copySubscriptionLink = async () => { if (!appConfig?.subscriptionUrl) return try { @@ -194,7 +186,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { - // Fallback for older browsers const textarea = document.createElement('textarea') textarea.value = appConfig.subscriptionUrl document.body.appendChild(textarea) @@ -206,24 +197,16 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { } } - // Handle deep link click - use miniapp redirect page like in miniapp index.html const handleConnect = (app: AppInfo) => { - // Validate deep link to prevent XSS if (!app.deepLink || !isValidDeepLink(app.deepLink)) { console.warn('Invalid or missing deep link:', 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}` - - // Check if it's a custom URL scheme (not http/https) const isCustomScheme = !/^https?:\/\//i.test(app.deepLink) - const tg = (window as any).Telegram?.WebApp - + const tg = (window as unknown as { Telegram?: { WebApp?: { openLink?: (url: string, options?: object) => void } } }).Telegram?.WebApp if (isCustomScheme && tg?.openLink) { - // For custom URL schemes - open redirect page in external browser via Telegram - // try_browser: true - показывает диалог для перехода во внешний браузер (важно для мобильных) try { tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true }) return @@ -231,67 +214,55 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { console.warn('tg.openLink failed:', e) } } - - // Fallback - direct navigation window.location.href = redirectUrl } - // Modal wrapper classes - bottom sheet on mobile, centered on desktop - const modalOverlayClass = "fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-end sm:items-center sm:justify-center" - const modalCardClass = "w-full sm:max-w-lg sm:mx-4 bg-dark-850 sm:rounded-2xl rounded-t-2xl rounded-b-none border-t border-x sm:border border-dark-700/50 shadow-2xl overflow-hidden" - const modalContentClass = "p-4 sm:p-6 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-6" - // Allow the sheet to almost fill the viewport height on phones - const modalScrollableClass = "h-[calc(100vh-1rem)] sm:h-auto sm:max-h-[85vh]" - + // Loading state if (isLoading) { return ( -
-
e.stopPropagation()}> -
-
-
-
+
+
e.stopPropagation()}> +
+
) } + // Error state if (error || !appConfig) { return ( -
-
e.stopPropagation()}> -
-
-

{t('common.error')}

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

{t('common.error')}

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

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

- -
-
-

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

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

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

+

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

+
@@ -301,75 +272,86 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // Step 1: Select platform if (!selectedPlatform) { return ( -
-
e.stopPropagation()}> - {/* Header - fixed */} -
-

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

-
- {/* Scrollable content */} -
-

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

- -
+ {/* Content */} +
+
{availablePlatforms.map((platform) => { const IconComponent = platformIconComponents[platform] + const isDetected = platform === detectedPlatform return ( ) })}
- {/* Footer - fixed with safe area */} -
+ {/* Footer */} +
@@ -379,62 +361,65 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { ) } - // Step 2: Select app (if not selected yet) + // Step 2: Select app if (!selectedApp) { return ( -
-
e.stopPropagation()}> - {/* Header - fixed */} -
+
+
e.stopPropagation()} + > + {/* Header */} +
- -

- {getPlatformName(selectedPlatform)} -

+
+

{getPlatformName(selectedPlatform)}

+

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

+
-
- {/* Scrollable content */} -
-

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

- + {/* Content */} +
{platformApps.length === 0 ? ( -
+
+
+ 📭 +

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

) : ( -
+
{platformApps.map((app) => ( ))}
@@ -445,46 +430,44 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { ) } - // Step 3: Show app instructions and connect button + // Step 3: App instructions return ( -
-
e.stopPropagation()}> - {/* Header - fixed */} -
+
+
e.stopPropagation()} + > + {/* Header */} +
- -

- {selectedApp.name} -

+
+

{selectedApp.name}

+

Инструкция по подключению

+
-
- {/* Scrollable content */} -
-
+ {/* Content */} +
+
{/* Step 1: Install */} {selectedApp.installationStep && ( -
-

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

-

+

+
+
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) => ( @@ -495,9 +478,7 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { rel="noopener noreferrer" className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-dark-700 text-dark-200 text-sm hover:bg-dark-600 transition-all" > - - - + {getLocalizedText(btn.buttonText)} ))} @@ -506,88 +487,53 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
)} - {/* Additional before add subscription */} - {selectedApp.additionalBeforeAddSubscriptionStep && ( -
- {selectedApp.additionalBeforeAddSubscriptionStep.title && ( -

- {getLocalizedText(selectedApp.additionalBeforeAddSubscriptionStep.title)} -

- )} -

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

-
- )} - {/* Step 2: Add subscription */} {selectedApp.addSubscriptionStep && ( -
-

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

-

+

+
+
2
+

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

+
+

{getLocalizedText(selectedApp.addSubscriptionStep.description)}

- {/* Connect button */} - {selectedApp.deepLink && ( - - )} - - {/* Copy link fallback */} - )} - -
- )} - {/* Additional after add subscription */} - {selectedApp.additionalAfterAddSubscriptionStep && ( -
- {selectedApp.additionalAfterAddSubscriptionStep.title && ( -

- {getLocalizedText(selectedApp.additionalAfterAddSubscriptionStep.title)} -

- )} -

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

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

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

-

+

+
+
3
+

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

+
+

{getLocalizedText(selectedApp.connectAndUseStep.description)}

@@ -595,9 +541,9 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
- {/* Footer - fixed with safe area */} -
-
From 6183f7b3e6b1b6a02e07577a6bd73b6471df56b0 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Tue, 20 Jan 2026 02:30:11 +0300 Subject: [PATCH 3/8] Refactor ConnectionModal component: adjust padding for modal background and update maximum height for better responsiveness across platforms. --- src/components/ConnectionModal.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 11c299a..981c4f0 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -272,9 +272,9 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // Step 1: Select platform if (!selectedPlatform) { return ( -
+
e.stopPropagation()} > {/* Header */} @@ -364,9 +364,9 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // Step 2: Select app if (!selectedApp) { return ( -
+
e.stopPropagation()} > {/* Header */} @@ -432,9 +432,9 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // Step 3: App instructions return ( -
+
e.stopPropagation()} > {/* Header */} From 5a7fda6e3aa02254a6501a8a998b7cb5577732a2 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Tue, 20 Jan 2026 02:38:02 +0300 Subject: [PATCH 4/8] Refactor ConnectionModal and Referral components: introduce ModalContent and CopyLinkButton helper components for improved structure and reusability. Update referral link handling in Referral component to utilize a dynamic bot username, enhancing link generation and sharing functionality. --- src/components/ConnectionModal.tsx | 629 ++++++++++++++++++----------- src/pages/Referral.tsx | 26 +- 2 files changed, 407 insertions(+), 248 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 981c4f0..dd1ed08 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -93,6 +93,82 @@ const platformIconComponents: Record = { appleTV: TvIcon, } +// Helper components +interface ModalContentProps { + title: string + subtitle?: string + onClose: () => void + onBack?: () => void + icon?: string + children: React.ReactNode +} + +const ModalContent = ({ title, subtitle, onClose, onBack, icon, children }: ModalContentProps) => ( + <> +
+
+ {onBack && ( + + )} + {icon && !onBack && ( +
+ {icon} +
+ )} +
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ +
+
+ {children} +
+ +) + +interface CopyLinkButtonProps { + copied: boolean + onClick: () => void + t: (key: string) => string + compact?: boolean +} + +const CopyLinkButton = ({ copied, onClick, t, compact }: CopyLinkButtonProps) => ( + +) + // Platform order for display const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'] @@ -272,89 +348,120 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // Step 1: Select platform if (!selectedPlatform) { return ( -
+
+ {/* Desktop: centered modal */} +
+
e.stopPropagation()} + > + +
+ {availablePlatforms.map((platform) => { + const IconComponent = platformIconComponents[platform] + const isDetected = platform === detectedPlatform + return ( + + ) + })} +
+
+
+ +
+
+
+ + {/* Mobile: bottom sheet */}
e.stopPropagation()} > - {/* Header */} -
-
-
- 📱 -
-
-

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

-

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

-
+
+ {/* Handle */} +
+
- -
- {/* Content */} -
-
- {availablePlatforms.map((platform) => { - const IconComponent = platformIconComponents[platform] - const isDetected = platform === detectedPlatform - return ( - +
+ + {/* Content */} +
+
+ {availablePlatforms.map((platform) => { + const IconComponent = platformIconComponents[platform] + const isDetected = platform === detectedPlatform + return ( + - ) - })} +

+ {getPlatformName(platform)} +

+ + ) + })} +
-
- {/* Footer */} -
- + {/* Footer */} +
+ +
@@ -363,189 +470,235 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // Step 2: Select app if (!selectedApp) { - return ( -
-
e.stopPropagation()} - > - {/* Header */} -
-
- -
-

{getPlatformName(selectedPlatform)}

-

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

+ const appListContent = platformApps.length === 0 ? ( +
+
+ 📭 +
+

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

+
+ ) : ( +
+ {platformApps.map((app) => ( + -
+
+ +
+ + ))} +
+ ) - {/* Content */} -
- {platformApps.length === 0 ? ( -
-
- 📭 + return ( +
+ {/* Desktop */} +
+
e.stopPropagation()}> + setSelectedPlatform(null)}> + {appListContent} + +
+
+ + {/* Mobile */} +
e.stopPropagation()}> +
+
+
+
+
+
+ +
+

{getPlatformName(selectedPlatform)}

+

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

-

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

- ) : ( -
- {platformApps.map((app) => ( - - ))} -
- )} + +
+
+ {appListContent} +
) } - // Step 3: App instructions + // Step 3: App instructions - shared content + const instructionsContent = ( +
+ {/* Step 1: Install */} + {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: Add subscription */} + {selectedApp.addSubscriptionStep && ( +
+
+
2
+

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

+
+

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

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

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

+
+

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

+
+ )} +
+ ) + return ( -
+
+ {/* Desktop: centered modal */} +
+
e.stopPropagation()} + > + setSelectedApp(null)} + > + {instructionsContent} + +
+ +
+
+
+ + {/* Mobile: bottom sheet */}
e.stopPropagation()} > - {/* Header */} -
-
- -
-

{selectedApp.name}

-

Инструкция по подключению

+
+ {/* Handle */} +
+
+
+ + {/* Header */} +
+
+ +
+

{selectedApp.name}

+

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

+
+
- -
- {/* Content */} -
-
- {/* Step 1: Install */} - {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: Add subscription */} - {selectedApp.addSubscriptionStep && ( -
-
-
2
-

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

-
-

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

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

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

-
-

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

-
- )} + {/* Content */} +
+ {instructionsContent}
-
- {/* Footer */} -
- + {/* Footer */} +
+ +
diff --git a/src/pages/Referral.tsx b/src/pages/Referral.tsx index 4526fd2..af3c681 100644 --- a/src/pages/Referral.tsx +++ b/src/pages/Referral.tsx @@ -68,6 +68,12 @@ export default function Referral() { queryFn: referralApi.getReferralInfo, }) + // Build referral link using frontend env variable for correct bot username + const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME + const referralLink = info?.referral_code && botUsername + ? `https://t.me/${botUsername}?start=${info.referral_code}` + : info?.referral_link || '' + const { data: terms } = useQuery({ queryKey: ['referral-terms'], queryFn: referralApi.getReferralTerms, @@ -90,15 +96,15 @@ export default function Referral() { }) const copyLink = () => { - if (info?.referral_link) { - navigator.clipboard.writeText(info.referral_link) + if (referralLink) { + navigator.clipboard.writeText(referralLink) setCopied(true) setTimeout(() => setCopied(false), 2000) } } const shareLink = () => { - if (!info?.referral_link) return + if (!referralLink) return const shareText = t('referral.shareMessage', { percent: info?.commission_percent || 0, botName: branding?.name || import.meta.env.VITE_APP_NAME || 'Cabinet', @@ -109,7 +115,7 @@ export default function Referral() { .share({ title: t('referral.title'), text: shareText, - url: info.referral_link, + url: referralLink, }) .catch(() => { // ignore cancellation errors @@ -118,7 +124,7 @@ export default function Referral() { } const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent( - info.referral_link + referralLink )}&text=${encodeURIComponent(shareText)}` window.open(telegramUrl, '_blank', 'noopener,noreferrer') } @@ -176,16 +182,16 @@ export default function Referral() {
- )} - {icon && !onBack && ( -
- {icon} -
- )} -
-

{title}

- {subtitle &&

{subtitle}

} -
-
- -
-
- {children} -
- -) - -interface CopyLinkButtonProps { - copied: boolean - onClick: () => void - t: (key: string) => string - compact?: boolean -} - -const CopyLinkButton = ({ copied, onClick, t, compact }: CopyLinkButtonProps) => ( - -) - // Platform order for display const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV', 'appleTV'] @@ -293,414 +217,316 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { window.location.href = redirectUrl } + // Modal wrapper - centered on all devices + const ModalWrapper = ({ children }: { children: React.ReactNode }) => ( +
+
e.stopPropagation()} + > + {children} +
+
+ ) + // Loading state if (isLoading) { return ( -
-
e.stopPropagation()}> -
-
-
+ +
+
-
+
) } // Error state 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')}

+
-
+ ) } // Step 1: Select platform if (!selectedPlatform) { return ( -
- {/* Desktop: centered modal */} -
-
e.stopPropagation()} + + {/* Header */} +
+
+
+ 📱 +
+
+

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

+

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

+
+
+ +
+ + {/* Platforms grid */} +
+
+ {availablePlatforms.map((platform) => { + const IconComponent = platformIconComponents[platform] + const isDetected = platform === detectedPlatform + return ( + + ) + })} +
+
+ + {/* Copy link */} +
+ - ) - })} -
- -
- -
-
+ {copied ? : } + {copied ? t('subscription.connection.copied') : t('subscription.connection.copyLink')} +
- - {/* Mobile: bottom sheet */} -
e.stopPropagation()} - > -
- {/* Handle */} -
-
-
- - {/* Header */} -
-
-
- 📱 -
-
-

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

-

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

-
-
- -
- - {/* Content */} -
-
- {availablePlatforms.map((platform) => { - const IconComponent = platformIconComponents[platform] - const isDetected = platform === detectedPlatform - return ( - - ) - })} -
-
- - {/* Footer */} -
- -
-
-
-
+ ) } // Step 2: Select app if (!selectedApp) { - const appListContent = platformApps.length === 0 ? ( -
-
- 📭 -
-

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

-
- ) : ( -
- {platformApps.map((app) => ( - - ))} -
- ) - return ( -
- {/* Desktop */} -
-
e.stopPropagation()}> - setSelectedPlatform(null)}> - {appListContent} - + + {/* Header */} +
+
+ +
+

{getPlatformName(selectedPlatform)}

+

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

+
+
- {/* Mobile */} -
e.stopPropagation()}> -
-
-
-
-
-
- -
-

{getPlatformName(selectedPlatform)}

-

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

-
+ {/* Apps list */} +
+ {platformApps.length === 0 ? ( +
+
+ 📭
- +

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

-
- {appListContent} -
-
-
-
- ) - } - - // Step 3: App instructions - shared content - const instructionsContent = ( -
- {/* Step 1: Install */} - {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) => ( - + {platformApps.map((app) => ( + ))}
)}
- )} - - {/* Step 2: Add subscription */} - {selectedApp.addSubscriptionStep && ( -
-
-
2
-

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

-
-

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

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

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

-
-

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

-
- )} -
- ) + + ) + } + // Step 3: App instructions return ( -
- {/* Desktop: centered modal */} -
-
e.stopPropagation()} - > - setSelectedApp(null)} - > - {instructionsContent} - -
- + + {/* Header */} +
+
+ +
+

{selectedApp.name}

+

{t('subscription.connection.instructions') || 'Инструкция'}

+
- {/* Mobile: bottom sheet */} -
e.stopPropagation()} - > -
- {/* Handle */} -
-
-
- - {/* Header */} -
-
- -
-

{selectedApp.name}

-

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

+ {/* Instructions */} +
+ {/* Step 1: Install */} + {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)} + + ))} +
+ )}
-
+ )} - {/* Content */} -
- {instructionsContent} + {/* Step 2: Add subscription */} + {selectedApp.addSubscriptionStep && ( +
+
+
2
+
+

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

+

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

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

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

+

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

+
+
-
+ )}
-
+ + {/* Footer */} +
+ +
+ ) } From 075f03e46a7688cba3eb7d38373ffc96c1af2d40 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Tue, 20 Jan 2026 02:45:54 +0300 Subject: [PATCH 6/8] Enhance ConnectionModal component: implement body scroll locking when the modal is open, and adjust modal wrapper styling for better alignment on mobile and desktop devices. --- src/components/ConnectionModal.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 9a6bf13..a42ef56 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -149,6 +149,15 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { setDetectedPlatform(detectPlatform()) }, []) + // Lock body scroll when modal is open + useEffect(() => { + const originalStyle = window.getComputedStyle(document.body).overflow + document.body.style.overflow = 'hidden' + return () => { + document.body.style.overflow = originalStyle + } + }, []) + const getLocalizedText = (text: LocalizedText | undefined): string => { if (!text) return '' const lang = i18n.language || 'en' @@ -217,11 +226,15 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { window.location.href = redirectUrl } - // Modal wrapper - centered on all devices + // Modal wrapper - centered on desktop, top on mobile const ModalWrapper = ({ children }: { children: React.ReactNode }) => (
Date: Tue, 20 Jan 2026 02:49:28 +0300 Subject: [PATCH 7/8] Update ConnectionModal component: remove fallback text for instructions and add missing translation keys for 'instructions' in English and Russian locales. --- src/components/ConnectionModal.tsx | 2 +- src/locales/en.json | 3 ++- src/locales/ru.json | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index a42ef56..fc5a92d 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -438,7 +438,7 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {

{selectedApp.name}

-

{t('subscription.connection.instructions') || 'Инструкция'}

+

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