story-research-zapwall/components/paymentModal/PaymentModalComponents.tsx
2026-01-15 11:31:09 +01:00

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>
)
}