From 76c9d6448aa2cf79a856076a781c3f285633ee10 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sun, 22 Mar 2026 05:16:56 +0300 Subject: [PATCH] feat: add QR code and command to deeplink auth fallback, fix polling on tab return --- src/components/TelegramLoginButton.tsx | 206 +++++++++++++++++-------- src/locales/en.json | 3 + src/locales/ru.json | 3 + 3 files changed, 144 insertions(+), 68 deletions(-) diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 71bcc99..3601c16 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -2,11 +2,13 @@ import { useEffect, useRef, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; +import { QRCodeSVG } from 'qrcode.react'; import { brandingApi, type TelegramWidgetConfig } from '../api/branding'; import { authApi } from '../api/auth'; import { useAuthStore } from '../store/auth'; import { useNavigate } from 'react-router'; import { consumeCampaignSlug } from '../utils/campaign'; +import { copyToClipboard } from '../utils/clipboard'; interface TelegramLoginButtonProps { referralCode?: string; @@ -30,8 +32,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto const [deepLinkBotUsername, setDeepLinkBotUsername] = useState(''); const [deepLinkPolling, setDeepLinkPolling] = useState(false); const [deepLinkError, setDeepLinkError] = useState(''); + const [copied, setCopied] = useState(false); const pollTimeoutRef = useRef>(null); const expireTimeoutRef = useRef>(null); + const copiedTimeoutRef = useRef>(null); const loginWithDeepLink = useAuthStore((s) => s.loginWithDeepLink); @@ -61,15 +65,12 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto }; }, []); - // Cleanup polling and expire timeout on unmount + // Cleanup all timeouts on unmount useEffect(() => { return () => { - if (pollTimeoutRef.current) { - clearTimeout(pollTimeoutRef.current); - } - if (expireTimeoutRef.current) { - clearTimeout(expireTimeoutRef.current); - } + if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current); + if (expireTimeoutRef.current) clearTimeout(expireTimeoutRef.current); + if (copiedTimeoutRef.current) clearTimeout(copiedTimeoutRef.current); }; }, []); @@ -335,6 +336,64 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto } }, [scriptFailed, deepLinkToken, deepLinkPolling, startDeepLinkAuth]); + // Resume polling immediately when user returns to the page (e.g. after confirming in Telegram) + // Browsers throttle setTimeout in background tabs, so polling may have stalled. + useEffect(() => { + if (!deepLinkPolling || !deepLinkToken) return; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible' && mountedRef.current) { + // Clear any pending throttled timer and trigger an immediate poll + if (pollTimeoutRef.current) { + clearTimeout(pollTimeoutRef.current); + pollTimeoutRef.current = null; + } + const capturedCampaign = capturedCampaignRef.current; + const immediatePoll = async () => { + if (!mountedRef.current) return; + try { + await loginWithDeepLink(deepLinkToken, capturedCampaign); + if (expireTimeoutRef.current) { + clearTimeout(expireTimeoutRef.current); + expireTimeoutRef.current = null; + } + if (mountedRef.current) { + setDeepLinkPolling(false); + navigate('/'); + } + } catch (err: unknown) { + if (!mountedRef.current) return; + if (isAxiosError(err)) { + if (err.response?.status === 202) { + pollTimeoutRef.current = setTimeout(immediatePoll, DEEPLINK_POLL_INTERVAL_MS); + return; + } + if (err.response?.status === 410) { + setDeepLinkPolling(false); + setDeepLinkToken(null); + setDeepLinkError(t('auth.deepLinkExpired')); + return; + } + } + setDeepLinkPolling(false); + setDeepLinkError(t('common.error')); + } + }; + immediatePoll(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + // Sever any in-flight recursive immediatePoll chain from this effect cycle + if (pollTimeoutRef.current) { + clearTimeout(pollTimeoutRef.current); + pollTimeoutRef.current = null; + } + }; + }, [deepLinkPolling, deepLinkToken, loginWithDeepLink, navigate, t]); + if (!botUsername || botUsername === 'your_bot') { return (
@@ -345,79 +404,90 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto // Deep link fallback UI if (scriptFailed) { + const resolvedBotUsername = deepLinkBotUsername || botUsername; const deepLinkUrl = deepLinkToken - ? `https://t.me/${deepLinkBotUsername || botUsername}?start=webauth_${deepLinkToken}` + ? `https://t.me/${resolvedBotUsername}?start=webauth_${deepLinkToken}` : ''; + const startCommand = deepLinkToken ? `/start webauth_${deepLinkToken}` : ''; return ( -
-
- {/* Info message */} -

- {t('auth.telegramWidgetBlocked')} -

+
+ {/* Info message */} +

+ {t('auth.telegramWidgetBlocked')} +

- {deepLinkToken && deepLinkUrl ? ( - <> - {/* Deep link button */} - - - - - {t('auth.openBotToLogin')} - - - {/* Polling status */} - {deepLinkPolling && ( -
- - {t('auth.waitingForConfirmation')} -
- )} - - ) : deepLinkError ? ( + {deepLinkToken && deepLinkUrl ? ( + <> + {/* QR Code */}
-

{deepLinkError}

+
+ +
+

{t('auth.scanQrToLogin')}

+
+ + {/* Open bot button */} + + + + + {t('auth.openBotToLogin')} + + + {/* Manual command */} +
+

{t('auth.orSendCommand')}

- ) : ( -
- - {t('common.loading')} -
- )} -
- {/* Bot link fallback */} -
-

{t('auth.orOpenInApp')}

- - - - - @{botUsername} - -
+ {/* Polling status */} + {deepLinkPolling && ( +
+ + {t('auth.waitingForConfirmation')} +
+ )} + + ) : deepLinkError ? ( +
+

{deepLinkError}

+ +
+ ) : ( +
+ + {t('common.loading')} +
+ )}
); } diff --git a/src/locales/en.json b/src/locales/en.json index 42942a2..1728e45 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -180,6 +180,9 @@ "waitingForConfirmation": "Waiting for confirmation...", "deepLinkExpired": "Link expired. Please try again.", "tryAgain": "Try Again", + "scanQrToLogin": "Scan QR code with your phone camera", + "orSendCommand": "Or send the bot this command:", + "commandCopied": "Command copied", "welcomeBack": "Welcome back!", "loginTitle": "Login to Cabinet", "loginSubtitle": "Sign in with Telegram or use your email", diff --git a/src/locales/ru.json b/src/locales/ru.json index 99c85b6..c361682 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -183,6 +183,9 @@ "waitingForConfirmation": "Ожидание подтверждения...", "deepLinkExpired": "Ссылка истекла. Попробуйте снова.", "tryAgain": "Попробовать снова", + "scanQrToLogin": "Отсканируйте QR-код камерой телефона", + "orSendCommand": "Или отправьте боту команду:", + "commandCopied": "Команда скопирована", "welcomeBack": "Добро пожаловать!", "loginTitle": "Вход в личный кабинет", "loginSubtitle": "Войдите через Telegram или используйте email",