From e19836f1b542bc7085e52ff771bc0900be9e5996 Mon Sep 17 00:00:00 2001 From: darkify <109320650+daRk8238@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:19:16 +0000 Subject: [PATCH 1/2] feat: add 4-step sponsor onboarding flow --- src/pages/SponsorOnboarding.tsx | 355 ++++++++++++++++++++++++++++++++ src/router/index.tsx | 5 + src/services/pool.service.ts | 9 + src/stores/app.store.ts | 19 +- 4 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 src/pages/SponsorOnboarding.tsx create mode 100644 src/services/pool.service.ts diff --git a/src/pages/SponsorOnboarding.tsx b/src/pages/SponsorOnboarding.tsx new file mode 100644 index 0000000..4ba615c --- /dev/null +++ b/src/pages/SponsorOnboarding.tsx @@ -0,0 +1,355 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { motion, AnimatePresence } from 'framer-motion' +import type { Variants } from 'framer-motion' +import { useQuery } from '@tanstack/react-query' +import { Wallet, BarChart3, ArrowRight, Check, AlertTriangle, ExternalLink } from 'lucide-react' +import { Button } from '../components/ui/Button' +import { Card } from '../components/ui/Card' +import { Spinner } from '../components/ui/Spinner' +import { poolService } from '../services/pool.service' +import { useAppStore } from '../stores/app.store' +import { useWallet } from '../hooks/useWallet' +import { GRANTFOX_URL } from '../constants/config' + +const steps = [ + { title: 'Welcome', icon: Wallet }, + { title: 'Risks', icon: AlertTriangle }, + { title: 'Pool Health', icon: BarChart3 }, + { title: 'Deposit', icon: ArrowRight }, +] + +const fadeSlide: Variants = { + initial: { opacity: 0, x: 40 }, + animate: { opacity: 1, x: 0, transition: { duration: 0.35, ease: 'easeOut' } }, + exit: { opacity: 0, x: -40, transition: { duration: 0.25, ease: 'easeIn' } }, +} + +function StepIndicator({ current }: { current: number }) { + return ( +
+
+ {steps.map((s, i) => ( +
+
+ {i < current ? : i + 1} +
+ + {s.title} + +
+ ))} +
+
+
+
+
+ ) +} + +function StepWelcome({ onNext }: { onNext: () => void }) { + return ( + +
+ +
+

+ Welcome to the Sponsor Pool +

+

+ StepFi connects sponsors like you with verified learners who need + affordable financing for education, tools, and career growth. +

+

+ When you deposit USDC into the pool, your capital gets deployed to + real learner loans. You earn yield from the interest learners pay back, + and you can withdraw your deposit plus earned yield at any time. +

+ +
+ ) +} + +const risks = [ + { + title: 'Default Risk', + body: 'Learners may fail to repay their loans. While StepFi uses on-chain reputation scores to vet borrowers, past performance does not guarantee future results. Defaults reduce pool returns and may impact principal.', + severity: 'high', + }, + { + title: 'Smart Contract Risk', + body: 'The pool is managed by Stellar smart contracts that have been developed and tested, but no software is guaranteed bug-free. Exploits or vulnerabilities could result in loss of funds.', + severity: 'medium', + }, + { + title: 'Market & Liquidity Risk', + body: 'If a large number of sponsors withdraw simultaneously, the pool may temporarily hold insufficient liquid capital to process all withdrawals. Withdrawals are processed on a first-come, first-served basis from available liquidity.', + severity: 'medium', + }, + { + title: 'Protocol Risk', + body: 'StepFi is an early-stage protocol. The platform, its smart contracts, and its business model may change or be discontinued. There is no guarantee of continued operation or future returns.', + severity: 'high', + }, +] + +function StepRisks({ onNext }: { onNext: () => void }) { + return ( + +
+
+ +
+

+ Understand the Risks +

+

+ Sponsor pools offer attractive returns, but they are not risk-free. + Please read each risk carefully before depositing. +

+
+ +
+ {risks.map((risk) => ( + +
+
+
+

{risk.title}

+

{risk.body}

+
+
+ + ))} +
+ + + + ) +} + +function formatCurrency(value: number): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value) +} + +function StepPoolHealth({ onNext }: { onNext: () => void }) { + const [depositAmount, setDepositAmount] = useState('') + const { data: pool, isLoading } = useQuery({ + queryKey: ['pool-info'], + queryFn: () => poolService.getPoolInfo(), + refetchInterval: 30_000, + }) + + const depositNum = parseFloat(depositAmount) || 0 + const apy = pool?.apy ?? 0 + const yearlyYield = depositNum * apy + const monthlyYield = yearlyYield / 12 + + return ( + +
+
+ +
+

+ Current Pool Health +

+

+ Real-time metrics from the StepFi liquidity pool. +

+
+ + {isLoading ? ( +
+ +
+ ) : pool ? ( +
+ +

Total Deposits

+

+ {formatCurrency(pool.totalDeposits)} +

+
+ +

APY

+

+ {(apy * 100).toFixed(1)}% +

+
+ +

Available Liquidity

+

+ {formatCurrency(pool.availableLiquidity)} +

+
+ +

Locked in Loans

+

+ {formatCurrency(pool.lockedLiquidity)} +

+
+
+ ) : ( + +

Unable to load pool data.

+
+ )} + + +

Yield Preview

+

+ Enter a deposit amount to see your estimated returns. +

+
+ $ + setDepositAmount(e.target.value)} + className="w-full bg-bg border border-border rounded-xl px-8 py-2.5 text-text-primary + font-display font-bold text-lg outline-none focus:border-brand transition-colors + [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> +
+ {depositNum > 0 && ( +
+
+ Yearly yield + ${yearlyYield.toFixed(2)} +
+
+ Monthly yield + ${monthlyYield.toFixed(2)} +
+
+ )} +
+ + +
+ ) +} + +function StepDeposit({ onComplete }: { onComplete: () => void }) { + const { isConnected, connectFreighter } = useWallet() + + return ( + +
+ +
+

+ Make Your First Deposit +

+

+ Connect your Stellar wallet and deposit USDC to start earning yield + while funding real learner dreams. You can deposit any amount and + withdraw anytime. +

+ + {!isConnected ? ( + + ) : ( +
+

+ Your wallet is connected. Head to the sponsor dashboard to make + your first deposit. +

+ +
+ )} + +
+

+ No wallet yet? You can also contribute via + {' '} + + GrantFox + . +

+
+
+ ) +} + +export function SponsorOnboarding() { + const [step, setStep] = useState(0) + const setOnboardingComplete = useAppStore((s) => s.setOnboardingComplete) + const navigate = useNavigate() + + const handleNext = () => { + if (step < steps.length - 1) { + setStep(step + 1) + } + } + + const handleComplete = () => { + setOnboardingComplete(true) + navigate('/sponsors') + } + + return ( +
+ + + + {step === 0 && } + {step === 1 && } + {step === 2 && } + {step === 3 && } + + + {step > 0 && ( +
+ +
+ )} +
+ ) +} diff --git a/src/router/index.tsx b/src/router/index.tsx index e58f550..af430b9 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -7,6 +7,7 @@ import { Vendors } from '../pages/Vendors' import { VendorRegister } from '../pages/VendorRegister' import { VendorDashboard } from '../pages/VendorDashboard' import { Sponsors } from '../pages/Sponsors' +import { SponsorOnboarding } from '../pages/SponsorOnboarding' import { Vouch } from '../pages/Vouch' import { LearnerProfile } from '../pages/LearnerProfile' import { NotFound } from '../pages/NotFound' @@ -40,6 +41,10 @@ const router = createBrowserRouter([ path: '/sponsors', element: , }, + { + path: '/sponsors/onboarding', + element: , + }, { path: '/vouch', element: , diff --git a/src/services/pool.service.ts b/src/services/pool.service.ts new file mode 100644 index 0000000..9582676 --- /dev/null +++ b/src/services/pool.service.ts @@ -0,0 +1,9 @@ +import { api } from './api' +import type { PoolInfo } from '../types' + +export const poolService = { + getPoolInfo: async (): Promise => { + const res = await api.get('/pool') + return res.data + }, +} diff --git a/src/stores/app.store.ts b/src/stores/app.store.ts index 8a43342..53fca42 100644 --- a/src/stores/app.store.ts +++ b/src/stores/app.store.ts @@ -1,11 +1,22 @@ import { create } from 'zustand' +import { persist } from 'zustand/middleware' interface AppStore { mobileMenuOpen: boolean + onboardingComplete: boolean setMobileMenuOpen: (open: boolean) => void + setOnboardingComplete: (complete: boolean) => void } -export const useAppStore = create((set) => ({ - mobileMenuOpen: false, - setMobileMenuOpen: (mobileMenuOpen) => set({ mobileMenuOpen }), -})) +export const useAppStore = create()( + persist( + (set) => ({ + mobileMenuOpen: false, + onboardingComplete: false, + setMobileMenuOpen: (mobileMenuOpen) => set({ mobileMenuOpen }), + setOnboardingComplete: (onboardingComplete) => + set({ onboardingComplete }), + }), + { name: 'stepfi-app' } + ) +) From 00d909174f12c5996146d1fcf63c0dd3705c669c Mon Sep 17 00:00:00 2001 From: darkify <109320650+daRk8238@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:25:55 +0000 Subject: [PATCH 2/2] fix: close mobile menu on link click instead of useEffect --- src/components/layout/Navbar.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index a6c5b30..1625bd5 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -24,10 +24,6 @@ export function Navbar() { return () => window.removeEventListener('scroll', handle) }, []) - useEffect(() => { - setMobileOpen(false) - }, [pathname]) - return (