diff --git a/src/app/components/telegram-login/telegram-login.component.ts b/src/app/components/telegram-login/telegram-login.component.ts index 689e373..e27854c 100644 --- a/src/app/components/telegram-login/telegram-login.component.ts +++ b/src/app/components/telegram-login/telegram-login.component.ts @@ -19,22 +19,53 @@ export class TelegramLoginComponent implements OnDestroy { webSessionID = signal(''); qrStatus = signal<'loading' | 'ready' | 'expired' | 'error'>('loading'); encodedQrUrl = computed(() => encodeURIComponent(this.loginUrl())); + awaitingTelegramReturn = signal(false); private readonly pollIntervalMs = 5000; private pollTimer?: ReturnType; + private mobileFallbackTimeout?: ReturnType; + private readonly handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + this.clearMobileFallbackTimeout(); + return; + } + + this.resumeLoginPolling(); + }; + private readonly handleWindowFocus = () => { + this.resumeLoginPolling(); + }; + private readonly handlePageShow = () => { + this.resumeLoginPolling(); + }; constructor() { effect(() => { if (this.showDialog()) { this.initQrLogin(); } else { + this.awaitingTelegramReturn.set(false); + this.clearMobileFallbackTimeout(); this.stopPolling(); } }); + + if (typeof window !== 'undefined') { + document.addEventListener('visibilitychange', this.handleVisibilityChange); + window.addEventListener('focus', this.handleWindowFocus); + window.addEventListener('pageshow', this.handlePageShow); + } } ngOnDestroy(): void { + this.clearMobileFallbackTimeout(); this.stopPolling(); + + if (typeof window !== 'undefined') { + document.removeEventListener('visibilitychange', this.handleVisibilityChange); + window.removeEventListener('focus', this.handleWindowFocus); + window.removeEventListener('pageshow', this.handlePageShow); + } } close(): void { @@ -43,16 +74,21 @@ export class TelegramLoginComponent implements OnDestroy { } openTelegramLogin(): void { + const webSessionID = this.webSessionID(); const url = this.loginUrl(); - if (!url) return; + if (!url || !webSessionID) return; - window.open(url, '_blank'); if (!this.pollTimer) { - const webSessionID = this.webSessionID(); - if (webSessionID) { - this.startPolling(webSessionID); - } + this.startPolling(webSessionID); } + + if (this.isMobileBrowser()) { + this.awaitingTelegramReturn.set(true); + this.launchTelegramOnMobile(webSessionID, url); + return; + } + + window.open(url, '_blank', 'noopener,noreferrer'); } refreshQr(): void { @@ -94,6 +130,8 @@ export class TelegramLoginComponent implements OnDestroy { this.authService.checkSessionOnce(webSessionID).subscribe({ next: (session) => { if (session?.active) { + this.awaitingTelegramReturn.set(false); + this.clearMobileFallbackTimeout(); this.stopPolling(); this.authService.onTelegramLoginComplete(); } @@ -111,4 +149,57 @@ export class TelegramLoginComponent implements OnDestroy { this.pollTimer = undefined; } } + + private resumeLoginPolling(): void { + if (!this.showDialog() || !this.awaitingTelegramReturn()) { + return; + } + + const webSessionID = this.webSessionID(); + if (!webSessionID) { + this.awaitingTelegramReturn.set(false); + return; + } + + if (!this.pollTimer) { + this.startPolling(webSessionID); + } + + this.authService.checkSessionOnce(webSessionID).subscribe(session => { + if (session?.active) { + this.awaitingTelegramReturn.set(false); + this.stopPolling(); + this.authService.onTelegramLoginComplete(); + } + }); + } + + private launchTelegramOnMobile(webSessionID: string, fallbackUrl: string): void { + this.clearMobileFallbackTimeout(); + this.mobileFallbackTimeout = setTimeout(() => { + if (typeof document !== 'undefined' && document.visibilityState === 'visible') { + window.location.assign(fallbackUrl); + } + }, 1200); + + window.location.assign(this.authService.getTelegramAppLoginUrl(webSessionID)); + } + + private clearMobileFallbackTimeout(): void { + if (this.mobileFallbackTimeout) { + clearTimeout(this.mobileFallbackTimeout); + this.mobileFallbackTimeout = undefined; + } + } + + private isMobileBrowser(): boolean { + if (typeof navigator === 'undefined') { + return false; + } + + const userAgent = navigator.userAgent || navigator.vendor; + const isTouchMac = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; + + return /Android|iPhone|iPad|iPod|IEMobile|Opera Mini/i.test(userAgent) || isTouchMac; + } } diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index b6dcef8..59849b3 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -84,10 +84,16 @@ export class AuthService { /** Generate the Telegram login URL for bot-based auth */ getTelegramLoginUrl(webSessionID = this.generateGuid()): string { - const botUsername = (environment as Record)['telegramBot'] as string || 'DexarSupport_bot'; + const botUsername = this.getTelegramBotUsername(); return `https://t.me/${botUsername}?start=${encodeURIComponent(webSessionID)}`; } + /** Generate the Telegram app deep link for mobile auth handoff. */ + getTelegramAppLoginUrl(webSessionID: string): string { + const botUsername = this.getTelegramBotUsername(); + return `tg://resolve?domain=${encodeURIComponent(botUsername)}&start=${encodeURIComponent(webSessionID)}`; + } + /** Get QR code data URL for Telegram login */ getTelegramQrUrl(): string { return this.getTelegramLoginUrl(); @@ -348,4 +354,8 @@ export class AuthService { document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`; } + + private getTelegramBotUsername(): string { + return (environment as Record)['telegramBot'] as string || 'DexarSupport_bot'; + } }