diff --git a/app/api/proxy/onramp/session/route.ts b/app/api/proxy/onramp/session/route.ts
new file mode 100644
index 0000000..8d3354c
--- /dev/null
+++ b/app/api/proxy/onramp/session/route.ts
@@ -0,0 +1,38 @@
+/**
+ * Proxy: POST /api/proxy/onramp/session
+ *
+ * Forwards session creation to the onramp-service so the frontend
+ * can initiate a Transak payment session without CORS issues.
+ */
+
+import { NextRequest, NextResponse } from 'next/server'
+
+const ONRAMP_URL = process.env.ONRAMP_SERVICE_URL || 'http://payment-onramp:3002'
+
+export async function POST(req: NextRequest) {
+ try {
+ const body = await req.json()
+
+ const res = await fetch(`${ONRAMP_URL}/api/onramp/transak/session`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ walletAddress: body.walletAddress,
+ fiatAmount: body.fiatAmount,
+ fiatCurrency: body.fiatCurrency,
+ email: body.email,
+ }),
+ })
+
+ if (!res.ok) {
+ const err = await res.text()
+ return NextResponse.json({ error: err }, { status: res.status })
+ }
+
+ const data = await res.json()
+ return NextResponse.json(data)
+ } catch (error) {
+ console.error('Onramp session proxy error:', error)
+ return NextResponse.json({ error: 'Failed to create session' }, { status: 500 })
+ }
+}
diff --git a/app/pay/page.tsx b/app/pay/page.tsx
new file mode 100644
index 0000000..aa21b0a
--- /dev/null
+++ b/app/pay/page.tsx
@@ -0,0 +1,34 @@
+'use client'
+
+import { useSearchParams } from 'next/navigation'
+import { Suspense } from 'react'
+import PaymentFlow from '@/components/pay/PaymentFlow'
+
+function PayPage() {
+ const params = useSearchParams()
+
+ const prefill = {
+ amount: params.get('amount') ? parseFloat(params.get('amount')!) : undefined,
+ currency: params.get('currency') || undefined,
+ recipientEmail: params.get('to') || undefined,
+ recipientName: params.get('name') || undefined,
+ description: params.get('note') || undefined,
+ recipientWallet: params.get('wallet') || undefined,
+ flowId: params.get('flow') || undefined,
+ funnelId: params.get('funnel') || undefined,
+ }
+
+ return
+}
+
+export default function PayPageWrapper() {
+ return (
+
+
+
+ }>
+
+
+ )
+}
diff --git a/app/pay/request/page.tsx b/app/pay/request/page.tsx
new file mode 100644
index 0000000..09a0fc5
--- /dev/null
+++ b/app/pay/request/page.tsx
@@ -0,0 +1,7 @@
+'use client'
+
+import RequestFlow from '@/components/pay/RequestFlow'
+
+export default function RequestPage() {
+ return
+}
diff --git a/components/pay/PaymentFlow.tsx b/components/pay/PaymentFlow.tsx
new file mode 100644
index 0000000..10e1db9
--- /dev/null
+++ b/components/pay/PaymentFlow.tsx
@@ -0,0 +1,605 @@
+'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 (
+
+
+
+ )
+ if (type === 'card') return (
+
+
+
+ )
+ return (
+
+
+
+ )
+}
+
+function CheckIcon() {
+ return (
+
+
+
+ )
+}
+
+function ArrowIcon() {
+ return (
+
+
+
+ )
+}
+
+// ── Progress Bar ──
+
+function StepProgress({ current, steps }: { current: Step; steps: Step[] }) {
+ const visibleSteps = steps.filter(s => s !== 'processing' && s !== 'complete')
+ const currentIdx = visibleSteps.indexOf(current)
+
+ return (
+
+ {visibleSteps.map((step, i) => (
+
+ ))}
+
+ )
+}
+
+// ── Main Component ──
+
+export default function PaymentFlow({ prefill }: PaymentFlowProps) {
+ const { isAuthenticated, username, did, login } = useAuthStore()
+
+ // Form state
+ const [step, setStep] = useState(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(null)
+ const [transakUrl, setTransakUrl] = useState(null)
+ const [sessionId, setSessionId] = useState(null)
+ const [error, setError] = useState(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 (
+
+
+
+ {/* Header */}
+
+
+ {step !== 'amount' && step !== 'complete' && (
+
+
+
+
+
+ )}
+
+ {step === 'complete' ? 'Payment sent' : 'Send money'}
+
+
+ {isAuthenticated ? (
+
+
+ {username}
+
+ ) : (
+
+ Sign in
+
+ )}
+
+
+ {step !== 'complete' && step !== 'processing' && (
+
+ )}
+
+ {/* Error banner */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* ── Step 1: Amount ── */}
+ {step === 'amount' && (
+
+
+
You send
+
+
+ {
+ 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
+ />
+ 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 => (
+ {c.code}
+ ))}
+
+
+
+ {numericAmount > 0 && (
+
+ {selectedCurrency?.symbol}{numericAmount.toFixed(2)} {currency} {'\u2192'} ~{numericAmount.toFixed(2)} USDC
+
+ )}
+
+
+
+
+ Recipient gets
+
+ ~{numericAmount > 0 ? numericAmount.toFixed(2) : '0.00'} USDC
+
+
+
+ 1:1 stablecoin conversion. CRDT tokens are minted to the recipient's EncryptID wallet.
+
+
+
+
+
+ Continue
+
+
+
+ )}
+
+ {/* ── Step 2: Recipient ── */}
+ {step === 'recipient' && (
+
+
+
Who are you sending to?
+
They'll receive CRDT tokens in their EncryptID wallet
+
+
+
+ Recipient email
+ 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
+ />
+
+
+
+ Name (optional)
+ 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"
+ />
+
+
+
+ What's it for? (optional)
+ 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"
+ />
+
+
+ {/* Your email (payer) */}
+
+
Your email (for receipt)
+
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"
+ />
+
We'll send you a confirmation receipt
+
+
+
+
+
+
+ Continue
+
+
+
+ )}
+
+ {/* ── Step 3: Payment Method ── */}
+ {step === 'method' && (
+
+
+
How would you like to pay?
+
+ Sending {formatCurrency(numericAmount, currency)} to {recipientName || recipientEmail}
+
+
+
+
+ {PAYMENT_METHODS.map(m => (
+
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"
+ >
+
+
+
+
+
{m.label}
+
{m.description}
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* ── Step 4: Review ── */}
+ {step === 'review' && method && (
+
+
+
Review and confirm
+
+ {/* Transfer visualization */}
+
+
+
+ {username?.charAt(0)?.toUpperCase() || '?'}
+
+
+ {username || 'You'}
+
+
+
+
+
+ {formatCurrency(numericAmount, currency)}
+
+
+
+
+
+
+
+
+ {(recipientName || recipientEmail).charAt(0)?.toUpperCase() || '?'}
+
+
+ {recipientName || recipientEmail}
+
+
+
+
+ {/* Details */}
+
+
+ You send
+ {formatCurrency(numericAmount, currency)}
+
+
+ Fee ({method === 'card' ? '~4%' : method === 'bank_transfer' ? '~1%' : '0%'})
+ -{formatCurrency(fee, currency)}
+
+
+ They receive
+ ~{totalReceived.toFixed(2)} CRDT
+
+
+ {description && (
+
+ Note
+ {description}
+
+ )}
+
+
+ Payment method
+ {PAYMENT_METHODS.find(m2 => m2.id === method)?.label}
+
+
+
+ Arrives
+ {PAYMENT_METHODS.find(m2 => m2.id === method)?.eta}
+
+
+
+ Recipient email
+ {recipientEmail}
+
+
+ {senderEmail && (
+
+ Receipt to
+ {senderEmail}
+
+ )}
+
+
+
+
+
+ CRDT tokens are backed 1:1 by USDC stablecoins held in escrow on Base. The recipient will be notified by email.
+
+
+
+
+
+ Confirm and send {formatCurrency(numericAmount, currency)}
+
+
+
+ )}
+
+ {/* ── Processing ── */}
+ {step === 'processing' && !transakUrl && (
+
+
+
Setting up your payment...
+
You'll be redirected to complete the payment
+
+ )}
+
+ {/* ── Complete ── */}
+ {step === 'complete' && (
+
+
+
+
Payment sent!
+
+ {formatCurrency(numericAmount, currency)} is on its way to {recipientName || recipientEmail}
+
+
+
+
+ Amount
+ {formatCurrency(numericAmount, currency)}
+
+
+ CRDT minted
+ ~{totalReceived.toFixed(2)} CRDT
+
+
+ To
+ {recipientEmail}
+
+ {sessionId && (
+
+ Reference
+ {sessionId.slice(0, 12)}...
+
+ )}
+
+
+
+
+ The recipient will receive an email notification when the tokens arrive in their wallet.
+ {senderEmail && ` A receipt has been sent to ${senderEmail}.`}
+
+
+
+
+
+
{ 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
+
+
+ Done
+
+
+
+ )}
+
+ {/* Footer */}
+
+
+ Powered by rSpace Payment Infrastructure
+
+
+
+ {/* Transak Widget Modal */}
+ {transakUrl && (
+
{ setTransakUrl(null); setStep('review') }}
+ onComplete={handleTransakComplete}
+ />
+ )}
+
+
+ )
+}
diff --git a/components/pay/RequestFlow.tsx b/components/pay/RequestFlow.tsx
new file mode 100644
index 0000000..d0130d4
--- /dev/null
+++ b/components/pay/RequestFlow.tsx
@@ -0,0 +1,246 @@
+'use client'
+
+import { useState, useCallback } from 'react'
+import { useAuthStore } from '@/lib/auth'
+
+type Step = 'details' | 'share'
+
+interface RequestFlowProps {
+ onCreated?: (link: string) => void
+}
+
+const CURRENCIES = [
+ { code: 'USD', symbol: '$' },
+ { code: 'EUR', symbol: '\u20ac' },
+ { code: 'GBP', symbol: '\u00a3' },
+]
+
+export default function RequestFlow({ onCreated }: RequestFlowProps) {
+ const { isAuthenticated, username, login } = useAuthStore()
+
+ const [step, setStep] = useState('details')
+ const [amount, setAmount] = useState('')
+ const [currency, setCurrency] = useState('USD')
+ const [description, setDescription] = useState('')
+ const [recipientEmail, setRecipientEmail] = useState('')
+ const [paymentLink, setPaymentLink] = useState('')
+ const [copied, setCopied] = useState(false)
+
+ const numericAmount = parseFloat(amount) || 0
+ const selectedCurrency = CURRENCIES.find(c => c.code === currency)
+
+ const handleCreate = useCallback(() => {
+ // Build the payment link with prefilled params
+ const params = new URLSearchParams()
+ if (numericAmount > 0) params.set('amount', numericAmount.toString())
+ if (currency !== 'USD') params.set('currency', currency)
+ if (recipientEmail) params.set('to', recipientEmail)
+ if (username) params.set('name', username)
+ if (description) params.set('note', description)
+
+ const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'https://rfunds.online'
+ const link = `${baseUrl}/pay?${params.toString()}`
+ setPaymentLink(link)
+ setStep('share')
+ onCreated?.(link)
+ }, [numericAmount, currency, recipientEmail, username, description, onCreated])
+
+ const handleCopy = useCallback(() => {
+ navigator.clipboard.writeText(paymentLink)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }, [paymentLink])
+
+ return (
+
+
+
+ {/* Header */}
+
+
+ {step === 'share' ? 'Share payment link' : 'Request money'}
+
+ {isAuthenticated ? (
+
+
+ {username}
+
+ ) : (
+
+ Sign in
+
+ )}
+
+
+ {/* ── Step 1: Details ── */}
+ {step === 'details' && (
+
+
+
How much do you want to receive?
+
+
+ {
+ 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
+ />
+ 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 => (
+ {c.code}
+ ))}
+
+
+
+
+
+
+
+
+ Create payment link
+
+
+
+ )}
+
+ {/* ── Step 2: Share ── */}
+ {step === 'share' && (
+
+
+
+
+
Payment link ready
+
+ Share this link to receive {selectedCurrency?.symbol}{numericAmount.toFixed(2)} {currency}
+
+
+ {/* Link display */}
+
+
+
+
+ {copied ? 'Copied!' : 'Copy'}
+
+
+
+
+ {/* Summary */}
+
+
+ Amount
+ {selectedCurrency?.symbol}{numericAmount.toFixed(2)} {currency}
+
+ {description && (
+
+ Note
+ {description}
+
+ )}
+ {recipientEmail && (
+
+ Notify
+ {recipientEmail}
+
+ )}
+
+
+ {/* Share options */}
+
+
+ Email link
+
+
{
+ if (navigator.share) {
+ navigator.share({
+ title: `Payment request: ${selectedCurrency?.symbol}${numericAmount.toFixed(2)}`,
+ text: description || `Payment of ${selectedCurrency?.symbol}${numericAmount.toFixed(2)} ${currency}`,
+ url: paymentLink,
+ }).catch(() => {})
+ } else {
+ handleCopy()
+ }
+ }}
+ className="flex-1 py-2.5 bg-slate-100 text-slate-700 rounded-xl text-xs font-medium hover:bg-slate-200 transition-colors"
+ >
+ Share
+
+
+
+
+
+ { setStep('details'); setAmount(''); setDescription(''); setPaymentLink('') }}
+ className="w-full py-3 bg-white border border-slate-200 text-slate-700 rounded-xl text-sm font-medium hover:bg-slate-50 transition-colors"
+ >
+ Create another request
+
+
+
+ )}
+
+ {/* Footer */}
+
+
+ Powered by rSpace Payment Infrastructure
+
+
+
+
+ )
+}