'use client' import dynamic from 'next/dynamic' import Link from 'next/link' import { useState, useCallback, useEffect, useRef } from 'react' import { useAuthStore } from '@/lib/auth' import { AuthButton } from '@/components/AuthButton' import { starterNodes } from '@/lib/presets' import { serializeState, deserializeState, saveToLocal, loadFromLocal, listSavedSpaces, deleteFromLocal } from '@/lib/state' import type { FlowNode, SpaceConfig, IntegrationConfig } from '@/lib/types' import { deployNodes, getFlow, syncNodesToBackend, deposit, listFlows, fromSmallestUnit, initiateOnRamp, type BackendFlow, type DeployResult, } from '@/lib/api/flows-client' import TransakWidget from '@/components/TransakWidget' const FlowCanvas = dynamic(() => import('@/components/FlowCanvas'), { ssr: false, loading: () => (
Loading flow editor...
), }) export default function SpacePage() { const { isAuthenticated, token } = useAuthStore() const [currentNodes, setCurrentNodes] = useState(starterNodes) const [integrations, setIntegrations] = useState() const [spaceName, setSpaceName] = useState('') const [showSaveDialog, setShowSaveDialog] = useState(false) const [showLoadDialog, setShowLoadDialog] = useState(false) const [savedSpaces, setSavedSpaces] = useState([]) const [copied, setCopied] = useState(false) const [loaded, setLoaded] = useState(false) const nodesRef = useRef(starterNodes) const integrationsRef = useRef() // Backend integration state const [deployedFlow, setDeployedFlow] = useState(null) const [idMap, setIdMap] = useState>({}) const [showDeployDialog, setShowDeployDialog] = useState(false) const [showDepositDialog, setShowDepositDialog] = useState(false) const [showLoadFlowDialog, setShowLoadFlowDialog] = useState(false) const [deployName, setDeployName] = useState('') const [ownerAddress, setOwnerAddress] = useState('') const [deploying, setDeploying] = useState(false) const [syncing, setSyncing] = useState(false) const [depositFunnelId, setDepositFunnelId] = useState('') const [depositAmount, setDepositAmount] = useState('') const [depositing, setDepositing] = useState(false) const [backendFlows, setBackendFlows] = useState([]) const [statusMessage, setStatusMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null) const [transakUrl, setTransakUrl] = useState(null) const [showFundDropdown, setShowFundDropdown] = useState(false) // Load saved owner address useEffect(() => { if (typeof window === 'undefined') return const saved = localStorage.getItem('rfunds-owner-address') if (saved) setOwnerAddress(saved) }, []) const showStatus = useCallback((text: string, type: 'success' | 'error') => { setStatusMessage({ text, type }) setTimeout(() => setStatusMessage(null), 4000) }, []) const handleIntegrationsChange = useCallback((config: IntegrationConfig) => { setIntegrations(config) integrationsRef.current = config }, []) // Load from URL hash on mount useEffect(() => { if (typeof window === 'undefined') return const hash = window.location.hash.slice(1) if (hash.startsWith('s=')) { const compressed = hash.slice(2) const state = deserializeState(compressed) if (state) { setCurrentNodes(state.nodes) nodesRef.current = state.nodes if (state.integrations) { setIntegrations(state.integrations) integrationsRef.current = state.integrations } } } setLoaded(true) }, []) const handleNodesChange = useCallback((nodes: FlowNode[]) => { nodesRef.current = nodes }, []) const handleShare = useCallback(() => { const compressed = serializeState(nodesRef.current, integrationsRef.current) const url = `${window.location.origin}/space#s=${compressed}` navigator.clipboard.writeText(url).then(() => { setCopied(true) setTimeout(() => setCopied(false), 2000) }) }, []) const handleSave = useCallback(() => { if (!spaceName.trim()) return saveToLocal(spaceName.trim(), nodesRef.current, integrationsRef.current) setShowSaveDialog(false) setSpaceName('') }, [spaceName]) const handleLoadOpen = useCallback(() => { setSavedSpaces(listSavedSpaces()) setShowLoadDialog(true) }, []) const handleLoadSpace = useCallback((config: SpaceConfig) => { setCurrentNodes(config.nodes) nodesRef.current = config.nodes if (config.integrations) { setIntegrations(config.integrations) integrationsRef.current = config.integrations } setShowLoadDialog(false) }, []) const handleDeleteSpace = useCallback((name: string) => { deleteFromLocal(name) setSavedSpaces(listSavedSpaces()) }, []) const handleReset = useCallback(() => { if (confirm('Reset canvas to a single empty funnel? This cannot be undone.')) { setCurrentNodes([...starterNodes]) nodesRef.current = [...starterNodes] setDeployedFlow(null) setIdMap({}) window.history.replaceState(null, '', window.location.pathname) } }, []) // ─── Backend Integration Handlers ─────────────────────────── const handleDeploy = useCallback(async () => { if (!deployName.trim() || !ownerAddress.trim()) return setDeploying(true) try { localStorage.setItem('rfunds-owner-address', ownerAddress.trim()) const result: DeployResult = await deployNodes( deployName.trim(), ownerAddress.trim(), nodesRef.current ) setDeployedFlow(result.flow) setIdMap(result.idMap) setShowDeployDialog(false) setDeployName('') showStatus(`Deployed "${result.flow.name}" (${result.flow.funnels.length} funnels, ${result.flow.outcomes.length} outcomes)`, 'success') } catch (err) { showStatus(`Deploy failed: ${err instanceof Error ? err.message : 'Unknown error'}`, 'error') } finally { setDeploying(false) } }, [deployName, ownerAddress, showStatus]) const handleSync = useCallback(async () => { if (!deployedFlow) return setSyncing(true) try { const latest = await getFlow(deployedFlow.id) setDeployedFlow(latest) const updated = syncNodesToBackend(nodesRef.current, latest, idMap) setCurrentNodes(updated) nodesRef.current = updated showStatus('Synced live balances from backend', 'success') } catch (err) { showStatus(`Sync failed: ${err instanceof Error ? err.message : 'Unknown error'}`, 'error') } finally { setSyncing(false) } }, [deployedFlow, idMap, showStatus]) const handleDeposit = useCallback(async () => { if (!deployedFlow || !depositFunnelId || !depositAmount) return setDepositing(true) try { const amount = parseFloat(depositAmount) if (isNaN(amount) || amount <= 0) throw new Error('Invalid amount') const tx = await deposit(deployedFlow.id, depositFunnelId, amount) showStatus(`Deposited $${amount} USDC (tx: ${tx.status})`, 'success') setShowDepositDialog(false) setDepositAmount('') // Auto-sync after deposit const latest = await getFlow(deployedFlow.id) setDeployedFlow(latest) const updated = syncNodesToBackend(nodesRef.current, latest, idMap) setCurrentNodes(updated) nodesRef.current = updated } catch (err) { showStatus(`Deposit failed: ${err instanceof Error ? err.message : 'Unknown error'}`, 'error') } finally { setDepositing(false) } }, [deployedFlow, depositFunnelId, depositAmount, idMap, showStatus]) const handlePayWithCard = useCallback(async () => { if (!deployedFlow || deployedFlow.funnels.length === 0) return setShowFundDropdown(false) try { const funnelId = depositFunnelId || deployedFlow.funnels[0].id const amount = depositAmount ? parseFloat(depositAmount) : 100 const result = await initiateOnRamp(deployedFlow.id, funnelId, amount, 'USD') if (result.widgetUrl) { setTransakUrl(result.widgetUrl) } else { showStatus('No widget URL returned from on-ramp provider', 'error') } } catch (err) { showStatus(`Card payment failed: ${err instanceof Error ? err.message : 'Unknown error'}`, 'error') } }, [deployedFlow, depositFunnelId, depositAmount, showStatus]) const handleLoadFlowOpen = useCallback(async () => { if (!ownerAddress.trim()) { showStatus('Set your wallet address first (via Deploy dialog)', 'error') return } try { const flows = await listFlows(ownerAddress.trim()) setBackendFlows(flows) setShowLoadFlowDialog(true) } catch (err) { showStatus(`Failed to list flows: ${err instanceof Error ? err.message : 'Unknown error'}`, 'error') } }, [ownerAddress, showStatus]) const handleLoadFlow = useCallback((flow: BackendFlow) => { setDeployedFlow(flow) // Build idMap from backend flow — map by index (best effort without original mapping) const newIdMap: Record = {} const funnelNodes = nodesRef.current.filter(n => n.type === 'funnel') const outcomeNodes = nodesRef.current.filter(n => n.type === 'outcome') flow.funnels.forEach((f, i) => { if (funnelNodes[i]) newIdMap[funnelNodes[i].id] = f.id }) flow.outcomes.forEach((o, i) => { if (outcomeNodes[i]) newIdMap[outcomeNodes[i].id] = o.id }) setIdMap(newIdMap) setShowLoadFlowDialog(false) showStatus(`Loaded flow "${flow.name}" (${flow.status})`, 'success') }, [showStatus]) // Set auth cookie for middleware (so server-side can check) useEffect(() => { if (token) { document.cookie = `encryptid_token=${token}; path=/; max-age=86400; SameSite=Lax` } else { document.cookie = 'encryptid_token=; path=/; max-age=0' } }, [token]) if (!loaded) { return (
Loading...
) } if (!isAuthenticated) { return (

Sign in to access your Space

EncryptID passkey authentication is required to create and edit funding flows.

Back to home
) } return (
{/* Status Toast */} {statusMessage && (
{statusMessage.text}
)} {/* Toolbar */}
rF
rFunds | Your Space | {deployedFlow && ( <> | {deployedFlow.status} · {deployedFlow.name} )}
{/* Local operations */} {/* Separator */}
{/* Backend operations */} {deployedFlow && ( <>
{showFundDropdown && ( <>
setShowFundDropdown(false)} />
)}
)} {/* Separator */}
{/* Canvas */}
n.id))} initialNodes={currentNodes} mode="space" onNodesChange={handleNodesChange} integrations={integrations} onIntegrationsChange={handleIntegrationsChange} />
{/* Save Dialog */} {showSaveDialog && (
setShowSaveDialog(false)}>
e.stopPropagation()}>

Save Space

setSpaceName(e.target.value)} placeholder="Space name..." className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-4 text-slate-800" autoFocus onKeyDown={e => e.key === 'Enter' && handleSave()} />
)} {/* Load Dialog */} {showLoadDialog && (
setShowLoadDialog(false)}>
e.stopPropagation()}>

Load Space

{savedSpaces.length === 0 ? (

No saved spaces yet.

) : (
{savedSpaces.map((space) => (
))}
)}
)} {/* Deploy Dialog */} {showDeployDialog && (
setShowDeployDialog(false)}>
e.stopPropagation()}>

Deploy to Backend

Creates a live flow on the server with real threshold logic, overflow distribution, and fiat on-ramp support.

setDeployName(e.target.value)} placeholder="Flow name..." className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-3 text-slate-800" autoFocus /> setOwnerAddress(e.target.value)} placeholder="Owner wallet address (0x...)" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mb-3 text-slate-800 font-mono text-xs" />
{nodesRef.current.filter(n => n.type === 'funnel').length} funnels •{' '} {nodesRef.current.filter(n => n.type === 'outcome').length} outcomes will be created on Base Sepolia (USDC)
)} {/* Deposit Dialog */} {showDepositDialog && deployedFlow && (
setShowDepositDialog(false)}>
e.stopPropagation()}>

Deposit USDC

Simulate a deposit into a funnel. If the deposit pushes the balance above MAX threshold, overflow will automatically distribute.

$ setDepositAmount(e.target.value)} placeholder="Amount (USDC)" className="w-full pl-7 pr-3 py-2 border border-slate-200 rounded-lg text-sm text-slate-800" min="0" step="0.01" />
)} {/* Transak Widget */} {transakUrl && ( setTransakUrl(null)} onComplete={(orderId) => { showStatus(`Card payment completed (order: ${orderId})`, 'success') // Auto-sync after card payment if (deployedFlow) { getFlow(deployedFlow.id).then((latest) => { setDeployedFlow(latest) const updated = syncNodesToBackend(nodesRef.current, latest, idMap) setCurrentNodes(updated) nodesRef.current = updated }).catch(() => {}) } }} /> )} {/* Load Backend Flow Dialog */} {showLoadFlowDialog && (
setShowLoadFlowDialog(false)}>
e.stopPropagation()}>

Deployed Flows

{backendFlows.length === 0 ? (

No deployed flows yet.

) : (
{backendFlows.map((flow) => ( ))}
)}
)}
) }