fix: prevent popup cascade when Telegram callback doesn't fire

- Add persistent popupClosed listener that resets state even after
  per-call listener is removed by timeout
- Increase safety timeout from 5s to 30s to give user time to interact
- On WebAppPopupOpened error, keep popupOpen=true to block retries
  instead of resetting and allowing infinite error loop
- Remove callback param from showPopup to avoid dual-listener conflicts
- Remove debug console logs
This commit is contained in:
c0mrade
2026-02-04 10:10:07 +03:00
parent 9284eca810
commit 2d00a5c21f

View File

@@ -184,6 +184,17 @@ function createDialogController(): DialogController {
const inTelegram = isInTelegramWebApp(); const inTelegram = isInTelegramWebApp();
let popupOpen = false; let popupOpen = false;
// Persistent listener: resets popupOpen whenever Telegram reports popup closed,
// even if our per-call listener was already removed.
if (inTelegram) {
const tg = getTelegram();
if (tg?.onEvent) {
tg.onEvent('popupClosed', (() => {
popupOpen = false;
}) as (...args: unknown[]) => void);
}
}
function showPopupSafe<T>( function showPopupSafe<T>(
params: Parameters<NonNullable<TelegramWebApp['showPopup']>>[0], params: Parameters<NonNullable<TelegramWebApp['showPopup']>>[0],
mapResult: (buttonId: string) => T, mapResult: (buttonId: string) => T,
@@ -203,8 +214,6 @@ function createDialogController(): DialogController {
popupOpen = true; popupOpen = true;
let settled = false; let settled = false;
console.log('[popup] opening', JSON.stringify(params));
const cleanup = () => { const cleanup = () => {
if (settled) return; if (settled) return;
settled = true; settled = true;
@@ -214,7 +223,6 @@ function createDialogController(): DialogController {
}; };
const onPopupClosed = ((...args: unknown[]) => { const onPopupClosed = ((...args: unknown[]) => {
console.log('[popup] popupClosed event', JSON.stringify(args));
if (settled) return; if (settled) return;
const event = args[0] as { button_id?: string } | undefined; const event = args[0] as { button_id?: string } | undefined;
const buttonId = event?.button_id ?? ''; const buttonId = event?.button_id ?? '';
@@ -223,24 +231,29 @@ function createDialogController(): DialogController {
}) as (...args: unknown[]) => void; }) as (...args: unknown[]) => void;
const timer = setTimeout(() => { const timer = setTimeout(() => {
console.log('[popup] timeout — force reset');
if (settled) return; if (settled) return;
cleanup(); cleanup();
resolve(mapResult('')); resolve(mapResult(''));
}, 5_000); }, 30_000);
tg.onEvent?.('popupClosed', onPopupClosed); tg.onEvent?.('popupClosed', onPopupClosed);
try { try {
tg.showPopup(params, (buttonId) => { tg.showPopup(params);
console.log('[popup] callback fired, buttonId=', buttonId);
if (settled) return;
cleanup();
resolve(mapResult(buttonId));
});
console.log('[popup] showPopup called OK');
} catch (e) { } catch (e) {
console.error('[popup] showPopup threw', e); // Telegram SDK says a popup is already open — don't reset popupOpen,
// so subsequent calls are silently blocked until the popup actually closes.
const isAlreadyOpen = e instanceof Error && e.message?.includes('WebAppPopupOpened');
if (isAlreadyOpen) {
settled = true;
clearTimeout(timer);
tg.offEvent?.('popupClosed', onPopupClosed);
// popupOpen stays true — the persistent listener will reset it
resolve(mapResult(''));
return;
}
cleanup(); cleanup();
resolve(fallback()); resolve(fallback());
} }