diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 0f51801..23048a1 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -4,17 +4,48 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { useAuth } from "@/lib/useAuth"; import { listWallets, type WalletView } from "@/lib/wallets"; +import { + getSponsorshipConfig, + type SponsorshipConfig, +} from "@/lib/sponsorship"; import { DashboardShell } from "@/components/dashboard/DashboardShell"; export default function DashboardHome() { const { user, token, loading, logout } = useAuth(); const [wallets, setWallets] = useState(null); + const [sponsorshipByWalletId, setSponsorshipByWalletId] = useState< + Map + >(new Map()); useEffect(() => { if (!token) return; + let aborted = false; listWallets(token) - .then(setWallets) - .catch(() => setWallets([])); + .then(async (ws) => { + if (aborted) return; + setWallets(ws); + // Fetch sponsorship configs in parallel so the wallet list never has to wait on them. + // A single failed sponsorship fetch must not blank out the whole row. + const results = await Promise.allSettled( + ws.map((w) => getSponsorshipConfig(token, w.id)), + ); + if (aborted) return; + const map = new Map(); + ws.forEach((w, i) => { + const r = results[i]; + map.set(w.id, r.status === "fulfilled" ? r.value : null); + }); + setSponsorshipByWalletId(map); + }) + .catch(() => { + // Gate on the same `aborted` flag the .then already uses so we don't call + // setState on an unmounted component when listWallets rejects late. + if (aborted) return; + setWallets([]); + }); + return () => { + aborted = true; + }; }, [token]); if (loading || !user) { @@ -74,7 +105,11 @@ export default function DashboardHome() { ) : (
{wallets.map((w) => ( - + ))}
)} @@ -101,8 +136,21 @@ function EmptyState() { ); } -function WalletCard({ wallet }: { wallet: WalletView }) { +function formatXlm(stroops: number): string { + return (stroops / 10_000_000).toFixed(2); +} + +function WalletCard({ + wallet, + sponsorship, +}: { + wallet: WalletView; + sponsorship?: SponsorshipConfig | null; +}) { const short = `${wallet.address.slice(0, 6)}…${wallet.address.slice(-6)}`; + const sponsorEnabled = sponsorship?.enabled === true; + const dailyBudget = sponsorship?.daily_budget_stroops; + return (
@@ -124,7 +172,7 @@ function WalletCard({ wallet }: { wallet: WalletView }) {
-
+

Network

@@ -139,6 +187,36 @@ function WalletCard({ wallet }: { wallet: WalletView }) {

Base

XLM

+
+

Gas Sponsor

+
+ + + {sponsorEnabled ? "Enabled" : "Off"} + +
+ {/* Daily-budget cap is rendered only when the API returns a numeric budget. + The progress bar for daily-spend consumption is intentionally omitted for now + because the current API response does not include a "fees_spent_today_stroops" + field. When that lands, swap this label for a fill-bar the same way the wallet + card already handles other grid cells. */} + {typeof dailyBudget === "number" && dailyBudget > 0 && ( +

+ {formatXlm(dailyBudget)} XLM/day cap +

+ )} +
); diff --git a/frontend/src/lib/sponsorship.ts b/frontend/src/lib/sponsorship.ts new file mode 100644 index 0000000..959e0fe --- /dev/null +++ b/frontend/src/lib/sponsorship.ts @@ -0,0 +1,22 @@ +/** Sponsorship API calls + types. */ + +import { apiFetch } from "./api"; + +export type SponsorshipConfig = { + wallet_id: string; + enabled: boolean; + max_fee_per_tx_stroops: number; + daily_budget_stroops: number; + created_at: string | null; + updated_at: string | null; + /** Not yet returned by the API; reserved for future consumption tracking. */ + fees_spent_today_stroops?: number; +}; + +/** Fetch the sponsorship config for a wallet (JWT login token required). */ +export function getSponsorshipConfig(token: string, walletId: string) { + return apiFetch( + `/v1/wallets/${walletId}/sponsorship`, + { token }, + ); +}