diff --git a/src/components/PaytoDialog/index.tsx b/src/components/PaytoDialog/index.tsx
index 02ee6633..0be4cc78 100644
--- a/src/components/PaytoDialog/index.tsx
+++ b/src/components/PaytoDialog/index.tsx
@@ -88,7 +88,11 @@ export default function PaytoDialog({
setCompletedPaymentDetails(null)
closeModal()
releaseBodyScrollLocks()
+ return
}
+ // Nested on ZapDialog: Radix scroll-lock can block clicks on this dialog.
+ const id = requestAnimationFrame(() => releaseBodyScrollLocks())
+ return () => cancelAnimationFrame(id)
}, [open])
useEffect(() => {
diff --git a/src/components/ZapDialog/Nip57QuickZapButton.tsx b/src/components/ZapDialog/Nip57QuickZapButton.tsx
index 1fe15030..f6698ca1 100644
--- a/src/components/ZapDialog/Nip57QuickZapButton.tsx
+++ b/src/components/ZapDialog/Nip57QuickZapButton.tsx
@@ -3,30 +3,44 @@ import { Skeleton } from '@/components/ui/skeleton'
import { superchatLightningAccentClass } from '@/lib/superchat-ui'
import { cn } from '@/lib/utils'
import { Zap } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
export default function Nip57QuickZapButton({
label,
zapping,
onClick,
+ onCancel,
className
}: {
label: string
zapping: boolean
onClick: () => void
+ onCancel: () => void
className?: string
}) {
+ const { t } = useTranslation()
+
+ if (zapping) {
+ return (
+
+
+
+
+ )
+ }
+
return (
)
diff --git a/src/components/ZapDialog/index.tsx b/src/components/ZapDialog/index.tsx
index 8ae3892e..0023b15c 100644
--- a/src/components/ZapDialog/index.tsx
+++ b/src/components/ZapDialog/index.tsx
@@ -95,7 +95,7 @@ export default function ZapDialog({
return groupPaymentMethodsForDisplay(merged, senderPaytoTypes)
}, [recipientPayment, senderPaytoTypes])
- const { canQuickNip57Zap, quickZapLabel, sendQuickZap, zapping } = useNip57QuickZap({
+ const { canQuickNip57Zap, quickZapLabel, sendQuickZap, cancelZap, zapping } = useNip57QuickZap({
enabled: open,
recipientPubkey: pubkey,
referencedEvent: event,
@@ -108,7 +108,12 @@ export default function ZapDialog({
paymentGroups.length > 0 || canQuickNip57Zap ? (
<>
{canQuickNip57Zap ? (
-
+
) : null}
{paymentGroups.length > 0 ? (
{
+ if (enabled) {
+ ignoreResultRef.current = false
+ return
+ }
+ ignoreResultRef.current = true
+ setZapping(false)
+ }, [enabled])
+
const lightningAddressOptionsKey = useMemo(
() =>
buildOrderedZapLightningAddresses({
@@ -68,7 +78,6 @@ export function useNip57QuickZap(opts: {
const canQuickNip57Zap =
enabled &&
isLoggedIn &&
- isWalletConnected &&
defaultZapSats >= 1 &&
nip57Addresses !== null &&
nip57Addresses.length > 0 &&
@@ -84,10 +93,16 @@ export function useNip57QuickZap(opts: {
n: formatSatsGrouped(defaultZapSats)
})
+ const cancelZap = useCallback(() => {
+ ignoreResultRef.current = true
+ setZapping(false)
+ }, [])
+
const sendQuickZap = useCallback(() => {
if (!canQuickNip57Zap || zapping || !nip57Addresses?.length) return
checkLogin(async () => {
if (!pubkey) return
+ ignoreResultRef.current = false
try {
setZapping(true)
const zapResult = await lightning.zap(
@@ -102,7 +117,7 @@ export function useNip57QuickZap(opts: {
candidates: nip57Addresses
}
)
- if (!zapResult) return
+ if (ignoreResultRef.current || !zapResult) return
if (includePublicZapReceipt && zapResult.zapReceipt === null) {
toast.warning(
t(
@@ -142,6 +157,7 @@ export function useNip57QuickZap(opts: {
canQuickNip57Zap,
quickZapLabel,
sendQuickZap,
+ cancelZap,
zapping
}
}
diff --git a/src/lib/webln-payment.ts b/src/lib/webln-payment.ts
index c92893e7..161b6d4b 100644
--- a/src/lib/webln-payment.ts
+++ b/src/lib/webln-payment.ts
@@ -73,3 +73,28 @@ export async function sendWebLNPaymentWithRetry(
}
throw lastError
}
+
+const DEFAULT_WEBLN_PAYMENT_TIMEOUT_MS = 90_000
+
+/** Same as {@link sendWebLNPaymentWithRetry} but rejects when the wallet never responds. */
+export async function sendWebLNPaymentWithRetryAndTimeout(
+ provider: WebLNProvider,
+ invoice: string,
+ options?: { maxAttempts?: number; timeoutMs?: number }
+): Promise<{ preimage: string }> {
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_WEBLN_PAYMENT_TIMEOUT_MS
+ let timeoutId: ReturnType | undefined
+ try {
+ return await Promise.race([
+ sendWebLNPaymentWithRetry(provider, invoice, options?.maxAttempts ?? 3),
+ new Promise((_, reject) => {
+ timeoutId = setTimeout(
+ () => reject(new Error('Wallet payment timed out')),
+ timeoutMs
+ )
+ })
+ ])
+ } finally {
+ if (timeoutId !== undefined) clearTimeout(timeoutId)
+ }
+}
diff --git a/src/services/lightning.service.ts b/src/services/lightning.service.ts
index dc355441..8367bbd3 100644
--- a/src/services/lightning.service.ts
+++ b/src/services/lightning.service.ts
@@ -6,10 +6,7 @@ import {
} from '@/constants'
import { getProfileFromEvent, getZapInfoFromEvent } from '@/lib/event-metadata'
import { closeModal, init, launchPaymentModal } from '@getalby/bitcoin-connect-react'
-import {
- isNwcWalletServiceInfoError,
- sendWebLNPaymentWithRetry
-} from '@/lib/webln-payment'
+import { sendWebLNPaymentWithRetryAndTimeout } from '@/lib/webln-payment'
import { Invoice } from '@getalby/lightning-tools'
import { bech32 } from '@scure/base'
import { WebLNProvider } from '@webbtc/webln-types'
@@ -145,7 +142,7 @@ class LightningService {
if (this.provider) {
try {
- const { preimage } = await sendWebLNPaymentWithRetry(this.provider, pr)
+ const { preimage } = await sendWebLNPaymentWithRetryAndTimeout(this.provider, pr)
closeOuterModel?.()
const zapReceipt =
relays.length > 0
@@ -160,9 +157,7 @@ class LightningService {
onPaymentFlowComplete?.(result)
return result
} catch (error) {
- if (!isNwcWalletServiceInfoError(error)) {
- throw error
- }
+ logger.info('WebLN zap payment unavailable, falling back to invoice UI', { error })
}
}
@@ -302,15 +297,13 @@ class LightningService {
): Promise {
if (this.provider) {
try {
- const { preimage } = await sendWebLNPaymentWithRetry(this.provider, invoice)
+ const { preimage } = await sendWebLNPaymentWithRetryAndTimeout(this.provider, invoice)
closeOuterModel?.()
const result = { preimage, invoice }
onPaymentFlowComplete?.(result)
return result
} catch (error) {
- if (!isNwcWalletServiceInfoError(error)) {
- throw error
- }
+ logger.info('WebLN invoice payment unavailable, falling back to invoice UI', { error })
}
}