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;
}
export interface TelegramWidgetConfig {
bot_username: string;
size: 'large' | 'medium' | 'small';
radius: number;
userpic: boolean;
request_access: boolean;
}
export interface AnalyticsCounters {
yandex_metrika_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)
updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => {
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data);

View File

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

View File

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

View File

@@ -1681,7 +1681,16 @@
"projectName": "Project name",
"quickPresets": "Quick presets",
"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": {
"accentColor": "Accent color",

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ function TelegramLinkWidget() {
const redirectUrl = `${window.location.origin}/auth/link/telegram/callback?csrf_state=${encodeURIComponent(csrfState)}`;
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-size', 'small');
script.setAttribute('data-radius', '8');

View File

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