diff --git a/frontend/app/incoming/page.tsx b/frontend/app/incoming/page.tsx index b2bdf52..5d0b643 100644 --- a/frontend/app/incoming/page.tsx +++ b/frontend/app/incoming/page.tsx @@ -43,7 +43,12 @@ export default function IncomingPage() {

Loading incoming streams...

) : ( - + { + // Withdraw action is currently handled in the dashboard context. + }} + /> )} diff --git a/frontend/components/IncomingStreams.tsx b/frontend/components/IncomingStreams.tsx index 603beae..55d4fc5 100644 --- a/frontend/components/IncomingStreams.tsx +++ b/frontend/components/IncomingStreams.tsx @@ -1,33 +1,25 @@ 'use client'; import React, { useState } from 'react'; -import toast from "react-hot-toast"; import type { Stream } from '@/lib/dashboard'; interface IncomingStreamsProps { streams: Stream[]; + onWithdraw: (stream: Stream) => Promise; + withdrawingStreamId?: string | null; } -const IncomingStreams: React.FC = ({ streams }) => { +const IncomingStreams: React.FC = ({ + streams, + onWithdraw, + withdrawingStreamId = null, +}) => { const [filter, setFilter] = useState<'All' | 'Active' | 'Completed' | 'Paused'>('All'); const filteredStreams = filter === 'All' ? streams : streams.filter(s => s.status === filter); - const handleWithdraw = async () => { - const toastId = toast.loading("Transaction pending..."); - - try { - // Simulate async transaction (replace with real blockchain call later) - await new Promise((resolve) => setTimeout(resolve, 2000)); - - toast.success("Withdrawal successful!", { id: toastId }); - } catch { - toast.error("Transaction failed.", { id: toastId }); - } - }; - const handleFilterChange = (e: React.ChangeEvent) => { setFilter(e.target.value as 'All' | 'Active' | 'Completed' | 'Paused'); }; @@ -84,14 +76,16 @@ const IncomingStreams: React.FC = ({ streams }) => { diff --git a/frontend/components/dashboard/dashboard-view.tsx b/frontend/components/dashboard/dashboard-view.tsx index e979a0d..0f96ffe 100644 --- a/frontend/components/dashboard/dashboard-view.tsx +++ b/frontend/components/dashboard/dashboard-view.tsx @@ -29,6 +29,7 @@ import { createStream as sorobanCreateStream, topUpStream as sorobanTopUp, cancelStream as sorobanCancel, + withdrawFromStream as sorobanWithdraw, toBaseUnits, toDurationSeconds, getTokenAddress, @@ -149,11 +150,15 @@ function renderStats(snapshot: DashboardSnapshot | null) {

Total Received

-

{formatCurrency(snapshot.totalReceived)}

+

+ {formatCurrency(snapshot.totalReceived)} +

Total Value Locked

-

{formatCurrency(snapshot.totalValueLocked)}

+

+ {formatCurrency(snapshot.totalValueLocked)} +

); @@ -185,9 +190,7 @@ function renderAnalytics(snapshot: DashboardSnapshot | null) { ? "No data" : formatAnalyticsValue(metric.value!, metric.format)} - - {isUnavailable ? metric.unavailableText : metric.detail} - + {isUnavailable ? metric.unavailableText : metric.detail} ); })} @@ -207,7 +210,10 @@ function renderStreams(

My Active Streams

- {snapshot.outgoingStreams.filter(s => s.status === "Active").length} total + + {snapshot.outgoingStreams.filter((s) => s.status === "Active").length}{" "} + total +
@@ -228,8 +234,7 @@ function renderStreams( key={stream.id} className="cursor-pointer hover:bg-white/5" onClick={(e) => { - // Prevent row click if clicking buttons - if ((e.target as HTMLElement).closest('button')) return; + if ((e.target as HTMLElement).closest("button")) return; onShowDetails(stream); }} > @@ -328,14 +333,18 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { const [templates, setTemplates] = React.useState([]); const [templatesHydrated, setTemplatesHydrated] = React.useState(false); const [templateNameInput, setTemplateNameInput] = React.useState(""); - const [editingTemplateId, setEditingTemplateId] = React.useState< - string | null - >(null); + const [editingTemplateId, setEditingTemplateId] = React.useState( + null, + ); const [selectedTemplateId, setSelectedTemplateId] = React.useState< string | null >(null); const [streamFormMessage, setStreamFormMessage] = React.useState(null); + + // merged states + const [withdrawingIncomingStreamId, setWithdrawingIncomingStreamId] = + React.useState(null); const [isFormSubmitting, setIsFormSubmitting] = React.useState(false); // --- Snapshot State (merged) --- @@ -365,8 +374,12 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { }; const isTemplateNameValid = templateNameInput.trim().length > 0; - const saveTemplateButtonLabel = editingTemplateId ? "Update Template" : "Save Template"; - const requiredFieldsCompleted = Object.values(streamForm).filter(v => v.trim().length > 0).length; + const saveTemplateButtonLabel = editingTemplateId + ? "Update Template" + : "Save Template"; + const requiredFieldsCompleted = Object.values(streamForm).filter( + (v) => v.trim().length > 0, + ).length; const handleClearTemplateEditor = () => { setTemplateNameInput(""); @@ -399,9 +412,7 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { if (!cancelled) { setSnapshot(null); setSnapshotError( - error instanceof Error - ? error.message - : "Failed to fetch dashboard data.", + error instanceof Error ? error.message : "Failed to fetch dashboard data.", ); } } finally { @@ -446,7 +457,10 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { : t, ), ); - setStreamFormMessage({ text: `Template "${cleanedName}" updated.`, tone: "success" }); + setStreamFormMessage({ + text: `Template "${cleanedName}" updated.`, + tone: "success", + }); setSelectedTemplateId(editingTemplateId); setEditingTemplateId(null); setTemplateNameInput(""); @@ -462,7 +476,10 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { setTemplates((prev) => [newTemplate, ...prev]); setSelectedTemplateId(newTemplate.id); setTemplateNameInput(""); - setStreamFormMessage({ text: `Template "${cleanedName}" saved.`, tone: "success" }); + setStreamFormMessage({ + text: `Template "${cleanedName}" saved.`, + tone: "success", + }); }; const handleEditTemplate = (templateId: string) => { @@ -498,36 +515,33 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { // ── Optimistic helpers ────────────────────────────────────────────────────── - /** Mark a stream as cancelled in local state. */ const removeStreamLocally = (streamId: string) => { setSnapshot((prev) => { if (!prev) return prev; return { ...prev, outgoingStreams: prev.outgoingStreams.map((s) => - s.id === streamId ? { ...s, status: "Cancelled" as "Active" | "Completed" | "Paused" } : s, + s.id === streamId + ? { ...s, status: "Cancelled" as "Active" | "Completed" | "Paused" } + : s, ), activeStreamsCount: Math.max(0, prev.activeStreamsCount - 1), }; }); }; - /** Add top-up amount to a stream in local state. */ const topUpStreamLocally = (streamId: string, amount: number) => { setSnapshot((prev) => { if (!prev) return prev; return { ...prev, outgoingStreams: prev.outgoingStreams.map((s) => - s.id === streamId - ? { ...s, deposited: s.deposited + amount } - : s, + s.id === streamId ? { ...s, deposited: s.deposited + amount } : s, ), }; }); }; - /** Prepend a new stream to local state after creation. */ const addStreamLocally = (data: StreamFormData) => { const newStream: Stream = { id: `stream-${Date.now()}`, @@ -570,7 +584,6 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { toast.success("Stream created successfully!", { id: toastId }); } catch (err) { toast.error(toSorobanErrorMessage(err), { id: toastId }); - // Re-throw so the wizard's isSubmitting state resets properly throw err; } }; @@ -609,7 +622,29 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { } }; - const handleFormCreateStream = async (event: React.FormEvent) => { + const handleIncomingWithdraw = async (stream: Stream) => { + const toastId = toast.loading("Withdrawing stream funds…"); + setWithdrawingIncomingStreamId(stream.id); + + try { + await sorobanWithdraw(session, { + streamId: BigInt(stream.id.replace(/\D/g, "") || "0"), + }); + + const refreshed = await fetchDashboardData(session.publicKey); + setSnapshot(refreshed); + toast.success("Withdrawal successful!", { id: toastId }); + } catch (err) { + toast.error(toSorobanErrorMessage(err), { id: toastId }); + throw err; + } finally { + setWithdrawingIncomingStreamId(null); + } + }; + + const handleFormCreateStream = async ( + event: React.FormEvent, + ) => { event.preventDefault(); const hasRequiredFields = streamForm.recipient.trim() && @@ -645,7 +680,9 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { return; } - const durationSeconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000); + const durationSeconds = Math.floor( + (endDate.getTime() - startDate.getTime()) / 1000, + ); if (durationSeconds <= 0) { setStreamFormMessage({ text: "End time must be after start time.", @@ -703,7 +740,11 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { return (
- +
); } @@ -732,8 +773,8 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {

No stream data yet

- Your account is connected, but there are no active or historical - stream records available yet. + Your account is connected, but there are no active or historical stream + records available yet.

  • Create your first payment stream
  • @@ -757,7 +798,7 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { snapshot, (stream: Stream) => setModal({ type: "topup", stream }), (stream: Stream) => setModal({ type: "cancel", stream }), - (stream: Stream) => setModal({ type: "details", stream }) + (stream: Stream) => setModal({ type: "details", stream }), )} {renderRecentActivity(snapshot)} @@ -783,8 +824,8 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {

    Template Library

    - Save recurring stream settings once, apply instantly, then - override before submitting. + Save recurring stream settings once, apply instantly, then override + before submitting.

    @@ -831,9 +872,7 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { >
    {template.name} - - Updated {formatTemplateUpdatedAt(template.updatedAt)} - + Updated {formatTemplateUpdatedAt(template.updatedAt)}
    @@ -979,9 +1010,7 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { Note