-
Exchange rate
-
- 1 cNGN = {formatCurrency(transaction.exchangeRate, 'NGN')}
-
+ {/* Action Footer (Non-printable) */}
+
+
+
+
+
-
-
Fees paid
-
{formatCurrency(transaction.fees, 'NGN')} (1%)
+
+
+ Receipt automatically sent to: {transaction.email}
+
-
- Total settlement time
- {transaction.totalTime}
+
+
+
+ {/* Bank Statement Match */}
+
+
+
+
+
Check your bank app for:
+
+
+ Credit Alert:{' '}
+
+ {formatCurrency(transaction.amount, 'NGN')}
+
+
+
+ From: AFRAMP TECHNOLOGIES LTD
+
+
+ Reference:{' '}
+ {transaction.reference}
+
+
-
+
+ If you don't see this, wait 24 hours or{' '}
+
+ Contact Support
+
+
+
- {/* Action Footer (Non-printable) */}
-
-
-
-
- {/* Bank Statement Match */}
-
-
-
-
-
Check your bank app for:
-
-
- Credit Alert:{' '}
-
- {formatCurrency(transaction.amount, 'NGN')}
-
-
-
- From: AFRAMP TECHNOLOGIES LTD
-
-
- Reference:{' '}
- {transaction.reference}
-
-
+ {/* Feedback Section */}
+
+
How was your experience?
+
+ {[1, 2, 3, 4, 5].map((s) => (
+ setRating(s)}
+ className={`p-2 transition-transform hover:scale-125 ${rating >= s ? 'text-yellow-500' : 'text-muted-foreground/30'}`}
+ >
+ = s ? 'fill-current' : ''}`} />
+
+ ))}
-
-
- If you don't see this, wait 24 hours or{' '}
-
- Contact Support
-
-
-
-
- {/* What's Next */}
-
-
-
-
- Withdraw More
-
-
-
-
-
- History
-
-
-
-
-
- Dashboard
-
-
-
-
- {/* Feedback Section */}
-
-
How was your experience?
-
- {[1, 2, 3, 4, 5].map((s) => (
- setRating(s)}
- className={`p-2 transition-transform hover:scale-125 ${rating >= s ? 'text-yellow-500' : 'text-muted-foreground/30'}`}
- >
- = s ? 'fill-current' : ''}`} />
-
- ))}
-
-
- {rating > 0 && (
-
-
+ {rating > 0 && (
+
- Add quick rating (optional)
-
-
- )}
-
+
+ Add quick rating (optional)
+
+
+ )}
+
+
-
+
)
}
diff --git a/app/page.tsx b/app/page.tsx
index 9abde97..afd0b29 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -5,7 +5,6 @@ import { LogoMarquee } from '@/components/logo-marquee'
import { BlockchainNetworks } from '@/components/blockchain-networks'
import { BentoGrid } from '@/components/bento-grid'
import { HowItWorks } from '@/components/how-it-works'
-import { Pricing } from '@/components/pricing'
import { FinalCTA } from '@/components/final-cta'
import { Footer } from '@/components/footer'
@@ -19,7 +18,6 @@ export default function Home() {
-
diff --git a/app/receive/page.tsx b/app/receive/page.tsx
new file mode 100644
index 0000000..1e1f684
--- /dev/null
+++ b/app/receive/page.tsx
@@ -0,0 +1,5 @@
+import { ReceivePageClient } from '@/components/receive/receive-page-client'
+
+export default function ReceivePage() {
+ return
+}
diff --git a/app/send/page.tsx b/app/send/page.tsx
new file mode 100644
index 0000000..f1744dc
--- /dev/null
+++ b/app/send/page.tsx
@@ -0,0 +1,5 @@
+import { SendPageClient } from '@/components/send/send-page-client'
+
+export default function SendPage() {
+ return
+}
diff --git a/app/wallet-setup/page.tsx b/app/wallet-setup/page.tsx
new file mode 100644
index 0000000..a668424
--- /dev/null
+++ b/app/wallet-setup/page.tsx
@@ -0,0 +1,45 @@
+import Link from 'next/link'
+import { ArrowLeft, Shield } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+
+export default function WalletSetupPage() {
+ return (
+
+
+
+
+
+
+ Wallet Setup
+
+
+ Secure your wallet in 3 quick steps
+
+
+ Your feature highlights are complete. Next, we will generate and verify your recovery
+ phrase so your funds stay protected.
+
+
+
+
+
+ Continue
+
+
+
+ )
+}
diff --git a/app/welcome/page.tsx b/app/welcome/page.tsx
new file mode 100644
index 0000000..87d0243
--- /dev/null
+++ b/app/welcome/page.tsx
@@ -0,0 +1,55 @@
+import Link from 'next/link'
+import { ArrowRight, ShieldCheck } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+
+export default function WelcomePage() {
+ return (
+
+
+
+
+
+ FINANCE REDEFINED
+
+
+
+
+ Aframp
+
+ Welcome to the future of finance
+
+
+ Securely manage your digital assets, trade with confidence, and build your wealth with
+ Aframp.
+
+
+
+
+
+ Get Started
+
+
+
+
+ Log In
+
+
+
+
+ )
+}
diff --git a/components/bills/__tests__/category-page.test.tsx b/components/bills/__tests__/category-page.test.tsx
new file mode 100644
index 0000000..8934bd9
--- /dev/null
+++ b/components/bills/__tests__/category-page.test.tsx
@@ -0,0 +1,55 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { CategoryPageClient } from '@/components/bills/category-page-client'
+import '@testing-library/jest-dom'
+
+// Mock the hooks
+jest.mock('@/hooks/use-bills-data', () => ({
+ useBillsData: () => ({
+ categories: [{ id: 'electricity', name: 'Electricity', icon: '⚡' }],
+ recentBillers: [
+ {
+ id: 'aedc',
+ name: 'AEDC',
+ category: 'electricity',
+ description: 'Power provider',
+ logo: '💡',
+ },
+ ],
+ loading: false,
+ }),
+}))
+
+jest.mock('@/hooks/use-wallet-connection', () => ({
+ useWalletConnection: () => ({
+ address: 'GABC...XYZ',
+ connected: true,
+ }),
+}))
+
+describe('CategoryPageClient', () => {
+ it('renders the correct category title', () => {
+ render(
)
+ // Use getAllByText because it appears in header and breadcrumbs, then check the H1 specifically
+ const headings = screen.getAllByText(/Electricity/i)
+ expect(headings[0]).toBeInTheDocument()
+ })
+
+ it('filters billers based on search input', () => {
+ render(
)
+
+ expect(screen.getByText('AEDC')).toBeInTheDocument()
+
+ // Updated to match the actual placeholder: "Search for a provider..."
+ const searchInput = screen.getByPlaceholderText(/Search for a provider/i)
+ fireEvent.change(searchInput, { target: { value: 'NonExistent' } })
+
+ expect(screen.queryByText('AEDC')).not.toBeInTheDocument()
+ })
+
+ it('navigates back to the bills landing page', () => {
+ render(
)
+ // Matches "Back to Categories" instead of just "Back"
+ const backButton = screen.getByRole('link', { name: /back/i })
+ expect(backButton).toHaveAttribute('href', '/bills')
+ })
+})
diff --git a/components/bills/bills-page-client.tsx b/components/bills/bills-page-client.tsx
index fd1c754..d2cd0ef 100644
--- a/components/bills/bills-page-client.tsx
+++ b/components/bills/bills-page-client.tsx
@@ -5,6 +5,8 @@ import { motion } from 'framer-motion'
import Link from 'next/link'
import { ArrowLeft, Search } from 'lucide-react'
import { Button } from '@/components/ui/button'
+import { ThemeToggle } from '@/components/theme-toggle'
+import { useWalletConnection } from '@/hooks/use-wallet-connection'
// Note: Input component will be created separately
import { CountrySelector } from './country-selector'
import { CategoryGrid } from '@/components/bills/category-grid'
@@ -21,6 +23,8 @@ export function BillsPageClient() {
const [selectedCountry, setSelectedCountry] = useState('NG')
const { categories, transactions, recentBillers, scheduledPayments, loading } =
useBillsData(selectedCountry)
+ const { address, connected } = useWalletConnection()
+ const headerAddress = address ? `${address.slice(0, 4)}...${address.slice(-4)}` : ''
// Debounced search
const [debouncedSearch, setDebouncedSearch] = useState(searchQuery)
@@ -54,6 +58,13 @@ export function BillsPageClient() {
+
+ {connected && headerAddress ? (
+
+
+ {headerAddress}
+
+ ) : null}
c.id === categorySlug)
+ const headerAddress = address ? `${address.slice(0, 4)}...${address.slice(-4)}` : ''
+
+ const filteredBillers = useMemo(() => {
+ return recentBillers.filter((biller) => {
+ const matchesCategory = biller.category === categorySlug
+ const matchesSearch =
+ biller.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ biller.description.toLowerCase().includes(searchQuery.toLowerCase())
+ return matchesCategory && matchesSearch
+ })
+ }, [recentBillers, categorySlug, searchQuery])
+
+ return (
+
+
+
+
+
+ Back to Categories
+
+
+
+ {connected && (
+
+
+ {headerAddress}
+
+ )}
+
+
+
+
+
+
+
+
+
+ {category?.icon || '🧾'}
+
+
+
+ {category?.name || categorySlug}
+
+
+ Fast and secure {category?.name.toLowerCase()} bill payments
+
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ Advanced Filters
+
+
+
+ {loading ? (
+
+ {[1, 2, 3, 4, 5, 6].map((i) => (
+
+ ))}
+
+ ) : (
+
+ {filteredBillers.length > 0 ? (
+
+ {filteredBillers.map((biller) => (
+
+ ))}
+
+ ) : (
+
+
+ No billers found
+
+ We couldn't find any {category?.name.toLowerCase()} providers matching "
+ {searchQuery}" in this country.
+
+ setSearchQuery('')}>
+ Clear Search Query
+
+
+ )}
+
+ )}
+
+
+ )
+}
+
+function BillerCard({ biller }: { biller: Biller }) {
+ return (
+
+
+
+
+
+ {biller.logo}
+
+
+ {biller.trending && (
+
+ Trending
+
+ )}
+ {biller.popular && (
+
+ Popular
+
+ )}
+
+
+
+ {biller.name}
+
+
+ {biller.description}
+
+
+
+ Pay Now
+
+
+
+
+
+ )
+}
diff --git a/components/bills/fee-breakdown.tsx b/components/bills/fee-breakdown.tsx
new file mode 100644
index 0000000..0ed8546
--- /dev/null
+++ b/components/bills/fee-breakdown.tsx
@@ -0,0 +1,73 @@
+'use client'
+
+import { Info } from 'lucide-react'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip'
+
+interface FeeBreakdownProps {
+ amount: number
+ baseFee: number
+ percentageFee: number
+ currency?: string
+}
+
+export function FeeBreakdown({
+ amount,
+ baseFee,
+ percentageFee,
+ currency = '₦',
+}: FeeBreakdownProps) {
+ const calcPercentageFee = amount * percentageFee
+ const totalFee = baseFee + calcPercentageFee
+ const totalAmount = amount + totalFee
+
+ return (
+
+
+ Subtotal
+
+ {currency}
+ {amount.toLocaleString()}
+
+
+
+
+
Processing Fee
+
+
+
+
+
+
+
+
+
+ Flat fee: {currency}
+ {baseFee}
+
+ {percentageFee > 0 && (
+ Processing: {(percentageFee * 100).toFixed(1)}%
+ )}
+
+
+
+
+
+ {currency}
+ {totalFee.toLocaleString()}
+
+
+
+ Total to Pay
+
+ {currency}
+ {totalAmount.toLocaleString()}
+
+
+
+ )
+}
diff --git a/components/bills/payment-form.tsx b/components/bills/payment-form.tsx
new file mode 100644
index 0000000..323a028
--- /dev/null
+++ b/components/bills/payment-form.tsx
@@ -0,0 +1,262 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+import { Loader2, AlertCircle, CheckCircle2, ChevronRight, Calendar } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { BillerSchema } from '@/lib/biller-schemas'
+import { PaymentMethod, PaymentMethodSelector } from './payment-method-selector'
+import { FeeBreakdown } from './fee-breakdown'
+import { toast } from 'sonner'
+import { motion, AnimatePresence } from 'framer-motion'
+import { cn } from '@/lib/utils'
+import { Checkbox } from '@/components/ui/checkbox'
+
+interface PaymentFormProps {
+ schema: BillerSchema
+}
+
+export function PaymentForm({ schema }: PaymentFormProps) {
+ const [isValidating, setIsValidating] = useState(false)
+ const [validatedAccount, setValidatedAccount] = useState(null)
+ const [paymentMethod, setPaymentMethod] = useState('card')
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [showSchedule, setShowSchedule] = useState(false)
+
+ // Generate dynamic Zod schema
+ const formSchemaObject: any = {}
+ schema.fields.forEach((field) => {
+ let validator: any = z.string()
+
+ if (field.validation.required) {
+ validator = validator.min(1, field.validation.message || `${field.label} is required`)
+ }
+
+ if (field.validation.pattern) {
+ validator = validator.regex(new RegExp(field.validation.pattern), field.validation.message)
+ }
+
+ if (field.validation.minLength && field.type === 'number') {
+ const minVal = field.validation.minLength
+ validator = validator.refine((val: string) => {
+ const num = parseFloat(val)
+ return !isNaN(num) && num >= minVal
+ }, field.validation.message || `Minimum value is ${minVal}`)
+ }
+
+ formSchemaObject[field.name] = validator
+ })
+
+ const formSchema = z.object(formSchemaObject)
+ type FormValues = z.infer
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ setValue,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(formSchema),
+ mode: 'onChange',
+ })
+
+ const amountValue = watch('amount' as any) || (schema.fields.find(f => f.name === 'package') ?
+ schema.fields.find(f => f.name === 'package')?.options?.find(o => o.value === watch('package' as any))?.label.split('₦')[1]?.replace(',', '') : 0) || 0
+
+ const parsedAmount = typeof amountValue === 'string' ? parseFloat(amountValue.replace(/[^0-9.]/g, '')) : amountValue
+
+ // Mock real-time account validation
+ const accountValue = watch(schema.fields[0].name as any)
+ useEffect(() => {
+ if (accountValue && accountValue.length >= 10 && !errors[schema.fields[0].name]) {
+ const delayDebounceFn = setTimeout(() => {
+ validateAccount()
+ }, 1000)
+ return () => clearTimeout(delayDebounceFn)
+ } else {
+ setValidatedAccount(null)
+ }
+ }, [accountValue, errors[schema.fields[0].name]])
+
+ const validateAccount = async () => {
+ setIsValidating(true)
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1500))
+ setIsValidating(false)
+
+ // Random mock name
+ const mockNames = ['John Doe', 'Sarah Williams', 'Emeka Azikiwe', 'Kofi Mensah', 'Jane Smith']
+ setValidatedAccount(mockNames[Math.floor(Math.random() * mockNames.length)])
+ }
+
+ const onSubmit = async (_data: FormValues) => {
+ setIsProcessing(true)
+ // Simulate payment processing
+ await new Promise((resolve) => setTimeout(resolve, 3000))
+
+ setIsProcessing(false)
+ toast.success('Payment Successful!', {
+ description: `Your payment to ${schema.name} has been processed.`,
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/components/bills/payment-method-selector.tsx b/components/bills/payment-method-selector.tsx
new file mode 100644
index 0000000..d2e2233
--- /dev/null
+++ b/components/bills/payment-method-selector.tsx
@@ -0,0 +1,85 @@
+'use client'
+
+import { Check, CreditCard, Landmark, Wallet } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+export type PaymentMethod = 'card' | 'bank_transfer' | 'wallet'
+
+interface PaymentMethodSelectorProps {
+ selected: PaymentMethod
+ onSelect: (method: PaymentMethod) => void
+}
+
+const methods = [
+ {
+ id: 'card',
+ name: 'Card Payment',
+ icon: CreditCard,
+ description: 'Pay with Visa or Mastercard',
+ },
+ {
+ id: 'bank_transfer',
+ name: 'Bank Transfer',
+ icon: Landmark,
+ description: 'Transfer from your bank app',
+ },
+ {
+ id: 'wallet',
+ name: 'Aframp Wallet',
+ icon: Wallet,
+ description: 'Use your Aframp balance',
+ },
+] as const
+
+export function PaymentMethodSelector({
+ selected,
+ onSelect,
+}: PaymentMethodSelectorProps) {
+ return (
+
+
+
+ {methods.map((method) => {
+ const isSelected = selected === method.id
+ return (
+
onSelect(method.id as PaymentMethod)}
+ className={cn(
+ 'flex items-center gap-4 p-4 rounded-2xl border transition-all duration-200 outline-none',
+ isSelected
+ ? 'border-primary bg-primary/5 ring-1 ring-primary'
+ : 'border-border bg-card hover:border-primary/50 hover:bg-muted/50'
+ )}
+ >
+
+
+
+
+
{method.name}
+
+ {method.description}
+
+
+ {isSelected && (
+
+
+
+ )}
+
+ )
+ })}
+
+
+ )
+}
diff --git a/components/bills/recent-billers.tsx b/components/bills/recent-billers.tsx
index 8368820..7dbe55b 100644
--- a/components/bills/recent-billers.tsx
+++ b/components/bills/recent-billers.tsx
@@ -2,9 +2,9 @@
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
+import Link from 'next/link'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
-import { Button } from '@/components/ui/button'
import { Clock, Star } from 'lucide-react'
interface Biller {
@@ -95,47 +95,46 @@ export function RecentBillers({ billers, searchQuery, loading }: RecentBillersPr
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
whileHover={{ y: -2 }}
- className="group cursor-pointer"
>
-
-
-
-
- {biller.logo}
-
-
-
-
-
- {biller.name}
-
- {biller.popular && (
-
- )}
+
+
+
+
+
+ {biller.logo}
-
- {biller.description}
-
+
+
+
+ {biller.name}
+
+ {biller.popular && (
+
+ )}
+
+
+
+ {biller.description}
+
-
-
- {biller.category.replace('-', ' ')}
-
+
+
+ {biller.category.replace('-', ' ')}
+
-
-
- Pay Now
-
+
+
+ Pay Now
+
+
-
-
-
+
+
+
))}
diff --git a/components/dashboard/dashboard-layout.tsx b/components/dashboard/dashboard-layout.tsx
index e3aa16c..c19634a 100644
--- a/components/dashboard/dashboard-layout.tsx
+++ b/components/dashboard/dashboard-layout.tsx
@@ -2,24 +2,20 @@
import { motion } from 'framer-motion'
import Link from 'next/link'
-import { Home, LogOut } from 'lucide-react'
+import { Home } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { ThemeToggle } from '@/components/theme-toggle'
import { EthPriceTicker } from '@/components/dashboard/eth-price-ticker'
import { BalanceProvider } from '@/contexts/balance-context'
+import { ConnectButton } from '@/components/Wallet'
+
interface DashboardLayoutProps {
children: React.ReactNode
walletAddress?: string
}
export function DashboardLayout({ children, walletAddress }: DashboardLayoutProps) {
- const handleDisconnect = () => {
- localStorage.removeItem('walletName')
- localStorage.removeItem('walletAddress')
- window.location.href = '/'
- }
-
return (
@@ -41,16 +37,13 @@ export function DashboardLayout({ children, walletAddress }: DashboardLayoutProp
+
Home
-
-
- Disconnect
-
diff --git a/components/navbar.tsx b/components/navbar.tsx
index 208d4d1..fbd01dc 100644
--- a/components/navbar.tsx
+++ b/components/navbar.tsx
@@ -11,7 +11,6 @@ import { ConnectButton } from '@/components/Wallet'
const navItems = [
{ label: 'Features', href: '#features' },
{ label: 'How it Works', href: '#how-it-works' },
- { label: 'Pricing', href: '#pricing' },
]
export function Navbar() {
@@ -72,24 +71,14 @@ export function Navbar() {
- {/* CTA Buttons */}
-
+ {/* Right side: Theme Toggle and Connect Button */}
+
-
- Explore
-
-
- {/* Mobile Menu Button */}
-
-
+ {/* Mobile Menu Button - inside the right group */}
setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
diff --git a/components/offramp/confirmation-checklist.tsx b/components/offramp/confirmation-checklist.tsx
index ca15939..228739e 100644
--- a/components/offramp/confirmation-checklist.tsx
+++ b/components/offramp/confirmation-checklist.tsx
@@ -24,6 +24,7 @@ interface ConfirmationChecklistProps {
memo: boolean
}>
>
+ isSubmitting?: boolean
}
export function ConfirmationChecklist({
@@ -35,6 +36,7 @@ export function ConfirmationChecklist({
setIsValid,
checkedItems,
setCheckedItems,
+ isSubmitting = false,
}: ConfirmationChecklistProps) {
const handleCheck = (key: keyof typeof checkedItems) => {
const newChecked = { ...checkedItems, [key]: !checkedItems[key] }
@@ -122,7 +124,7 @@ export function ConfirmationChecklist({
Confirm & Send Crypto
diff --git a/components/offramp/kyc-signature.tsx b/components/offramp/kyc-signature.tsx
index c38ef74..978e6e2 100644
--- a/components/offramp/kyc-signature.tsx
+++ b/components/offramp/kyc-signature.tsx
@@ -5,7 +5,7 @@ import { ShieldCheck, Wallet, ChevronRight, Lock } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { BankAccount, signKycMessage } from '@/lib/offramp/bank-service'
import { toast } from 'sonner'
-import { useWalletConnection } from '@/hooks/use-wallet-connection'
+import { useWallet } from '@/hooks/useWallet'
interface KYCSignatureProps {
account: BankAccount
@@ -16,17 +16,17 @@ interface KYCSignatureProps {
export function KYCSignature({ account, amount, onSigned, onBack }: KYCSignatureProps) {
const [isSigning, setIsSigning] = React.useState(false)
- const { address, connected } = useWalletConnection()
+ const { publicKey, isConnected } = useWallet()
const handleSign = async () => {
- if (!connected || !address) {
- toast.error('Please connect your wallet first')
- return
- }
-
setIsSigning(true)
try {
- const signature = await signKycMessage(address, amount, account.accountNumber)
+ if (!isConnected || !publicKey) {
+ toast.error('Please connect your Stellar wallet first')
+ return
+ }
+
+ const signature = await signKycMessage(publicKey, amount, account.accountNumber)
onSigned(signature)
toast.success('Message signed successfully')
} catch {
@@ -73,10 +73,10 @@ export function KYCSignature({ account, amount, onSigned, onBack }: KYCSignature
Signing Wallet
- {connected ? address : 'No wallet connected'}
+ {isConnected && publicKey ? publicKey : 'No wallet connected'}
- {connected && (
+ {isConnected && publicKey && (
)}
@@ -86,7 +86,7 @@ export function KYCSignature({ account, amount, onSigned, onBack }: KYCSignature
{isSigning ? (
diff --git a/components/offramp/offramp-bank-details-client.tsx b/components/offramp/offramp-bank-details-client.tsx
new file mode 100644
index 0000000..9337139
--- /dev/null
+++ b/components/offramp/offramp-bank-details-client.tsx
@@ -0,0 +1,183 @@
+'use client'
+
+import * as React from 'react'
+import Link from 'next/link'
+import { ArrowLeft, Landmark, ShieldCheck, ChevronRight } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { BankAccount } from '@/lib/offramp/bank-service'
+import { BankAccountForm } from '@/components/offramp/bank-account-form'
+import { KYCSignature } from '@/components/offramp/kyc-signature'
+import { SavedAccounts } from '@/components/offramp/saved-accounts'
+import { MOCK_ORDER } from '@/lib/offramp/mock-api'
+import { useRouter } from 'next/navigation'
+
+export function OfframpBankDetailsClient() {
+ const [step, setStep] = React.useState<'select' | 'verify' | 'sign'>('select')
+ const [selectedAccount, setSelectedAccount] = React.useState
(null)
+ const router = useRouter()
+
+ const handleAccountSelect = (account: BankAccount) => {
+ setSelectedAccount(account)
+ setStep('sign')
+ }
+
+ const handleVerified = (account: BankAccount) => {
+ setSelectedAccount(account)
+ setStep('sign')
+ }
+
+ const handleSigned = (_signature: string) => {
+ // In a real app, we would store this signature with the order
+ router.push('/offramp/review')
+ }
+
+ return (
+
+
+ {/* Header Section */}
+
+
+
+
+
+
+
+
Bank Details
+
Step 2 of 4: Verification
+
+
+
+ {/* Progress Bar */}
+
+
+
+ {/* Subtle Background Glow */}
+
+
+
+ {step === 'select' && (
+
+
+
+
+
+
Select Bank Account
+
+ Choose a previously used account or add a new one for your settlement.
+
+
+
+
+
setStep('verify')} />
+
+
+
+ setStep('verify')}
+ variant="outline"
+ className="w-full h-14 rounded-2xl border-white/[0.05] bg-white/[0.02] hover:bg-white/[0.05] hover:border-white/10 text-foreground font-semibold group transition-all"
+ >
+
+ Add New Bank Account
+
+
+
+
+ )}
+
+ {step === 'verify' && (
+
+
+
+
+
+
New Bank Account
+
+ Enter your Nigerian bank account details.
+
+
+
+
+
+
setStep('select')}
+ className="w-full h-12 rounded-xl text-muted-foreground hover:text-foreground"
+ >
+ Back to saved accounts
+
+
+ )}
+
+ {step === 'sign' && selectedAccount && (
+
setStep('verify')}
+ />
+ )}
+
+
+ {/* Security Badge Footer */}
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+
+ Military-Grade Encryption (AES-256)
+
+
+
+
+ )
+}
+
+function PlusIcon(props: React.SVGProps) {
+ return (
+
+ )
+}
diff --git a/components/offramp/offramp-page-client.tsx b/components/offramp/offramp-page-client.tsx
index e9f504b..0febacb 100644
--- a/components/offramp/offramp-page-client.tsx
+++ b/components/offramp/offramp-page-client.tsx
@@ -3,14 +3,14 @@
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
-import { LogOut } from 'lucide-react'
-import { Button } from '@/components/ui/button'
+import { ThemeToggle } from '@/components/theme-toggle'
+import { ConnectButton } from '@/components/Wallet'
import { OfframpCalculator } from '@/components/offramp/offramp-calculator'
-import { useWalletConnection } from '@/hooks/use-wallet-connection'
+import { useWallet } from '@/hooks/useWallet'
import { useOfframpRate } from '@/hooks/use-offramp-rate'
import { useOfframpForm } from '@/hooks/use-offramp-form'
import { useOfframpBalances } from '@/hooks/use-offramp-balances'
-import { formatCurrency, truncateAddress } from '@/lib/onramp/formatters'
+import { formatCurrency } from '@/lib/onramp/formatters'
import { formatUsd, formatRateCountdown } from '@/lib/offramp/formatters'
import type { OfframpOrder } from '@/types/offramp'
@@ -26,7 +26,9 @@ const assetUsdRates: Record = {
export function OfframpPageClient() {
const router = useRouter()
- const { address, connected, loading, disconnect } = useWalletConnection()
+ const { publicKey, isConnecting } = useWallet()
+ const address = publicKey || ''
+ const loading = isConnecting
const [lockExpiresAt, setLockExpiresAt] = useState(null)
const [rateOverride, setRateOverride] = useState(0)
@@ -58,7 +60,7 @@ export function OfframpPageClient() {
router.prefetch('/offramp/bank-details')
}, [router])
- const [lockCountdownTick, setLockCountdownTick] = useState(0)
+ const [, setLockCountdownTick] = useState(0)
useEffect(() => {
if (!lockExpiresAt) return
@@ -73,9 +75,7 @@ export function OfframpPageClient() {
if (!lockExpiresAt) return null
const seconds = Math.max(Math.floor((lockExpiresAt - new Date().getTime()) / 1000), 0)
return seconds
- }, [lockExpiresAt, lockCountdownTick])
-
-
+ }, [lockExpiresAt])
const usdEquivalent = useMemo(() => {
const usdRate = assetUsdRates[selectedAsset.asset]
@@ -109,8 +109,6 @@ export function OfframpPageClient() {
router.push(`/offramp/bank-details?order=${order.id}`)
}
- const headerAddress = truncateAddress(address, 4)
-
if (loading) {
return (
@@ -148,24 +146,8 @@ export function OfframpPageClient() {
- {connected ? (
-
-
- {headerAddress}
-
- ) : null}
-
-
- Disconnect
-
+
+
diff --git a/components/offramp/offramp-wallet-guard.tsx b/components/offramp/offramp-wallet-guard.tsx
new file mode 100644
index 0000000..53c7323
--- /dev/null
+++ b/components/offramp/offramp-wallet-guard.tsx
@@ -0,0 +1,52 @@
+'use client'
+
+import * as React from 'react'
+import { useRouter } from 'next/navigation'
+import { useWallet } from '@/hooks/useWallet'
+import { Button } from '@/components/ui/button'
+
+interface OfframpWalletGuardProps {
+ children: React.ReactNode
+}
+
+export function OfframpWalletGuard({ children }: OfframpWalletGuardProps) {
+ const router = useRouter()
+ const { isConnected, isConnecting } = useWallet()
+
+ if (isConnecting) {
+ return (
+
+
+
+
Checking wallet connection...
+
+
+ )
+ }
+
+ if (!isConnected) {
+ return (
+
+
+
Wallet required
+
+ Please connect your Stellar wallet from the dashboard before starting an offramp
+ withdrawal.
+
+
+ router.push('/')}>Back to home
+ router.push('/dashboard')}
+ className="border-border"
+ >
+ Go to dashboard
+
+
+
+
+ )
+ }
+
+ return <>{children}>
+}
diff --git a/components/offramp/step-review.tsx b/components/offramp/step-review.tsx
index c1c536a..7b9e0f5 100644
--- a/components/offramp/step-review.tsx
+++ b/components/offramp/step-review.tsx
@@ -8,9 +8,14 @@ import { SettlementAddress } from './settlement-address'
import { ConfirmationChecklist } from './confirmation-checklist'
import { MOCK_ORDER, OfframpOrder } from '@/lib/offramp/mock-api'
import { useRouter } from 'next/navigation'
+import { useWallet } from '@/hooks/useWallet'
+import { buildOfframpPaymentXdr } from '@/lib/offramp/stellar-offramp'
+import { toast } from 'sonner'
export function StepReview() {
const router = useRouter()
+ const { publicKey, isConnected, network, signTransaction } = useWallet()
+
// In a real app, we'd fetch the order ID from URL or context
const [order, setOrder] = React.useState(null)
@@ -22,6 +27,7 @@ export function StepReview() {
memo: false,
})
const [isValid, setIsValid] = React.useState(false)
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
React.useEffect(() => {
// Simulate fetching order data
@@ -31,21 +37,53 @@ export function StepReview() {
if (!order)
return Loading order details...
- const handleConfirm = () => {
- // Proceed to next step (e.g. pending status page)
- console.warn('Order confirmed:', order.id)
- // router.push(`/offramp/status/${order.id}`)
- alert('Order Confirmed! Redirecting to status page...')
+ const handleConfirm = async () => {
+ if (!isValid) return
+
+ if (!isConnected || !publicKey) {
+ toast.error('Please connect your Stellar wallet before sending crypto.')
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ const xdr = await buildOfframpPaymentXdr({
+ sourcePublicKey: publicKey,
+ destination: order.settlementAddress,
+ amount: order.sourceAmount,
+ assetCode: order.sourceAsset,
+ network: network,
+ memo: order.memo,
+ })
+
+ const result = await signTransaction(xdr)
+
+ if (!result || !result.signedTxXdr) {
+ toast.error(result?.error || 'Signing was cancelled or failed.')
+ return
+ }
+
+ // Persist for demo / status page
+ if (typeof window !== 'undefined') {
+ localStorage.setItem(`offramp:signedTx:${order.id}`, result.signedTxXdr)
+ }
+
+ toast.success('Transaction signed. Redirecting to processing...')
+ router.push(`/offramp/processing/${encodeURIComponent(order.id)}`)
+ } catch (error) {
+ console.error('Failed to prepare or sign transaction', error)
+ toast.error('Failed to prepare transaction. Please try again.')
+ } finally {
+ setIsSubmitting(false)
+ }
}
const handleEdit = () => {
// Go back to previous step
- console.warn('Edit requested')
router.back()
}
const handleRefresh = () => {
- console.warn('Refreshing rate...')
if (order) {
setOrder({
...order,
@@ -109,6 +147,7 @@ export function StepReview() {
setIsValid={setIsValid}
checkedItems={checkedItems}
setCheckedItems={setCheckedItems}
+ isSubmitting={isSubmitting}
/>
diff --git a/components/onboarding/feature-highlights-carousel.tsx b/components/onboarding/feature-highlights-carousel.tsx
new file mode 100644
index 0000000..52de269
--- /dev/null
+++ b/components/onboarding/feature-highlights-carousel.tsx
@@ -0,0 +1,294 @@
+'use client'
+
+import { useCallback, useEffect, useMemo, useState, type KeyboardEvent } from 'react'
+import useEmblaCarousel from 'embla-carousel-react'
+import { ArrowLeft, ArrowRight, Globe2, ShieldCheck, Sparkles, Zap } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { cn } from '@/lib/utils'
+
+export type FeatureHighlightIllustration = 'security' | 'ease' | 'freedom' | 'network'
+
+export interface FeatureHighlightSlide {
+ id: string
+ title: string
+ description: string
+ illustration: FeatureHighlightIllustration
+}
+
+interface FeatureHighlightsCarouselProps {
+ slides?: FeatureHighlightSlide[]
+ onComplete: () => void
+ onSkip?: () => void
+ onBack?: () => void
+ autoAdvanceMs?: number
+ className?: string
+}
+
+const DEFAULT_SLIDES: FeatureHighlightSlide[] = [
+ {
+ id: 'security',
+ title: 'Secure Digital Wallet',
+ description:
+ 'Experience military-grade protection with encrypted storage, multi-factor checks, and recovery safeguards.',
+ illustration: 'security',
+ },
+ {
+ id: 'ease',
+ title: 'Built for Everyday Ease',
+ description:
+ 'Move from sign-up to your first transaction in minutes with guided flows and familiar actions.',
+ illustration: 'ease',
+ },
+ {
+ id: 'freedom',
+ title: 'Trade with Total Freedom',
+ description:
+ 'Buy, sell, and move assets across borders with low friction and real-time settlement confidence.',
+ illustration: 'freedom',
+ },
+ {
+ id: 'network',
+ title: 'Connected Financial Network',
+ description:
+ 'Link local money rails with global digital assets through a trusted, always-on payment network.',
+ illustration: 'network',
+ },
+]
+
+function Illustration({ variant }: { variant: FeatureHighlightIllustration }) {
+ const iconByVariant = {
+ security: ShieldCheck,
+ ease: Sparkles,
+ freedom: ArrowRight,
+ network: Globe2,
+ } as const
+
+ const Icon = iconByVariant[variant]
+
+ return (
+
+
+
+
+
+
+ {variant === 'network' && (
+
+ )}
+
+ {variant === 'freedom' && (
+
+ )}
+
+
+
+
+
+ )
+}
+
+export function FeatureHighlightsCarousel({
+ slides = DEFAULT_SLIDES,
+ onComplete,
+ onSkip,
+ onBack,
+ autoAdvanceMs = 0,
+ className,
+}: FeatureHighlightsCarouselProps) {
+ const [emblaRef, emblaApi] = useEmblaCarousel({ align: 'start', loop: false, dragFree: false })
+ const [selectedIndex, setSelectedIndex] = useState(0)
+
+ const slideCount = slides.length
+ const isLastSlide = selectedIndex === slideCount - 1
+ const currentSlide = slides[selectedIndex]
+
+ const scrollPrev = useCallback(() => {
+ emblaApi?.scrollPrev()
+ }, [emblaApi])
+
+ const scrollNext = useCallback(() => {
+ emblaApi?.scrollNext()
+ }, [emblaApi])
+
+ const scrollTo = useCallback(
+ (index: number) => {
+ emblaApi?.scrollTo(index)
+ },
+ [emblaApi]
+ )
+
+ const selectSlide = useCallback(() => {
+ if (!emblaApi) return
+ setSelectedIndex(emblaApi.selectedScrollSnap())
+ }, [emblaApi])
+
+ useEffect(() => {
+ if (!emblaApi) return
+ emblaApi.on('select', selectSlide)
+ emblaApi.on('reInit', selectSlide)
+ return () => {
+ emblaApi.off('select', selectSlide)
+ emblaApi.off('reInit', selectSlide)
+ }
+ }, [emblaApi, selectSlide])
+
+ useEffect(() => {
+ if (!emblaApi || autoAdvanceMs <= 0) return
+ const timer = window.setInterval(() => {
+ if (emblaApi.selectedScrollSnap() >= slideCount - 1) {
+ window.clearInterval(timer)
+ return
+ }
+ emblaApi.scrollNext()
+ }, autoAdvanceMs)
+ return () => window.clearInterval(timer)
+ }, [autoAdvanceMs, emblaApi, slideCount])
+
+ const handleBack = useCallback(() => {
+ if (selectedIndex > 0) {
+ scrollPrev()
+ return
+ }
+ onBack?.()
+ }, [onBack, scrollPrev, selectedIndex])
+
+ const handleNext = useCallback(() => {
+ if (isLastSlide) {
+ onComplete()
+ return
+ }
+ scrollNext()
+ }, [isLastSlide, onComplete, scrollNext])
+
+ const handleKeyboard = useCallback(
+ (event: KeyboardEvent
) => {
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault()
+ handleBack()
+ }
+ if (event.key === 'ArrowRight') {
+ event.preventDefault()
+ handleNext()
+ }
+ if (event.key === 'Home') {
+ event.preventDefault()
+ scrollTo(0)
+ }
+ if (event.key === 'End') {
+ event.preventDefault()
+ scrollTo(slideCount - 1)
+ }
+ },
+ [handleBack, handleNext, scrollTo, slideCount]
+ )
+
+ const srProgressLabel = useMemo(
+ () => `Slide ${selectedIndex + 1} of ${slideCount}: ${currentSlide?.title ?? ''}`,
+ [currentSlide?.title, selectedIndex, slideCount]
+ )
+
+ return (
+
+
+ 0 ? 'Previous slide' : 'Back to welcome'}
+ variant="ghost"
+ size="icon-sm"
+ className="rounded-full text-emerald-100 hover:bg-emerald-500/15 hover:text-emerald-50"
+ onClick={handleBack}
+ type="button"
+ >
+
+
+ Feature Highlights
+
+
+
+
+
+ {slides.map((slide, index) => (
+
+
+
+
+ {slide.title}
+
+
{slide.description}
+
+
+ ))}
+
+
+
+
+
+ {slides.map((slide, index) => (
+ scrollTo(index)}
+ type="button"
+ />
+ ))}
+
+
+
+ {isLastSlide ? 'Continue to Wallet Setup' : 'Next'}
+
+
+
+ Skip Introduction
+
+
+
+
+ {srProgressLabel}
+
+
+ )
+}
+
+export { DEFAULT_SLIDES }
diff --git a/components/onramp/onramp-page-client.tsx b/components/onramp/onramp-page-client.tsx
index 9c9fd0c..9c0c820 100644
--- a/components/onramp/onramp-page-client.tsx
+++ b/components/onramp/onramp-page-client.tsx
@@ -1,10 +1,11 @@
'use client'
-import { useEffect, useMemo, useState, useRef } from 'react'
+import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
-import { LogOut } from 'lucide-react'
-import { Button } from '@/components/ui/button'
+import { ThemeToggle } from '@/components/theme-toggle'
+import { ConnectButton } from '@/components/Wallet'
+import { useWallet } from '@/hooks/useWallet'
import {
Dialog,
DialogContent,
@@ -19,17 +20,19 @@ import { useOnrampForm } from '@/hooks/use-onramp-form'
import { useWalletConnection } from '@/hooks/use-wallet-connection'
import { OnrampTestUtils } from '@/components/onramp/onramp-test-utils'
import type { CryptoAsset, FiatCurrency } from '@/types/onramp'
-import { formatCurrency, truncateAddress } from '@/lib/onramp/formatters'
+import { formatCurrency } from '@/lib/onramp/formatters'
import { isValidStellarAddress } from '@/lib/onramp/validation'
import type { OnrampOrder } from '@/types/onramp'
+import { Button } from '@/components/ui/button' // Added missing import for Button
const ORDER_KEY = 'onramp:latest-order'
export function OnrampPageClient() {
const router = useRouter()
+ const { isConnected: storeConnected, publicKey } = useWallet()
const { address, addresses, connected, loading, updateAddress, disconnect } =
useWalletConnection()
- const walletConnected = Boolean(address) || connected
+ const walletConnected = Boolean(address) || connected || storeConnected || Boolean(publicKey)
const [walletModalOpen, setWalletModalOpen] = useState(false)
const [disconnectModalOpen, setDisconnectModalOpen] = useState(false)
const [rateOverride, setRateOverride] = useState(0)
@@ -40,42 +43,48 @@ export function OnrampPageClient() {
form.state.cryptoAsset
)
- // Use ref to track previous values and avoid setState in effect
- const prevRateRef = useRef(undefined)
- const prevCurrencyRef = useRef(undefined)
- const prevAssetRef = useRef(undefined)
-
+ // Sync rate override when rate changes - using useEffect with proper async pattern
useEffect(() => {
- if (data?.rate && data.rate !== prevRateRef.current) {
- prevRateRef.current = data.rate
- // eslint-disable-next-line react-hooks/set-state-in-effect
- setRateOverride(data.rate)
+ if (data?.rate) {
+ const timer = setTimeout(() => {
+ setRateOverride(data.rate)
+ }, 0)
+ return () => clearTimeout(timer)
}
}, [data?.rate])
+ // Reset rate override when currency or asset changes
useEffect(() => {
- const currencyKey = `${form.state.fiatCurrency}-${form.state.cryptoAsset}`
- const prevKey = `${prevCurrencyRef.current}-${prevAssetRef.current}`
-
- if (currencyKey !== prevKey) {
- prevCurrencyRef.current = form.state.fiatCurrency
- prevAssetRef.current = form.state.cryptoAsset
- // eslint-disable-next-line react-hooks/set-state-in-effect
+ const timer = setTimeout(() => {
setRateOverride(0)
- }
+ }, 0)
+ return () => clearTimeout(timer)
}, [form.state.fiatCurrency, form.state.cryptoAsset])
useEffect(() => {
router.prefetch('/onramp/payment')
}, [router])
- // Fix ESLint: Use setTimeout to avoid direct setState in effect
+ // Only show modal if definitely not connected after loading
useEffect(() => {
if (!loading && !walletConnected) {
- const timer = setTimeout(() => setWalletModalOpen(true), 0)
+ const timer = setTimeout(() => {
+ // Double check after timeout to avoid flicker
+ if (!walletConnected) {
+ const timer2 = setTimeout(() => {
+ setWalletModalOpen(true)
+ }, 0)
+ return () => clearTimeout(timer2)
+ }
+ }, 500)
+ return () => clearTimeout(timer)
+ } else if (walletConnected && walletModalOpen) {
+ const timer = setTimeout(() => {
+ setWalletModalOpen(false)
+ }, 0)
return () => clearTimeout(timer)
}
- }, [loading, walletConnected])
+ }, [loading, walletConnected, walletModalOpen])
const handleCopy = async () => {
try {
@@ -124,7 +133,6 @@ export function OnrampPageClient() {
setDisconnectModalOpen(true)
}
- const headerAddress = useMemo(() => truncateAddress(address, 4), [address])
const processingFeeLabel =
form.state.paymentMethod === 'bank_transfer'
? 'FREE'
@@ -169,24 +177,8 @@ export function OnrampPageClient() {
- {walletConnected ? (
-
-
- {headerAddress}
-
- ) : null}
-
-
- Disconnect
-
+
+
diff --git a/components/pricing.tsx b/components/pricing.tsx
deleted file mode 100644
index d52596a..0000000
--- a/components/pricing.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-'use client'
-
-import { motion, useInView } from 'framer-motion'
-import { useRef } from 'react'
-import { Check } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-
-const plans = [
- {
- name: 'Personal',
- description: 'For individuals getting started with crypto',
- price: 'Free',
- priceNote: 'No monthly fees',
- features: [
- 'Buy crypto from ₦2,000',
- 'Pay bills & airtime',
- 'Send to 12 countries',
- 'Basic analytics',
- 'Email support',
- ],
- cta: 'Get Started Free',
- highlighted: false,
- },
- {
- name: 'Business',
- description: 'For SMEs accepting cNGN payments',
- price: '₦5,000',
- priceNote: '/month',
- features: [
- 'Everything in Personal',
- 'Accept cNGN payments',
- '0.5% transaction fees',
- 'Business dashboard',
- 'Priority support',
- 'API access',
- 'Multi-user access',
- ],
- cta: 'Start Free Trial',
- highlighted: true,
- },
- {
- name: 'Enterprise',
- description: 'For large organizations & fintechs',
- price: 'Custom',
- priceNote: 'Contact us',
- features: [
- 'Everything in Business',
- 'Custom integration',
- 'Volume discounts',
- 'Dedicated manager',
- 'SLA guarantee',
- 'White-label options',
- 'Compliance support',
- ],
- cta: 'Contact Sales',
- highlighted: false,
- },
-]
-
-function BorderBeam() {
- return (
-
- )
-}
-
-export function Pricing() {
- const ref = useRef(null)
- const isInView = useInView(ref, { once: true, margin: '-100px' })
-
- return (
-
- )
-}
diff --git a/components/receive/receive-page-client.tsx b/components/receive/receive-page-client.tsx
new file mode 100644
index 0000000..ed9bc05
--- /dev/null
+++ b/components/receive/receive-page-client.tsx
@@ -0,0 +1,470 @@
+'use client'
+
+import { useState } from 'react'
+import { useRouter } from 'next/navigation'
+import {
+ ArrowLeft,
+ Copy,
+ Check,
+ Share2,
+ Download,
+ Link2,
+ MessageCircle,
+ Twitter,
+ ChevronDown,
+} from 'lucide-react'
+import QRCode from 'react-qr-code'
+import { Button } from '@/components/ui/button'
+import { cn } from '@/lib/utils'
+
+interface Asset {
+ symbol: string
+ name: string
+ color: string
+ bgColor: string
+ icon: string
+}
+
+const ASSETS: Asset[] = [
+ {
+ symbol: 'XLM',
+ name: 'Stellar Lumens',
+ color: 'text-sky-500',
+ bgColor: 'bg-sky-500/10 border-sky-500/30',
+ icon: '✦',
+ },
+ {
+ symbol: 'USDC',
+ name: 'USD Coin',
+ color: 'text-blue-500',
+ bgColor: 'bg-blue-500/10 border-blue-500/30',
+ icon: '$',
+ },
+ {
+ symbol: 'BTC',
+ name: 'Bitcoin',
+ color: 'text-amber-500',
+ bgColor: 'bg-amber-500/10 border-amber-500/30',
+ icon: '₿',
+ },
+ {
+ symbol: 'ETH',
+ name: 'Ethereum',
+ color: 'text-indigo-500',
+ bgColor: 'bg-indigo-500/10 border-indigo-500/30',
+ icon: 'Ξ',
+ },
+]
+
+// Mock wallet address — in production, pull from wallet context
+const WALLET_ADDRESS = 'GBSN2ZJBRFWTQHWRJQE4GKDJJDSGPVTLQNQCQX7QR5W5VKHNHQH'
+
+interface ReceivePageClientProps {
+ walletAddress?: string
+}
+
+export function ReceivePageClient({ walletAddress = WALLET_ADDRESS }: ReceivePageClientProps) {
+ const router = useRouter()
+ const [selectedAsset, setSelectedAsset] = useState