feat: migrate Telegram Login Widget to v23 with admin-configurable settings

- Upgrade telegram-widget.js from v22 to v23 (TelegramLoginButton + ConnectedAccounts)
- TelegramLoginButton fetches widget config from API instead of hardcoded values
- Add TelegramWidgetConfig type and API method with fallback to env defaults
- Remove botUsername prop — now sourced from backend API
- Add useEffect cleanup to prevent orphaned iframes on unmount
- Add TELEGRAM_WIDGET category to admin interface settings menu
- Add i18n translations for widget settings (ru, en, zh, fa)
This commit is contained in:
Fringg
2026-03-07 01:48:18 +03:00
parent 83fbd0e445
commit 2c65ca8a7f
9 changed files with 97 additions and 27 deletions

View File

@@ -23,6 +23,14 @@ export interface EmailAuthEnabled {
enabled: boolean; enabled: boolean;
} }
export interface TelegramWidgetConfig {
bot_username: string;
size: 'large' | 'medium' | 'small';
radius: number;
userpic: boolean;
request_access: boolean;
}
export interface AnalyticsCounters { export interface AnalyticsCounters {
yandex_metrika_id: string; yandex_metrika_id: string;
google_ads_id: string; google_ads_id: string;
@@ -251,6 +259,24 @@ export const brandingApi = {
} }
}, },
// Get Telegram widget config (public, no auth required)
getTelegramWidgetConfig: async (): Promise<TelegramWidgetConfig> => {
try {
const response = await apiClient.get<TelegramWidgetConfig>(
'/cabinet/branding/telegram-widget',
);
return response.data;
} catch {
return {
bot_username: import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '',
size: 'large',
radius: 8,
userpic: true,
request_access: true,
};
}
},
// Update analytics counters (admin only) // Update analytics counters (admin only)
updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => { updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => {
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data); const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data);

View File

@@ -1,42 +1,58 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
interface TelegramLoginButtonProps { interface TelegramLoginButtonProps {
botUsername: string;
referralCode?: string; referralCode?: string;
} }
export default function TelegramLoginButton({ export default function TelegramLoginButton({ referralCode }: TelegramLoginButtonProps) {
botUsername,
referralCode,
}: TelegramLoginButtonProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { data: widgetConfig } = useQuery<TelegramWidgetConfig>({
queryKey: ['telegram-widget-config'],
queryFn: brandingApi.getTelegramWidgetConfig,
staleTime: 60000,
});
const botUsername =
widgetConfig?.bot_username || import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '';
// Load widget script // Load widget script
useEffect(() => { useEffect(() => {
if (!containerRef.current || !botUsername) return; if (!containerRef.current || !botUsername || !widgetConfig) return;
// Clear previous widget using safe DOM API const container = containerRef.current;
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild); // Clear previous widget
while (container.firstChild) {
container.removeChild(container.firstChild);
} }
// Get current URL for redirect
const redirectUrl = `${window.location.origin}/auth/telegram/callback`; const redirectUrl = `${window.location.origin}/auth/telegram/callback`;
// Create script element for Telegram Login Widget
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'https://telegram.org/js/telegram-widget.js?22'; script.src = 'https://telegram.org/js/telegram-widget.js?23';
script.setAttribute('data-telegram-login', botUsername); script.setAttribute('data-telegram-login', botUsername);
script.setAttribute('data-size', 'large'); script.setAttribute('data-size', widgetConfig.size);
script.setAttribute('data-radius', '8'); script.setAttribute('data-radius', String(widgetConfig.radius));
script.setAttribute('data-userpic', String(widgetConfig.userpic));
script.setAttribute('data-auth-url', redirectUrl); script.setAttribute('data-auth-url', redirectUrl);
script.setAttribute('data-request-access', 'write'); if (widgetConfig.request_access) {
script.setAttribute('data-request-access', 'write');
}
script.async = true; script.async = true;
containerRef.current.appendChild(script); container.appendChild(script);
}, [botUsername]);
return () => {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
};
}, [botUsername, widgetConfig]);
if (!botUsername || botUsername === 'your_bot') { if (!botUsername || botUsername === 'your_bot') {
return ( return (
@@ -48,10 +64,8 @@ export default function TelegramLoginButton({
return ( return (
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
{/* Telegram Widget will be inserted here */}
<div ref={containerRef} className="flex justify-center" /> <div ref={containerRef} className="flex justify-center" />
{/* Fallback link for mobile */}
<div className="text-center"> <div className="text-center">
<p className="mb-2 text-xs text-gray-500">{t('auth.orOpenInApp')}</p> <p className="mb-2 text-xs text-gray-500">{t('auth.orOpenInApp')}</p>
<a <a

View File

@@ -67,6 +67,7 @@ export const MENU_SECTIONS: MenuSection[] = [
'INTERFACE_SUBSCRIPTION', 'INTERFACE_SUBSCRIPTION',
'CONNECT_BUTTON', 'CONNECT_BUTTON',
'MINIAPP', 'MINIAPP',
'TELEGRAM_WIDGET',
'HAPP', 'HAPP',
'SKIP', 'SKIP',
'ADDITIONAL', 'ADDITIONAL',

View File

@@ -1681,7 +1681,16 @@
"projectName": "Project name", "projectName": "Project name",
"quickPresets": "Quick presets", "quickPresets": "Quick presets",
"resetAllColors": "Reset all colors", "resetAllColors": "Reset all colors",
"statusColors": "Status colors" "statusColors": "Status colors",
"categories": {
"TELEGRAM_WIDGET": "Telegram Login Widget"
},
"settingNames": {
"Telegram Widget Size": "Widget Size",
"Telegram Widget Radius": "Corner Radius",
"Telegram Widget Userpic": "Show User Photo",
"Telegram Widget Request Access": "Request Access"
}
}, },
"theme": { "theme": {
"accentColor": "Accent color", "accentColor": "Accent color",

View File

@@ -1341,7 +1341,16 @@
"projectName": "نام پروژه", "projectName": "نام پروژه",
"quickPresets": "پیش‌تنظیم‌های سریع", "quickPresets": "پیش‌تنظیم‌های سریع",
"resetAllColors": "بازنشانی همه رنگ‌ها", "resetAllColors": "بازنشانی همه رنگ‌ها",
"statusColors": "رنگ‌های وضعیت" "statusColors": "رنگ‌های وضعیت",
"categories": {
"TELEGRAM_WIDGET": "Telegram Login Widget"
},
"settingNames": {
"Telegram Widget Size": "اندازه ویجت",
"Telegram Widget Radius": "شعاع گوشه",
"Telegram Widget Userpic": "نمایش تصویر کاربر",
"Telegram Widget Request Access": "درخواست دسترسی"
}
}, },
"buttons": { "buttons": {
"color": "رنگ دکمه", "color": "رنگ دکمه",

View File

@@ -1762,6 +1762,10 @@
"Subscription Show Devices": "Показывать устройства", "Subscription Show Devices": "Показывать устройства",
"Telegram Stars Enabled": "Stars включены", "Telegram Stars Enabled": "Stars включены",
"Telegram Stars Rate Rub": "Курс Stars (₽)", "Telegram Stars Rate Rub": "Курс Stars (₽)",
"Telegram Widget Size": "Размер виджета",
"Telegram Widget Radius": "Скругление углов",
"Telegram Widget Userpic": "Показывать аватар",
"Telegram Widget Request Access": "Запрос доступа",
"Tribute Api Key": "API ключ", "Tribute Api Key": "API ключ",
"Tribute Donate Link": "Ссылка доната", "Tribute Donate Link": "Ссылка доната",
"Tribute Enabled": "Tribute включен", "Tribute Enabled": "Tribute включен",
@@ -2164,6 +2168,7 @@
"PAL24": "PAL24", "PAL24": "PAL24",
"WATA": "Wata", "WATA": "Wata",
"TELEGRAM": "Telegram", "TELEGRAM": "Telegram",
"TELEGRAM_WIDGET": "Telegram Login Widget",
"SUBSCRIPTIONS_CORE": "Основные", "SUBSCRIPTIONS_CORE": "Основные",
"SIMPLE_SUBSCRIPTION": "Простая подписка", "SIMPLE_SUBSCRIPTION": "Простая подписка",
"PERIODS": "Периоды", "PERIODS": "Периоды",

View File

@@ -1379,7 +1379,16 @@
"projectName": "项目名称", "projectName": "项目名称",
"quickPresets": "快速预设", "quickPresets": "快速预设",
"resetAllColors": "重置所有颜色", "resetAllColors": "重置所有颜色",
"statusColors": "状态颜色" "statusColors": "状态颜色",
"categories": {
"TELEGRAM_WIDGET": "Telegram Login Widget"
},
"settingNames": {
"Telegram Widget Size": "小部件大小",
"Telegram Widget Radius": "圆角半径",
"Telegram Widget Userpic": "显示头像",
"Telegram Widget Request Access": "请求访问权限"
}
}, },
"buttons": { "buttons": {
"color": "按钮颜色", "color": "按钮颜色",

View File

@@ -44,7 +44,7 @@ function TelegramLinkWidget() {
const redirectUrl = `${window.location.origin}/auth/link/telegram/callback?csrf_state=${encodeURIComponent(csrfState)}`; const redirectUrl = `${window.location.origin}/auth/link/telegram/callback?csrf_state=${encodeURIComponent(csrfState)}`;
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'https://telegram.org/js/telegram-widget.js?22'; script.src = 'https://telegram.org/js/telegram-widget.js?23';
script.setAttribute('data-telegram-login', botUsername); script.setAttribute('data-telegram-login', botUsername);
script.setAttribute('data-size', 'small'); script.setAttribute('data-size', 'small');
script.setAttribute('data-radius', '8'); script.setAttribute('data-radius', '8');

View File

@@ -485,10 +485,7 @@ export default function Login() {
</p> </p>
</div> </div>
) : ( ) : (
<TelegramLoginButton <TelegramLoginButton referralCode={referralCode || undefined} />
botUsername={botUsername}
referralCode={referralCode || undefined}
/>
)} )}
</div> </div>