From dae41a269f42127b734cc0e1de938f5ce3164f8a Mon Sep 17 00:00:00 2001 From: Hikmah Oladele <178912792+Hikmaholadele@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:52:32 +0000 Subject: [PATCH 1/2] fix(tracker): migrate both call sites to shared useTransactionTracker hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrate incoming-content.tsx to use useTransactionTracker, ensuring the confirming state is never skipped (was jumping submitted→confirmed via a 1.5s timeout) - Migrate stream-details-content.tsx to use useTransactionTracker, replacing ad-hoc boolean state (withdrawing/cancelling/pausing/resuming) with the shared state machine - Both call sites now flow: signing → submitted → confirming → confirmed - TransactionTracker component renders from both pages via the shared hook Closes #1268 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .../src/app/incoming/incoming-content.tsx | 47 ++------- .../streams/[id]/stream-details-content.tsx | 97 ++++++++++++------- 2 files changed, 74 insertions(+), 70 deletions(-) diff --git a/frontend/src/app/incoming/incoming-content.tsx b/frontend/src/app/incoming/incoming-content.tsx index a5af7ede..579941e0 100644 --- a/frontend/src/app/incoming/incoming-content.tsx +++ b/frontend/src/app/incoming/incoming-content.tsx @@ -3,7 +3,7 @@ import React from "react"; import toast from "react-hot-toast"; import TransactionTracker, { - type TransactionStatus, + useTransactionTracker, } from "@/components/TransactionTracker"; import { IncomingStreamCard } from "@/components/streams/IncomingStreamCard"; import { Skeleton } from "@/components/ui/Skeleton"; @@ -17,13 +17,6 @@ import { useWithdrawIncomingStream, } from "@/hooks/useIncomingStreams"; -interface TrackerState { - status: TransactionStatus; - txHash?: string; - error?: string; - streamId?: string; -} - function LoadingCard() { return (
@@ -41,48 +34,29 @@ function LoadingCard() { export default function IncomingContent() { const { session, status, isHydrated } = useWallet(); - const [tracker, setTracker] = React.useState({ - status: "idle", - }); + const tracker = useTransactionTracker(); const incomingStreamsQuery = useIncomingStreams(session?.publicKey); const withdrawMutation = useWithdrawIncomingStream( session, session?.publicKey, { - onSuccess: async (result, stream) => { - setTracker({ - status: "submitted", - txHash: result.txHash, - streamId: String(stream.streamId), - }); - toast.success(`Withdrawal submitted for stream #${stream.streamId}`); - - window.setTimeout(() => { - setTracker((current) => - current.txHash === result.txHash - ? { ...current, status: "confirmed" } - : current, - ); - }, 1500); + onSuccess: async (result, _stream) => { + tracker.submit(result.txHash); + toast.success(`Withdrawal submitted for stream #${_stream.streamId}`); + // Move to confirming — TransactionTracker polls the indexer from here + tracker.confirm(); }, - onError: (error, stream) => { + onError: (error) => { const message = toSorobanErrorMessage(error); - setTracker({ - status: "failed", - error: message, - streamId: String(stream.streamId), - }); + tracker.fail(message); toast.error(message); }, }, ); const handleWithdraw = async (stream: IncomingStreamRecord) => { - setTracker({ - status: "signing", - streamId: String(stream.streamId), - }); + tracker.start(); try { await withdrawMutation.mutateAsync(stream); @@ -192,7 +166,6 @@ export default function IncomingContent() { action="withdraw" txHash={tracker.txHash} error={tracker.error} - streamId={tracker.streamId} /> )} diff --git a/frontend/src/app/streams/[id]/stream-details-content.tsx b/frontend/src/app/streams/[id]/stream-details-content.tsx index 1c8cf265..216ea65a 100644 --- a/frontend/src/app/streams/[id]/stream-details-content.tsx +++ b/frontend/src/app/streams/[id]/stream-details-content.tsx @@ -9,6 +9,9 @@ import { Button } from "@/components/ui/Button"; import toast from "react-hot-toast"; import { useWallet } from "@/context/wallet-context"; import { useStreamEvents } from "@/hooks/useStreamEvents"; +import TransactionTracker, { + useTransactionTracker, +} from "@/components/TransactionTracker"; import { withdrawFromStream, cancelStream, @@ -68,6 +71,7 @@ const EVENT_STYLES: Record(null); const [events, setEvents] = useState([]); @@ -76,10 +80,6 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [withdrawing, setWithdrawing] = useState(false); - const [cancelling, setCancelling] = useState(false); - const [pausing, setPausing] = useState(false); - const [resuming, setResuming] = useState(false); const [topUpAmount, setTopUpAmount] = useState(""); const [showTopUp, setShowTopUp] = useState(false); const [showCancelModal, setShowCancelModal] = useState(false); @@ -200,15 +200,18 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) toast.error("Please connect your wallet"); return; } - setWithdrawing(true); + tracker.start(); try { - await withdrawFromStream(session, { streamId: BigInt(streamId) }); + const result = await withdrawFromStream(session, { streamId: BigInt(streamId) }); + tracker.submit(result.txHash); + tracker.confirm(); toast.success("Withdrawal successful!"); await fetchStream(); + tracker.succeed(); } catch (err) { - toast.error(toSorobanErrorMessage(err)); - } finally { - setWithdrawing(false); + const message = toSorobanErrorMessage(err); + tracker.fail(message); + toast.error(message); } }; @@ -221,15 +224,21 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) toast.error("Please enter a valid amount"); return; } + tracker.start(); try { const amount = toBaseUnits(topUpAmount); - await topUpStream(session, { streamId: BigInt(streamId), amount }); + const result = await topUpStream(session, { streamId: BigInt(streamId), amount }); + tracker.submit(result.txHash); + tracker.confirm(); toast.success("Stream topped up successfully!"); setShowTopUp(false); setTopUpAmount(""); await fetchStream(); + tracker.succeed(); } catch (err) { - toast.error(toSorobanErrorMessage(err)); + const message = toSorobanErrorMessage(err); + tracker.fail(message); + toast.error(message); } }; @@ -238,15 +247,18 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) toast.error("Please connect your wallet"); return; } - setPausing(true); + tracker.start(); try { - await pauseStream(session, { streamId: BigInt(streamId) }); + const result = await pauseStream(session, { streamId: BigInt(streamId) }); + tracker.submit(result.txHash); + tracker.confirm(); toast.success("Stream paused"); await fetchStream(); + tracker.succeed(); } catch (err) { - toast.error(toSorobanErrorMessage(err)); - } finally { - setPausing(false); + const message = toSorobanErrorMessage(err); + tracker.fail(message); + toast.error(message); } }; @@ -255,15 +267,18 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) toast.error("Please connect your wallet"); return; } - setResuming(true); + tracker.start(); try { - await resumeStream(session, { streamId: BigInt(streamId) }); + const result = await resumeStream(session, { streamId: BigInt(streamId) }); + tracker.submit(result.txHash); + tracker.confirm(); toast.success("Stream resumed"); await fetchStream(); + tracker.succeed(); } catch (err) { - toast.error(toSorobanErrorMessage(err)); - } finally { - setResuming(false); + const message = toSorobanErrorMessage(err); + tracker.fail(message); + toast.error(message); } }; @@ -272,16 +287,19 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) toast.error("Please connect your wallet"); return; } - setCancelling(true); + tracker.start(); try { - await cancelStream(session, { streamId: BigInt(streamId) }); + const result = await cancelStream(session, { streamId: BigInt(streamId) }); + tracker.submit(result.txHash); + tracker.confirm(); toast.success("Stream cancelled"); setShowCancelModal(false); await fetchStream(); + tracker.succeed(); } catch (err) { - toast.error(toSorobanErrorMessage(err)); - } finally { - setCancelling(false); + const message = toSorobanErrorMessage(err); + tracker.fail(message); + toast.error(message); } }; @@ -408,12 +426,12 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) {isRecipient && ( )} @@ -433,22 +451,22 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) {!stream.isPaused ? ( ) : ( )} @@ -457,7 +475,7 @@ export default function StreamDetailsContent({ streamId }: { streamId: string }) {isSender && (
)} + {/* Transaction Tracker */} + {tracker.status !== "idle" && ( +
+ +
+ )} + {/* Event History */}

Event History

From 319b0dd178ec6bb1a21b4c57cb87e5fcba18f22e Mon Sep 17 00:00:00 2001 From: Hikmah Oladele <178912792+Hikmaholadele@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:30:28 +0000 Subject: [PATCH 2/2] fix(frontend): remove unused React import from incoming-content.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the unused `import React from "react"` that caused the Frontend CI build to fail with "'React' is declared but its value is never read." 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- frontend/src/app/incoming/incoming-content.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/incoming/incoming-content.tsx b/frontend/src/app/incoming/incoming-content.tsx index 579941e0..0e924422 100644 --- a/frontend/src/app/incoming/incoming-content.tsx +++ b/frontend/src/app/incoming/incoming-content.tsx @@ -1,6 +1,5 @@ "use client"; -import React from "react"; import toast from "react-hot-toast"; import TransactionTracker, { useTransactionTracker,