From 91f0e9e2fcd0d9c3f3dc7f7e31b763244350f754 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 7 Mar 2026 02:37:12 +0300 Subject: [PATCH] feat: TelegramLoginButton with OIDC popup + legacy widget fallback - Add oidc_enabled/oidc_client_id to TelegramWidgetConfig interface and fallback - Add loginTelegramOIDC API method for id_token auth - Add loginWithTelegramOIDC to auth store - Rewrite TelegramLoginButton: OIDC popup when enabled, legacy widget otherwise - Extend Window.Telegram type with Login SDK in vite-env.d.ts --- src/api/auth.ts | 14 ++++ src/api/branding.ts | 4 + src/components/TelegramLoginButton.tsx | 100 +++++++++++++++++++++++-- src/store/auth.ts | 16 ++++ src/vite-env.d.ts | 15 ++++ 5 files changed, 142 insertions(+), 7 deletions(-) diff --git a/src/api/auth.ts b/src/api/auth.ts index 88fd9ec..228e8f7 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -49,6 +49,20 @@ export const authApi = { return response.data; }, + // Telegram OIDC authentication (popup flow with id_token) + loginTelegramOIDC: async ( + idToken: string, + campaignSlug?: string | null, + referralCode?: string | null, + ): Promise => { + const response = await apiClient.post('/cabinet/auth/telegram/oidc', { + id_token: idToken, + campaign_slug: campaignSlug || undefined, + referral_code: referralCode || undefined, + }); + return response.data; + }, + // Email login loginEmail: async ( email: string, diff --git a/src/api/branding.ts b/src/api/branding.ts index f146f58..235d511 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -29,6 +29,8 @@ export interface TelegramWidgetConfig { radius: number; userpic: boolean; request_access: boolean; + oidc_enabled: boolean; + oidc_client_id: string; } export interface AnalyticsCounters { @@ -273,6 +275,8 @@ export const brandingApi = { radius: 8, userpic: true, request_access: true, + oidc_enabled: false, + oidc_client_id: '', }; } }, diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index ad09aed..8077acb 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -1,7 +1,9 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { brandingApi, type TelegramWidgetConfig } from '../api/branding'; +import { useAuthStore } from '../store/auth'; +import { useNavigate } from 'react-router'; interface TelegramLoginButtonProps { referralCode?: string; @@ -9,7 +11,11 @@ interface TelegramLoginButtonProps { export default function TelegramLoginButton({ referralCode }: TelegramLoginButtonProps) { const { t } = useTranslation(); + const navigate = useNavigate(); const containerRef = useRef(null); + const [oidcLoading, setOidcLoading] = useState(false); + const [oidcError, setOidcError] = useState(''); + const loginWithTelegramOIDC = useAuthStore((s) => s.loginWithTelegramOIDC); const { data: widgetConfig } = useQuery({ queryKey: ['telegram-widget-config'], @@ -19,14 +25,68 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto const botUsername = widgetConfig?.bot_username || import.meta.env.VITE_TELEGRAM_BOT_USERNAME || ''; + const isOIDC = widgetConfig?.oidc_enabled && widgetConfig?.oidc_client_id; - // Load widget script + // OIDC callback handler + const handleOIDCCallback = useCallback( + async (data: { id_token?: string; error?: string }) => { + if (data.error || !data.id_token) { + setOidcError(data.error || t('auth.loginFailed')); + setOidcLoading(false); + return; + } + try { + setOidcLoading(true); + await loginWithTelegramOIDC(data.id_token); + navigate('/'); + } catch (err: unknown) { + const error = err as { response?: { data?: { detail?: string } } }; + setOidcError(error.response?.data?.detail || t('common.error')); + } finally { + setOidcLoading(false); + } + }, + [loginWithTelegramOIDC, navigate, t], + ); + + // Load OIDC script and init useEffect(() => { - if (!containerRef.current || !botUsername || !widgetConfig) return; + if (!isOIDC || !widgetConfig?.oidc_client_id) return; + + const scriptId = 'telegram-login-oidc-script'; + let script = document.getElementById(scriptId) as HTMLScriptElement | null; + + const initTelegramLogin = () => { + if (window.Telegram?.Login) { + window.Telegram.Login.init( + { + client_id: widgetConfig.oidc_client_id, + request_access: widgetConfig.request_access ? ['write'] : undefined, + lang: document.documentElement.lang || 'en', + }, + handleOIDCCallback, + ); + } + }; + + if (!script) { + script = document.createElement('script'); + script.id = scriptId; + script.src = 'https://oauth.telegram.org/js/telegram-login.js?3'; + script.async = true; + script.onload = initTelegramLogin; + document.head.appendChild(script); + } else { + // Script already loaded, just re-init + initTelegramLogin(); + } + }, [isOIDC, widgetConfig?.oidc_client_id, widgetConfig?.request_access, handleOIDCCallback]); + + // Legacy widget effect (only when NOT OIDC) + useEffect(() => { + if (isOIDC || !containerRef.current || !botUsername || !widgetConfig) return; const container = containerRef.current; - - // Clear previous widget while (container.firstChild) { container.removeChild(container.firstChild); } @@ -52,7 +112,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto container.removeChild(container.firstChild); } }; - }, [botUsername, widgetConfig]); + }, [isOIDC, botUsername, widgetConfig]); if (!botUsername || botUsername === 'your_bot') { return ( @@ -64,7 +124,33 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto return (
-
+ {/* OIDC mode: custom button that opens popup */} + {isOIDC ? ( +
+ + {oidcError &&

{oidcError}

} +
+ ) : ( + /* Legacy widget mode: iframe-based widget */ +
+ )}

{t('auth.orOpenInApp')}

diff --git a/src/store/auth.ts b/src/store/auth.ts index f7e555f..c147b31 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -37,6 +37,7 @@ interface AuthState { checkAdminStatus: () => Promise; loginWithTelegram: (initData: string) => Promise; loginWithTelegramWidget: (data: TelegramWidgetData) => Promise; + loginWithTelegramOIDC: (idToken: string) => Promise; loginWithEmail: (email: string, password: string) => Promise; loginWithOAuth: ( provider: string, @@ -285,6 +286,21 @@ export const useAuthStore = create()( await get().checkAdminStatus(); }, + loginWithTelegramOIDC: async (idToken) => { + const campaignSlug = consumeCampaignSlug(); + const referralCode = consumeReferralCode(); + const response = await authApi.loginTelegramOIDC(idToken, campaignSlug, referralCode); + tokenStorage.setTokens(response.access_token, response.refresh_token); + set({ + accessToken: response.access_token, + refreshToken: response.refresh_token, + user: response.user, + isAuthenticated: true, + pendingCampaignBonus: response.campaign_bonus || null, + }); + await get().checkAdminStatus(); + }, + loginWithEmail: async (email, password) => { const campaignSlug = consumeCampaignSlug(); const referralCode = consumeReferralCode(); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 7c33e43..c13e555 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -19,8 +19,23 @@ interface TelegramWebAppGlobal { offEvent?: (event: string, callback: () => void) => void; } +/** Telegram Login JS SDK — loaded from https://oauth.telegram.org/js/telegram-login.js */ +interface TelegramLoginGlobal { + init: ( + options: { + client_id: string; + request_access?: string[]; + lang?: string; + }, + callback: (data: { id_token?: string; user?: Record; error?: string }) => void, + ) => void; + open: () => void; + auth: () => void; +} + interface Window { Telegram?: { WebApp?: TelegramWebAppGlobal; + Login?: TelegramLoginGlobal; }; }