@@ -26,6 +26,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
const [ oidcError , setOidcError ] = useState ( '' ) ;
const [ oidcError , setOidcError ] = useState ( '' ) ;
const [ scriptLoaded , setScriptLoaded ] = useState ( false ) ;
const [ scriptLoaded , setScriptLoaded ] = useState ( false ) ;
const [ scriptFailed , setScriptFailed ] = useState ( false ) ;
const [ scriptFailed , setScriptFailed ] = useState ( false ) ;
// Lets the user opt into deep-link auth manually, without waiting for the
// Telegram widget script to fail. See #<issue-number>.
const [ manualDeepLink , setManualDeepLink ] = useState ( false ) ;
const showDeepLinkUI = scriptFailed || manualDeepLink ;
const loginWithTelegramOIDC = useAuthStore ( ( s ) = > s . loginWithTelegramOIDC ) ;
const loginWithTelegramOIDC = useAuthStore ( ( s ) = > s . loginWithTelegramOIDC ) ;
// Deep link auth state
// Deep link auth state
@@ -168,7 +172,12 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
const loginWithTelegramWidget = useAuthStore ( ( s ) = > s . loginWithTelegramWidget ) ;
const loginWithTelegramWidget = useAuthStore ( ( s ) = > s . loginWithTelegramWidget ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( isOIDC || ! containerRef . current || ! botUsername || ! widgetConfig ) return ;
// showDeepLinkUI обязан быть в зависимостях: пока он true, контейнер
// виджета размонтирован, а при возврате «Назад к виджету» сам по с е б е
// эффект не перезапустится — на legacy-пути scriptLoaded не меняется
// никогда, поэтому ни одна из остальных зависимостей не дрогнет, и
// пользователь получил бы пустое место вместо виджета.
if ( showDeepLinkUI || isOIDC || ! containerRef . current || ! botUsername || ! widgetConfig ) return ;
const container = containerRef . current ;
const container = containerRef . current ;
while ( container . firstChild ) {
while ( container . firstChild ) {
@@ -229,7 +238,15 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
container . removeChild ( container . firstChild ) ;
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
// Deep link auth: request token and start polling with recursive setTimeout
const startDeepLinkAuth = useCallback ( async ( ) = > {
const startDeepLinkAuth = useCallback ( async ( ) = > {
@@ -336,9 +353,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
}
}
} , [ botUsername , loginWithDeepLink , navigate , t ] ) ;
} , [ botUsername , loginWithDeepLink , navigate , t ] ) ;
// Auto-start deep link auth when script fails (with cancellation for Strict Mode)
// Auto-start deep link auth when script fails OR the user opts in manually
// (with cancellation for Strict Mode)
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( scriptFailed && ! deepLinkToken && ! deepLinkPolling ) {
if ( showDeepLinkUI && ! deepLinkToken && ! deepLinkPolling ) {
let cancelled = false ;
let cancelled = false ;
const start = async ( ) = > {
const start = async ( ) = > {
if ( ! cancelled ) await startDeepLinkAuth ( ) ;
if ( ! cancelled ) await startDeepLinkAuth ( ) ;
@@ -348,7 +366,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
cancelled = true ;
cancelled = true ;
} ;
} ;
}
}
} , [ scriptFailed , deepLinkToken , deepLinkPolling , startDeepLinkAuth ] ) ;
} , [ showDeepLinkUI , deepLinkToken , deepLinkPolling , startDeepLinkAuth ] ) ;
// Resume polling immediately when user returns to the page (e.g. after confirming in Telegram)
// Resume polling immediately when user returns to the page (e.g. after confirming in Telegram)
// Browsers throttle setTimeout in background tabs, so polling may have stalled.
// Browsers throttle setTimeout in background tabs, so polling may have stalled.
@@ -431,8 +449,9 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
) ;
) ;
}
}
// Deep link fallback UI
// Deep link UI — shown either as an automatic fallback (widget script
if ( scriptFailed ) {
// failed to load) or because the user explicitly chose this method.
if ( showDeepLinkUI ) {
const resolvedBotUsername = deepLinkBotUsername || botUsername ;
const resolvedBotUsername = deepLinkBotUsername || botUsername ;
const deepLinkUrl = deepLinkToken
const deepLinkUrl = deepLinkToken
? ` https://t.me/ ${ resolvedBotUsername } ?start=webauth_ ${ deepLinkToken } `
? ` https://t.me/ ${ resolvedBotUsername } ?start=webauth_ ${ deepLinkToken } `
@@ -443,7 +462,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
< div className = "flex flex-col items-center space-y-5" >
< div className = "flex flex-col items-center space-y-5" >
{ /* Info message */ }
{ /* Info message */ }
< p className = "max-w-xs text-center text-xs text-dark-400" >
< p className = "max-w-xs text-center text-xs text-dark-400" >
{ t ( 'auth.telegramWidgetBlocked' ) }
{ t ( scriptFailed ? 'auth.telegramWidgetBlocked' : 'auth.deepLinkIntro' ) }
< / p >
< / p >
{ deepLinkToken && deepLinkUrl ? (
{ deepLinkToken && deepLinkUrl ? (
@@ -517,6 +536,27 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
{ t ( 'common.loading' ) }
{ t ( 'common.loading' ) }
< / div >
< / div >
) }
) }
{ /* Only offer a way back if the widget actually works — if the
script failed there is nothing to go back to. */ }
{ ! scriptFailed && (
< button
type = "button"
onClick = { ( ) = > {
if ( pollTimeoutRef . current ) clearTimeout ( pollTimeoutRef . current ) ;
if ( expireTimeoutRef . current ) clearTimeout ( expireTimeoutRef . current ) ;
pollTimeoutRef . current = null ;
expireTimeoutRef . current = null ;
setDeepLinkToken ( null ) ;
setDeepLinkPolling ( false ) ;
setDeepLinkError ( '' ) ;
setManualDeepLink ( false ) ;
} }
className = "text-xs text-dark-400 underline decoration-dotted transition-colors hover:text-dark-300"
>
{ t ( 'auth.backToWidget' ) }
< / button >
) }
< / div >
< / div >
) ;
) ;
}
}
@@ -551,24 +591,41 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
< div ref = { containerRef } className = "flex justify-center" / >
< div ref = { containerRef } className = "flex justify-center" / >
) }
) }
< div className = "text-center" >
{ /* Referral deep link — only relevant for not-yet-registered users who
< p className = "mb-2 text-xs text-dark-400" > { t ( 'auth.orOpenInApp' ) } < / p >
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 && (
< a
< a
href = {
href = { ` https://t.me/ ${ botUsername } ?start= ${ encodeURIComponent ( referralCode ) } ` }
referralCode
? ` https://t.me/ ${ botUsername } ?start= ${ encodeURIComponent ( referralCode ) } `
: ` https://t.me/ ${ botUsername } `
}
target = "_blank"
target = "_blank"
rel = "noopener noreferrer"
rel = "noopener noreferrer"
className = "text-telegram-blue inline-flex items-center text-sm hover:underline"
className = "text-telegram-blue inline-flex items-center text-x s hover:underline"
>
>
< svg className = "mr-1 h-4 w-4" viewBox = "0 0 24 24" fill = "currentColor" >
{ t ( 'auth.orOpenInApp' ) } & nbsp ; @ { botUsername }
< path d = "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" / >
< / svg >
@ { botUsername }
< / a >
< / a >
) }
< div className = "flex w-full max-w-xs items-center gap-3" >
< div className = "h-px flex-1 bg-dark-700" / >
< span className = "text-[11px] text-dark-500" > { t ( 'common.or' ) } < / span >
< div className = "h-px flex-1 bg-dark-700" / >
< / div >
< / div >
{ /* Manual opt-in: same deep-link flow used as the anti-block fallback,
offered here as an explicit equal alternative to the widget for
users who'd rather confirm in the bot than type a phone number. */ }
< button
type = "button"
onClick = { ( ) = > setManualDeepLink ( true ) }
className = "inline-flex items-center gap-2 rounded-lg border border-dark-700 bg-dark-800/50 px-6 py-3 text-sm font-medium text-dark-200 transition-colors hover:border-dark-600 hover:bg-dark-800"
>
< svg className = "h-5 w-5 text-telegram-blue" viewBox = "0 0 24 24" fill = "currentColor" >
< path d = "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" / >
< / svg >
{ t ( 'auth.loginWithBot' ) }
< / button >
< / div >
< / div >
) ;
) ;
}
}