From d92fae79a6f05b5c5caeef38592fa00a2806e318 Mon Sep 17 00:00:00 2001 From: Mozez155 Date: Mon, 22 Jun 2026 12:44:43 +0100 Subject: [PATCH] feat(dashboard): add Gas Sponsorship overview page at /dashboard/sponsorship Shows per-wallet sponsorship status and budget at a glance. Removes the 'soon' flag from the sidebar navigation item now that the page is live. --- .../src/app/dashboard/sponsorship/page.tsx | 176 ++++++++++++++++++ frontend/src/components/dashboard/Sidebar.tsx | 2 +- frontend/src/lib/sponsorship.ts | 29 +++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/dashboard/sponsorship/page.tsx create mode 100644 frontend/src/lib/sponsorship.ts diff --git a/frontend/src/app/dashboard/sponsorship/page.tsx b/frontend/src/app/dashboard/sponsorship/page.tsx new file mode 100644 index 0000000..334e92e --- /dev/null +++ b/frontend/src/app/dashboard/sponsorship/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { useAuth } from "@/lib/useAuth"; +import { listWallets, type WalletView } from "@/lib/wallets"; +import { + getSponsorshipConfig, + stroopsToXlm, + type SponsorshipConfig, +} from "@/lib/sponsorship"; +import { DashboardShell } from "@/components/dashboard/DashboardShell"; + +type Row = { + wallet: WalletView; + config: SponsorshipConfig | null; +}; + +export default function SponsorshipPage() { + const { user, token, loading, logout } = useAuth(); + const [rows, setRows] = useState(null); + + useEffect(() => { + if (!token) return; + listWallets(token) + .then(async (wallets) => { + // Fetch only the sponsorship config per wallet (not full wallet details). + const settled = await Promise.all( + wallets.map(async (wallet) => { + const config = await getSponsorshipConfig(wallet.id, token).catch( + () => null, + ); + return { wallet, config }; + }), + ); + setRows(settled); + }) + .catch(() => setRows([])); + }, [token]); + + if (loading || !user) { + return ( +
+ Loading… +
+ ); + } + + return ( + +
+ {/* How it works */} +
+
+ + ⛽ + +

+ How gas sponsorship works +

+
+

+ Gas sponsorship lets your wallets pay the Stellar network fee on + behalf of your users, so they can transact without holding XLM. + Enable it per wallet and set spend controls — a maximum fee per + transaction and a daily budget — to keep costs predictable. Once a + wallet hits its daily budget, sponsorship pauses automatically until + the next day. +

+ + Read the documentation → + +
+ +
+ +

+ Sponsorship across your wallets +

+ +
+ {rows === null ? ( +

Loading wallets…

+ ) : rows.length === 0 ? ( + + ) : ( +
+ {rows.map((row) => ( + + ))} +
+ )} +
+
+ + ); +} + +function EmptyState() { + return ( +
+

No master wallets yet

+

+ Create a master wallet first, then enable gas sponsorship to start + covering network fees for your users. +

+ + New Master Wallet + +
+ ); +} + +function SponsorshipCard({ row }: { row: Row }) { + const { wallet, config } = row; + const enabled = config?.enabled ?? false; + + return ( +
+
+
+

+ {wallet.label ?? "Master wallet"} +

+

+ {wallet.network} +

+
+ + + {enabled ? "Enabled" : "Disabled"} + +
+ +
+ +
+
+

Max fee per tx

+

+ {stroopsToXlm(config?.per_tx_fee_cap_stroops)} +

+
+
+

Daily budget

+

+ {stroopsToXlm(config?.daily_budget_stroops)} +

+
+
+ + + Manage settings → + +
+ ); +} diff --git a/frontend/src/components/dashboard/Sidebar.tsx b/frontend/src/components/dashboard/Sidebar.tsx index ac560c3..649d029 100644 --- a/frontend/src/components/dashboard/Sidebar.tsx +++ b/frontend/src/components/dashboard/Sidebar.tsx @@ -6,7 +6,7 @@ import { Logo } from "@/components/Logo"; const NAV: { label: string; href: string; icon: string; soon?: boolean }[] = [ { label: "Home", href: "/dashboard", icon: "⌂" }, - { label: "Gas Sponsorship", href: "/dashboard/sponsorship", icon: "⛽", soon: true }, + { label: "Gas Sponsorship", href: "/dashboard/sponsorship", icon: "⛽" }, { label: "Asset Recovery", href: "/dashboard/recovery", icon: "↺" }, { label: "Developers", href: "/dashboard/developers", icon: "›_" }, { label: "Audit Logs", href: "/dashboard/audit", icon: "▤" }, diff --git a/frontend/src/lib/sponsorship.ts b/frontend/src/lib/sponsorship.ts new file mode 100644 index 0000000..aba910d --- /dev/null +++ b/frontend/src/lib/sponsorship.ts @@ -0,0 +1,29 @@ +/** Gas sponsorship API calls + types, mirroring the octo backend. */ + +"use client"; + +import { apiFetch } from "./api"; + +export type SponsorshipConfig = { + enabled: boolean; + per_tx_fee_cap_stroops: number | null; + daily_budget_stroops: number | null; + spent_today_stroops: number; +}; + +/** Fetch a wallet's gas sponsorship config (enabled state + spend controls). */ +export function getSponsorshipConfig(walletId: string, token: string) { + return apiFetch(`/v1/wallets/${walletId}/sponsorship`, { + token, + }); +} + +/** + * Format integer stroops as a human-readable XLM string (2 dp). + * Raw stroop values are for the API only and must never be shown to end users. + * Returns "—" when no limit is configured. + */ +export function stroopsToXlm(stroops: number | null | undefined): string { + if (stroops == null) return "—"; + return `${(stroops / 10_000_000).toFixed(2)} XLM`; +}