Merge pull request #539 from off-art/feat/manual-deeplink-login

This commit is contained in:
Fringg
2026-08-06 02:16:11 +03:00
5 changed files with 89 additions and 20 deletions

View File

@@ -26,6 +26,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
const [oidcError, setOidcError] = useState(''); const [oidcError, setOidcError] = useState('');
const [scriptLoaded, setScriptLoaded] = useState(false); const [scriptLoaded, setScriptLoaded] = useState(false);
const [scriptFailed, setScriptFailed] = useState(false); const [scriptFailed, setScriptFailed] = useState(false);
// Lets the user opt into deep-link auth manually, without waiting for the
// Telegram widget script to fail. See #<issue-number>.
const [manualDeepLink, setManualDeepLink] = useState(false);
const showDeepLinkUI = scriptFailed || manualDeepLink;
const loginWithTelegramOIDC = useAuthStore((s) => s.loginWithTelegramOIDC); const loginWithTelegramOIDC = useAuthStore((s) => s.loginWithTelegramOIDC);
// Deep link auth state // Deep link auth state
@@ -168,7 +172,12 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
const loginWithTelegramWidget = useAuthStore((s) => s.loginWithTelegramWidget); const loginWithTelegramWidget = useAuthStore((s) => s.loginWithTelegramWidget);
useEffect(() => { useEffect(() => {
if (isOIDC || !containerRef.current || !botUsername || !widgetConfig) return; // showDeepLinkUI обязан быть в зависимостях: пока он true, контейнер
// виджета размонтирован, а при возврате «Назад к виджету» сам по себе
// эффект не перезапустится — на legacy-пути scriptLoaded не меняется
// никогда, поэтому ни одна из остальных зависимостей не дрогнет, и
// пользователь получил бы пустое место вместо виджета.
if (showDeepLinkUI || isOIDC || !containerRef.current || !botUsername || !widgetConfig) return;
const container = containerRef.current; const container = containerRef.current;
while (container.firstChild) { while (container.firstChild) {
@@ -229,7 +238,15 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
container.removeChild(container.firstChild); container.removeChild(container.firstChild);
} }
}; };
}, [isOIDC, botUsername, widgetConfig, loginWithTelegramWidget, navigate, handleScriptFailed]); }, [
showDeepLinkUI,
isOIDC,
botUsername,
widgetConfig,
loginWithTelegramWidget,
navigate,
handleScriptFailed,
]);
// Deep link auth: request token and start polling with recursive setTimeout // Deep link auth: request token and start polling with recursive setTimeout
const startDeepLinkAuth = useCallback(async () => { const startDeepLinkAuth = useCallback(async () => {
@@ -336,9 +353,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
} }
}, [botUsername, loginWithDeepLink, navigate, t]); }, [botUsername, loginWithDeepLink, navigate, t]);
// Auto-start deep link auth when script fails (with cancellation for Strict Mode) // Auto-start deep link auth when script fails OR the user opts in manually
// (with cancellation for Strict Mode)
useEffect(() => { useEffect(() => {
if (scriptFailed && !deepLinkToken && !deepLinkPolling) { if (showDeepLinkUI && !deepLinkToken && !deepLinkPolling) {
let cancelled = false; let cancelled = false;
const start = async () => { const start = async () => {
if (!cancelled) await startDeepLinkAuth(); if (!cancelled) await startDeepLinkAuth();
@@ -348,7 +366,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
cancelled = true; cancelled = true;
}; };
} }
}, [scriptFailed, deepLinkToken, deepLinkPolling, startDeepLinkAuth]); }, [showDeepLinkUI, deepLinkToken, deepLinkPolling, startDeepLinkAuth]);
// Resume polling immediately when user returns to the page (e.g. after confirming in Telegram) // 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. // Browsers throttle setTimeout in background tabs, so polling may have stalled.
@@ -431,8 +449,9 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
); );
} }
// Deep link fallback UI // Deep link UI — shown either as an automatic fallback (widget script
if (scriptFailed) { // failed to load) or because the user explicitly chose this method.
if (showDeepLinkUI) {
const resolvedBotUsername = deepLinkBotUsername || botUsername; const resolvedBotUsername = deepLinkBotUsername || botUsername;
const deepLinkUrl = deepLinkToken const deepLinkUrl = deepLinkToken
? `https://t.me/${resolvedBotUsername}?start=webauth_${deepLinkToken}` ? `https://t.me/${resolvedBotUsername}?start=webauth_${deepLinkToken}`
@@ -443,7 +462,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
<div className="flex flex-col items-center space-y-5"> <div className="flex flex-col items-center space-y-5">
{/* Info message */} {/* Info message */}
<p className="max-w-xs text-center text-xs text-dark-400"> <p className="max-w-xs text-center text-xs text-dark-400">
{t('auth.telegramWidgetBlocked')} {t(scriptFailed ? 'auth.telegramWidgetBlocked' : 'auth.deepLinkIntro')}
</p> </p>
{deepLinkToken && deepLinkUrl ? ( {deepLinkToken && deepLinkUrl ? (
@@ -517,6 +536,27 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
{t('common.loading')} {t('common.loading')}
</div> </div>
)} )}
{/* Only offer a way back if the widget actually works — if the
script failed there is nothing to go back to. */}
{!scriptFailed && (
<button
type="button"
onClick={() => {
if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current);
if (expireTimeoutRef.current) clearTimeout(expireTimeoutRef.current);
pollTimeoutRef.current = null;
expireTimeoutRef.current = null;
setDeepLinkToken(null);
setDeepLinkPolling(false);
setDeepLinkError('');
setManualDeepLink(false);
}}
className="text-xs text-dark-400 underline decoration-dotted transition-colors hover:text-dark-300"
>
{t('auth.backToWidget')}
</button>
)}
</div> </div>
); );
} }
@@ -551,24 +591,41 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
<div ref={containerRef} className="flex justify-center" /> <div ref={containerRef} className="flex justify-center" />
)} )}
<div className="text-center"> {/* Referral deep link — only relevant for not-yet-registered users who
<p className="mb-2 text-xs text-dark-400">{t('auth.orOpenInApp')}</p> arrived via a referral link; the bot itself handles registering
them with the code attached. Hidden otherwise to avoid a third,
visually-identical "Telegram" entry point next to the two auth
methods below. */}
{referralCode && (
<a <a
href={ href={`https://t.me/${botUsername}?start=${encodeURIComponent(referralCode)}`}
referralCode
? `https://t.me/${botUsername}?start=${encodeURIComponent(referralCode)}`
: `https://t.me/${botUsername}`
}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-telegram-blue inline-flex items-center text-sm hover:underline" className="text-telegram-blue inline-flex items-center text-xs hover:underline"
> >
<svg className="mr-1 h-4 w-4" viewBox="0 0 24 24" fill="currentColor"> {t('auth.orOpenInApp')}&nbsp;@{botUsername}
<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> </a>
)}
<div className="flex w-full max-w-xs items-center gap-3">
<div className="h-px flex-1 bg-dark-700" />
<span className="text-[11px] text-dark-500">{t('common.or')}</span>
<div className="h-px flex-1 bg-dark-700" />
</div> </div>
{/* Manual opt-in: same deep-link flow used as the anti-block fallback,
offered here as an explicit equal alternative to the widget for
users who'd rather confirm in the bot than type a phone number. */}
<button
type="button"
onClick={() => setManualDeepLink(true)}
className="inline-flex items-center gap-2 rounded-lg border border-dark-700 bg-dark-800/50 px-6 py-3 text-sm font-medium text-dark-200 transition-colors hover:border-dark-600 hover:bg-dark-800"
>
<svg className="h-5 w-5 text-telegram-blue" 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.loginWithBot')}
</button>
</div> </div>
); );
} }

View File

@@ -207,6 +207,9 @@
"orOpenInApp": "Or open the bot in the app", "orOpenInApp": "Or open the bot in the app",
"loginFailed": "Login Failed", "loginFailed": "Login Failed",
"telegramWidgetBlocked": "Telegram login widget is unavailable. Use the bot to sign in:", "telegramWidgetBlocked": "Telegram login widget is unavailable. Use the bot to sign in:",
"deepLinkIntro": "Confirm sign-in right in the bot — no phone number needed:",
"loginWithBot": "Login via bot",
"backToWidget": "Back to widget login",
"openBotToLogin": "Open bot to sign in", "openBotToLogin": "Open bot to sign in",
"waitingForConfirmation": "Waiting for confirmation...", "waitingForConfirmation": "Waiting for confirmation...",
"deepLinkExpired": "Link expired. Please try again.", "deepLinkExpired": "Link expired. Please try again.",

View File

@@ -199,6 +199,9 @@
"orOpenInApp": "یا ربات را در برنامه باز کنید", "orOpenInApp": "یا ربات را در برنامه باز کنید",
"loginFailed": "ورود ناموفق", "loginFailed": "ورود ناموفق",
"telegramWidgetBlocked": "ویجت ورود تلگرام در دسترس نیست. از طریق ربات وارد شوید:", "telegramWidgetBlocked": "ویجت ورود تلگرام در دسترس نیست. از طریق ربات وارد شوید:",
"deepLinkIntro": "ورود را مستقیماً در ربات تأیید کنید — بدون نیاز به شماره تلفن:",
"loginWithBot": "ورود از طریق ربات",
"backToWidget": "بازگشت به ورود با ویجت",
"openBotToLogin": "باز کردن ربات برای ورود", "openBotToLogin": "باز کردن ربات برای ورود",
"waitingForConfirmation": "در انتظار تایید...", "waitingForConfirmation": "در انتظار تایید...",
"deepLinkExpired": "لینک منقضی شده است. لطفا دوباره تلاش کنید.", "deepLinkExpired": "لینک منقضی شده است. لطفا دوباره تلاش کنید.",

View File

@@ -210,6 +210,9 @@
"orOpenInApp": "Или откройте бота в приложении", "orOpenInApp": "Или откройте бота в приложении",
"loginFailed": "Ошибка входа", "loginFailed": "Ошибка входа",
"telegramWidgetBlocked": "Виджет входа через Telegram недоступен. Войдите через бота:", "telegramWidgetBlocked": "Виджет входа через Telegram недоступен. Войдите через бота:",
"deepLinkIntro": "Подтвердите вход прямо в боте — без ввода номера телефона:",
"loginWithBot": "Войти через бота",
"backToWidget": "Назад к входу через виджет",
"openBotToLogin": "Открыть бота для входа", "openBotToLogin": "Открыть бота для входа",
"waitingForConfirmation": "Ожидание подтверждения...", "waitingForConfirmation": "Ожидание подтверждения...",
"deepLinkExpired": "Ссылка истекла. Попробуйте снова.", "deepLinkExpired": "Ссылка истекла. Попробуйте снова.",

View File

@@ -199,6 +199,9 @@
"orOpenInApp": "或在应用中打开机器人", "orOpenInApp": "或在应用中打开机器人",
"loginFailed": "登录失败", "loginFailed": "登录失败",
"telegramWidgetBlocked": "Telegram登录小部件不可用。请使用机器人登录", "telegramWidgetBlocked": "Telegram登录小部件不可用。请使用机器人登录",
"deepLinkIntro": "直接在机器人中确认登录 — 无需手机号:",
"loginWithBot": "通过机器人登录",
"backToWidget": "返回小部件登录",
"openBotToLogin": "打开机器人登录", "openBotToLogin": "打开机器人登录",
"waitingForConfirmation": "等待确认...", "waitingForConfirmation": "等待确认...",
"deepLinkExpired": "链接已过期,请重试。", "deepLinkExpired": "链接已过期,请重试。",