Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
da4b2cb1db
  1. 4
      src/components/PaytoDialog/index.tsx
  2. 26
      src/components/ZapDialog/Nip57QuickZapButton.tsx
  3. 9
      src/components/ZapDialog/index.tsx
  4. 24
      src/hooks/useNip57QuickZap.ts
  5. 25
      src/lib/webln-payment.ts
  6. 17
      src/services/lightning.service.ts

4
src/components/PaytoDialog/index.tsx

@ -88,7 +88,11 @@ export default function PaytoDialog({ @@ -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(() => {

26
src/components/ZapDialog/Nip57QuickZapButton.tsx

@ -3,30 +3,44 @@ import { Skeleton } from '@/components/ui/skeleton' @@ -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 (
<div className={cn('mb-3 flex gap-2', className)}>
<Button type="button" className="min-w-0 flex-1 justify-start gap-2" disabled>
<Skeleton className="size-4 shrink-0 rounded-full" aria-hidden />
<span className="min-w-0 truncate">{label}</span>
</Button>
<Button type="button" variant="outline" className="shrink-0" onClick={onCancel}>
{t('Cancel')}
</Button>
</div>
)
}
return (
<Button
type="button"
className={cn('mb-3 w-full justify-start gap-2', className)}
onClick={onClick}
disabled={zapping}
>
{zapping ? (
<Skeleton className="size-4 shrink-0 rounded-full" aria-hidden />
) : (
<Zap className={cn('size-4 shrink-0', superchatLightningAccentClass)} aria-hidden />
)}
<Zap className={cn('size-4 shrink-0', superchatLightningAccentClass)} aria-hidden />
<span className="min-w-0 truncate">{label}</span>
</Button>
)

9
src/components/ZapDialog/index.tsx

@ -95,7 +95,7 @@ export default function ZapDialog({ @@ -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({ @@ -108,7 +108,12 @@ export default function ZapDialog({
paymentGroups.length > 0 || canQuickNip57Zap ? (
<>
{canQuickNip57Zap ? (
<Nip57QuickZapButton label={quickZapLabel} zapping={zapping} onClick={sendQuickZap} />
<Nip57QuickZapButton
label={quickZapLabel}
zapping={zapping}
onClick={sendQuickZap}
onCancel={cancelZap}
/>
) : null}
{paymentGroups.length > 0 ? (
<PaymentMethodsSection

24
src/hooks/useNip57QuickZap.ts

@ -6,7 +6,7 @@ import lightning from '@/services/lightning.service' @@ -6,7 +6,7 @@ import lightning from '@/services/lightning.service'
import noteStatsService from '@/services/note-stats.service'
import type { RecipientPaymentData } from '@/hooks/useRecipientAlternativePayments'
import { NostrEvent } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { formatSatsGrouped } from '@/lib/lightning'
@ -22,10 +22,20 @@ export function useNip57QuickZap(opts: { @@ -22,10 +22,20 @@ export function useNip57QuickZap(opts: {
const { t } = useTranslation()
const { pubkey, account, checkLogin } = useNostr()
const isLoggedIn = Boolean(pubkey && account && account.signerType !== 'npub')
const { isWalletConnected, defaultZapSats, defaultZapComment, includePublicZapReceipt } = useZap()
const { defaultZapSats, defaultZapComment, includePublicZapReceipt } = useZap()
const [zapping, setZapping] = useState(false)
const ignoreResultRef = useRef(false)
const enabled = opts.enabled ?? false
useEffect(() => {
if (enabled) {
ignoreResultRef.current = false
return
}
ignoreResultRef.current = true
setZapping(false)
}, [enabled])
const lightningAddressOptionsKey = useMemo(
() =>
buildOrderedZapLightningAddresses({
@ -68,7 +78,6 @@ export function useNip57QuickZap(opts: { @@ -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: { @@ -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: { @@ -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: { @@ -142,6 +157,7 @@ export function useNip57QuickZap(opts: {
canQuickNip57Zap,
quickZapLabel,
sendQuickZap,
cancelZap,
zapping
}
}

25
src/lib/webln-payment.ts

@ -73,3 +73,28 @@ export async function sendWebLNPaymentWithRetry( @@ -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<typeof setTimeout> | undefined
try {
return await Promise.race([
sendWebLNPaymentWithRetry(provider, invoice, options?.maxAttempts ?? 3),
new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() => reject(new Error('Wallet payment timed out')),
timeoutMs
)
})
])
} finally {
if (timeoutId !== undefined) clearTimeout(timeoutId)
}
}

17
src/services/lightning.service.ts

@ -6,10 +6,7 @@ import { @@ -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 { @@ -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 { @@ -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 { @@ -302,15 +297,13 @@ class LightningService {
): Promise<PaymentFlowResult> {
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 })
}
}

Loading…
Cancel
Save