mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: deep link авторизация при блокировке oauth.telegram.org
Добавлен fallback через бота когда виджет Telegram не загружается: - Таймаут 8 сек на загрузку скрипта (OIDC + legacy) - Автоматический переход на deep link auth - Polling каждые 2.5 сек до подтверждения в боте - ConnectedAccounts: таймаут + сообщение при недоступности - Переводы: ru, en, zh, fa
This commit is contained in:
@@ -277,6 +277,24 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
requestDeepLinkToken: async (): Promise<{
|
||||
token: string;
|
||||
bot_username: string;
|
||||
expires_in: number;
|
||||
}> => {
|
||||
const response = await apiClient.post<{
|
||||
token: string;
|
||||
bot_username: string;
|
||||
expires_in: number;
|
||||
}>('/cabinet/auth/deeplink/request');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
pollDeepLinkToken: async (token: string): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/deeplink/poll', { token });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getMergePreview: async (mergeToken: string): Promise<MergePreviewResponse> => {
|
||||
const response = await apiClient.get<MergePreviewResponse>(
|
||||
`/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
|
||||
import { authApi } from '../api/auth';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
@@ -9,6 +10,9 @@ interface TelegramLoginButtonProps {
|
||||
referralCode?: string;
|
||||
}
|
||||
|
||||
const SCRIPT_LOAD_TIMEOUT_MS = 8000;
|
||||
const DEEPLINK_POLL_INTERVAL_MS = 2500;
|
||||
|
||||
export default function TelegramLoginButton({ referralCode }: TelegramLoginButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -16,8 +20,18 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
const [oidcLoading, setOidcLoading] = useState(false);
|
||||
const [oidcError, setOidcError] = useState('');
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||
const [scriptFailed, setScriptFailed] = useState(false);
|
||||
const loginWithTelegramOIDC = useAuthStore((s) => s.loginWithTelegramOIDC);
|
||||
|
||||
// Deep link auth state
|
||||
const [deepLinkToken, setDeepLinkToken] = useState<string | null>(null);
|
||||
const [deepLinkBotUsername, setDeepLinkBotUsername] = useState<string>('');
|
||||
const [deepLinkPolling, setDeepLinkPolling] = useState(false);
|
||||
const [deepLinkError, setDeepLinkError] = useState('');
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval>>(null);
|
||||
|
||||
const loginWithDeepLink = useAuthStore((s) => s.loginWithDeepLink);
|
||||
|
||||
const { data: widgetConfig } = useQuery<TelegramWidgetConfig>({
|
||||
queryKey: ['telegram-widget-config'],
|
||||
queryFn: brandingApi.getTelegramWidgetConfig,
|
||||
@@ -28,7 +42,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
widgetConfig?.bot_username || import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '';
|
||||
const isOIDC = Boolean(widgetConfig?.oidc_enabled && widgetConfig?.oidc_client_id);
|
||||
|
||||
// OIDC callback handler (ref pattern to avoid stale closures and unnecessary re-inits)
|
||||
// OIDC callback handler
|
||||
const handleOIDCCallbackRef =
|
||||
useRef<(data: { id_token?: string; error?: string }) => void>(undefined);
|
||||
const mountedRef = useRef(true);
|
||||
@@ -40,6 +54,15 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Cleanup polling on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
handleOIDCCallbackRef.current = async (data: { id_token?: string; error?: string }) => {
|
||||
if (!mountedRef.current) return;
|
||||
if (data.error || !data.id_token) {
|
||||
@@ -65,7 +88,14 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
}
|
||||
};
|
||||
|
||||
// Load OIDC script and init
|
||||
// Handle script load failure (timeout or error)
|
||||
const handleScriptFailed = useCallback(() => {
|
||||
if (!mountedRef.current || scriptLoaded) return;
|
||||
setScriptFailed(true);
|
||||
setOidcError('');
|
||||
}, [scriptLoaded]);
|
||||
|
||||
// Load OIDC script with timeout
|
||||
useEffect(() => {
|
||||
if (!isOIDC || !widgetConfig?.oidc_client_id) return;
|
||||
|
||||
@@ -85,27 +115,45 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
}
|
||||
};
|
||||
|
||||
// Set up timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
handleScriptFailed();
|
||||
}
|
||||
}, SCRIPT_LOAD_TIMEOUT_MS);
|
||||
|
||||
if (!script) {
|
||||
script = document.createElement('script');
|
||||
script.id = scriptId;
|
||||
script.src = 'https://oauth.telegram.org/js/telegram-login.js?3';
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
setScriptLoaded(true);
|
||||
initTelegramLogin();
|
||||
};
|
||||
script.onerror = () => {
|
||||
setOidcError(t('auth.loginFailed'));
|
||||
clearTimeout(timeoutId);
|
||||
handleScriptFailed();
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
// Script already loaded, just re-init
|
||||
clearTimeout(timeoutId);
|
||||
setScriptLoaded(true);
|
||||
initTelegramLogin();
|
||||
}
|
||||
}, [isOIDC, widgetConfig?.oidc_client_id, widgetConfig?.request_access, t]);
|
||||
|
||||
// Legacy widget effect (only when NOT OIDC)
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [
|
||||
isOIDC,
|
||||
widgetConfig?.oidc_client_id,
|
||||
widgetConfig?.request_access,
|
||||
t,
|
||||
scriptLoaded,
|
||||
handleScriptFailed,
|
||||
]);
|
||||
|
||||
// Legacy widget effect with timeout
|
||||
const loginWithTelegramWidget = useAuthStore((s) => s.loginWithTelegramWidget);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -148,15 +196,102 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
}
|
||||
script.async = true;
|
||||
|
||||
// Timeout for legacy widget
|
||||
const timeoutId = setTimeout(() => {
|
||||
// If container still has no iframe child (widget didn't render), mark as failed
|
||||
if (container && !container.querySelector('iframe')) {
|
||||
handleScriptFailed();
|
||||
}
|
||||
}, SCRIPT_LOAD_TIMEOUT_MS);
|
||||
|
||||
script.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
handleScriptFailed();
|
||||
};
|
||||
|
||||
container.appendChild(script);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
delete (window as unknown as Record<string, unknown>)[callbackName];
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
};
|
||||
}, [isOIDC, botUsername, widgetConfig, loginWithTelegramWidget, navigate]);
|
||||
}, [isOIDC, botUsername, widgetConfig, loginWithTelegramWidget, navigate, handleScriptFailed]);
|
||||
|
||||
// Deep link auth: request token and start polling
|
||||
const startDeepLinkAuth = useCallback(async () => {
|
||||
setDeepLinkError('');
|
||||
try {
|
||||
const { token, bot_username } = await authApi.requestDeepLinkToken();
|
||||
setDeepLinkToken(token);
|
||||
setDeepLinkBotUsername(bot_username || botUsername);
|
||||
setDeepLinkPolling(true);
|
||||
|
||||
// Start polling
|
||||
const intervalId = setInterval(async () => {
|
||||
if (!mountedRef.current) {
|
||||
clearInterval(intervalId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await loginWithDeepLink(token);
|
||||
// Success - auth store is updated, navigate
|
||||
clearInterval(intervalId);
|
||||
if (mountedRef.current) {
|
||||
setDeepLinkPolling(false);
|
||||
navigate('/');
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { status?: number } };
|
||||
if (error.response?.status === 202) {
|
||||
// Still pending, continue polling
|
||||
return;
|
||||
}
|
||||
if (error.response?.status === 410) {
|
||||
// Token expired
|
||||
clearInterval(intervalId);
|
||||
if (mountedRef.current) {
|
||||
setDeepLinkPolling(false);
|
||||
setDeepLinkToken(null);
|
||||
setDeepLinkError(t('auth.deepLinkExpired'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Other error - stop polling
|
||||
clearInterval(intervalId);
|
||||
if (mountedRef.current) {
|
||||
setDeepLinkPolling(false);
|
||||
setDeepLinkError(t('common.error'));
|
||||
}
|
||||
}
|
||||
}, DEEPLINK_POLL_INTERVAL_MS);
|
||||
|
||||
pollIntervalRef.current = intervalId;
|
||||
|
||||
// Auto-expire after token TTL
|
||||
setTimeout(
|
||||
() => {
|
||||
clearInterval(intervalId);
|
||||
if (mountedRef.current && !useAuthStore.getState().isAuthenticated) {
|
||||
setDeepLinkPolling(false);
|
||||
setDeepLinkToken(null);
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
); // 5 minutes
|
||||
} catch {
|
||||
setDeepLinkError(t('common.error'));
|
||||
}
|
||||
}, [botUsername, loginWithDeepLink, navigate, t]);
|
||||
|
||||
// Auto-start deep link auth when script fails
|
||||
useEffect(() => {
|
||||
if (scriptFailed && !deepLinkToken && !deepLinkPolling) {
|
||||
startDeepLinkAuth();
|
||||
}
|
||||
}, [scriptFailed, deepLinkToken, deepLinkPolling, startDeepLinkAuth]);
|
||||
|
||||
if (!botUsername || botUsername === 'your_bot') {
|
||||
return (
|
||||
@@ -166,9 +301,88 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
);
|
||||
}
|
||||
|
||||
// Deep link fallback UI
|
||||
if (scriptFailed) {
|
||||
const deepLinkUrl = deepLinkToken
|
||||
? `https://t.me/${deepLinkBotUsername || botUsername}?start=webauth_${deepLinkToken}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
{/* Info message */}
|
||||
<p className="max-w-xs text-center text-xs text-dark-400">
|
||||
{t('auth.telegramWidgetBlocked')}
|
||||
</p>
|
||||
|
||||
{deepLinkToken && deepLinkUrl ? (
|
||||
<>
|
||||
{/* Deep link button */}
|
||||
<a
|
||||
href={deepLinkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-[#54a9eb] px-6 py-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-[#4a96d2]"
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
{t('auth.openBotToLogin')}
|
||||
</a>
|
||||
|
||||
{/* Polling status */}
|
||||
{deepLinkPolling && (
|
||||
<div className="flex items-center gap-2 text-xs text-dark-400">
|
||||
<span className="h-3 w-3 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
||||
{t('auth.waitingForConfirmation')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : deepLinkError ? (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<p className="text-xs text-red-500">{deepLinkError}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startDeepLinkAuth}
|
||||
className="text-sm text-accent-400 transition-colors hover:text-accent-300"
|
||||
>
|
||||
{t('auth.tryAgain')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-xs text-dark-400">
|
||||
<span className="h-3 w-3 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bot link fallback */}
|
||||
<div className="text-center">
|
||||
<p className="mb-2 text-xs text-dark-500">{t('auth.orOpenInApp')}</p>
|
||||
<a
|
||||
href={
|
||||
referralCode
|
||||
? `https://t.me/${botUsername}?start=${encodeURIComponent(referralCode)}`
|
||||
: `https://t.me/${botUsername}`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-telegram-blue inline-flex items-center text-sm hover:underline"
|
||||
>
|
||||
<svg className="mr-1 h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
@{botUsername}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Normal widget UI (when script loads successfully)
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
{/* OIDC mode: custom button that opens popup */}
|
||||
{isOIDC ? (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<button
|
||||
@@ -193,7 +407,6 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
||||
{oidcError && <p className="text-xs text-red-500">{oidcError}</p>}
|
||||
</div>
|
||||
) : (
|
||||
/* Legacy widget mode: iframe-based widget */
|
||||
<div ref={containerRef} className="flex justify-center" />
|
||||
)}
|
||||
|
||||
|
||||
@@ -175,6 +175,10 @@
|
||||
"authenticating": "Authenticating...",
|
||||
"orOpenInApp": "Or open the bot in the app",
|
||||
"loginFailed": "Login Failed",
|
||||
"telegramWidgetBlocked": "Telegram login widget is unavailable. Use the bot to sign in:",
|
||||
"openBotToLogin": "Open bot to sign in",
|
||||
"waitingForConfirmation": "Waiting for confirmation...",
|
||||
"deepLinkExpired": "Link expired. Please try again.",
|
||||
"tryAgain": "Try Again",
|
||||
"welcomeBack": "Welcome back!",
|
||||
"loginTitle": "Login to Cabinet",
|
||||
@@ -3864,6 +3868,7 @@
|
||||
"linkSuccess": "Account connected successfully",
|
||||
"unlinkSuccess": "Account disconnected successfully",
|
||||
"linkError": "Failed to connect account",
|
||||
"telegramLinkUnavailable": "Telegram linking is temporarily unavailable. Use the bot:",
|
||||
"unlinkError": "Failed to disconnect account",
|
||||
"goToAccounts": "Connected Accounts",
|
||||
"linking": "Linking account...",
|
||||
|
||||
@@ -167,6 +167,10 @@
|
||||
"authenticating": "در حال تایید هویت...",
|
||||
"orOpenInApp": "یا ربات را در برنامه باز کنید",
|
||||
"loginFailed": "ورود ناموفق",
|
||||
"telegramWidgetBlocked": "ویجت ورود تلگرام در دسترس نیست. از طریق ربات وارد شوید:",
|
||||
"openBotToLogin": "باز کردن ربات برای ورود",
|
||||
"waitingForConfirmation": "در انتظار تایید...",
|
||||
"deepLinkExpired": "لینک منقضی شده است. لطفا دوباره تلاش کنید.",
|
||||
"tryAgain": "تلاش مجدد",
|
||||
"welcomeBack": "خوش آمدید!",
|
||||
"loginTitle": "ورود به کابین",
|
||||
@@ -3258,6 +3262,7 @@
|
||||
"linkSuccess": "حساب با موفقیت متصل شد",
|
||||
"unlinkSuccess": "اتصال حساب با موفقیت قطع شد",
|
||||
"linkError": "اتصال حساب ناموفق بود",
|
||||
"telegramLinkUnavailable": "اتصال تلگرام موقتاً در دسترس نیست. از ربات استفاده کنید:",
|
||||
"unlinkError": "قطع اتصال ناموفق بود",
|
||||
"goToAccounts": "حسابهای متصل",
|
||||
"linking": "در حال اتصال حساب...",
|
||||
|
||||
@@ -178,6 +178,10 @@
|
||||
"authenticating": "Авторизация...",
|
||||
"orOpenInApp": "Или откройте бота в приложении",
|
||||
"loginFailed": "Ошибка входа",
|
||||
"telegramWidgetBlocked": "Виджет входа через Telegram недоступен. Войдите через бота:",
|
||||
"openBotToLogin": "Открыть бота для входа",
|
||||
"waitingForConfirmation": "Ожидание подтверждения...",
|
||||
"deepLinkExpired": "Ссылка истекла. Попробуйте снова.",
|
||||
"tryAgain": "Попробовать снова",
|
||||
"welcomeBack": "Добро пожаловать!",
|
||||
"loginTitle": "Вход в личный кабинет",
|
||||
@@ -4421,6 +4425,7 @@
|
||||
"linkSuccess": "Аккаунт успешно привязан",
|
||||
"unlinkSuccess": "Аккаунт успешно отвязан",
|
||||
"linkError": "Не удалось привязать аккаунт",
|
||||
"telegramLinkUnavailable": "Привязка Telegram временно недоступна. Используйте бота:",
|
||||
"unlinkError": "Не удалось отвязать аккаунт",
|
||||
"goToAccounts": "Привязанные аккаунты",
|
||||
"linking": "Привязка аккаунта...",
|
||||
|
||||
@@ -167,6 +167,10 @@
|
||||
"authenticating": "正在验证...",
|
||||
"orOpenInApp": "或在应用中打开机器人",
|
||||
"loginFailed": "登录失败",
|
||||
"telegramWidgetBlocked": "Telegram登录小部件不可用。请使用机器人登录:",
|
||||
"openBotToLogin": "打开机器人登录",
|
||||
"waitingForConfirmation": "等待确认...",
|
||||
"deepLinkExpired": "链接已过期,请重试。",
|
||||
"tryAgain": "重试",
|
||||
"welcomeBack": "欢迎回来!",
|
||||
"loginTitle": "登录个人中心",
|
||||
@@ -3257,6 +3261,7 @@
|
||||
"linkSuccess": "账户关联成功",
|
||||
"unlinkSuccess": "账户取消关联成功",
|
||||
"linkError": "关联账户失败",
|
||||
"telegramLinkUnavailable": "Telegram关联暂时不可用。请使用机器人:",
|
||||
"unlinkError": "取消关联失败",
|
||||
"goToAccounts": "关联账户",
|
||||
"linking": "正在关联账户...",
|
||||
|
||||
@@ -25,6 +25,8 @@ const isLinkableProvider = (provider: string): boolean =>
|
||||
// SessionStorage key for Telegram link CSRF state
|
||||
export const LINK_TELEGRAM_STATE_KEY = 'link_telegram_state';
|
||||
|
||||
const LINK_SCRIPT_LOAD_TIMEOUT_MS = 8000;
|
||||
|
||||
/** Telegram account linking widget (browser only). Supports OIDC popup and legacy widget. */
|
||||
function TelegramLinkWidget() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -34,6 +36,7 @@ function TelegramLinkWidget() {
|
||||
const queryClient = useQueryClient();
|
||||
const [oidcLoading, setOidcLoading] = useState(false);
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||
const [scriptFailed, setScriptFailed] = useState(false);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const { data: widgetConfig } = useQuery<TelegramWidgetConfig>({
|
||||
@@ -65,6 +68,12 @@ function TelegramLinkWidget() {
|
||||
[navigate, queryClient, showToast, t],
|
||||
);
|
||||
|
||||
// Handle script load failure (timeout or error)
|
||||
const handleScriptFailed = useCallback(() => {
|
||||
if (!mountedRef.current || scriptLoaded) return;
|
||||
setScriptFailed(true);
|
||||
}, [scriptLoaded]);
|
||||
|
||||
// OIDC callback handler (ref pattern to avoid stale closures)
|
||||
const handleOIDCCallbackRef =
|
||||
useRef<(data: { id_token?: string; error?: string }) => void>(undefined);
|
||||
@@ -95,7 +104,7 @@ function TelegramLinkWidget() {
|
||||
}
|
||||
};
|
||||
|
||||
// Load OIDC script and init
|
||||
// Load OIDC script and init with timeout
|
||||
useEffect(() => {
|
||||
if (!isOIDC || !widgetConfig?.oidc_client_id) return;
|
||||
|
||||
@@ -116,24 +125,43 @@ function TelegramLinkWidget() {
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
handleScriptFailed();
|
||||
}
|
||||
}, LINK_SCRIPT_LOAD_TIMEOUT_MS);
|
||||
|
||||
if (!script) {
|
||||
script = document.createElement('script');
|
||||
script.id = scriptId;
|
||||
script.src = 'https://oauth.telegram.org/js/telegram-login.js?3';
|
||||
script.async = true;
|
||||
script.onload = () => initTelegramLogin();
|
||||
script.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
initTelegramLogin();
|
||||
};
|
||||
script.onerror = () => {
|
||||
if (mountedRef.current) {
|
||||
showToast({ type: 'error', message: t('profile.accounts.linkError') });
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
handleScriptFailed();
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
initTelegramLogin();
|
||||
}
|
||||
}, [isOIDC, widgetConfig?.oidc_client_id, widgetConfig?.request_access, showToast, t]);
|
||||
|
||||
// Legacy widget effect (only when NOT OIDC)
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [
|
||||
isOIDC,
|
||||
widgetConfig?.oidc_client_id,
|
||||
widgetConfig?.request_access,
|
||||
showToast,
|
||||
t,
|
||||
scriptLoaded,
|
||||
handleScriptFailed,
|
||||
]);
|
||||
|
||||
// Legacy widget effect (only when NOT OIDC) with timeout
|
||||
useEffect(() => {
|
||||
if (isOIDC || !containerRef.current || !botUsername) return;
|
||||
|
||||
@@ -177,20 +205,58 @@ function TelegramLinkWidget() {
|
||||
script.setAttribute('data-request-access', 'write');
|
||||
script.async = true;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (container && !container.querySelector('iframe')) {
|
||||
handleScriptFailed();
|
||||
}
|
||||
}, LINK_SCRIPT_LOAD_TIMEOUT_MS);
|
||||
|
||||
script.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
handleScriptFailed();
|
||||
};
|
||||
|
||||
container.appendChild(script);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
delete (window as unknown as Record<string, unknown>)[callbackName];
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
};
|
||||
}, [isOIDC, botUsername, navigate, showToast, t, queryClient, handleLinkResult]);
|
||||
}, [
|
||||
isOIDC,
|
||||
botUsername,
|
||||
navigate,
|
||||
showToast,
|
||||
t,
|
||||
queryClient,
|
||||
handleLinkResult,
|
||||
handleScriptFailed,
|
||||
]);
|
||||
|
||||
if (!botUsername && !isOIDC) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Script failed to load - show unavailable message with bot link
|
||||
if (scriptFailed) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<p className="text-xs text-dark-400">{t('profile.accounts.telegramLinkUnavailable')}</p>
|
||||
<a
|
||||
href={`https://t.me/${botUsername}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-accent-400 transition-colors hover:text-accent-300"
|
||||
>
|
||||
@{botUsername}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isOIDC) {
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -45,6 +45,7 @@ interface AuthState {
|
||||
state: string,
|
||||
deviceId?: string | null,
|
||||
) => Promise<void>;
|
||||
loginWithDeepLink: (token: string) => Promise<void>;
|
||||
registerWithEmail: (
|
||||
email: string,
|
||||
password: string,
|
||||
@@ -318,6 +319,19 @@ export const useAuthStore = create<AuthState>()(
|
||||
await get().checkAdminStatus();
|
||||
},
|
||||
|
||||
loginWithDeepLink: async (token) => {
|
||||
const response = await authApi.pollDeepLinkToken(token);
|
||||
tokenStorage.setTokens(response.access_token, response.refresh_token);
|
||||
set({
|
||||
accessToken: response.access_token,
|
||||
refreshToken: response.refresh_token,
|
||||
user: response.user,
|
||||
isAuthenticated: true,
|
||||
pendingCampaignBonus: response.campaign_bonus || null,
|
||||
});
|
||||
await get().checkAdminStatus();
|
||||
},
|
||||
|
||||
registerWithEmail: async (email, password, firstName, referralCode) => {
|
||||
const code = referralCode || consumeReferralCode() || undefined;
|
||||
const response = await authApi.registerEmailStandalone({
|
||||
|
||||
Reference in New Issue
Block a user