From 6411604852422aa8066af6cc57df012a4e452d22 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 15 Feb 2026 09:21:44 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20add=20funding=20sources=20=E2=80=94=20c?= =?UTF-8?q?ard=20payments=20via=20Transak=20and=20funding=20source=20manag?= =?UTF-8?q?ement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FundingSource types: card, crypto_wallet, safe_treasury, bank_transfer - TransakWidget component with iframe postMessage handling - FundingSourcesPanel with CRUD, config forms per type, and Fund Now action - FunnelNode edit modal: new Funding Sources section - Space page: Fund dropdown with Deposit (Crypto) + Pay with Card options Co-Authored-By: Claude Opus 4.6 --- app/space/page.tsx | 94 ++++++- components/FundingSourcesPanel.tsx | 388 +++++++++++++++++++++++++++++ components/TransakWidget.tsx | 73 ++++++ components/nodes/FunnelNode.tsx | 41 ++- lib/types.ts | 26 ++ 5 files changed, 610 insertions(+), 12 deletions(-) create mode 100644 components/FundingSourcesPanel.tsx create mode 100644 components/TransakWidget.tsx diff --git a/app/space/page.tsx b/app/space/page.tsx index 3d1f00f..c7bedef 100644 --- a/app/space/page.tsx +++ b/app/space/page.tsx @@ -15,9 +15,11 @@ import { 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, @@ -59,6 +61,8 @@ export default function SpacePage() { 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(() => { @@ -210,6 +214,23 @@ export default function SpacePage() { } }, [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') @@ -354,17 +375,48 @@ export default function SpacePage() { > {syncing ? 'Syncing...' : 'Sync'} - +
+ + {showFundDropdown && ( + <> +
setShowFundDropdown(false)} /> +
+ + +
+ + )} +
)} + {/* Delete button */} + +
+ ) + })} + + )} + + {/* Fund Now button (for deployed flows with card/bank source) */} + {isDeployed && cardOrBankSource && ( + + )} + + {fundingError && ( +

{fundingError}

+ )} + + {/* Type picker */} + {showTypePicker && !configType && ( +
+ {(Object.entries(SOURCE_TYPE_META) as [FundingSourceType, typeof SOURCE_TYPE_META['card']][]).map( + ([type, meta]) => ( + + ) + )} +
+ )} + + {/* Config form based on selected type */} + {configType && ( +
+
+ + + Configure {SOURCE_TYPE_META[configType].label} + +
+ + {/* Label input (all types) */} + setConfigLabel(e.target.value)} + placeholder="Label..." + className="w-full text-xs px-2 py-1.5 border border-slate-200 rounded mb-2 text-slate-800" + /> + + {/* Card / Bank Transfer config */} + {(configType === 'card' || configType === 'bank_transfer') && ( + <> + + + + setConfigDefaultAmount(e.target.value)} + placeholder="100" + className="w-full text-xs px-2 py-1.5 border border-slate-200 rounded mb-2 text-slate-800" + min="0" + step="1" + /> + + )} + + {/* Crypto Wallet config */} + {configType === 'crypto_wallet' && ( + <> + +
+ + {funnelWalletAddress && ( + + )} +
+ + )} + + {/* Safe Treasury config */} + {configType === 'safe_treasury' && ( + <> + + setConfigSafeAddress(e.target.value)} + placeholder="0x..." + className="w-full text-xs px-2 py-1.5 border border-slate-200 rounded mb-2 text-slate-800 font-mono" + /> + + + + )} + +
+ + +
+
+ )} + + {/* Add button */} + {!showTypePicker && !configType && ( + + )} + + {/* Transak Widget */} + {transakUrl && ( + setTransakUrl(null)} + onComplete={(orderId) => { + console.log('Transak order completed:', orderId) + }} + /> + )} + + ) +} diff --git a/components/TransakWidget.tsx b/components/TransakWidget.tsx new file mode 100644 index 0000000..01b2863 --- /dev/null +++ b/components/TransakWidget.tsx @@ -0,0 +1,73 @@ +'use client' + +import { useEffect, useCallback } from 'react' + +interface TransakWidgetProps { + widgetUrl: string + onClose: () => void + onComplete?: (orderId: string) => void +} + +export default function TransakWidget({ widgetUrl, onClose, onComplete }: TransakWidgetProps) { + const handleMessage = useCallback( + (event: MessageEvent) => { + // Transak sends postMessage events from its iframe + const data = event.data + if (!data || typeof data !== 'object') return + + switch (data.event_id || data.eventName) { + case 'TRANSAK_ORDER_SUCCESSFUL': { + const orderId = data.data?.id || data.data?.orderId || 'unknown' + onComplete?.(orderId) + onClose() + break + } + case 'TRANSAK_ORDER_FAILED': { + // Brief display then close — the parent can show a status message + onClose() + break + } + case 'TRANSAK_WIDGET_CLOSE': { + onClose() + break + } + } + }, + [onClose, onComplete] + ) + + useEffect(() => { + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, [handleMessage]) + + return ( +
+
e.stopPropagation()} + > + {/* Close button */} + + +