mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix: recursive setTimeout, Strict Mode guard, isAxiosError
- Replace setInterval with recursive setTimeout to prevent overlapping async polls - Add cancellation flag in auto-start effect for React Strict Mode double-mount - Use isAxiosError() instead of unsafe type assertion - Remove consumeCampaignSlug/consumeReferralCode from loginWithDeepLink (fired on every poll tick, destroying values after first 202)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { isAxiosError } from 'axios';
|
||||||
import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
|
import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
|
||||||
import { authApi } from '../api/auth';
|
import { authApi } from '../api/auth';
|
||||||
import { useAuthStore } from '../store/auth';
|
import { useAuthStore } from '../store/auth';
|
||||||
@@ -28,7 +29,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
const [deepLinkBotUsername, setDeepLinkBotUsername] = useState<string>('');
|
const [deepLinkBotUsername, setDeepLinkBotUsername] = useState<string>('');
|
||||||
const [deepLinkPolling, setDeepLinkPolling] = useState(false);
|
const [deepLinkPolling, setDeepLinkPolling] = useState(false);
|
||||||
const [deepLinkError, setDeepLinkError] = useState('');
|
const [deepLinkError, setDeepLinkError] = useState('');
|
||||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval>>(null);
|
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||||
const expireTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
const expireTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||||
|
|
||||||
const loginWithDeepLink = useAuthStore((s) => s.loginWithDeepLink);
|
const loginWithDeepLink = useAuthStore((s) => s.loginWithDeepLink);
|
||||||
@@ -58,8 +59,8 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
// Cleanup polling and expire timeout on unmount
|
// Cleanup polling and expire timeout on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (pollIntervalRef.current) {
|
if (pollTimeoutRef.current) {
|
||||||
clearInterval(pollIntervalRef.current);
|
clearTimeout(pollTimeoutRef.current);
|
||||||
}
|
}
|
||||||
if (expireTimeoutRef.current) {
|
if (expireTimeoutRef.current) {
|
||||||
clearTimeout(expireTimeoutRef.current);
|
clearTimeout(expireTimeoutRef.current);
|
||||||
@@ -82,9 +83,8 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
let message = t('common.error');
|
let message = t('common.error');
|
||||||
if (err && typeof err === 'object' && 'response' in err) {
|
if (isAxiosError(err) && err.response?.data?.detail) {
|
||||||
const resp = (err as { response?: { data?: { detail?: string } } }).response;
|
message = err.response.data.detail;
|
||||||
if (resp?.data?.detail) message = resp.data.detail;
|
|
||||||
}
|
}
|
||||||
setOidcError(message);
|
setOidcError(message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -224,14 +224,14 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
};
|
};
|
||||||
}, [isOIDC, botUsername, widgetConfig, loginWithTelegramWidget, navigate, handleScriptFailed]);
|
}, [isOIDC, botUsername, widgetConfig, loginWithTelegramWidget, navigate, handleScriptFailed]);
|
||||||
|
|
||||||
// Deep link auth: request token and start polling
|
// Deep link auth: request token and start polling with recursive setTimeout
|
||||||
const startDeepLinkAuth = useCallback(async () => {
|
const startDeepLinkAuth = useCallback(async () => {
|
||||||
setDeepLinkError('');
|
setDeepLinkError('');
|
||||||
|
|
||||||
// Clear any previous timers
|
// Clear any previous timers
|
||||||
if (pollIntervalRef.current) {
|
if (pollTimeoutRef.current) {
|
||||||
clearInterval(pollIntervalRef.current);
|
clearTimeout(pollTimeoutRef.current);
|
||||||
pollIntervalRef.current = null;
|
pollTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
if (expireTimeoutRef.current) {
|
if (expireTimeoutRef.current) {
|
||||||
clearTimeout(expireTimeoutRef.current);
|
clearTimeout(expireTimeoutRef.current);
|
||||||
@@ -245,16 +245,12 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
setDeepLinkBotUsername(bot_username || botUsername);
|
setDeepLinkBotUsername(bot_username || botUsername);
|
||||||
setDeepLinkPolling(true);
|
setDeepLinkPolling(true);
|
||||||
|
|
||||||
// Start polling
|
// Recursive setTimeout prevents overlapping async calls
|
||||||
const intervalId = setInterval(async () => {
|
const poll = async () => {
|
||||||
if (!mountedRef.current) {
|
if (!mountedRef.current) return;
|
||||||
clearInterval(intervalId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await loginWithDeepLink(token);
|
await loginWithDeepLink(token);
|
||||||
// Success - auth store is updated, navigate
|
// Success — auth store is updated, navigate
|
||||||
clearInterval(intervalId);
|
|
||||||
if (expireTimeoutRef.current) {
|
if (expireTimeoutRef.current) {
|
||||||
clearTimeout(expireTimeoutRef.current);
|
clearTimeout(expireTimeoutRef.current);
|
||||||
expireTimeoutRef.current = null;
|
expireTimeoutRef.current = null;
|
||||||
@@ -264,38 +260,36 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error = err as { response?: { status?: number } };
|
if (!mountedRef.current) return;
|
||||||
if (error.response?.status === 202) {
|
if (isAxiosError(err)) {
|
||||||
// Still pending, continue polling
|
if (err.response?.status === 202) {
|
||||||
|
// Still pending — schedule next poll
|
||||||
|
pollTimeoutRef.current = setTimeout(poll, DEEPLINK_POLL_INTERVAL_MS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (error.response?.status === 410) {
|
if (err.response?.status === 410) {
|
||||||
// Token expired
|
// Token expired
|
||||||
clearInterval(intervalId);
|
|
||||||
if (mountedRef.current) {
|
|
||||||
setDeepLinkPolling(false);
|
setDeepLinkPolling(false);
|
||||||
setDeepLinkToken(null);
|
setDeepLinkToken(null);
|
||||||
setDeepLinkError(t('auth.deepLinkExpired'));
|
setDeepLinkError(t('auth.deepLinkExpired'));
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Other error - stop polling
|
}
|
||||||
clearInterval(intervalId);
|
// Other error — stop polling
|
||||||
if (mountedRef.current) {
|
|
||||||
setDeepLinkPolling(false);
|
setDeepLinkPolling(false);
|
||||||
setDeepLinkError(t('common.error'));
|
setDeepLinkError(t('common.error'));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, DEEPLINK_POLL_INTERVAL_MS);
|
|
||||||
|
|
||||||
pollIntervalRef.current = intervalId;
|
// Start first poll
|
||||||
|
pollTimeoutRef.current = setTimeout(poll, DEEPLINK_POLL_INTERVAL_MS);
|
||||||
|
|
||||||
// Auto-expire after server-provided TTL
|
// Auto-expire after server-provided TTL
|
||||||
const expireId = setTimeout(
|
expireTimeoutRef.current = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
if (pollIntervalRef.current) {
|
if (pollTimeoutRef.current) {
|
||||||
clearInterval(pollIntervalRef.current);
|
clearTimeout(pollTimeoutRef.current);
|
||||||
pollIntervalRef.current = null;
|
pollTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
if (mountedRef.current && !useAuthStore.getState().isAuthenticated) {
|
if (mountedRef.current && !useAuthStore.getState().isAuthenticated) {
|
||||||
setDeepLinkPolling(false);
|
setDeepLinkPolling(false);
|
||||||
@@ -305,17 +299,22 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
},
|
},
|
||||||
(expires_in || 300) * 1000,
|
(expires_in || 300) * 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
expireTimeoutRef.current = expireId;
|
|
||||||
} catch {
|
} catch {
|
||||||
setDeepLinkError(t('common.error'));
|
setDeepLinkError(t('common.error'));
|
||||||
}
|
}
|
||||||
}, [botUsername, loginWithDeepLink, navigate, t]);
|
}, [botUsername, loginWithDeepLink, navigate, t]);
|
||||||
|
|
||||||
// Auto-start deep link auth when script fails
|
// Auto-start deep link auth when script fails (with cancellation for Strict Mode)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scriptFailed && !deepLinkToken && !deepLinkPolling) {
|
if (scriptFailed && !deepLinkToken && !deepLinkPolling) {
|
||||||
startDeepLinkAuth();
|
let cancelled = false;
|
||||||
|
const start = async () => {
|
||||||
|
if (!cancelled) await startDeepLinkAuth();
|
||||||
|
};
|
||||||
|
start();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [scriptFailed, deepLinkToken, deepLinkPolling, startDeepLinkAuth]);
|
}, [scriptFailed, deepLinkToken, deepLinkPolling, startDeepLinkAuth]);
|
||||||
|
|
||||||
|
|||||||
@@ -320,8 +320,6 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
loginWithDeepLink: async (token) => {
|
loginWithDeepLink: async (token) => {
|
||||||
consumeCampaignSlug();
|
|
||||||
consumeReferralCode();
|
|
||||||
const response = await authApi.pollDeepLinkToken(token);
|
const response = await authApi.pollDeepLinkToken(token);
|
||||||
tokenStorage.setTokens(response.access_token, response.refresh_token);
|
tokenStorage.setTokens(response.access_token, response.refresh_token);
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in New Issue
Block a user