150 lines
4.9 KiB
TypeScript
150 lines
4.9 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import QRCode from 'react-qr-code'
|
||
import type { AlbyInvoice } from '@/types/alby'
|
||
import { getAlbyService, isWebLNAvailable } from '@/lib/alby'
|
||
import { AlbyInstaller } from './AlbyInstaller'
|
||
|
||
interface PaymentModalProps {
|
||
invoice: AlbyInvoice
|
||
onClose: () => void
|
||
onPaymentComplete: () => void
|
||
}
|
||
|
||
export function PaymentModal({ invoice, onClose, onPaymentComplete }: PaymentModalProps) {
|
||
const [copied, setCopied] = useState(false)
|
||
const [timeRemaining, setTimeRemaining] = useState<number | null>(null)
|
||
const paymentUrl = `lightning:${invoice.invoice}`
|
||
|
||
// Calculate time remaining until invoice expiry
|
||
useEffect(() => {
|
||
if (invoice.expiresAt) {
|
||
const updateTimeRemaining = () => {
|
||
const now = Math.floor(Date.now() / 1000)
|
||
const remaining = invoice.expiresAt - now
|
||
setTimeRemaining(remaining > 0 ? remaining : 0)
|
||
}
|
||
|
||
updateTimeRemaining()
|
||
const interval = setInterval(updateTimeRemaining, 1000)
|
||
|
||
return () => clearInterval(interval)
|
||
}
|
||
}, [invoice.expiresAt])
|
||
|
||
const formatTimeRemaining = (seconds: number): string => {
|
||
if (seconds <= 0) return 'Expired'
|
||
const minutes = Math.floor(seconds / 60)
|
||
const secs = seconds % 60
|
||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||
}
|
||
|
||
const handleCopy = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(invoice.invoice)
|
||
setCopied(true)
|
||
setTimeout(() => setCopied(false), 2000)
|
||
} catch (e) {
|
||
console.error('Failed to copy:', e)
|
||
}
|
||
}
|
||
|
||
const handleOpenWallet = async () => {
|
||
try {
|
||
const alby = getAlbyService()
|
||
|
||
if (!isWebLNAvailable()) {
|
||
throw new Error('WebLN is not available. Please install Alby or another Lightning wallet extension.')
|
||
}
|
||
|
||
await alby.enable()
|
||
await alby.sendPayment(invoice.invoice)
|
||
onPaymentComplete()
|
||
} catch (e) {
|
||
const error = e instanceof Error ? e : new Error(String(e))
|
||
console.error('Payment failed:', error)
|
||
|
||
if (error.message.includes('user rejected') || error.message.includes('cancelled')) {
|
||
return
|
||
}
|
||
|
||
alert(`Payment failed: ${error.message}`)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||
<AlbyInstaller />
|
||
<div className="flex justify-between items-center mb-4">
|
||
<div>
|
||
<h2 className="text-xl font-bold">Pay {invoice.amount} sats</h2>
|
||
{timeRemaining !== null && (
|
||
<p className={`text-sm ${timeRemaining <= 60 ? 'text-red-600 font-semibold' : 'text-gray-600'}`}>
|
||
Time remaining: {formatTimeRemaining(timeRemaining)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-gray-500 hover:text-gray-700 text-2xl"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<p className="text-sm text-gray-600 mb-2">Lightning Invoice:</p>
|
||
<div className="bg-gray-100 p-3 rounded break-all text-sm font-mono mb-4">
|
||
{invoice.invoice}
|
||
</div>
|
||
|
||
{/* QR Code */}
|
||
<div className="flex justify-center mb-4">
|
||
<div className="bg-white p-4 rounded-lg border-2 border-gray-200">
|
||
<QRCode
|
||
value={paymentUrl}
|
||
size={200}
|
||
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
|
||
viewBox="0 0 256 256"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<p className="text-xs text-gray-500 text-center mb-2">
|
||
Scan with your Lightning wallet to pay
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={handleCopy}
|
||
className="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium transition-colors"
|
||
>
|
||
{copied ? 'Copied!' : 'Copy Invoice'}
|
||
</button>
|
||
<button
|
||
onClick={handleOpenWallet}
|
||
className="flex-1 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg font-medium transition-colors"
|
||
>
|
||
Pay with Alby
|
||
</button>
|
||
</div>
|
||
|
||
{timeRemaining !== null && timeRemaining <= 0 && (
|
||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||
<p className="text-sm text-red-700 font-semibold mb-2">
|
||
This invoice has expired
|
||
</p>
|
||
<p className="text-xs text-red-600">
|
||
Please close this modal and try again to generate a new invoice.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<p className="text-xs text-gray-500 mt-4 text-center">
|
||
Payment will be automatically verified once completed
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|