)}
+
+ {/* 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 3/5] 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 2d6f8d97546f7ca69540924aa0637f720500bef3 Mon Sep 17 00:00:00 2001
From: Fringg
Date: Thu, 6 Aug 2026 02:11:14 +0300
Subject: [PATCH 4/5] =?UTF-8?q?fix(admin):=20=D0=B4=D0=BE=D0=BF=D0=BE?=
=?UTF-8?q?=D0=BB=D0=BD=D0=B8=D1=82=D1=8C=20=D1=82=D0=B8=D0=BF=20sort=5Fby?=
=?UTF-8?q?=20=D0=B8=20=D0=BA=D0=B8=D1=82=D0=B0=D0=B9=D1=81=D0=BA=D1=83?=
=?UTF-8?q?=D1=8E=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D1=83=20=D1=81=D0=BE?=
=?UTF-8?q?=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Дополнения к PR #534 при мерже.
Юнион sort_by в adminUsers.ts не знал про subscription_end_date. Ошибки
компиляции нет только потому, что вызывающий код собирает params как
Record и приводит его каст-выражением, то есть юнион там
вообще не проверяется. Но это единственная запись о том, какие значения
принимает эндпоинт, и она разъехалась с реальностью.
Китайская строка «按到期» ставит после 按 глагол, тогда как все соседние
пункты — 按日期, 按余额, 按活跃度, 按消费 — существительные. Сам словарь
кабинета для этого поля использует 到期时间 (subscription.expiresAt,
admin.users.detail.expiresAt, promo.expiresIn), поэтому привёл к той же форме.
Русскую строку не трогал: «по истечению» здесь дательный падеж критерия, как
у соседей («по дате», «по балансу»), а не предложный «по истечении» из
временного оборота.
---
src/api/adminUsers.ts | 3 ++-
src/locales/zh.json | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts
index abd4863..56ace51 100644
--- a/src/api/adminUsers.ts
+++ b/src/api/adminUsers.ts
@@ -456,7 +456,8 @@ export const adminUsersApi = {
| 'traffic'
| 'last_activity'
| 'total_spent'
- | 'purchase_count';
+ | 'purchase_count'
+ | 'subscription_end_date';
} = {},
): Promise => {
const response = await apiClient.get('/cabinet/admin/users', { params });
diff --git a/src/locales/zh.json b/src/locales/zh.json
index d6b353e..87b3dce 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -2963,7 +2963,7 @@
"byBalance": "按余额",
"byActivity": "按活跃度",
"bySpent": "按消费",
- "byExpiry": "按到期"
+ "byExpiry": "按到期时间"
},
"pagination": {
"showing": "显示 {{from}}-{{to}},共 {{total}}"
From c1088325aabaf43de6f60653ff650c3cfb1beb2f Mon Sep 17 00:00:00 2001
From: Fringg
Date: Thu, 6 Aug 2026 02:16:02 +0300
Subject: [PATCH 5/5] =?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": "链接已过期,请重试。",