162 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|