From da1926f0e1ab7f117aef120ac7648bdd50add72c Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 5 Mar 2026 02:33:33 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20review=20findings=20=E2=80=94=20polling?= =?UTF-8?q?=20fallback,=20sessionStorage=20cleanup,=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 5s polling for 90s after opening external browser link (Mini App) - Show info toast "Continue in browser" when opening external OAuth - Fix premature sessionStorage cleanup: peek-then-clear pattern - Use useIsTelegram() hook instead of direct isInTelegramWebApp() - Add continueInBrowser i18n key (ru, en, zh, fa) --- src/locales/en.json | 1 + src/locales/fa.json | 1 + src/locales/ru.json | 1 + src/locales/zh.json | 1 + src/pages/ConnectedAccounts.tsx | 23 +++++++++++++++++++---- src/pages/OAuthCallback.tsx | 17 +++++++++-------- 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index cbfdb5a..698c36e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -3618,6 +3618,7 @@ "linkingTelegram": "Connecting Telegram...", "returnToTelegram": "Return to Telegram to continue", "openTelegram": "Open Telegram", + "continueInBrowser": "Continue authorization in the opened browser", "providers": { "telegram": "Telegram", "email": "Email", diff --git a/src/locales/fa.json b/src/locales/fa.json index 66be39b..29efa7e 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -3054,6 +3054,7 @@ "linkingTelegram": "در حال اتصال تلگرام...", "returnToTelegram": "برای ادامه به تلگرام بازگردید", "openTelegram": "باز کردن تلگرام", + "continueInBrowser": "ادامه احراز هویت در مرورگر باز شده", "providers": { "telegram": "تلگرام", "email": "ایمیل", diff --git a/src/locales/ru.json b/src/locales/ru.json index 5a2f195..80fe7b8 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -4178,6 +4178,7 @@ "linkingTelegram": "Привязка Telegram...", "returnToTelegram": "Вернитесь в Telegram, чтобы продолжить", "openTelegram": "Открыть Telegram", + "continueInBrowser": "Продолжите авторизацию в открывшемся браузере", "providers": { "telegram": "Telegram", "email": "Email", diff --git a/src/locales/zh.json b/src/locales/zh.json index 210dc8c..c6a9058 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -3053,6 +3053,7 @@ "linkingTelegram": "正在关联 Telegram...", "returnToTelegram": "返回 Telegram 继续操作", "openTelegram": "打开 Telegram", + "continueInBrowser": "请在打开的浏览器中继续授权", "providers": { "telegram": "Telegram", "email": "邮箱", diff --git a/src/pages/ConnectedAccounts.tsx b/src/pages/ConnectedAccounts.tsx index d85abb6..75e26cb 100644 --- a/src/pages/ConnectedAccounts.tsx +++ b/src/pages/ConnectedAccounts.tsx @@ -10,8 +10,8 @@ import { Button } from '@/components/primitives/Button'; import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import ProviderIcon from '../components/ProviderIcon'; import { LINK_OAUTH_STATE_KEY, LINK_OAUTH_PROVIDER_KEY } from './LinkOAuthCallback'; -import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK'; -import { usePlatform } from '@/platform/hooks/usePlatform'; +import { getTelegramInitData } from '../hooks/useTelegramSDK'; +import { usePlatform, useIsTelegram } from '@/platform/hooks/usePlatform'; import type { LinkedProvider } from '../types'; const OAUTH_PROVIDERS = ['google', 'yandex', 'discord', 'vk']; @@ -97,9 +97,10 @@ export default function ConnectedAccounts() { const [confirmingUnlink, setConfirmingUnlink] = useState(null); const [linkingProvider, setLinkingProvider] = useState(null); + const [waitingExternalLink, setWaitingExternalLink] = useState(false); const blurTimeoutRef = useRef>(undefined); - const inTelegram = isInTelegramWebApp(); + const inTelegram = useIsTelegram(); const platform = usePlatform(); useEffect(() => { @@ -112,8 +113,17 @@ export default function ConnectedAccounts() { queryKey: ['linked-providers'], queryFn: () => authApi.getLinkedProviders(), refetchOnWindowFocus: true, + // Poll every 5s while waiting for external browser OAuth to complete + refetchInterval: waitingExternalLink ? 5000 : false, }); + // Stop polling after 90 seconds + useEffect(() => { + if (!waitingExternalLink) return; + const timeout = setTimeout(() => setWaitingExternalLink(false), 90_000); + return () => clearTimeout(timeout); + }, [waitingExternalLink]); + const unlinkMutation = useMutation({ mutationFn: (provider: string) => authApi.unlinkProvider(provider), onSuccess: () => { @@ -151,8 +161,13 @@ export default function ConnectedAccounts() { // Mini App: open in external browser to avoid WebView OAuth restrictions. // The callback will use server-complete flow (auth via state token, no JWT). platform.openLink(authorize_url); - // Reset loading state — user stays in Mini App setLinkingProvider(null); + // Start polling for linked providers (external browser has no way to notify Mini App) + setWaitingExternalLink(true); + showToast({ + type: 'info', + message: t('profile.accounts.continueInBrowser'), + }); } else { // Regular browser: navigate within the same tab. // Save state in sessionStorage for the callback page to verify. diff --git a/src/pages/OAuthCallback.tsx b/src/pages/OAuthCallback.tsx index d1c2b2c..01d07b9 100644 --- a/src/pages/OAuthCallback.tsx +++ b/src/pages/OAuthCallback.tsx @@ -23,19 +23,19 @@ export function getAndClearOAuthState(): { state: string; provider: string } | n return { state, provider }; } -/** - * Check if this is an account LINKING callback (user was already authenticated). - * Returns the saved link state or null. - */ -function getAndClearLinkOAuthState(): { state: string; provider: string } | null { +/** Read link OAuth state without clearing (cleared only after successful match). */ +function peekLinkOAuthState(): { state: string; provider: string } | null { const state = sessionStorage.getItem(LINK_OAUTH_STATE_KEY); const provider = sessionStorage.getItem(LINK_OAUTH_PROVIDER_KEY); - sessionStorage.removeItem(LINK_OAUTH_STATE_KEY); - sessionStorage.removeItem(LINK_OAUTH_PROVIDER_KEY); if (!state || !provider) return null; return { state, provider }; } +function clearLinkOAuthState(): void { + sessionStorage.removeItem(LINK_OAUTH_STATE_KEY); + sessionStorage.removeItem(LINK_OAUTH_PROVIDER_KEY); +} + type CallbackMode = 'login' | 'link-browser' | 'link-server'; /** Result after successful server-complete linking from external browser. */ @@ -78,8 +78,9 @@ export default function OAuthCallback() { let savedProvider: string | null = null; let savedState: string | null = null; - const linkSaved = getAndClearLinkOAuthState(); + const linkSaved = peekLinkOAuthState(); if (linkSaved && linkSaved.state === urlState) { + clearLinkOAuthState(); mode = 'link-browser'; savedProvider = linkSaved.provider; savedState = linkSaved.state;