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.
+
+
+ Get Started
+
+
+ )
+}
+
+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}
+
+
+
+ ))}
+
+
+
+ I Understand
+
+
+ )
+}
+
+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)}
+
+
+ )}
+
+
+
+ Continue
+
+
+ )
+}
+
+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 ? (
+
+ Connect Freighter Wallet
+
+ ) : (
+
+
+ Your wallet is connected. Head to the sponsor dashboard to make
+ your first deposit.
+
+
+ Go to Sponsor Dashboard
+
+
+ )}
+
+
+
+ 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 && (
+
+ setStep(step - 1)}
+ className="text-sm text-text-muted hover:text-text-secondary transition-colors"
+ >
+ Back
+
+
+ )}
+
+ )
+}
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 (
setMobileOpen(false)}
className="px-4 py-3 rounded-lg text-sm font-medium"
style={{
color: pathname === link.href