203 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useMemo, useState, useCallback } from 'react'
import QRCode from 'react-qr-code'
import type { AlbyInvoice } from '@/types/alby'
import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
import { AlbyInstaller } from './AlbyInstaller'
import { t } from '@/lib/i18n'
interface PaymentModalProps {
invoice: AlbyInvoice
onClose: () => void
onPaymentComplete: () => void
}
function useInvoiceTimer(expiresAt?: number) {
const [timeRemaining, setTimeRemaining] = useState<number | null>(null)
useEffect(() => {
if (!expiresAt) {
return
}
const updateTimeRemaining = () => {
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
}
function PaymentHeader({
amount,
timeRemaining,
onClose,
}: {
amount: number
timeRemaining: number | null
onClose: () => void
}) {
const timeLabel = useMemo(() => {
if (timeRemaining === null) {
return null
}
if (timeRemaining <= 0) {
return t('payment.expired')
}
const minutes = Math.floor(timeRemaining / 60)
const secs = timeRemaining % 60
return `${minutes}:${secs.toString().padStart(2, '0')}`
}, [timeRemaining])
return (
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-xl font-bold text-neon-cyan">{t('payment.modal.zapAmount', { amount })}</h2>
{timeLabel && (
<p className={`text-sm ${timeRemaining !== null && timeRemaining <= 60 ? 'text-red-400 font-semibold' : 'text-cyber-accent/70'}`}>
{t('payment.modal.timeRemaining', { time: timeLabel })}
</p>
)}
</div>
<button onClick={onClose} className="text-cyber-accent hover:text-neon-cyan text-2xl transition-colors">
×
</button>
</div>
)
}
function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paymentUrl: string }) {
return (
<div className="mb-4">
<p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p>
<div className="bg-cyber-darker border border-neon-cyan/20 p-3 rounded break-all text-sm font-mono mb-4 text-neon-cyan">{invoiceText}</div>
<div className="flex justify-center mb-4">
<div className="bg-cyber-dark p-4 rounded-lg border-2 border-neon-cyan/30">
<QRCode
value={paymentUrl}
size={200}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
viewBox="0 0 256 256"
/>
</div>
</div>
<p className="text-xs text-cyber-accent/70 text-center mb-2">{t('payment.modal.scanQr')}</p>
</div>
)
}
function PaymentActions({
copied,
onCopy,
onOpenWallet,
}: {
copied: boolean
onCopy: () => Promise<void>
onOpenWallet: () => void
}) {
return (
<div className="flex gap-2">
<button
onClick={() => {
void onCopy()
}}
className="flex-1 px-4 py-2 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
>
{copied ? t('payment.modal.copied') : t('payment.modal.copyInvoice')}
</button>
<button
onClick={onOpenWallet}
className="flex-1 px-4 py-2 bg-neon-cyan/20 border border-neon-cyan/50 hover:bg-neon-cyan/30 hover:border-neon-cyan text-neon-cyan hover:text-neon-green rounded-lg font-medium transition-colors"
>
{t('payment.modal.payWithAlby')}
</button>
</div>
)
}
function ExpiredNotice({ show }: { show: boolean }) {
if (!show) {
return null
}
return (
<div className="mt-4 p-3 bg-red-900/20 border border-red-400/50 rounded-lg">
<p className="text-sm text-red-400 font-semibold mb-2">{t('payment.modal.invoiceExpired')}</p>
<p className="text-xs text-red-400/80">{t('payment.modal.invoiceExpiredHelp')}</p>
</div>
)
}
function usePaymentModalState(invoice: AlbyInvoice, onPaymentComplete: () => void) {
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(async () => {
try {
await navigator.clipboard.writeText(invoice.invoice)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (e) {
console.error('Failed to copy:', e)
setErrorMessage(t('payment.modal.copyFailed'))
}
}, [invoice.invoice])
const handleOpenWallet = useCallback(async () => {
try {
const alby = getAlbyService()
if (!isWebLNAvailable()) {
throw new Error(t('payment.modal.weblnNotAvailable'))
}
await alby.enable()
await alby.sendPayment(invoice.invoice)
onPaymentComplete()
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e))
if (error.message.includes('user rejected') || error.message.includes('cancelled')) {
return
}
console.error('Payment failed:', error)
setErrorMessage(error.message)
}
}, [invoice.invoice, onPaymentComplete])
return { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet }
}
export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentModalProps) {
const { copied, errorMessage, paymentUrl, timeRemaining, handleCopy, handleOpenWallet } =
usePaymentModalState(invoice, onPaymentComplete)
const handleOpenWalletSync = () => {
void handleOpenWallet()
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto shadow-glow-cyan">
<AlbyInstaller />
<PaymentHeader amount={invoice.amount} timeRemaining={timeRemaining} onClose={onClose} />
<InvoiceDisplay invoiceText={invoice.invoice} paymentUrl={paymentUrl} />
<PaymentActions
copied={copied}
onCopy={handleCopy}
onOpenWallet={handleOpenWalletSync}
/>
<ExpiredNotice show={timeRemaining !== null && timeRemaining <= 0} />
{errorMessage && (
<p className="text-xs text-red-400 mt-3 text-center" role="alert">
{errorMessage}
</p>
)}
<p className="text-xs text-cyber-accent/70 mt-4 text-center">
{t('payment.modal.autoVerify')}
</p>
</div>
</div>
)
}