mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
fix: review findings — polling fallback, sessionStorage cleanup, UX
- 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)
This commit is contained in:
@@ -3618,6 +3618,7 @@
|
|||||||
"linkingTelegram": "Connecting Telegram...",
|
"linkingTelegram": "Connecting Telegram...",
|
||||||
"returnToTelegram": "Return to Telegram to continue",
|
"returnToTelegram": "Return to Telegram to continue",
|
||||||
"openTelegram": "Open Telegram",
|
"openTelegram": "Open Telegram",
|
||||||
|
"continueInBrowser": "Continue authorization in the opened browser",
|
||||||
"providers": {
|
"providers": {
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -3054,6 +3054,7 @@
|
|||||||
"linkingTelegram": "در حال اتصال تلگرام...",
|
"linkingTelegram": "در حال اتصال تلگرام...",
|
||||||
"returnToTelegram": "برای ادامه به تلگرام بازگردید",
|
"returnToTelegram": "برای ادامه به تلگرام بازگردید",
|
||||||
"openTelegram": "باز کردن تلگرام",
|
"openTelegram": "باز کردن تلگرام",
|
||||||
|
"continueInBrowser": "ادامه احراز هویت در مرورگر باز شده",
|
||||||
"providers": {
|
"providers": {
|
||||||
"telegram": "تلگرام",
|
"telegram": "تلگرام",
|
||||||
"email": "ایمیل",
|
"email": "ایمیل",
|
||||||
|
|||||||
@@ -4178,6 +4178,7 @@
|
|||||||
"linkingTelegram": "Привязка Telegram...",
|
"linkingTelegram": "Привязка Telegram...",
|
||||||
"returnToTelegram": "Вернитесь в Telegram, чтобы продолжить",
|
"returnToTelegram": "Вернитесь в Telegram, чтобы продолжить",
|
||||||
"openTelegram": "Открыть Telegram",
|
"openTelegram": "Открыть Telegram",
|
||||||
|
"continueInBrowser": "Продолжите авторизацию в открывшемся браузере",
|
||||||
"providers": {
|
"providers": {
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
|||||||
@@ -3053,6 +3053,7 @@
|
|||||||
"linkingTelegram": "正在关联 Telegram...",
|
"linkingTelegram": "正在关联 Telegram...",
|
||||||
"returnToTelegram": "返回 Telegram 继续操作",
|
"returnToTelegram": "返回 Telegram 继续操作",
|
||||||
"openTelegram": "打开 Telegram",
|
"openTelegram": "打开 Telegram",
|
||||||
|
"continueInBrowser": "请在打开的浏览器中继续授权",
|
||||||
"providers": {
|
"providers": {
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { Button } from '@/components/primitives/Button';
|
|||||||
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
|
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
|
||||||
import ProviderIcon from '../components/ProviderIcon';
|
import ProviderIcon from '../components/ProviderIcon';
|
||||||
import { LINK_OAUTH_STATE_KEY, LINK_OAUTH_PROVIDER_KEY } from './LinkOAuthCallback';
|
import { LINK_OAUTH_STATE_KEY, LINK_OAUTH_PROVIDER_KEY } from './LinkOAuthCallback';
|
||||||
import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK';
|
import { getTelegramInitData } from '../hooks/useTelegramSDK';
|
||||||
import { usePlatform } from '@/platform/hooks/usePlatform';
|
import { usePlatform, useIsTelegram } from '@/platform/hooks/usePlatform';
|
||||||
import type { LinkedProvider } from '../types';
|
import type { LinkedProvider } from '../types';
|
||||||
|
|
||||||
const OAUTH_PROVIDERS = ['google', 'yandex', 'discord', 'vk'];
|
const OAUTH_PROVIDERS = ['google', 'yandex', 'discord', 'vk'];
|
||||||
@@ -97,9 +97,10 @@ export default function ConnectedAccounts() {
|
|||||||
|
|
||||||
const [confirmingUnlink, setConfirmingUnlink] = useState<string | null>(null);
|
const [confirmingUnlink, setConfirmingUnlink] = useState<string | null>(null);
|
||||||
const [linkingProvider, setLinkingProvider] = useState<string | null>(null);
|
const [linkingProvider, setLinkingProvider] = useState<string | null>(null);
|
||||||
|
const [waitingExternalLink, setWaitingExternalLink] = useState(false);
|
||||||
const blurTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const blurTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = useIsTelegram();
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -112,8 +113,17 @@ export default function ConnectedAccounts() {
|
|||||||
queryKey: ['linked-providers'],
|
queryKey: ['linked-providers'],
|
||||||
queryFn: () => authApi.getLinkedProviders(),
|
queryFn: () => authApi.getLinkedProviders(),
|
||||||
refetchOnWindowFocus: true,
|
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({
|
const unlinkMutation = useMutation({
|
||||||
mutationFn: (provider: string) => authApi.unlinkProvider(provider),
|
mutationFn: (provider: string) => authApi.unlinkProvider(provider),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -151,8 +161,13 @@ export default function ConnectedAccounts() {
|
|||||||
// Mini App: open in external browser to avoid WebView OAuth restrictions.
|
// Mini App: open in external browser to avoid WebView OAuth restrictions.
|
||||||
// The callback will use server-complete flow (auth via state token, no JWT).
|
// The callback will use server-complete flow (auth via state token, no JWT).
|
||||||
platform.openLink(authorize_url);
|
platform.openLink(authorize_url);
|
||||||
// Reset loading state — user stays in Mini App
|
|
||||||
setLinkingProvider(null);
|
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 {
|
} else {
|
||||||
// Regular browser: navigate within the same tab.
|
// Regular browser: navigate within the same tab.
|
||||||
// Save state in sessionStorage for the callback page to verify.
|
// Save state in sessionStorage for the callback page to verify.
|
||||||
|
|||||||
@@ -23,19 +23,19 @@ export function getAndClearOAuthState(): { state: string; provider: string } | n
|
|||||||
return { state, provider };
|
return { state, provider };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Read link OAuth state without clearing (cleared only after successful match). */
|
||||||
* Check if this is an account LINKING callback (user was already authenticated).
|
function peekLinkOAuthState(): { state: string; provider: string } | null {
|
||||||
* Returns the saved link state or null.
|
|
||||||
*/
|
|
||||||
function getAndClearLinkOAuthState(): { state: string; provider: string } | null {
|
|
||||||
const state = sessionStorage.getItem(LINK_OAUTH_STATE_KEY);
|
const state = sessionStorage.getItem(LINK_OAUTH_STATE_KEY);
|
||||||
const provider = sessionStorage.getItem(LINK_OAUTH_PROVIDER_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;
|
if (!state || !provider) return null;
|
||||||
return { state, provider };
|
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';
|
type CallbackMode = 'login' | 'link-browser' | 'link-server';
|
||||||
|
|
||||||
/** Result after successful server-complete linking from external browser. */
|
/** Result after successful server-complete linking from external browser. */
|
||||||
@@ -78,8 +78,9 @@ export default function OAuthCallback() {
|
|||||||
let savedProvider: string | null = null;
|
let savedProvider: string | null = null;
|
||||||
let savedState: string | null = null;
|
let savedState: string | null = null;
|
||||||
|
|
||||||
const linkSaved = getAndClearLinkOAuthState();
|
const linkSaved = peekLinkOAuthState();
|
||||||
if (linkSaved && linkSaved.state === urlState) {
|
if (linkSaved && linkSaved.state === urlState) {
|
||||||
|
clearLinkOAuthState();
|
||||||
mode = 'link-browser';
|
mode = 'link-browser';
|
||||||
savedProvider = linkSaved.provider;
|
savedProvider = linkSaved.provider;
|
||||||
savedState = linkSaved.state;
|
savedState = linkSaved.state;
|
||||||
|
|||||||
Reference in New Issue
Block a user