From d03b9bdad785ac7710f30527f230eeb793e1089b Mon Sep 17 00:00:00 2001 From: "Artem (OpenIN)" Date: Mon, 3 Aug 2026 17:46:28 +0000 Subject: [PATCH 1/3] feat(auth): allow manual opt-in for deep-link Telegram login Currently the deep-link auth flow (t.me/{bot}?start=webauth_{token}) is only triggered automatically as a fallback when the Telegram widget script (oauth.telegram.org or telegram.org/js/telegram-widget.js) fails to load. Users on unaffected networks have no way to choose this login method even when they'd prefer confirming in the bot over typing a phone number into the widget popup. This adds a small 'Login via bot' link next to the existing widget, reusing the exact same startDeepLinkAuth/poll logic already used by the automatic fallback. No changes to the fallback behavior itself. --- src/components/TelegramLoginButton.tsx | 50 ++++++++++++++++++++++---- src/locales/en.json | 3 ++ src/locales/ru.json | 3 ++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 15dc748..e388376 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -26,6 +26,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto const [oidcError, setOidcError] = useState(''); const [scriptLoaded, setScriptLoaded] = 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 #. + const [manualDeepLink, setManualDeepLink] = useState(false); + const showDeepLinkUI = scriptFailed || manualDeepLink; const loginWithTelegramOIDC = useAuthStore((s) => s.loginWithTelegramOIDC); // Deep link auth state @@ -336,9 +340,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto } }, [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(() => { - if (scriptFailed && !deepLinkToken && !deepLinkPolling) { + if (showDeepLinkUI && !deepLinkToken && !deepLinkPolling) { let cancelled = false; const start = async () => { if (!cancelled) await startDeepLinkAuth(); @@ -348,7 +353,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto 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) // Browsers throttle setTimeout in background tabs, so polling may have stalled. @@ -431,8 +436,9 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto ); } - // Deep link fallback UI - if (scriptFailed) { + // Deep link UI — shown either as an automatic fallback (widget script + // failed to load) or because the user explicitly chose this method. + if (showDeepLinkUI) { const resolvedBotUsername = deepLinkBotUsername || botUsername; const deepLinkUrl = deepLinkToken ? `https://t.me/${resolvedBotUsername}?start=webauth_${deepLinkToken}` @@ -443,7 +449,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
{/* Info message */}

- {t('auth.telegramWidgetBlocked')} + {t(scriptFailed ? 'auth.telegramWidgetBlocked' : 'auth.deepLinkIntro')}

{deepLinkToken && deepLinkUrl ? ( @@ -517,6 +523,27 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto {t('common.loading')}
)} + + {/* Only offer a way back if the widget actually works — if the + script failed there is nothing to go back to. */} + {!scriptFailed && ( + + )} ); } @@ -569,6 +596,17 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto @{botUsername} + + {/* Manual opt-in: same deep-link flow used as the anti-block fallback, + offered here as an explicit alternative for users who'd rather + confirm in the bot than type a phone number into the widget. */} + ); } diff --git a/src/locales/en.json b/src/locales/en.json index b07b36d..8bd981e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -207,6 +207,9 @@ "orOpenInApp": "Or open the bot in the app", "loginFailed": "Login Failed", "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 (no phone number)", + "backToWidget": "Back to widget login", "openBotToLogin": "Open bot to sign in", "waitingForConfirmation": "Waiting for confirmation...", "deepLinkExpired": "Link expired. Please try again.", diff --git a/src/locales/ru.json b/src/locales/ru.json index 080e88f..971a829 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -210,6 +210,9 @@ "orOpenInApp": "Или откройте бота в приложении", "loginFailed": "Ошибка входа", "telegramWidgetBlocked": "Виджет входа через Telegram недоступен. Войдите через бота:", + "deepLinkIntro": "Подтвердите вход прямо в боте — без ввода номера телефона:", + "loginWithBot": "Войти через бота (без номера телефона)", + "backToWidget": "Назад к входу через виджет", "openBotToLogin": "Открыть бота для входа", "waitingForConfirmation": "Ожидание подтверждения...", "deepLinkExpired": "Ссылка истекла. Попробуйте снова.", From 75424dba473925b99d993d0e52f33067f14125ce Mon Sep 17 00:00:00 2001 From: "Artem (OpenIN)" Date: Mon, 3 Aug 2026 18:23:16 +0000 Subject: [PATCH 2/3] refactor(auth): consolidate three Telegram entry points into two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the passive '@bot_username' link (opens the bot chat with no auth purpose) for the common case — it's now redundant with the new 'Login via bot' button, which offers the same open-bot action plus QR and actual authentication. - Keep the referral deep link (bot start=) only when a referralCode prop is present — that's a distinct registration flow for not-yet-registered users, not a login method. - Add an 'or' divider between the widget and the manual deep-link button so the two remaining options read as equal alternatives rather than a stack of similar-looking Telegram links. - Shorten the deep-link button label to 'Login via bot' — the no-phone-number framing is already implied and shown once the flow starts. No changes to the OIDC/widget button logic itself. --- src/components/TelegramLoginButton.tsx | 36 +++++++++++++++----------- src/locales/en.json | 2 +- src/locales/ru.json | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index e388376..05d79de 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -578,33 +578,39 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
)} -
-

{t('auth.orOpenInApp')}

+ {/* Referral deep link — only relevant for not-yet-registered users who + 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 && ( - - - - @{botUsername} + {t('auth.orOpenInApp')} @{botUsername} + )} + +
+
+ {t('common.or')} +
{/* Manual opt-in: same deep-link flow used as the anti-block fallback, - offered here as an explicit alternative for users who'd rather - confirm in the bot than type a phone number into the widget. */} + offered here as an explicit equal alternative to the widget for + users who'd rather confirm in the bot than type a phone number. */}
diff --git a/src/locales/en.json b/src/locales/en.json index 8bd981e..80e72e4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -208,7 +208,7 @@ "loginFailed": "Login Failed", "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 (no phone number)", + "loginWithBot": "Login via bot", "backToWidget": "Back to widget login", "openBotToLogin": "Open bot to sign in", "waitingForConfirmation": "Waiting for confirmation...", diff --git a/src/locales/ru.json b/src/locales/ru.json index 971a829..2feebe2 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -211,7 +211,7 @@ "loginFailed": "Ошибка входа", "telegramWidgetBlocked": "Виджет входа через Telegram недоступен. Войдите через бота:", "deepLinkIntro": "Подтвердите вход прямо в боте — без ввода номера телефона:", - "loginWithBot": "Войти через бота (без номера телефона)", + "loginWithBot": "Войти через бота", "backToWidget": "Назад к входу через виджет", "openBotToLogin": "Открыть бота для входа", "waitingForConfirmation": "Ожидание подтверждения...", From c1088325aabaf43de6f60653ff650c3cfb1beb2f Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 6 Aug 2026 02:16:02 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(auth):=20=D0=B2=D0=B5=D1=80=D0=BD=D1=83?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=20=C2=AB=D0=9D=D0=B0=D0=B7=D0=B0=D0=B4?= =?UTF-8?q?=C2=BB=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B8=D1=82=D1=8C=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=D0=BB=D0=B8=20=D0=B4=D0=BE=20=D1=87=D0=B5?= =?UTF-8?q?=D1=82=D1=8B=D1=80=D1=91=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дополнения к PR #539 при мерже. Кнопка «Назад к входу через виджет» оставляла пустое место. Пока открыт deep-link-экран, контейнер виджета размонтирован ранним return'ом, а эффект, который вставляет скрипт Telegram, от этого не перезапускается: его зависимости не меняются. Единственная зависимость, способная дрогнуть, — handleScriptFailed через scriptLoaded, но на legacy-пути scriptLoaded не выставляется никогда (только в OIDC-ветке). Так что при возврате контейнер монтировался обратно уже пустым, и войти через виджет было нельзя до перезагрузки страницы. Добавлен showDeepLinkUI в зависимости и ранний выход: на входе в deep-link отрабатывает cleanup, на выходе скрипт вставляется заново. Локали были только en и ru, а в кабинете их четыре. fallbackLng — 'ru', поэтому персидские и китайские пользователи увидели бы русский текст на самом первом экране. Именно этот класс регрессии описан в шапке locales.test.ts, и поймать его тест не может: он сравнивает только en и ru. Добавлены fa и zh; в zh термин «виджет» приведён к тому же 小部件, что уже используется в telegramWidgetBlocked. --- src/components/TelegramLoginButton.tsx | 17 +++++++++++++++-- src/locales/fa.json | 3 +++ src/locales/zh.json | 3 +++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 05d79de..9cc0c59 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -172,7 +172,12 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto const loginWithTelegramWidget = useAuthStore((s) => s.loginWithTelegramWidget); useEffect(() => { - if (isOIDC || !containerRef.current || !botUsername || !widgetConfig) return; + // showDeepLinkUI обязан быть в зависимостях: пока он true, контейнер + // виджета размонтирован, а при возврате «Назад к виджету» сам по себе + // эффект не перезапустится — на legacy-пути scriptLoaded не меняется + // никогда, поэтому ни одна из остальных зависимостей не дрогнет, и + // пользователь получил бы пустое место вместо виджета. + if (showDeepLinkUI || isOIDC || !containerRef.current || !botUsername || !widgetConfig) return; const container = containerRef.current; while (container.firstChild) { @@ -233,7 +238,15 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto 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 const startDeepLinkAuth = useCallback(async () => { diff --git a/src/locales/fa.json b/src/locales/fa.json index fe8a782..f135511 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -199,6 +199,9 @@ "orOpenInApp": "یا ربات را در برنامه باز کنید", "loginFailed": "ورود ناموفق", "telegramWidgetBlocked": "ویجت ورود تلگرام در دسترس نیست. از طریق ربات وارد شوید:", + "deepLinkIntro": "ورود را مستقیماً در ربات تأیید کنید — بدون نیاز به شماره تلفن:", + "loginWithBot": "ورود از طریق ربات", + "backToWidget": "بازگشت به ورود با ویجت", "openBotToLogin": "باز کردن ربات برای ورود", "waitingForConfirmation": "در انتظار تایید...", "deepLinkExpired": "لینک منقضی شده است. لطفا دوباره تلاش کنید.", diff --git a/src/locales/zh.json b/src/locales/zh.json index 87b3dce..754fb98 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -199,6 +199,9 @@ "orOpenInApp": "或在应用中打开机器人", "loginFailed": "登录失败", "telegramWidgetBlocked": "Telegram登录小部件不可用。请使用机器人登录:", + "deepLinkIntro": "直接在机器人中确认登录 — 无需手机号:", + "loginWithBot": "通过机器人登录", + "backToWidget": "返回小部件登录", "openBotToLogin": "打开机器人登录", "waitingForConfirmation": "等待确认...", "deepLinkExpired": "链接已过期,请重试。",