)}
+
+ {/* 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 03/11] 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 eafc563128b68f49f0a1f03a30c899b527ce4734 Mon Sep 17 00:00:00 2001
From: Case211 <87642841+Case211@users.noreply.github.com>
Date: Tue, 4 Aug 2026 23:38:20 +0500
Subject: [PATCH 04/11] =?UTF-8?q?feat(users):=20=D0=BA=D0=BD=D0=BE=D0=BF?=
=?UTF-8?q?=D0=BA=D0=B0=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?=
=?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BA=D0=B8=20=D0=B2=20?=
=?UTF-8?q?=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B5=20=D0=BF=D0=BE?=
=?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
В мультитарифном режиме у пользователя несколько подписок, и убрать
лишнюю — например, отработавший триал — можно было только через экран
«Массовые действия». Во вкладке «Подписка» карточки подписки видны и
открываются, но удалить выбранную нечем.
Кнопка живёт в детальном виде подписки и требует подтверждения тем же
inline-механизмом, что и отмена автоплатежа; ключ подтверждения включает
id подписки, чтобы взведённое согласие не пережило переключение на
соседнюю. Активная платная подписка на сервере защищена от случайного
удаления, поэтому для неё запрос идёт с явным force — намерение админ
уже подтвердил.
---
src/api/adminUsers.ts | 13 +++++++
.../admin/userDetail/SubscriptionTab.tsx | 37 +++++++++++++++++++
src/locales/en.json | 6 ++-
src/locales/fa.json | 6 ++-
src/locales/ru.json | 6 ++-
src/locales/zh.json | 6 ++-
src/pages/AdminUserDetail.tsx | 19 ++++++++++
7 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts
index abd4863..4df5c37 100644
--- a/src/api/adminUsers.ts
+++ b/src/api/adminUsers.ts
@@ -518,6 +518,19 @@ export const adminUsersApi = {
return response.data;
},
+ // Delete one of the user's subscriptions (multi-tariff: trials pile up)
+ deleteSubscription: async (
+ userId: number,
+ subId: number,
+ force = false,
+ ): Promise<{ status: string }> => {
+ const response = await apiClient.delete(
+ `/cabinet/admin/users/${userId}/subscriptions/${subId}`,
+ { params: force ? { force: true } : undefined },
+ );
+ return response.data;
+ },
+
// Update status
updateStatus: async (
userId: number,
diff --git a/src/components/admin/userDetail/SubscriptionTab.tsx b/src/components/admin/userDetail/SubscriptionTab.tsx
index a8b83c8..40e66de 100644
--- a/src/components/admin/userDetail/SubscriptionTab.tsx
+++ b/src/components/admin/userDetail/SubscriptionTab.tsx
@@ -131,6 +131,7 @@ export interface SubscriptionTabProps {
onRemoveTraffic: (purchaseId: number) => Promise;
onResetDevices: () => Promise;
onCancelSbpRecurring: () => Promise;
+ onDeleteSubscription: () => Promise;
onDeleteDevice: (hwid: string) => Promise;
onRenameDevice: (hwid: string) => Promise;
onLoadDevices: () => Promise;
@@ -195,6 +196,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
onRemoveTraffic,
onResetDevices,
onCancelSbpRecurring,
+ onDeleteSubscription,
onDeleteDevice,
onRenameDevice,
onLoadDevices,
@@ -429,6 +431,41 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
)}
+ {/* Delete this subscription — in multi-tariff mode spent trials
+ pile up in the card, and removing one used to be possible
+ only through the bulk-actions screen. */}
+ {hasPermission('users:subscription') && (
+