606 lines
27 KiB
TypeScript
606 lines
27 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useCallback, useEffect } from 'react'
|
|
import { useAuthStore } from '@/lib/auth'
|
|
import TransakWidget from '../TransakWidget'
|
|
|
|
// ── Types ──
|
|
|
|
type PaymentMethod = 'card' | 'bank_transfer' | 'crypto_wallet'
|
|
type Step = 'amount' | 'recipient' | 'method' | 'review' | 'processing' | 'complete'
|
|
|
|
interface PaymentFlowProps {
|
|
/** Pre-fill from a payment request link */
|
|
prefill?: {
|
|
recipientEmail?: string
|
|
recipientWallet?: string
|
|
recipientName?: string
|
|
amount?: number
|
|
currency?: string
|
|
description?: string
|
|
flowId?: string
|
|
funnelId?: string
|
|
}
|
|
}
|
|
|
|
const CURRENCIES = [
|
|
{ code: 'USD', symbol: '$', name: 'US Dollar' },
|
|
{ code: 'EUR', symbol: '\u20ac', name: 'Euro' },
|
|
{ code: 'GBP', symbol: '\u00a3', name: 'British Pound' },
|
|
]
|
|
|
|
const PAYMENT_METHODS: { id: PaymentMethod; label: string; description: string; icon: string; eta: string; fee: string }[] = [
|
|
{ id: 'bank_transfer', label: 'Bank transfer', description: 'ACH, SEPA, or local bank', icon: 'bank', eta: '1\u20133 business days', fee: '~1%' },
|
|
{ id: 'card', label: 'Debit or credit card', description: 'Visa, Mastercard, Apple Pay', icon: 'card', eta: 'Instant', fee: '~3\u20135%' },
|
|
{ id: 'crypto_wallet', label: 'Crypto wallet', description: 'Send USDC directly on Base', icon: 'wallet', eta: 'Instant', fee: 'Gas only' },
|
|
]
|
|
|
|
// ── Helpers ──
|
|
|
|
function formatCurrency(amount: number, currency: string): string {
|
|
const c = CURRENCIES.find(cc => cc.code === currency)
|
|
return `${c?.symbol || ''}${amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
}
|
|
|
|
// ── Icons ──
|
|
|
|
function MethodIcon({ type }: { type: string }) {
|
|
if (type === 'bank') return (
|
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21v-8.25M15.75 21v-8.25M8.25 21v-8.25M3 9l9-6 9 6m-1.5 12V10.332A48.36 48.36 0 0012 9.75c-2.551 0-5.056.2-7.5.582V21M3 21h18M12 6.75h.008v.008H12V6.75z" />
|
|
</svg>
|
|
)
|
|
if (type === 'card') return (
|
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
|
|
</svg>
|
|
)
|
|
return (
|
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6.75A2.25 2.25 0 0019.5 4.5h-15A2.25 2.25 0 002.25 6.75V9" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function CheckIcon() {
|
|
return (
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function ArrowIcon() {
|
|
return (
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
// ── Progress Bar ──
|
|
|
|
function StepProgress({ current, steps }: { current: Step; steps: Step[] }) {
|
|
const visibleSteps = steps.filter(s => s !== 'processing' && s !== 'complete')
|
|
const currentIdx = visibleSteps.indexOf(current as typeof visibleSteps[number])
|
|
|
|
return (
|
|
<div className="flex items-center gap-1 mb-8">
|
|
{visibleSteps.map((step, i) => (
|
|
<div key={step} className="flex items-center gap-1 flex-1">
|
|
<div className={`h-1 flex-1 rounded-full transition-colors duration-300 ${
|
|
i <= currentIdx ? 'bg-emerald-500' : 'bg-slate-200'
|
|
}`} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ── Main Component ──
|
|
|
|
export default function PaymentFlow({ prefill }: PaymentFlowProps) {
|
|
const { isAuthenticated, username, did, login } = useAuthStore()
|
|
|
|
// Form state
|
|
const [step, setStep] = useState<Step>(prefill?.amount ? 'recipient' : 'amount')
|
|
const [amount, setAmount] = useState(prefill?.amount?.toString() || '')
|
|
const [currency, setCurrency] = useState(prefill?.currency || 'USD')
|
|
const [recipientEmail, setRecipientEmail] = useState(prefill?.recipientEmail || '')
|
|
const [recipientName, setRecipientName] = useState(prefill?.recipientName || '')
|
|
const [description, setDescription] = useState(prefill?.description || '')
|
|
const [method, setMethod] = useState<PaymentMethod | null>(null)
|
|
const [transakUrl, setTransakUrl] = useState<string | null>(null)
|
|
const [sessionId, setSessionId] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [senderEmail, setSenderEmail] = useState('')
|
|
|
|
// Auto-fill sender email from auth
|
|
useEffect(() => {
|
|
if (isAuthenticated && username) {
|
|
// Try to fetch email from /api/me
|
|
fetch('/api/me')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.user?.email) setSenderEmail(data.user.email)
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
}, [isAuthenticated, username])
|
|
|
|
const numericAmount = parseFloat(amount) || 0
|
|
const fee = method === 'card' ? numericAmount * 0.04 : method === 'bank_transfer' ? numericAmount * 0.01 : 0
|
|
const totalReceived = numericAmount - fee
|
|
const selectedCurrency = CURRENCIES.find(c => c.code === currency)
|
|
|
|
const allSteps: Step[] = ['amount', 'recipient', 'method', 'review', 'processing', 'complete']
|
|
|
|
// ── Step handlers ──
|
|
|
|
const handleAmountNext = useCallback(() => {
|
|
if (numericAmount < 1) { setError('Minimum amount is 1.00'); return }
|
|
setError(null)
|
|
setStep('recipient')
|
|
}, [numericAmount])
|
|
|
|
const handleRecipientNext = useCallback(() => {
|
|
if (!recipientEmail && !prefill?.recipientWallet) {
|
|
setError('Recipient email is required')
|
|
return
|
|
}
|
|
setError(null)
|
|
setStep('method')
|
|
}, [recipientEmail, prefill?.recipientWallet])
|
|
|
|
const handleMethodSelect = useCallback((m: PaymentMethod) => {
|
|
setMethod(m)
|
|
setError(null)
|
|
setStep('review')
|
|
}, [])
|
|
|
|
const handleConfirm = useCallback(async () => {
|
|
setError(null)
|
|
setStep('processing')
|
|
|
|
if (method === 'crypto_wallet') {
|
|
// For direct crypto, just show the wallet address to send to
|
|
setStep('complete')
|
|
return
|
|
}
|
|
|
|
try {
|
|
// Create Transak session via onramp API
|
|
const paymentMethod = method === 'bank_transfer'
|
|
? 'us_bank_transfer,sepa_bank_transfer,gbp_bank_transfer'
|
|
: 'credit_debit_card'
|
|
|
|
const walletAddress = prefill?.recipientWallet || '0x0000000000000000000000000000000000000000'
|
|
|
|
const res = await fetch('/api/proxy/onramp/session', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
walletAddress,
|
|
fiatAmount: numericAmount,
|
|
fiatCurrency: currency,
|
|
email: senderEmail || recipientEmail,
|
|
paymentMethod,
|
|
}),
|
|
})
|
|
|
|
if (!res.ok) throw new Error('Failed to create payment session')
|
|
|
|
const data = await res.json()
|
|
setSessionId(data.sessionId)
|
|
setTransakUrl(data.redirectUrl)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Payment failed')
|
|
setStep('review')
|
|
}
|
|
}, [method, numericAmount, currency, senderEmail, recipientEmail, prefill])
|
|
|
|
const handleTransakComplete = useCallback((orderId: string) => {
|
|
setTransakUrl(null)
|
|
setSessionId(orderId)
|
|
setStep('complete')
|
|
}, [])
|
|
|
|
const handleBack = useCallback(() => {
|
|
const idx = allSteps.indexOf(step)
|
|
if (idx > 0) setStep(allSteps[idx - 1])
|
|
}, [step, allSteps])
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 flex items-start justify-center pt-8 pb-16 px-4">
|
|
<div className="w-full max-w-[480px]">
|
|
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-3">
|
|
{step !== 'amount' && step !== 'complete' && (
|
|
<button onClick={handleBack} className="w-8 h-8 rounded-full hover:bg-slate-200 flex items-center justify-center transition-colors">
|
|
<svg className="w-5 h-5 text-slate-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
<h1 className="text-lg font-semibold text-slate-900">
|
|
{step === 'complete' ? 'Payment sent' : 'Send money'}
|
|
</h1>
|
|
</div>
|
|
{isAuthenticated ? (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-white rounded-full border border-slate-200 text-sm text-slate-600">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
|
{username}
|
|
</div>
|
|
) : (
|
|
<button onClick={login} className="px-3 py-1.5 bg-emerald-600 text-white rounded-full text-sm font-medium hover:bg-emerald-500 transition-colors">
|
|
Sign in
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{step !== 'complete' && step !== 'processing' && (
|
|
<StepProgress current={step} steps={allSteps} />
|
|
)}
|
|
|
|
{/* Error banner */}
|
|
{error && (
|
|
<div className="mb-4 px-4 py-3 bg-red-50 border border-red-200 rounded-xl text-sm text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Step 1: Amount ── */}
|
|
{step === 'amount' && (
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-6">
|
|
<label className="text-sm font-medium text-slate-500 block mb-4">You send</label>
|
|
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<input
|
|
type="text"
|
|
inputMode="decimal"
|
|
value={amount}
|
|
onChange={e => {
|
|
const v = e.target.value.replace(/[^0-9.]/g, '')
|
|
if (v.split('.').length <= 2) setAmount(v)
|
|
}}
|
|
placeholder="0.00"
|
|
className="flex-1 text-4xl font-light text-slate-900 outline-none bg-transparent placeholder:text-slate-300"
|
|
autoFocus
|
|
/>
|
|
<select
|
|
value={currency}
|
|
onChange={e => setCurrency(e.target.value)}
|
|
className="text-lg font-medium text-slate-700 bg-slate-100 px-3 py-2 rounded-xl border-0 outline-none cursor-pointer hover:bg-slate-200 transition-colors appearance-none"
|
|
>
|
|
{CURRENCIES.map(c => (
|
|
<option key={c.code} value={c.code}>{c.code}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{numericAmount > 0 && (
|
|
<p className="text-sm text-slate-400 mt-2">
|
|
{selectedCurrency?.symbol}{numericAmount.toFixed(2)} {currency} {'\u2192'} ~{numericAmount.toFixed(2)} USDC
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="border-t border-slate-100 p-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm text-slate-500">Recipient gets</span>
|
|
<span className="text-sm font-semibold text-emerald-600">
|
|
~{numericAmount > 0 ? numericAmount.toFixed(2) : '0.00'} USDC
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-slate-400">
|
|
1:1 stablecoin conversion. CRDT tokens are minted to the recipient's EncryptID wallet.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="border-t border-slate-100 p-4">
|
|
<button
|
|
onClick={handleAmountNext}
|
|
disabled={numericAmount < 1}
|
|
className="w-full py-3.5 bg-emerald-600 text-white rounded-xl text-sm font-semibold hover:bg-emerald-500 disabled:bg-slate-200 disabled:text-slate-400 transition-colors"
|
|
>
|
|
Continue
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Step 2: Recipient ── */}
|
|
{step === 'recipient' && (
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-6">
|
|
<label className="text-sm font-medium text-slate-500 block mb-1">Who are you sending to?</label>
|
|
<p className="text-xs text-slate-400 mb-5">They'll receive CRDT tokens in their EncryptID wallet</p>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="text-xs font-medium text-slate-600 block mb-1.5">Recipient email</label>
|
|
<input
|
|
type="email"
|
|
value={recipientEmail}
|
|
onChange={e => setRecipientEmail(e.target.value)}
|
|
placeholder="name@example.com"
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl text-sm text-slate-900 outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100 transition-all"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-medium text-slate-600 block mb-1.5">Name (optional)</label>
|
|
<input
|
|
type="text"
|
|
value={recipientName}
|
|
onChange={e => setRecipientName(e.target.value)}
|
|
placeholder="Their name"
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl text-sm text-slate-900 outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100 transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-medium text-slate-600 block mb-1.5">What's it for? (optional)</label>
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
placeholder="e.g. Dinner split, Project contribution"
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl text-sm text-slate-900 outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100 transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Your email (payer) */}
|
|
<div className="pt-2 border-t border-slate-100">
|
|
<label className="text-xs font-medium text-slate-600 block mb-1.5">Your email (for receipt)</label>
|
|
<input
|
|
type="email"
|
|
value={senderEmail}
|
|
onChange={e => setSenderEmail(e.target.value)}
|
|
placeholder="your@email.com"
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl text-sm text-slate-900 outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100 transition-all"
|
|
/>
|
|
<p className="text-xs text-slate-400 mt-1">We'll send you a confirmation receipt</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-slate-100 p-4">
|
|
<button
|
|
onClick={handleRecipientNext}
|
|
disabled={!recipientEmail && !prefill?.recipientWallet}
|
|
className="w-full py-3.5 bg-emerald-600 text-white rounded-xl text-sm font-semibold hover:bg-emerald-500 disabled:bg-slate-200 disabled:text-slate-400 transition-colors"
|
|
>
|
|
Continue
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Step 3: Payment Method ── */}
|
|
{step === 'method' && (
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-6 pb-2">
|
|
<label className="text-sm font-medium text-slate-500 block mb-1">How would you like to pay?</label>
|
|
<p className="text-xs text-slate-400 mb-5">
|
|
Sending {formatCurrency(numericAmount, currency)} to {recipientName || recipientEmail}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="px-4 pb-4 space-y-2">
|
|
{PAYMENT_METHODS.map(m => (
|
|
<button
|
|
key={m.id}
|
|
onClick={() => handleMethodSelect(m.id)}
|
|
className="w-full flex items-center gap-4 p-4 rounded-xl border border-slate-200 hover:border-emerald-400 hover:bg-emerald-50/50 transition-all text-left group"
|
|
>
|
|
<div className="w-12 h-12 rounded-xl bg-slate-100 group-hover:bg-emerald-100 flex items-center justify-center text-slate-500 group-hover:text-emerald-600 transition-colors flex-shrink-0">
|
|
<MethodIcon type={m.icon} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-semibold text-slate-900">{m.label}</div>
|
|
<div className="text-xs text-slate-500">{m.description}</div>
|
|
</div>
|
|
<div className="text-right flex-shrink-0">
|
|
<div className="text-xs font-medium text-slate-700">{m.fee}</div>
|
|
<div className="text-xs text-slate-400">{m.eta}</div>
|
|
</div>
|
|
<ArrowIcon />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Step 4: Review ── */}
|
|
{step === 'review' && method && (
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-6">
|
|
<label className="text-sm font-medium text-slate-500 block mb-5">Review and confirm</label>
|
|
|
|
{/* Transfer visualization */}
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="flex-1 text-center">
|
|
<div className="w-12 h-12 rounded-full bg-emerald-100 flex items-center justify-center mx-auto mb-2">
|
|
<span className="text-lg">{username?.charAt(0)?.toUpperCase() || '?'}</span>
|
|
</div>
|
|
<div className="text-xs font-medium text-slate-700 truncate">
|
|
{username || 'You'}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-lg font-semibold text-slate-900">
|
|
{formatCurrency(numericAmount, currency)}
|
|
</div>
|
|
<svg className="w-8 h-4 text-emerald-500" fill="none" viewBox="0 0 32 16">
|
|
<path d="M0 8h28m0 0l-6-6m6 6l-6 6" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</div>
|
|
|
|
<div className="flex-1 text-center">
|
|
<div className="w-12 h-12 rounded-full bg-violet-100 flex items-center justify-center mx-auto mb-2">
|
|
<span className="text-lg">{(recipientName || recipientEmail).charAt(0)?.toUpperCase() || '?'}</span>
|
|
</div>
|
|
<div className="text-xs font-medium text-slate-700 truncate">
|
|
{recipientName || recipientEmail}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Details */}
|
|
<div className="space-y-3 border-t border-slate-100 pt-4">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">You send</span>
|
|
<span className="font-medium text-slate-900">{formatCurrency(numericAmount, currency)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Fee ({method === 'card' ? '~4%' : method === 'bank_transfer' ? '~1%' : '0%'})</span>
|
|
<span className="text-slate-600">-{formatCurrency(fee, currency)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm border-t border-dashed border-slate-200 pt-3">
|
|
<span className="text-slate-500">They receive</span>
|
|
<span className="font-semibold text-emerald-600">~{totalReceived.toFixed(2)} CRDT</span>
|
|
</div>
|
|
|
|
{description && (
|
|
<div className="flex justify-between text-sm pt-1">
|
|
<span className="text-slate-500">Note</span>
|
|
<span className="text-slate-700 text-right max-w-[60%] truncate">{description}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Payment method</span>
|
|
<span className="text-slate-700">{PAYMENT_METHODS.find(m2 => m2.id === method)?.label}</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Arrives</span>
|
|
<span className="text-slate-700">{PAYMENT_METHODS.find(m2 => m2.id === method)?.eta}</span>
|
|
</div>
|
|
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Recipient email</span>
|
|
<span className="text-slate-700">{recipientEmail}</span>
|
|
</div>
|
|
|
|
{senderEmail && (
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Receipt to</span>
|
|
<span className="text-slate-700">{senderEmail}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-amber-50 border-t border-amber-100 px-6 py-3">
|
|
<p className="text-xs text-amber-700">
|
|
CRDT tokens are backed 1:1 by USDC stablecoins held in escrow on Base. The recipient will be notified by email.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="border-t border-slate-100 p-4">
|
|
<button
|
|
onClick={handleConfirm}
|
|
className="w-full py-3.5 bg-emerald-600 text-white rounded-xl text-sm font-semibold hover:bg-emerald-500 transition-colors"
|
|
>
|
|
Confirm and send {formatCurrency(numericAmount, currency)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Processing ── */}
|
|
{step === 'processing' && !transakUrl && (
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
|
|
<div className="w-12 h-12 border-4 border-emerald-200 border-t-emerald-600 rounded-full animate-spin mx-auto mb-4" />
|
|
<p className="text-sm font-medium text-slate-700">Setting up your payment...</p>
|
|
<p className="text-xs text-slate-400 mt-1">You'll be redirected to complete the payment</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Complete ── */}
|
|
{step === 'complete' && (
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-8 text-center">
|
|
<div className="w-16 h-16 rounded-full bg-emerald-100 flex items-center justify-center mx-auto mb-4">
|
|
<svg className="w-8 h-8 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-xl font-semibold text-slate-900 mb-1">Payment sent!</h2>
|
|
<p className="text-sm text-slate-500 mb-6">
|
|
{formatCurrency(numericAmount, currency)} is on its way to {recipientName || recipientEmail}
|
|
</p>
|
|
|
|
<div className="bg-slate-50 rounded-xl p-4 text-left space-y-2 mb-6">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Amount</span>
|
|
<span className="font-medium text-slate-900">{formatCurrency(numericAmount, currency)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">CRDT minted</span>
|
|
<span className="font-medium text-emerald-600">~{totalReceived.toFixed(2)} CRDT</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">To</span>
|
|
<span className="text-slate-700">{recipientEmail}</span>
|
|
</div>
|
|
{sessionId && (
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Reference</span>
|
|
<span className="text-slate-500 font-mono text-xs">{sessionId.slice(0, 12)}...</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-slate-400">
|
|
The recipient will receive an email notification when the tokens arrive in their wallet.
|
|
{senderEmail && ` A receipt has been sent to ${senderEmail}.`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-slate-100 p-4 flex gap-2">
|
|
<button
|
|
onClick={() => { setStep('amount'); setAmount(''); setRecipientEmail(''); setRecipientName(''); setDescription(''); setMethod(null); setSessionId(null) }}
|
|
className="flex-1 py-3 bg-slate-100 text-slate-700 rounded-xl text-sm font-medium hover:bg-slate-200 transition-colors"
|
|
>
|
|
Send another
|
|
</button>
|
|
<a
|
|
href="/"
|
|
className="flex-1 py-3 bg-emerald-600 text-white rounded-xl text-sm font-medium hover:bg-emerald-500 transition-colors text-center"
|
|
>
|
|
Done
|
|
</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<div className="mt-6 text-center">
|
|
<p className="text-xs text-slate-400">
|
|
Powered by <span className="font-medium">rSpace</span> Payment Infrastructure
|
|
</p>
|
|
</div>
|
|
|
|
{/* Transak Widget Modal */}
|
|
{transakUrl && (
|
|
<TransakWidget
|
|
widgetUrl={transakUrl}
|
|
onClose={() => { setTransakUrl(null); setStep('review') }}
|
|
onComplete={handleTransakComplete}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|