From 6a52dad0a1c5b120db3a339f386ee733a0b02271 Mon Sep 17 00:00:00 2001 From: omoh5 Date: Thu, 30 Jul 2026 07:07:13 +0000 Subject: [PATCH] fix(frontend): remove duplicated streams/streams/[streamId] route and add redirect Delete the dead /streams/streams/:streamId route that was a leftover from refactoring streamId to id. Add a permanent redirect in next.config.ts so any external links to the old path land on the canonical stream details page. Closes #1084 --- frontend/next.config.ts | 12 +- .../app/streams/streams/[streamId]/page.tsx | 342 ------------------ 2 files changed, 11 insertions(+), 343 deletions(-) delete mode 100644 frontend/src/app/streams/streams/[streamId]/page.tsx diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa308..0bcacb53 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,17 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + async redirects() { + return [ + { + // Redirect legacy duplicated route `/streams/streams/:streamId` → `/streams/:id` + // See: https://github.com/LabsCrypt/flowfi/issues/1084 + source: "/streams/streams/:streamId", + destination: "/streams/:streamId", + permanent: true, + }, + ]; + }, }; export default nextConfig; diff --git a/frontend/src/app/streams/streams/[streamId]/page.tsx b/frontend/src/app/streams/streams/[streamId]/page.tsx deleted file mode 100644 index c602f843..00000000 --- a/frontend/src/app/streams/streams/[streamId]/page.tsx +++ /dev/null @@ -1,342 +0,0 @@ -"use client"; - -import Link from "next/link"; -import React from "react"; -import toast from "react-hot-toast"; - -import { TopUpModal } from "@/components/stream-creation/TopUpModal"; -import { Button } from "@/components/ui/Button"; -import { useWallet } from "@/context/wallet-context"; -import type { BackendStream } from "@/lib/api-types"; -import { - topUpStream as sorobanTopUp, - toBaseUnits, - toSorobanErrorMessage, -} from "@/lib/soroban"; -import { shortenPublicKey } from "@/lib/wallet"; -import { formatAmount } from "@/utils/amount"; -import { getApiBaseUrl } from "@/lib/api/_shared"; - -const API_BASE_URL = `${getApiBaseUrl()}/v1`; -const TOKEN_DECIMALS = 7; - -interface StreamDetailsPageProps { - params: { - streamId: string; - }; -} - -function toDisplayAmount(baseUnits: string): number { - const parsed = Number(baseUnits); - if (!Number.isFinite(parsed)) return 0; - return Number(formatAmount(BigInt(parsed), TOKEN_DECIMALS)); -} - -function formatUnixTimestamp(timestamp: number): string { - const date = new Date(timestamp * 1000); - if (Number.isNaN(date.getTime())) return "N/A"; - return date.toLocaleString(); -} - -function inferTokenSymbol(tokenAddress: string): string { - if (!tokenAddress) return "UNKNOWN"; - - const normalized = tokenAddress.toUpperCase(); - - // Improved mapping with FLOW added - if (normalized.includes("FLOW")) return "FLOW"; - if (normalized.includes("USDC")) return "USDC"; - if (normalized.includes("USDT")) return "USDT"; - if (normalized.includes("XLM")) return "XLM"; - if (normalized.includes("YUSDC")) return "yUSDC"; - if (normalized.includes("YXLM")) return "yXLM"; - - // Fallback: return original if known, else shorten - return tokenAddress.length > 10 - ? tokenAddress.slice(0, 6) + "..." + tokenAddress.slice(-4) - : tokenAddress; -} - -export default function StreamDetailsPage({ params }: StreamDetailsPageProps) { - const { session, status } = useWallet(); - - const [stream, setStream] = React.useState(null); - const [loading, setLoading] = React.useState(true); - const [error, setError] = React.useState(null); - const [showTopUpModal, setShowTopUpModal] = React.useState(false); - - const streamId = params.streamId; - const isValidStreamId = /^\d+$/.test(streamId); - - const loadStream = React.useCallback(async () => { - if (!isValidStreamId) { - setError("Stream id must be numeric."); - setLoading(false); - return; - } - - try { - setLoading(true); - setError(null); - - const response = await fetch(`${API_BASE_URL}/streams/${streamId}`, { - cache: "no-store", - }); - - if (!response.ok) { - if (response.status === 404) { - throw new Error("Stream not found."); - } - throw new Error(`Failed to load stream (${response.status}).`); - } - - const data = (await response.json()) as BackendStream; - setStream(data); - } catch (err) { - const message = err instanceof Error ? err.message : "Failed to load stream."; - setError(message); - setStream(null); - } finally { - setLoading(false); - } - }, [isValidStreamId, streamId]); - - React.useEffect(() => { - const initLoad = async () => { - await loadStream(); - }; - void initLoad(); - }, [loadStream]); - - const depositedAmount = stream ? toDisplayAmount(stream.depositedAmount) : 0; - const withdrawnAmount = stream ? toDisplayAmount(stream.withdrawnAmount) : 0; - const remainingAmount = Math.max(depositedAmount - withdrawnAmount, 0); - const tokenSymbol = stream ? inferTokenSymbol(stream.tokenAddress) : "TOKEN"; - - const isSender = Boolean( - session && stream && session.publicKey === stream.sender, - ); - const canTopUp = - Boolean(stream?.isActive) && - status === "connected" && - Boolean(session) && - isSender; - - let topUpHelper = ""; - if (!stream?.isActive) { - topUpHelper = "Only active streams can be topped up."; - } else if (status !== "connected") { - topUpHelper = "Connect your wallet to top up this stream."; - } else if (!isSender) { - topUpHelper = "Only the stream sender can top up this stream."; - } - - const handleTopUpConfirm = async (_streamId: string, amount: string) => { - if (!stream || !session) { - throw new Error("Wallet is not connected."); - } - - const toastId = toast.loading("Submitting top up transaction..."); - - try { - const amountInBaseUnits = toBaseUnits(amount); - - await sorobanTopUp(session, { - streamId: BigInt(stream.streamId), - amount: amountInBaseUnits, - }); - - setStream((previous) => { - if (!previous) return previous; - - let nextDepositedAmount = previous.depositedAmount; - try { - nextDepositedAmount = ( - BigInt(previous.depositedAmount) + amountInBaseUnits - ).toString(); - } catch { - nextDepositedAmount = previous.depositedAmount; - } - - return { - ...previous, - depositedAmount: nextDepositedAmount, - lastUpdateTime: Math.floor(Date.now() / 1000), - }; - }); - - setShowTopUpModal(false); - toast.success("Top up transaction submitted.", { id: toastId }); - } catch (err) { - toast.error(toSorobanErrorMessage(err), { id: toastId }); - throw err; - } - }; - - if (loading) { - return ( -
-
-
-

Loading stream...

-

Fetching stream details and latest balances.

-
-
- ); - } - - if (error || !stream) { - return ( -
-
-

Stream Details

-

Unable to load stream

-

{error ?? "The requested stream could not be loaded."}

-
- - Back to Dashboard - - -
-
-
- ); - } - - return ( -
-
-
-
-

Stream Details

-

Stream #{stream.streamId}

-

- Created {new Date(stream.createdAt).toLocaleString()} -

-
- -
- - Back - - -
-
- - {topUpHelper ?

{topUpHelper}

: null} - -
-
-

Overview

- {stream.isActive ? "Active" : "Inactive"} -
- -
-
-

Deposited

-

- {depositedAmount.toFixed(2)} {tokenSymbol} -

- Total funded to the stream. -
-
-

Withdrawn

-

- {withdrawnAmount.toFixed(2)} {tokenSymbol} -

- Amount claimed by recipient. -
-
-

Remaining

-

- {remainingAmount.toFixed(2)} {tokenSymbol} -

- Estimated balance still in stream. -
-
-
- -
-
-

Participants

- Sender and recipient wallets -
-
-
- Sender - {shortenPublicKey(stream.sender)} -
-
- Recipient - {shortenPublicKey(stream.recipient)} -
-
- Token Contract - {shortenPublicKey(stream.tokenAddress)} -
-
- Last Update - {formatUnixTimestamp(stream.lastUpdateTime)} -
-
-
- -
-
-

Indexed Events

- {stream.events?.length ?? 0} events -
- - {!stream.events || stream.events.length === 0 ? ( -
-

No indexed events yet for this stream.

-
- ) : ( -
- - - - - - - - - - - {stream.events.map((event) => ( - - - - - - - ))} - -
TypeAmountLedgerTimestamp
{event.eventType} - {event.amount - ? `${toDisplayAmount(event.amount).toFixed(2)} ${tokenSymbol}` - : "-"} - {event.ledgerSequence}{formatUnixTimestamp(event.timestamp)}
-
- )} -
-
- - {showTopUpModal ? ( - setShowTopUpModal(false)} - /> - ) : null} -
- ); -}