From 5edbad3215e3f7080edf92f363e63a25f661df7e Mon Sep 17 00:00:00 2001 From: akintewe <85641756+akintewe@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:24:55 +0100 Subject: [PATCH 1/6] feat: implement deposit flow with QR codes and optimistic updates - Add complete deposit flow with QR code generation for receiving addresses - Implement address display with copy functionality - Add transaction history for incoming payments with status badges - Implement optimistic updates system with immediate UI updates - Add rollback handling for failed transactions - Implement real-time status updates and transaction queue management - Add Stellar network monitoring for incoming transactions - Implement automatic balance refresh and transaction status indicators - Add WebSocket connections for real-time updates - Implement transaction queuing for better UX - Add proper error boundaries and clipboard API compatibility - Ensure mobile-responsive design - Add comprehensive API endpoints for deposit operations - Implement mock authentication for testing Resolves #131 --- .../src/app/api/deposit/[id]/route.ts | 165 ++++++++ .../src/app/api/deposit/monitor/route.ts | 182 ++++++++ .../src/app/api/deposit/route.ts | 129 ++++++ .../src/app/dashboard/deposit/page.tsx | 103 +++++ .../src/app/dashboard/layout.tsx | 4 +- paystell-frontend/src/app/layout.tsx | 5 +- paystell-frontend/src/app/test-login/page.tsx | 66 +++ .../src/components/dashboard/nav/index.tsx | 4 +- .../src/components/deposit/DepositFlow.tsx | 258 ++++++++++++ .../src/components/deposit/DepositForm.tsx | 228 ++++++++++ .../src/components/deposit/DepositHistory.tsx | 275 ++++++++++++ .../src/components/deposit/DepositQRCode.tsx | 202 +++++++++ .../deposit/__tests__/DepositFlow.test.tsx | 216 ++++++++++ .../monitoring/TransactionMonitor.tsx | 299 ++++++++++++++ .../optimistic/OptimisticTransactionList.tsx | 272 ++++++++++++ .../queue/TransactionQueueManager.tsx | 351 ++++++++++++++++ .../components/websocket/WebSocketStatus.tsx | 186 +++++++++ paystell-frontend/src/config/dashboard/nav.ts | 8 +- .../src/hooks/use-optimistic-transactions.ts | 161 ++++++++ .../src/hooks/use-stellar-monitoring.ts | 190 +++++++++ paystell-frontend/src/hooks/use-websocket.ts | 171 ++++++++ .../deposit/__tests__/deposit-utils.test.ts | 192 +++++++++ .../src/lib/deposit/deposit-utils.ts | 131 ++++++ .../src/lib/monitoring/stellar-monitor.ts | 276 +++++++++++++ .../__tests__/optimistic-store.test.ts | 254 ++++++++++++ .../src/lib/optimistic/optimistic-store.ts | 187 +++++++++ .../queue/__tests__/transaction-queue.test.ts | 356 ++++++++++++++++ .../src/lib/queue/transaction-queue.ts | 391 ++++++++++++++++++ paystell-frontend/src/lib/types/deposit.ts | 63 +++ .../src/lib/websocket/websocket-client.ts | 277 +++++++++++++ .../src/providers/MockAuthProvider.tsx | 151 +++++++ .../src/services/deposit.service.ts | 176 ++++++++ 32 files changed, 5924 insertions(+), 5 deletions(-) create mode 100644 paystell-frontend/src/app/api/deposit/[id]/route.ts create mode 100644 paystell-frontend/src/app/api/deposit/monitor/route.ts create mode 100644 paystell-frontend/src/app/api/deposit/route.ts create mode 100644 paystell-frontend/src/app/dashboard/deposit/page.tsx create mode 100644 paystell-frontend/src/app/test-login/page.tsx create mode 100644 paystell-frontend/src/components/deposit/DepositFlow.tsx create mode 100644 paystell-frontend/src/components/deposit/DepositForm.tsx create mode 100644 paystell-frontend/src/components/deposit/DepositHistory.tsx create mode 100644 paystell-frontend/src/components/deposit/DepositQRCode.tsx create mode 100644 paystell-frontend/src/components/deposit/__tests__/DepositFlow.test.tsx create mode 100644 paystell-frontend/src/components/monitoring/TransactionMonitor.tsx create mode 100644 paystell-frontend/src/components/optimistic/OptimisticTransactionList.tsx create mode 100644 paystell-frontend/src/components/queue/TransactionQueueManager.tsx create mode 100644 paystell-frontend/src/components/websocket/WebSocketStatus.tsx create mode 100644 paystell-frontend/src/hooks/use-optimistic-transactions.ts create mode 100644 paystell-frontend/src/hooks/use-stellar-monitoring.ts create mode 100644 paystell-frontend/src/hooks/use-websocket.ts create mode 100644 paystell-frontend/src/lib/deposit/__tests__/deposit-utils.test.ts create mode 100644 paystell-frontend/src/lib/deposit/deposit-utils.ts create mode 100644 paystell-frontend/src/lib/monitoring/stellar-monitor.ts create mode 100644 paystell-frontend/src/lib/optimistic/__tests__/optimistic-store.test.ts create mode 100644 paystell-frontend/src/lib/optimistic/optimistic-store.ts create mode 100644 paystell-frontend/src/lib/queue/__tests__/transaction-queue.test.ts create mode 100644 paystell-frontend/src/lib/queue/transaction-queue.ts create mode 100644 paystell-frontend/src/lib/types/deposit.ts create mode 100644 paystell-frontend/src/lib/websocket/websocket-client.ts create mode 100644 paystell-frontend/src/providers/MockAuthProvider.tsx create mode 100644 paystell-frontend/src/services/deposit.service.ts diff --git a/paystell-frontend/src/app/api/deposit/[id]/route.ts b/paystell-frontend/src/app/api/deposit/[id]/route.ts new file mode 100644 index 0000000..593b565 --- /dev/null +++ b/paystell-frontend/src/app/api/deposit/[id]/route.ts @@ -0,0 +1,165 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { DepositRequest } from "@/lib/types/deposit"; + +// In-memory store for deposit requests +// In production, use a database +const depositRequests = new Map(); + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { id } = params; + + // 2. Get deposit request + const deposit = depositRequests.get(id); + if (!deposit) { + return NextResponse.json( + { message: "Deposit request not found" }, + { status: 404 } + ); + } + + // 3. Check if user has access to this deposit + if (deposit.address !== session.user.id) { + return NextResponse.json( + { message: "Access denied" }, + { status: 403 } + ); + } + + return NextResponse.json({ + success: true, + deposit, + }); + } catch (error: unknown) { + console.error("Deposit retrieval error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to retrieve deposit"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { id } = params; + const updates = await request.json(); + + // 2. Get existing deposit request + const existingDeposit = depositRequests.get(id); + if (!existingDeposit) { + return NextResponse.json( + { message: "Deposit request not found" }, + { status: 404 } + ); + } + + // 3. Check if user has access to this deposit + if (existingDeposit.address !== session.user.id) { + return NextResponse.json( + { message: "Access denied" }, + { status: 403 } + ); + } + + // 4. Validate updates + const allowedUpdates = ["status", "transactionHash", "confirmedAt"]; + const validUpdates: Partial = {}; + + for (const [key, value] of Object.entries(updates)) { + if (allowedUpdates.includes(key)) { + (validUpdates as Record)[key] = value; + } + } + + // 5. Update deposit request + const updatedDeposit: DepositRequest = { + ...existingDeposit, + ...validUpdates, + }; + + depositRequests.set(id, updatedDeposit); + + return NextResponse.json({ + success: true, + deposit: updatedDeposit, + }); + } catch (error: unknown) { + console.error("Deposit update error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to update deposit"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { id } = params; + + // 2. Get existing deposit request + const existingDeposit = depositRequests.get(id); + if (!existingDeposit) { + return NextResponse.json( + { message: "Deposit request not found" }, + { status: 404 } + ); + } + + // 3. Check if user has access to this deposit + if (existingDeposit.address !== session.user.id) { + return NextResponse.json( + { message: "Access denied" }, + { status: 403 } + ); + } + + // 4. Delete deposit request + depositRequests.delete(id); + + return NextResponse.json({ + success: true, + message: "Deposit request deleted", + }); + } catch (error: unknown) { + console.error("Deposit deletion error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to delete deposit"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} diff --git a/paystell-frontend/src/app/api/deposit/monitor/route.ts b/paystell-frontend/src/app/api/deposit/monitor/route.ts new file mode 100644 index 0000000..f272911 --- /dev/null +++ b/paystell-frontend/src/app/api/deposit/monitor/route.ts @@ -0,0 +1,182 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { DepositMonitoringConfig } from "@/lib/types/deposit"; +import { isValidStellarAddress } from "@/lib/deposit/deposit-utils"; + +// In-memory store for monitoring configurations +// In production, use a database +const monitoringConfigs = new Map(); + +export async function POST(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { address, asset, minAmount, maxAmount, memo } = await request.json(); + + // 2. Input validation + if (!address || !asset) { + return NextResponse.json( + { message: "Address and asset are required" }, + { status: 400 } + ); + } + + // 3. Validate Stellar address + if (!isValidStellarAddress(address)) { + return NextResponse.json( + { message: "Invalid Stellar address" }, + { status: 400 } + ); + } + + // 4. Validate asset + const supportedAssets = ["XLM", "USDC", "USDT"]; + if (!supportedAssets.includes(asset)) { + return NextResponse.json( + { message: "Unsupported asset" }, + { status: 400 } + ); + } + + // 5. Validate amounts if provided + if (minAmount && (isNaN(parseFloat(minAmount)) || parseFloat(minAmount) <= 0)) { + return NextResponse.json( + { message: "Invalid minimum amount" }, + { status: 400 } + ); + } + + if (maxAmount && (isNaN(parseFloat(maxAmount)) || parseFloat(maxAmount) <= 0)) { + return NextResponse.json( + { message: "Invalid maximum amount" }, + { status: 400 } + ); + } + + if (minAmount && maxAmount && parseFloat(minAmount) > parseFloat(maxAmount)) { + return NextResponse.json( + { message: "Minimum amount cannot be greater than maximum amount" }, + { status: 400 } + ); + } + + // 6. Create monitoring configuration + const config: DepositMonitoringConfig = { + address, + asset, + minAmount: minAmount || undefined, + maxAmount: maxAmount || undefined, + memo: memo || undefined, + }; + + // 7. Store monitoring configuration + const key = `${address}_${asset}`; + monitoringConfigs.set(key, config); + + return NextResponse.json({ + success: true, + config, + message: "Monitoring started", + }); + } catch (error: unknown) { + console.error("Monitoring setup error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to setup monitoring"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function GET(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + const asset = searchParams.get("asset"); + + // 2. Get monitoring configurations + let configs = Array.from(monitoringConfigs.values()); + + // 3. Filter by address if provided + if (address) { + configs = configs.filter(config => config.address === address); + } + + // 4. Filter by asset if provided + if (asset) { + configs = configs.filter(config => config.asset === asset); + } + + return NextResponse.json({ + success: true, + configs, + total: configs.length, + }); + } catch (error: unknown) { + console.error("Monitoring retrieval error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to retrieve monitoring configs"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function DELETE(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const address = searchParams.get("address"); + const asset = searchParams.get("asset"); + + // 2. Input validation + if (!address || !asset) { + return NextResponse.json( + { message: "Address and asset are required" }, + { status: 400 } + ); + } + + // 3. Remove monitoring configuration + const key = `${address}_${asset}`; + const deleted = monitoringConfigs.delete(key); + + if (!deleted) { + return NextResponse.json( + { message: "Monitoring configuration not found" }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: "Monitoring stopped", + }); + } catch (error: unknown) { + console.error("Monitoring removal error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to remove monitoring"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} diff --git a/paystell-frontend/src/app/api/deposit/route.ts b/paystell-frontend/src/app/api/deposit/route.ts new file mode 100644 index 0000000..5132c9c --- /dev/null +++ b/paystell-frontend/src/app/api/deposit/route.ts @@ -0,0 +1,129 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { DepositRequest } from "@/lib/types/deposit"; +import { generateDepositId, calculateDepositExpiration, isValidStellarAddress } from "@/lib/deposit/deposit-utils"; +import { paymentRateLimit } from "@/middleware/rateLimit"; + +// In-memory store for deposit requests +// In production, use a database +const depositRequests = new Map(); + +export async function POST(request: NextRequest) { + try { + // Apply rate limiting + const rateLimitResponse = paymentRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { amount, asset, memo, customAddress } = await request.json(); + + // 2. Input validation + if (!asset) { + return NextResponse.json( + { message: "Asset is required" }, + { status: 400 } + ); + } + + // 3. Validate asset + const supportedAssets = ["XLM", "USDC", "USDT"]; + if (!supportedAssets.includes(asset)) { + return NextResponse.json( + { message: "Unsupported asset" }, + { status: 400 } + ); + } + + // 4. Validate amount if provided + if (amount && (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0)) { + return NextResponse.json( + { message: "Invalid amount" }, + { status: 400 } + ); + } + + // 5. Validate custom address if provided + if (customAddress && !isValidStellarAddress(customAddress)) { + return NextResponse.json( + { message: "Invalid Stellar address" }, + { status: 400 } + ); + } + + // 6. Create deposit request + const depositRequest: DepositRequest = { + id: generateDepositId(), + address: customAddress || session.user.id, // Use user ID as fallback + amount: amount || undefined, + asset, + memo: memo || undefined, + status: "pending", + createdAt: new Date().toISOString(), + expiresAt: calculateDepositExpiration(), + }; + + // 7. Store deposit request + depositRequests.set(depositRequest.id, depositRequest); + + return NextResponse.json({ + success: true, + deposit: depositRequest, + }); + } catch (error: unknown) { + console.error("Deposit creation error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to create deposit request"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} + +export async function GET(request: NextRequest) { + try { + // 1. Authentication check + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { message: "Authentication required" }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const userId = searchParams.get("userId"); + const status = searchParams.get("status"); + + // 2. Get deposits for user + let userDeposits = Array.from(depositRequests.values()) + .filter(deposit => deposit.address === session.user.id || deposit.address === userId); + + // 3. Filter by status if provided + if (status) { + userDeposits = userDeposits.filter(deposit => deposit.status === status); + } + + // 4. Sort by creation date (newest first) + userDeposits.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + return NextResponse.json({ + success: true, + deposits: userDeposits, + total: userDeposits.length, + }); + } catch (error: unknown) { + console.error("Deposit retrieval error:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to retrieve deposits"; + return NextResponse.json({ message: errorMessage }, { status: 500 }); + } +} diff --git a/paystell-frontend/src/app/dashboard/deposit/page.tsx b/paystell-frontend/src/app/dashboard/deposit/page.tsx new file mode 100644 index 0000000..8f89a63 --- /dev/null +++ b/paystell-frontend/src/app/dashboard/deposit/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { DepositFlow } from "@/components/deposit/DepositFlow"; +import { OptimisticTransactionList } from "@/components/optimistic/OptimisticTransactionList"; +import { TransactionMonitor } from "@/components/monitoring/TransactionMonitor"; +import { TransactionQueueManager } from "@/components/queue/TransactionQueueManager"; +import { WebSocketStatus } from "@/components/websocket/WebSocketStatus"; +import { useWalletStore } from "@/lib/wallet/wallet-store"; + +export default function DepositPage() { + const { isConnected, publicKey } = useWalletStore(); + const [activeTab, setActiveTab] = useState("deposit"); + + // Initialize monitoring for connected wallet + useEffect(() => { + if (isConnected && publicKey) { + // Start monitoring for XLM deposits + // This would typically be done through the monitoring hook + console.log(`Monitoring deposits for wallet: ${publicKey}`); + } + }, [isConnected, publicKey]); + + if (!isConnected) { + return ( +
+
+

Deposit Funds

+

+ Please connect your Stellar wallet to access deposit features +

+ + {useWalletStore.getState().error && ( +

{useWalletStore.getState().error}

+ )} +
+
+ ); + } + + return ( +
+
+

Deposit & Transaction Management

+

+ Manage your deposits, monitor transactions, and track optimistic updates +

+
+ + + + + Deposit + 💰 + + + Optimistic + + + + Monitor + 👁️ + + + Queue + 📋 + + + WebSocket + 🔌 + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/paystell-frontend/src/app/dashboard/layout.tsx b/paystell-frontend/src/app/dashboard/layout.tsx index 1c8cbe7..f1b3f70 100644 --- a/paystell-frontend/src/app/dashboard/layout.tsx +++ b/paystell-frontend/src/app/dashboard/layout.tsx @@ -8,13 +8,15 @@ import { useState, useMemo, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { Logo } from '@/components/dashboard/nav/Logo'; import { useAuth } from '@/providers/AuthProvider'; +import { useMockAuth } from '@/providers/MockAuthProvider'; import type { NavItem } from '@/components/dashboard/nav/types'; import type { Permission, UserRole } from '@/lib/types/user'; import { useRouter } from 'next/navigation'; export default function DashboardLayout({ children }: { children: React.ReactNode }) { const [isNavOpen, setIsNavOpen] = useState(false); - const { user, hasPermission, isLoading } = useAuth(); + // Use mock auth for testing - switch to useAuth() for production + const { user, hasPermission, isLoading } = useMockAuth(); const router = useRouter(); // Debug logging diff --git a/paystell-frontend/src/app/layout.tsx b/paystell-frontend/src/app/layout.tsx index 98c825a..940000b 100644 --- a/paystell-frontend/src/app/layout.tsx +++ b/paystell-frontend/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { AuthProvider } from "@/providers/AuthProvider"; +import { MockAuthProvider } from "@/providers/MockAuthProvider"; import { ThemeProvider } from "next-themes"; import { WalletProvider } from "@/providers/useWalletProvider"; import { Toaster } from "@/components/ui/sonner" @@ -24,10 +25,10 @@ export default function RootLayout({ - + {children} - + diff --git a/paystell-frontend/src/app/test-login/page.tsx b/paystell-frontend/src/app/test-login/page.tsx new file mode 100644 index 0000000..8f72911 --- /dev/null +++ b/paystell-frontend/src/app/test-login/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useState } from "react"; +import { useMockAuth } from "@/providers/MockAuthProvider"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function TestLoginPage() { + const { login } = useMockAuth(); + const router = useRouter(); + const [email, setEmail] = useState("test@example.com"); + const [password, setPassword] = useState("password"); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await login(email, password); + router.push("/dashboard/deposit"); + } catch (error) { + console.error("Login failed:", error); + } + }; + + return ( +
+ + + Test Login + + Use any credentials to test the deposit flow + + +
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+
+
+
+ ); +} diff --git a/paystell-frontend/src/components/dashboard/nav/index.tsx b/paystell-frontend/src/components/dashboard/nav/index.tsx index cd037a4..39aa3d5 100644 --- a/paystell-frontend/src/components/dashboard/nav/index.tsx +++ b/paystell-frontend/src/components/dashboard/nav/index.tsx @@ -7,6 +7,7 @@ import { NavItem } from "./nav-item"; import { navStyles } from "./styles"; import { Logo } from "@/components/dashboard/nav/Logo"; import { useAuth } from "@/providers/AuthProvider"; +import { useMockAuth } from "@/providers/MockAuthProvider"; import { useRouter } from "next/navigation"; import { IoLogOutOutline } from "react-icons/io5"; import { useEffect, useCallback } from "react"; @@ -19,7 +20,8 @@ export function Nav({ brand = { title: "PayStell" }, ...props }: NavProps) { - const { logout } = useAuth(); + // Use mock auth for testing - switch to useAuth() for production + const { logout } = useMockAuth(); const router = useRouter(); const handleMobileNavClose = useCallback( diff --git a/paystell-frontend/src/components/deposit/DepositFlow.tsx b/paystell-frontend/src/components/deposit/DepositFlow.tsx new file mode 100644 index 0000000..0fa74cd --- /dev/null +++ b/paystell-frontend/src/components/deposit/DepositFlow.tsx @@ -0,0 +1,258 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ArrowLeft, Download, Clock, QrCode } from "lucide-react"; +import { DepositForm } from "./DepositForm"; +import { DepositQRCode } from "./DepositQRCode"; +import { DepositHistory } from "./DepositHistory"; +import { DepositRequest, DepositTransaction } from "@/lib/types/deposit"; +import { useWalletStore } from "@/lib/wallet/wallet-store"; +import { toast } from "sonner"; + +interface DepositFlowProps { + className?: string; +} + +export function DepositFlow({ className }: DepositFlowProps) { + const { isConnected, publicKey } = useWalletStore(); + const [deposits, setDeposits] = useState([]); + const [transactions, setTransactions] = useState([]); + const [activeDeposit, setActiveDeposit] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // Load deposits and transactions from localStorage on mount + useEffect(() => { + loadDeposits(); + loadTransactions(); + }, []); + + const loadDeposits = () => { + try { + const stored = localStorage.getItem("paystell_deposits"); + if (stored) { + setDeposits(JSON.parse(stored)); + } + } catch (error) { + console.error("Error loading deposits:", error); + } + }; + + const loadTransactions = () => { + try { + const stored = localStorage.getItem("paystell_deposit_transactions"); + if (stored) { + setTransactions(JSON.parse(stored)); + } + } catch (error) { + console.error("Error loading transactions:", error); + } + }; + + const saveDeposits = (newDeposits: DepositRequest[]) => { + try { + localStorage.setItem("paystell_deposits", JSON.stringify(newDeposits)); + setDeposits(newDeposits); + } catch (error) { + console.error("Error saving deposits:", error); + } + }; + + const saveTransactions = (newTransactions: DepositTransaction[]) => { + try { + localStorage.setItem("paystell_deposit_transactions", JSON.stringify(newTransactions)); + setTransactions(newTransactions); + } catch (error) { + console.error("Error saving transactions:", error); + } + }; + + const handleCreateDeposit = (deposit: DepositRequest) => { + const newDeposits = [...deposits, deposit]; + saveDeposits(newDeposits); + setActiveDeposit(deposit); + toast.success("Deposit request created successfully"); + }; + + const handleViewDeposit = (deposit: DepositRequest) => { + setActiveDeposit(deposit); + }; + + const handleRefresh = async () => { + setIsLoading(true); + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + loadDeposits(); + loadTransactions(); + toast.success("Data refreshed"); + } catch (error) { + toast.error("Failed to refresh data"); + } finally { + setIsLoading(false); + } + }; + + const handleBackToForm = () => { + setActiveDeposit(null); + }; + + if (!isConnected) { + return ( + + +
+ +
+

Connect Your Wallet

+

+ Please connect your Stellar wallet to create deposit requests +

+
+
+
+
+ ); + } + + if (activeDeposit) { + return ( +
+
+ +
+

Deposit Request

+

+ ID: {activeDeposit.id} +

+
+
+ +
+ + + + + Deposit Instructions + + +
+

How to deposit:

+
    +
  1. Scan the QR code with your mobile wallet
  2. +
  3. Or copy the address and send funds manually
  4. +
  5. Wait for transaction confirmation
  6. +
  7. Your balance will update automatically
  8. +
+
+ +
+

Supported Assets:

+
    +
  • Stellar Lumens (XLM)
  • +
  • USD Coin (USDC)
  • +
  • Tether (USDT)
  • +
+
+ +
+

Important Notes:

+
    +
  • Only send supported assets to this address
  • +
  • Minimum deposit: 1 XLM
  • +
  • Deposit requests expire in 24 hours
  • +
  • Transactions may take a few minutes to confirm
  • +
+
+
+
+
+
+ ); + } + + return ( +
+
+

Deposit Funds

+

+ Create deposit requests and receive funds to your Stellar wallet +

+
+ + + + + + Create Deposit + + + + History + + + + +
+ + + + + Quick Deposit + + +
+

Your Wallet Address

+
+ + {publicKey} + +
+
+ +
+

Supported Assets

+
+ XLM + USDC + USDT +
+
+ +
+

Network

+

+ Stellar Testnet +

+
+
+
+
+
+ + + + +
+
+ ); +} diff --git a/paystell-frontend/src/components/deposit/DepositForm.tsx b/paystell-frontend/src/components/deposit/DepositForm.tsx new file mode 100644 index 0000000..3e532ab --- /dev/null +++ b/paystell-frontend/src/components/deposit/DepositForm.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +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 { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Download, QrCode } from "lucide-react"; +import { DepositRequest } from "@/lib/types/deposit"; +import { generateDepositId, calculateDepositExpiration, isValidStellarAddress } from "@/lib/deposit/deposit-utils"; +import { useWalletStore } from "@/lib/wallet/wallet-store"; +import { toast } from "sonner"; + +interface DepositFormProps { + onCreateDeposit: (deposit: DepositRequest) => void; + onCancel?: () => void; + className?: string; +} + +const SUPPORTED_ASSETS = [ + { value: "XLM", label: "Stellar Lumens (XLM)" }, + { value: "USDC", label: "USD Coin (USDC)" }, + { value: "USDT", label: "Tether (USDT)" }, +]; + +export function DepositForm({ onCreateDeposit, onCancel, className }: DepositFormProps) { + const { publicKey, isConnected } = useWalletStore(); + const [formData, setFormData] = useState({ + amount: "", + asset: "XLM", + memo: "", + customAddress: "", + useCustomAddress: false, + }); + const [isCreating, setIsCreating] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!isConnected || !publicKey) { + toast.error("Please connect your wallet first"); + return; + } + + setIsCreating(true); + + try { + const depositAddress = formData.useCustomAddress + ? formData.customAddress + : publicKey; + + if (!isValidStellarAddress(depositAddress)) { + toast.error("Invalid Stellar address"); + return; + } + + const deposit: DepositRequest = { + id: generateDepositId(), + address: depositAddress, + amount: formData.amount || undefined, + asset: formData.asset, + memo: formData.memo || undefined, + status: "pending", + createdAt: new Date().toISOString(), + expiresAt: calculateDepositExpiration(), + }; + + onCreateDeposit(deposit); + + // Reset form + setFormData({ + amount: "", + asset: "XLM", + memo: "", + customAddress: "", + useCustomAddress: false, + }); + } catch (error) { + console.error("Error creating deposit:", error); + toast.error("Failed to create deposit request"); + } finally { + setIsCreating(false); + } + }; + + const handleInputChange = (field: string, value: string | boolean) => { + setFormData(prev => ({ + ...prev, + [field]: value, + })); + }; + + return ( + + + + + Create Deposit Request + + + +
+ {/* Asset Selection */} +
+ + +
+ + {/* Amount Input */} +
+ + handleInputChange("amount", e.target.value)} + step="0.0000001" + min="0" + /> +

+ Leave empty to allow any amount +

+
+ + {/* Memo Input */} +
+ +