203 lines
5.8 KiB
TypeScript
203 lines
5.8 KiB
TypeScript
import { useMemo } from 'react'
|
|
import QRCode from 'react-qr-code'
|
|
import { Card, Button, Badge, ErrorState } from '../ui'
|
|
import { t } from '@/lib/i18n'
|
|
|
|
export function PaymentHeader({
|
|
amount,
|
|
timeRemaining,
|
|
}: {
|
|
amount: number
|
|
timeRemaining: number | null
|
|
}): React.ReactElement {
|
|
const timeLabel = useMemo((): string | null => {
|
|
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])
|
|
|
|
const isUrgent = timeRemaining !== null && timeRemaining <= 60
|
|
|
|
return (
|
|
<div className="mb-6">
|
|
<h2 className="text-2xl font-bold text-neon-cyan mb-2">{t('payment.modal.zapAmount', { amount })}</h2>
|
|
{timeLabel && (
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
variant={isUrgent ? 'error' : 'info'}
|
|
className={`text-base px-3 py-1 font-mono ${isUrgent ? 'animate-pulse' : ''}`}
|
|
aria-label={t('payment.modal.timeRemaining', { time: timeLabel })}
|
|
>
|
|
{timeLabel}
|
|
</Badge>
|
|
<span className={`text-sm ${isUrgent ? 'text-red-400 font-semibold' : 'text-cyber-accent/70'}`}>
|
|
{t('payment.modal.timeRemaining', { time: timeLabel })}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function InvoiceDisplay({ invoiceText, paymentUrl }: { invoiceText: string; paymentUrl: string }): React.ReactElement {
|
|
return (
|
|
<div className="mb-6">
|
|
<p className="text-sm text-cyber-accent mb-2">{t('payment.modal.lightningInvoice')}</p>
|
|
<Card variant="default" className="bg-cyber-darker border-neon-cyan/20 p-3 break-all text-sm font-mono mb-4 text-neon-cyan">
|
|
{invoiceText}
|
|
</Card>
|
|
<div className="flex justify-center mb-4">
|
|
<Card variant="default" className="bg-white p-6 border-4 border-neon-cyan/50 shadow-[0_0_20px_rgba(0,255,255,0.3)]">
|
|
<QRCode
|
|
value={paymentUrl}
|
|
size={300}
|
|
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
|
|
viewBox="0 0 256 256"
|
|
fgColor="#000000"
|
|
bgColor="#ffffff"
|
|
/>
|
|
</Card>
|
|
</div>
|
|
<p className="text-xs text-cyber-accent/70 text-center mb-2">{t('payment.modal.scanQr')}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function PaymentInstructions({ hasAlby }: { hasAlby: boolean }): React.ReactElement {
|
|
if (hasAlby) {
|
|
return (
|
|
<Card variant="default" className="bg-cyber-dark/50 mb-4 p-4">
|
|
<h3 className="text-sm font-semibold text-neon-cyan mb-2">{t('payment.modal.instructions.title')}</h3>
|
|
<ol className="list-decimal list-inside space-y-1 text-xs text-cyber-accent">
|
|
<li>{t('payment.modal.instructions.step1')}</li>
|
|
<li>{t('payment.modal.instructions.step2')}</li>
|
|
<li>{t('payment.modal.instructions.step3')}</li>
|
|
</ol>
|
|
</Card>
|
|
)
|
|
}
|
|
return (
|
|
<Card variant="default" className="bg-cyber-dark/50 mb-4 p-4">
|
|
<h3 className="text-sm font-semibold text-neon-cyan mb-2">{t('payment.modal.instructions.titleNoAlby')}</h3>
|
|
<ol className="list-decimal list-inside space-y-1 text-xs text-cyber-accent">
|
|
<li>{t('payment.modal.instructions.step1NoAlby')}</li>
|
|
<li>{t('payment.modal.instructions.step2NoAlby')}</li>
|
|
<li>{t('payment.modal.instructions.step3NoAlby')}</li>
|
|
</ol>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export function PaymentActionsWithAlby({
|
|
copied,
|
|
onCopy,
|
|
onOpenWallet,
|
|
}: {
|
|
copied: boolean
|
|
onCopy: () => Promise<void>
|
|
onOpenWallet: () => void
|
|
}): React.ReactElement {
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<Button
|
|
variant="success"
|
|
onClick={onOpenWallet}
|
|
className="w-full text-lg py-3 font-semibold"
|
|
size="large"
|
|
>
|
|
{t('payment.modal.payWithAlby')}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => {
|
|
void onCopy()
|
|
}}
|
|
className="w-full"
|
|
>
|
|
{copied ? t('payment.modal.copied') : t('payment.modal.copyInvoice')}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function PaymentActionsWithoutAlby({
|
|
copied,
|
|
onCopy,
|
|
}: {
|
|
copied: boolean
|
|
onCopy: () => Promise<void>
|
|
}): React.ReactElement {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => {
|
|
void onCopy()
|
|
}}
|
|
className="flex-1"
|
|
>
|
|
{copied ? t('payment.modal.copied') : t('payment.modal.copyInvoice')}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function PaymentActions({
|
|
copied,
|
|
onCopy,
|
|
onOpenWallet,
|
|
hasAlby,
|
|
}: {
|
|
copied: boolean
|
|
onCopy: () => Promise<void>
|
|
onOpenWallet: () => void
|
|
hasAlby: boolean
|
|
}): React.ReactElement {
|
|
if (hasAlby) {
|
|
return <PaymentActionsWithAlby copied={copied} onCopy={onCopy} onOpenWallet={onOpenWallet} />
|
|
}
|
|
return <PaymentActionsWithoutAlby copied={copied} onCopy={onCopy} />
|
|
}
|
|
|
|
export function ExpiredNotice({ show }: { show: boolean }): React.ReactElement | null {
|
|
if (!show) {
|
|
return null
|
|
}
|
|
return (
|
|
<Card variant="default" className="mt-4 p-3 bg-red-900/20 border-red-400/50">
|
|
<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>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export function PaymentError({
|
|
errorMessage,
|
|
onRetry,
|
|
}: {
|
|
errorMessage: string | null
|
|
onRetry?: () => void
|
|
}): React.ReactElement | null {
|
|
if (!errorMessage) {
|
|
return null
|
|
}
|
|
const errorObj = new Error(errorMessage)
|
|
return (
|
|
<div className="mt-3">
|
|
<ErrorState
|
|
message={errorMessage}
|
|
error={errorObj}
|
|
{...(onRetry !== undefined ? { onRetry } : {})}
|
|
showDocumentationLink
|
|
className="text-xs"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|