2026-01-15 11:31:09 +01:00

162 lines
4.8 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react'
import type { AlbyInvoice } from '@/types/alby'
import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
import { copyInvoiceToClipboard, openWalletForInvoice } from '@/lib/paymentModalHelpers'
import { AlbyInstaller } from './AlbyInstaller'
import { Modal } from './ui'
import { useToast } from './ui/ToastContainer'
import { t } from '@/lib/i18n'
import {
PaymentHeader,
InvoiceDisplay,
PaymentInstructions,
PaymentActions,
ExpiredNotice,
PaymentError,
} from './paymentModal/PaymentModalComponents'
interface PaymentModalProps {
invoice: AlbyInvoice
onClose: () => void
onPaymentComplete: () => void
}
function useInvoiceTimer(expiresAt?: number): number | null {
const [timeRemaining, setTimeRemaining] = useState<number | null>(null)
useEffect(() => {
if (!expiresAt) {
return
}
const updateTimeRemaining = (): void => {
const now = Math.floor(Date.now() / 1000)
const remaining = expiresAt - now
setTimeRemaining(remaining > 0 ? remaining : 0)
}
updateTimeRemaining()
const interval = setInterval(updateTimeRemaining, 1000)
return () => clearInterval(interval)
}, [expiresAt])
return timeRemaining
}
type PaymentModalState = {
copied: boolean
errorMessage: string | null
paymentUrl: string
timeRemaining: number | null
handleCopy: () => Promise<void>
handleOpenWallet: () => Promise<void>
}
function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => void, showToast: ((message: string, variant?: 'success' | 'info' | 'warning' | 'error', duration?: number) => void) | undefined): PaymentModalState {
const [copied, setCopied] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const paymentUrl = `lightning:${invoice.invoice}`
const timeRemaining = useInvoiceTimer(invoice.expiresAt)
const handleCopy = useCallback(
(): Promise<void> => copyInvoiceToClipboard({ invoice: invoice.invoice, setCopied, setErrorMessage, showToast: showToast ?? undefined }),
[invoice.invoice, showToast]
)
const handleOpenWallet = useCallback(
(): Promise<void> => openWalletForInvoice({ invoice: invoice.invoice, onPaymentComplete, setErrorMessage, showToast: showToast ?? undefined }),
[invoice.invoice, onPaymentComplete, showToast]
)
return { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet }
}
function useAlbyDetection(): boolean {
const [hasAlby, setHasAlby] = useState(false)
useEffect(() => {
const checkAlby = (): void => {
const alby = getAlbyService()
setHasAlby(isWebLNAvailable() && alby.isEnabled())
}
checkAlby()
const interval = setInterval(checkAlby, 1000)
return () => clearInterval(interval)
}, [])
return hasAlby
}
function PaymentModalContent({
invoice,
copied,
errorMessage,
paymentUrl,
timeRemaining,
handleCopy,
handleOpenWallet,
hasAlby,
}: {
invoice: AlbyInvoice
copied: boolean
errorMessage: string | null
paymentUrl: string
timeRemaining: number | null
handleCopy: () => Promise<void>
handleOpenWallet: () => void
hasAlby: boolean
}): React.ReactElement {
return (
<>
{!hasAlby && <AlbyInstaller />}
<PaymentHeader amount={invoice.amount} timeRemaining={timeRemaining} />
<PaymentInstructions hasAlby={hasAlby} />
<InvoiceDisplay invoiceText={invoice.invoice} paymentUrl={paymentUrl} />
<PaymentActions
copied={copied}
onCopy={handleCopy}
onOpenWallet={handleOpenWallet}
hasAlby={hasAlby}
/>
<ExpiredNotice show={timeRemaining !== null && timeRemaining <= 0} />
<PaymentError errorMessage={errorMessage} onRetry={handleOpenWallet} />
<p className="text-xs text-cyber-accent/70 mt-4 text-center">
{t('payment.modal.autoVerify')}
</p>
</>
)
}
export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentModalProps): React.ReactElement {
const { showToast } = useToast()
const { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet } =
usePaymentModalState(invoice, onPaymentComplete, showToast)
const hasAlby = useAlbyDetection()
const handleOpenWalletSync = (): void => {
void handleOpenWallet()
}
return (
<Modal
isOpen
onClose={onClose}
title={t('payment.modal.zapAmount', { amount: invoice.amount })}
size="medium"
aria-label={t('payment.modal.zapAmount', { amount: invoice.amount })}
>
<PaymentModalContent
invoice={invoice}
copied={copied}
errorMessage={errorMessage}
paymentUrl={paymentUrl}
timeRemaining={timeRemaining}
handleCopy={handleCopy}
handleOpenWallet={handleOpenWalletSync}
hasAlby={hasAlby}
/>
</Modal>
)
}