diff --git a/src/lib/react-remove-scroll-body-cleanup.ts b/src/lib/react-remove-scroll-body-cleanup.ts new file mode 100644 index 00000000..00b5fd62 --- /dev/null +++ b/src/lib/react-remove-scroll-body-cleanup.ts @@ -0,0 +1,37 @@ +/** + * Radix `Dialog` wraps the overlay in `react-remove-scroll`, which adds `block-interactivity-*` on + * `document.body` so only dialog “shard” nodes receive pointer events. Programmatic overlays that + * mount on `document.body` (e.g. Bitcoin Connect `bc-modal`) are not shards; if they appear while + * that class is still present, the UI can paint on top but ignore all clicks (notably after closing + * our Zap dialog from a secondary pane / sheet). + */ +export function stripReactRemoveScrollBodyLocks(): void { + if (typeof document === 'undefined') return + const body = document.body + const toRemove: string[] = [] + body.classList.forEach((c) => { + if (c.startsWith('block-interactivity-')) toRemove.push(c) + }) + for (const c of toRemove) { + body.classList.remove(c) + } +} + +/** Slightly longer than Radix dialog exit animation (`duration-200` in our `DialogContent`). */ +export const MS_AFTER_RADIX_DIALOG_FOR_EXTERNAL_MODAL = 280 + +/** + * Call `closeOuterModel` (e.g. close Zap `Dialog`), wait for scroll-lock cleanup when applicable, + * strip any stuck `block-interactivity-*` on `body`, then run `fn` (typically `launchPaymentModal`). + */ +export function runAfterReleasingRadixScrollLock( + closeOuterModel: (() => void) | undefined, + fn: () => void +): void { + closeOuterModel?.() + const ms = closeOuterModel != null ? MS_AFTER_RADIX_DIALOG_FOR_EXTERNAL_MODAL : 0 + window.setTimeout(() => { + stripReactRemoveScrollBodyLocks() + fn() + }, ms) +} diff --git a/src/services/lightning.service.ts b/src/services/lightning.service.ts index 180ac984..cf836cf4 100644 --- a/src/services/lightning.service.ts +++ b/src/services/lightning.service.ts @@ -20,6 +20,7 @@ import { queryService, replaceableEventService } from './client.service' import { getProfileFromEvent } from '@/lib/event-metadata' import { fetchWithTimeout } from '@/lib/fetch-with-timeout' import logger from '@/lib/logger' +import { runAfterReleasingRadixScrollLock } from '@/lib/react-remove-scroll-body-cleanup' export type TRecentSupporter = { pubkey: string; amount: number; comment?: string } @@ -103,10 +104,7 @@ class LightningService { } return new Promise((resolve) => { - // Close our Radix dialog first; opening bc-modal in the same turn can leave body - // pointer-events stuck so the payment UI is visible but inert (esp. from Sheet / secondary pane). - closeOuterModel?.() - window.setTimeout(() => { + runAfterReleasingRadixScrollLock(closeOuterModel, () => { let checkPaymentInterval: ReturnType | undefined let subCloser: SubCloser | undefined const { setPaid } = launchPaymentModal({ @@ -158,7 +156,7 @@ class LightningService { } ) } - }, 0) + }) }) } @@ -231,8 +229,7 @@ class LightningService { } return new Promise((resolve) => { - closeOuterModel?.() - window.setTimeout(() => { + runAfterReleasingRadixScrollLock(closeOuterModel, () => { let checkPaymentInterval: ReturnType | undefined let subCloser: SubCloser | undefined const { setPaid } = launchPaymentModal({ @@ -282,7 +279,7 @@ class LightningService { } ) } - }, 0) + }) }) } @@ -297,8 +294,7 @@ class LightningService { } return new Promise((resolve) => { - closeOuterModel?.() - window.setTimeout(() => { + runAfterReleasingRadixScrollLock(closeOuterModel, () => { launchPaymentModal({ invoice: invoice, onPaid: (response) => { @@ -308,7 +304,7 @@ class LightningService { resolve(null) } }) - }, 0) + }) }) }