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
48 changes: 10 additions & 38 deletions frontend/src/app/incoming/incoming-content.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use client";

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";
Expand All @@ -17,13 +16,6 @@ import {
useWithdrawIncomingStream,
} from "@/hooks/useIncomingStreams";

interface TrackerState {
status: TransactionStatus;
txHash?: string;
error?: string;
streamId?: string;
}

function LoadingCard() {
return (
<div className="rounded-[1.75rem] border border-white/45 bg-white/75 p-5 shadow-[0_20px_45px_rgba(15,23,42,0.06)]">
Expand All @@ -41,48 +33,29 @@ function LoadingCard() {

export default function IncomingContent() {
const { session, status, isHydrated } = useWallet();
const [tracker, setTracker] = React.useState<TrackerState>({
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);
Expand Down Expand Up @@ -192,7 +165,6 @@ export default function IncomingContent() {
action="withdraw"
txHash={tracker.txHash}
error={tracker.error}
streamId={tracker.streamId}
/>
</section>
)}
Expand Down
97 changes: 64 additions & 33 deletions frontend/src/app/streams/[id]/stream-details-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -68,6 +71,7 @@ const EVENT_STYLES: Record<string, { color: string; icon: string; label: string

export default function StreamDetailsContent({ streamId }: { streamId: string }) {
const { session, isHydrated } = useWallet();
const tracker = useTransactionTracker();

const [stream, setStream] = useState<StreamDetail | null>(null);
const [events, setEvents] = useState<BackendStreamEvent[]>([]);
Expand All @@ -76,10 +80,6 @@ export default function StreamDetailsContent({ streamId }: { streamId: string })
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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);
Expand Down Expand Up @@ -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);
}
};

Expand All @@ -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);
}
};

Expand All @@ -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);
}
};

Expand All @@ -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);
}
};

Expand All @@ -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);
}
};

Expand Down Expand Up @@ -408,12 +426,12 @@ export default function StreamDetailsContent({ streamId }: { streamId: string })
{isRecipient && (
<Button
onClick={handleWithdraw}
disabled={withdrawing || liveClaimable <= 0n}
disabled={tracker.status !== "idle" || liveClaimable <= 0n}
glow
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
{withdrawing ? "Withdrawing..." : "Withdraw"}
{tracker.status === "signing" ? "Signing..." : tracker.status === "submitted" || tracker.status === "confirming" ? "Processing..." : "Withdraw"}
</Button>
)}

Expand All @@ -433,22 +451,22 @@ export default function StreamDetailsContent({ streamId }: { streamId: string })
{!stream.isPaused ? (
<Button
onClick={handlePause}
disabled={pausing}
disabled={tracker.status !== "idle"}
variant="outline"
className="flex items-center gap-2"
>
<Pause className="h-4 w-4" />
{pausing ? "Pausing..." : "Pause"}
{tracker.status !== "idle" ? "Processing..." : "Pause"}
</Button>
) : (
<Button
onClick={handleResume}
disabled={resuming}
disabled={tracker.status !== "idle"}
variant="outline"
className="flex items-center gap-2"
>
<Play className="h-4 w-4" />
{resuming ? "Resuming..." : "Resume"}
{tracker.status !== "idle" ? "Processing..." : "Resume"}
</Button>
)}
</>
Expand All @@ -457,7 +475,7 @@ export default function StreamDetailsContent({ streamId }: { streamId: string })
{isSender && (
<Button
onClick={() => setShowCancelModal(true)}
disabled={cancelling}
disabled={tracker.status !== "idle"}
variant="outline"
className="flex items-center gap-2 border-red-500/50 text-red-400 hover:bg-red-500/10"
>
Expand All @@ -483,6 +501,19 @@ export default function StreamDetailsContent({ streamId }: { streamId: string })
</div>
)}

{/* Transaction Tracker */}
{tracker.status !== "idle" && (
<div className="glass-card p-6">
<TransactionTracker
status={tracker.status}
action="withdraw"
txHash={tracker.txHash}
error={tracker.error}
streamId={streamId}
/>
</div>
)}

{/* Event History */}
<div className="glass-card p-6">
<h3 className="text-lg font-semibold mb-4">Event History</h3>
Expand Down
Loading