Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 24 additions & 92 deletions samples/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,107 +1,39 @@
import { useState } from 'react'
import StepWizard from './components/StepWizard'
import Sidebar, { FlowKey } from './components/Sidebar'
import WebhookStream from './components/WebhookStream'
import CreateCustomer from './steps/CreateCustomer'
import CreateExternalAccount from './steps/CreateExternalAccount'
import CreateQuote from './steps/CreateQuote'
import SandboxFund from './steps/SandboxFund'
import PayoutFlow from './flows/PayoutFlow'
import EmbeddedWalletFlow from './flows/EmbeddedWalletFlow'

export default function App() {
const [activeStep, setActiveStep] = useState(0)
const [customerId, setCustomerId] = useState<string | null>(null)
const [externalAccountId, setExternalAccountId] = useState<string | null>(null)
const [quoteId, setQuoteId] = useState<string | null>(null)
const [selectedCountry, setSelectedCountry] = useState('MX')

const advance = () => setActiveStep((s) => s + 1)

const restartFromExternalAccount = () => {
setExternalAccountId(null)
setQuoteId(null)
setActiveStep(1)
}
const FLOW_META: Record<FlowKey, { title: string; subtitle: string }> = {
payout: {
title: 'Payout to Bank Account',
subtitle: 'Send a real time payment funded with USDC',
},
'embedded-wallet': {
title: 'Embedded Wallets',
subtitle: 'Create a wallet and move funds on behalf of a user',
},
}

const steps = [
{
title: '1. Create Customer',
summary: customerId ? `ID: ${customerId}` : null,
content: (
<CreateCustomer
disabled={activeStep !== 0}
onComplete={(data) => {
setCustomerId(data.id as string)
advance()
}}
/>
),
},
{
title: '2. Create External Account',
summary: externalAccountId ? `ID: ${externalAccountId}` : null,
content: (
<CreateExternalAccount
customerId={customerId}
disabled={activeStep !== 1}
selectedCountry={selectedCountry}
onCountryChange={setSelectedCountry}
onComplete={(data) => {
setExternalAccountId(data.id as string)
advance()
}}
/>
),
},
{
title: '3. Create Quote',
summary: quoteId ? `ID: ${quoteId}` : null,
content: (
<CreateQuote
customerId={customerId}
externalAccountId={externalAccountId}
selectedCountry={selectedCountry}
disabled={activeStep !== 2}
onComplete={(data) => {
setQuoteId((data.quoteId ?? data.id) as string)
advance()
}}
/>
),
},
{
title: '4. Simulate Funding (Sandbox Only)',
summary: activeStep > 3 ? 'Funded' : null,
content: (
<SandboxFund
quoteId={quoteId}
disabled={activeStep !== 3}
onComplete={() => advance()}
/>
),
},
]
export default function App() {
const [activeFlow, setActiveFlow] = useState<FlowKey>('payout')
const meta = FLOW_META[activeFlow]

return (
<div className="min-h-screen bg-gray-950 text-gray-100">
<div className="min-h-screen bg-gray-950 text-gray-100 pb-12">
<header className="border-b border-gray-800 px-6 py-4">
<h1 className="text-xl font-bold">Grid API Sample</h1>
<p className="text-sm text-gray-400">Send a real time payment funded with USDC</p>
<p className="text-sm text-gray-400">{meta.subtitle}</p>
</header>
<div className="flex">
<main className="w-3/5 p-6 border-r border-gray-800 min-h-[calc(100vh-73px)]">
<StepWizard steps={steps} activeStep={activeStep} />
{activeStep >= 1 && (
<button
onClick={restartFromExternalAccount}
className="mt-6 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm font-medium text-gray-300"
>
Start New Payment
</button>
)}
<Sidebar activeFlow={activeFlow} onSelect={setActiveFlow} />
<main className="flex-1 p-6 min-h-[calc(100vh-73px)] max-w-5xl">
<h2 className="text-lg font-semibold mb-4">{meta.title}</h2>
{activeFlow === 'payout' && <PayoutFlow key="payout" />}
{activeFlow === 'embedded-wallet' && <EmbeddedWalletFlow key="embedded-wallet" />}
</main>
<aside className="w-2/5 p-6 min-h-[calc(100vh-73px)]">
<WebhookStream />
</aside>
</div>
<WebhookStream />
</div>
)
}
55 changes: 55 additions & 0 deletions samples/frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export type FlowKey = 'payout' | 'embedded-wallet'

interface FlowEntry {
key: FlowKey
label: string
description: string
}

const FLOWS: FlowEntry[] = [
{
key: 'payout',
label: 'Payout to Bank Account',
description: 'Send a real-time payment funded with USDC',
},
{
key: 'embedded-wallet',
label: 'Embedded Wallets',
description: 'Create a wallet and move funds on behalf of a user',
},
]

interface SidebarProps {
activeFlow: FlowKey
onSelect: (flow: FlowKey) => void
}

export default function Sidebar({ activeFlow, onSelect }: SidebarProps) {
return (
<nav className="w-64 shrink-0 border-r border-gray-800 p-4 min-h-[calc(100vh-73px)]">
<h2 className="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-3 px-2">
Flows
</h2>
<ul className="space-y-1">
{FLOWS.map((flow) => {
const isActive = flow.key === activeFlow
return (
<li key={flow.key}>
<button
onClick={() => onSelect(flow.key)}
className={`w-full text-left rounded px-3 py-2 transition-colors ${
isActive
? 'bg-blue-600/20 border border-blue-600/40 text-blue-100'
: 'border border-transparent hover:bg-gray-800/60 text-gray-300'
}`}
>
<div className="text-sm font-medium">{flow.label}</div>
<div className="text-xs text-gray-500 mt-0.5">{flow.description}</div>
</button>
</li>
)
})}
</ul>
</nav>
)
}
91 changes: 59 additions & 32 deletions samples/frontend/src/components/WebhookStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export default function WebhookStream() {
const [events, setEvents] = useState<WebhookEvent[]>([])
const [connected, setConnected] = useState(false)
const [expandedIndex, setExpandedIndex] = useState<number | null>(null)
const [open, setOpen] = useState(false)
const [unread, setUnread] = useState(0)
const eventSourceRef = useRef<EventSource | null>(null)
const openRef = useRef(open)
openRef.current = open

useEffect(() => {
const connect = () => {
Expand All @@ -34,6 +38,7 @@ export default function WebhookStream() {
raw: event.data
}, ...prev])
}
if (!openRef.current) setUnread((n) => n + 1)
}
es.onerror = () => {
setConnected(false)
Expand All @@ -46,43 +51,65 @@ export default function WebhookStream() {
return () => eventSourceRef.current?.close()
}, [])

const toggle = () => {
setOpen((prev) => {
if (!prev) setUnread(0)
return !prev
})
}

return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 mb-3">
<h2 className="text-lg font-semibold">Webhooks</h2>
<div className="fixed bottom-0 left-0 right-0 z-40 border-t border-gray-800 bg-gray-950/95 backdrop-blur">
<button
onClick={toggle}
className="w-full flex items-center gap-3 px-4 py-2 text-left hover:bg-gray-900/60"
>
<span className="text-sm font-semibold text-gray-100">Webhooks</span>
<span className={`inline-block w-2 h-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-xs text-gray-500">{connected ? 'Connected' : 'Disconnected'}</span>
</div>
<div className="flex-1 overflow-y-auto space-y-2">
{events.length === 0 && (
<p className="text-sm text-gray-500">No webhook events received yet.</p>
{unread > 0 && !open && (
<span className="ml-1 px-1.5 py-0.5 text-xs font-mono bg-purple-900 text-purple-200 rounded">
{unread} new
</span>
)}
{events.map((evt, i) => (
<div key={i} className="bg-gray-900 border border-gray-700 rounded-lg p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 bg-purple-900 text-purple-300 text-xs rounded font-mono">
{evt.type}
</span>
<span className="text-xs text-gray-500">
{new Date(evt.timestamp).toLocaleTimeString()}
</span>
<span className="ml-auto text-xs text-gray-500">
{events.length} event{events.length === 1 ? '' : 's'}
</span>
<span className="text-xs text-gray-400">{open ? '▼' : '▲'}</span>
</button>
{open && (
<div className="h-72 overflow-y-auto px-4 pb-3 space-y-2 border-t border-gray-800">
{events.length === 0 ? (
<p className="text-sm text-gray-500 pt-3">No webhook events received yet.</p>
) : (
events.map((evt, i) => (
<div key={i} className="bg-gray-900 border border-gray-700 rounded-lg p-3 mt-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 bg-purple-900 text-purple-300 text-xs rounded font-mono">
{evt.type}
</span>
<span className="text-xs text-gray-500">
{new Date(evt.timestamp).toLocaleTimeString()}
</span>
</div>
<button
onClick={() => setExpandedIndex(expandedIndex === i ? null : i)}
className="text-xs text-gray-400 hover:text-gray-200"
>
{expandedIndex === i ? '▼' : '▶'}
</button>
</div>
{expandedIndex === i && (
<pre className="mt-2 text-xs font-mono text-gray-300 overflow-auto max-h-48">
{evt.raw}
</pre>
)}
</div>
<button
onClick={() => setExpandedIndex(expandedIndex === i ? null : i)}
className="text-xs text-gray-400 hover:text-gray-200"
>
{expandedIndex === i ? '\u25BC' : '\u25B6'}
</button>
</div>
{expandedIndex === i && (
<pre className="mt-2 text-xs font-mono text-gray-300 overflow-auto max-h-48">
{evt.raw}
</pre>
)}
</div>
))}
</div>
))
)}
</div>
)}
</div>
)
}
Loading
Loading