Skip to content
Merged
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
117 changes: 77 additions & 40 deletions frontend/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"use client";

import { useState } from "react";
import { useState, useEffect } from "react";

Check warning on line 3 in frontend/app/settings/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend CI

'useEffect' is defined but never used
import { Copy, Check, LogOut, Moon, Sun, Bell } from "lucide-react";
import { useWallet } from "@/context/wallet-context";
import { useRouter } from "next/navigation";
import { shortenPublicKey, formatNetwork } from "@/lib/wallet";

Check warning on line 7 in frontend/app/settings/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend CI

'shortenPublicKey' is defined but never used

export default function SettingsPage() {
const router = useRouter();
const { session, disconnect, isHydrated } = useWallet();
const [emailNotifications, setEmailNotifications] = useState(true);
// Use lazy initialization to avoid setState in useEffect
const [theme, setTheme] = useState<"light" | "dark">(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("flowfi-theme") as
Expand All @@ -21,9 +25,6 @@
});
const [copied, setCopied] = useState(false);

const connectedAddress =
"0x92f4D9b123456789ABCDEF123456789ABCDEF123";

const toggleTheme = () => {
const next = theme === "dark" ? "light" : "dark";
setTheme(next);
Expand All @@ -35,15 +36,26 @@
};

const copyAddress = async () => {
await navigator.clipboard.writeText(connectedAddress);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
if (session?.publicKey) {
await navigator.clipboard.writeText(session.publicKey);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
};

const handleDisconnect = () => {
console.log("Wallet disconnected");
disconnect();
router.push("/");
};

if (!isHydrated) {
return (
<div className="relative min-h-screen overflow-hidden bg-gradient-to-br from-zinc-950 via-zinc-900 to-black dark:from-white dark:via-gray-100 dark:to-gray-200 transition-colors flex items-center justify-center">
<div className="text-white dark:text-black">Loading...</div>
</div>
);
}

return (
<div className="relative min-h-screen overflow-hidden bg-gradient-to-br from-zinc-950 via-zinc-900 to-black dark:from-white dark:via-gray-100 dark:to-gray-200 transition-colors">

Expand Down Expand Up @@ -124,42 +136,67 @@
</div>

{/* Wallet Section */}
<div className="space-y-3">
<p className="font-medium text-white dark:text-black">
Connected Wallet
</p>

<div className="relative flex items-center justify-between bg-black/40 dark:bg-white/40 px-5 py-4 rounded-xl font-mono text-sm break-all text-white dark:text-black border border-white/10 dark:border-black/10">

<span className="pr-4">{connectedAddress}</span>
{session ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="font-medium text-white dark:text-black">
Connected Wallet
</p>
<div className="flex items-center gap-2">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-purple-500/20 text-purple-400 border border-purple-500/30">
{formatNetwork(session.network)}
</span>
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-500/20 text-blue-400 border border-blue-500/30">
{session.walletName}
</span>
</div>
</div>

<button
onClick={copyAddress}
className="ml-3 opacity-70 hover:opacity-100 transition"
>
{copied ? (
<Check size={18} className="text-green-400" />
) : (
<Copy size={18} />
<div className="relative flex items-center justify-between bg-black/40 dark:bg-white/40 px-5 py-4 rounded-xl font-mono text-sm break-all text-white dark:text-black border border-white/10 dark:border-black/10">
<span className="pr-4">{session.publicKey}</span>

<button
onClick={copyAddress}
className="ml-3 opacity-70 hover:opacity-100 transition flex-shrink-0"
>
{copied ? (
<Check size={18} className="text-green-400" />
) : (
<Copy size={18} />
)}
</button>

{copied && (
<span className="absolute -top-8 right-2 text-xs bg-black text-white dark:bg-white dark:text-black px-2 py-1 rounded-md shadow">
Copied
</span>
)}
</button>

{copied && (
<span className="absolute -top-8 right-2 text-xs bg-black text-white dark:bg-white dark:text-black px-2 py-1 rounded-md shadow">
Copied
</span>
)}
</div>
</div>
</div>
) : (
<div className="space-y-3">
<p className="font-medium text-white dark:text-black">
Wallet Status
</p>
<div className="flex items-center justify-between bg-black/40 dark:bg-white/40 px-5 py-4 rounded-xl text-white dark:text-black border border-white/10 dark:border-black/10">
<span>Not connected</span>
<a href="/" className="text-accent hover:opacity-80 transition font-semibold">

Check failure on line 183 in frontend/app/settings/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend CI

Do not use an `<a>` element to navigate to `/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages
Connect Wallet
</a>
</div>
</div>
)}

{/* Disconnect */}
<button
onClick={handleDisconnect}
className="w-full flex items-center justify-center gap-2 bg-red-600/90 hover:bg-red-600 transition px-4 py-3 rounded-xl text-white font-medium shadow-lg hover:shadow-red-500/30"
>
<LogOut size={18} />
Disconnect Wallet
</button>
{session && (
<button
onClick={handleDisconnect}
className="w-full flex items-center justify-center gap-2 bg-red-600/90 hover:bg-red-600 transition px-4 py-3 rounded-xl text-white font-medium shadow-lg hover:shadow-red-500/30"
>
<LogOut size={18} />
Disconnect Wallet
</button>
)}

</div>
</div>
Expand Down
57 changes: 41 additions & 16 deletions frontend/app/streams/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
"use client";

import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import LiveCounter from "@/components/Livecounter";
import ProgressBar from "@/components/Progressbar";
import { Button } from "@/components/ui/Button";
import toast from "react-hot-toast";
import { useWallet } from "@/context/wallet-context";
import { useStreamEvents } from "@/hooks/useStreamEvents";
import {
withdrawFromStream,
cancelStream,
topUpStream,
toSorobanErrorMessage,
} from "@/lib/soroban";
import type { WalletSession } from "@/lib/wallet";

Check warning on line 17 in frontend/app/streams/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend CI

'WalletSession' is defined but never used

interface StreamDetail {
id: string;
Expand All @@ -26,11 +28,15 @@
lastUpdateTime: number;
isActive: boolean;
status: string;
isPaused?: boolean;
pausedAt?: string;
}

export default function StreamDetailsPage() {
const params = useParams();
const router = useRouter();

Check warning on line 37 in frontend/app/streams/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend CI

'router' is assigned a value but never used
const streamId = params.id as string;
const { session, isHydrated } = useWallet();

const [stream, setStream] = useState<StreamDetail | null>(null);
const [loading, setLoading] = useState(true);
Expand All @@ -39,23 +45,18 @@
const [cancelling, setCancelling] = useState(false);
const [topUpAmount, setTopUpAmount] = useState("");
const [showTopUp, setShowTopUp] = useState(false);
const [session, setSession] = useState<WalletSession | null>(null);

// Mock session - in production this would come from a wallet context
useEffect(() => {
// This would normally come from a wallet provider context
// For now, we'll use a mock session
setSession({
walletId: "freighter",
publicKey: "GD...",
network: "TESTNET",
walletName: "Freighter",
connectedAt: new Date().toISOString(),
mocked: true,
});
}, []);
// SSE integration for real-time stream updates
const { events: streamEvents, connected, reconnecting } = useStreamEvents({

Check warning on line 50 in frontend/app/streams/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend CI

'reconnecting' is assigned a value but never used

Check warning on line 50 in frontend/app/streams/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend CI

'connected' is assigned a value but never used
streamIds: [streamId],
autoReconnect: true,
});

useEffect(() => {
if (!isHydrated || !session) {
return;
}

async function fetchStream() {
try {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
Expand All @@ -75,7 +76,31 @@
if (streamId) {
fetchStream();
}
}, [streamId]);
}, [streamId, session, isHydrated]);

// Handle SSE events to update stream state in real-time
useEffect(() => {
if (streamEvents.length > 0) {
const latestEvent = streamEvents[0];
console.log('Stream event received:', latestEvent);

// Re-fetch stream data to get the latest state from server
async function refetchStream() {
try {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
const response = await fetch(`${baseUrl}/v1/streams/${streamId}`);
if (response.ok) {
const data = await response.json();
setStream(data);
}
} catch (err) {
console.error('Failed to refresh stream:', err);
}
}

refetchStream();
}
}, [streamEvents, streamId]);

const handleWithdraw = async () => {
if (!session) {
Expand Down
89 changes: 48 additions & 41 deletions frontend/components/IncomingStreams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,20 @@ const ClaimableAmount: React.FC<{ stream: Stream }> = ({ stream }) => {
isActive: stream.status === 'Active' && stream.isActive,
});

const isPaused = stream.status === 'Paused';
const liveRate = stream.status === 'Active' && stream.ratePerSecond > 0;

return (
<div className="flex flex-col">
<span className={`font-bold tabular-nums ${liveRate ? 'text-emerald-600 dark:text-emerald-300' : 'text-gray-900 dark:text-gray-100'}`}>
<span className={`font-bold tabular-nums ${liveRate ? 'text-emerald-600 dark:text-emerald-300' : isPaused ? 'text-gray-400 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}>
{formatTokenAmount(claimable)} {stream.token}
</span>
<span className={`text-xs tabular-nums ${liveRate ? 'text-emerald-500 dark:text-emerald-400' : 'text-gray-400 dark:text-gray-500'}`}>
{liveRate
? `+${formatTokenAmount(stream.ratePerSecond)} ${stream.token}/sec`
: 'Stream inactive'}
<span className={`text-xs tabular-nums ${liveRate ? 'text-emerald-500 dark:text-emerald-400' : isPaused ? 'text-gray-400 dark:text-gray-500' : 'text-gray-400 dark:text-gray-500'}`}>
{isPaused
? 'Stream paused'
: liveRate
? `+${formatTokenAmount(stream.ratePerSecond)} ${stream.token}/sec`
: 'Stream inactive'}
</span>
</div>
);
Expand Down Expand Up @@ -98,42 +101,46 @@ const IncomingStreams: React.FC<IncomingStreamsProps> = ({
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredStreams.map((stream) => (
<tr key={stream.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-gray-100 font-mono">{stream.recipient}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Stream #{stream.id}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 tabular-nums">{formatTokenAmount(stream.deposited)} {stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100 font-bold tabular-nums">{formatTokenAmount(stream.withdrawn)} {stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<ClaimableAmount stream={stream} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
${stream.status === 'Active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
stream.status === 'Completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'}`}>
{stream.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
disabled={stream.status !== 'Active' || withdrawingStreamId === stream.id}
onClick={() => {
void onWithdraw(stream);
}}
className={`px-4 py-2 rounded-lg transition-all ${stream.status === 'Active'
? 'bg-accent text-white hover:bg-accent-hover shadow-lg'
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
}`}
>
{withdrawingStreamId === stream.id ? 'Withdrawing...' : 'Withdraw'}
</button>
</td>
</tr>
))}
{filteredStreams.map((stream) => {
const isPaused = stream.status === 'Paused';
return (
<tr key={stream.id} className={`hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${isPaused ? 'bg-gray-50/50 dark:bg-gray-800/50 opacity-75' : ''}`}>
<td className="px-6 py-4 whitespace-nowrap">
<div className={`text-sm font-mono ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{stream.recipient}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Stream #{stream.id}</div>
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{stream.token}</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm tabular-nums ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{formatTokenAmount(stream.deposited)} {stream.token}</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-bold tabular-nums ${isPaused ? 'text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>{formatTokenAmount(stream.withdrawn)} {stream.token}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<ClaimableAmount stream={stream} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
${stream.status === 'Active' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
stream.status === 'Paused' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
stream.status === 'Completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'}`}>
{stream.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
disabled={stream.status !== 'Active' || withdrawingStreamId === stream.id}
onClick={() => {
void onWithdraw(stream);
}}
className={`px-4 py-2 rounded-lg transition-all ${stream.status === 'Active'
? 'bg-accent text-white hover:bg-accent-hover shadow-lg'
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
}`}
>
{withdrawingStreamId === stream.id ? 'Withdrawing...' : 'Withdraw'}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
Expand Down
Loading
Loading