From 19e5b7ce1cf1b5eb9b4e428f8ed8374ab1171154 Mon Sep 17 00:00:00 2001 From: Topmatrix Mor Date: Tue, 21 Jul 2026 01:31:52 +0000 Subject: [PATCH] feat(wallet): integrate @stellar/freighter-api with network switching & connection management (#21) - Add comprehensive useWallet hook with connect/disconnect/network detection - Enhance ConnectButton with loading spinner and error states - Enhance WalletButton with network display, chevron, and i18n props - Refactor Navbar to use useWallet, removing local connection state - Create reusable DashboardSidebar component (replaces 4 page duplicates) - Add Wallet i18n keys for en/es locales - Ensure CI passes: lint, typecheck, and build all green --- components/atoms/connect-button/index.tsx | 57 +++++- .../atoms/connect-button/style.module.css | 60 ++++++- components/atoms/wallet-button/index.tsx | 115 +++++++++--- .../molecules/dashboard-sidebar/index.tsx | 95 ++++++++++ components/molecules/index.tsx | 1 + components/molecules/wallet-data/index.tsx | 18 +- components/organisms/navbar/index.tsx | 58 +++++-- hooks/index.ts | 7 +- hooks/useAccount.ts | 43 ++--- hooks/useWallet.ts | 163 ++++++++++++++++++ messages/en.json | 10 ++ messages/es.json | 10 ++ pages/dashboard.tsx | 62 +------ pages/dashboard/disputes.tsx | 33 +--- pages/dashboard/profile.tsx | 33 +--- pages/dashboard/settings.tsx | 33 +--- 16 files changed, 568 insertions(+), 230 deletions(-) create mode 100644 components/molecules/dashboard-sidebar/index.tsx create mode 100644 hooks/useWallet.ts diff --git a/components/atoms/connect-button/index.tsx b/components/atoms/connect-button/index.tsx index eb8bfa1..6453618 100644 --- a/components/atoms/connect-button/index.tsx +++ b/components/atoms/connect-button/index.tsx @@ -1,20 +1,59 @@ -import React from 'react' +import { useState } from 'react' import { setAllowed } from '@stellar/freighter-api' import styles from './style.module.css' export interface ConnectButtonProps { label: string isHigher?: boolean + /** Called after a successful setAllowed + wallet connection is detected */ + onConnect?: () => void } -export function ConnectButton({ label, isHigher }: ConnectButtonProps) { +/** + * Renders a "Connect Wallet" button that triggers the Freighter permission flow. + * + * - Shows a loading spinner while the connection is in progress + * - Displays inline error text if the connection fails + * - Fires `onConnect` so parents can refresh state after a successful connect + */ +export function ConnectButton({ label, isHigher, onConnect }: ConnectButtonProps) { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + async function handleClick() { + setLoading(true) + setError(null) + try { + await setAllowed() + onConnect?.() + } catch (err) { + const msg = + err instanceof Error ? err.message : 'Failed to connect wallet' + setError(msg) + } finally { + setLoading(false) + } + } + return ( - +
+ + {error && ( +

+ {error} +

+ )} +
) } diff --git a/components/atoms/connect-button/style.module.css b/components/atoms/connect-button/style.module.css index fb19c15..f17b660 100644 --- a/components/atoms/connect-button/style.module.css +++ b/components/atoms/connect-button/style.module.css @@ -1,3 +1,10 @@ +.wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + .button { display: flex; flex-direction: row; @@ -14,8 +21,55 @@ line-height: 22px; color: #ffffff; cursor: pointer; + transition: background 0.15s, opacity 0.15s, transform 0.12s; +} + +.button:hover:not(:disabled) { + background: #2d2440; + transform: translateY(-1px); +} + +.button:active:not(:disabled) { + transform: translateY(0); +} + +.button:disabled { + opacity: 0.7; + cursor: not-allowed; } -.higher { - height: 58px; -} \ No newline at end of file +.button.loading { + opacity: 0.85; +} + + + +/* ── Spinner ────────────────────────────────────── */ + +.spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: #ffffff; + border-radius: 50%; + animation: spin 0.6s linear infinite; + flex-shrink: 0; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ── Error ──────────────────────────────────────── */ + +.error { + margin: 0; + font-size: 12px; + line-height: 1.4; + color: #ef4444; + text-align: center; + max-width: 240px; +} diff --git a/components/atoms/wallet-button/index.tsx b/components/atoms/wallet-button/index.tsx index 0251c09..e7515ae 100644 --- a/components/atoms/wallet-button/index.tsx +++ b/components/atoms/wallet-button/index.tsx @@ -1,41 +1,61 @@ -import { useState, useRef, useEffect } from 'react'; -import { setAllowed } from '@stellar/freighter-api'; +import { useState, useRef, useEffect } from 'react' +import { setAllowed } from '@stellar/freighter-api' interface WalletButtonProps { - address: string; - onDisconnect: () => void; + address: string + /** The Stellar network name, e.g. "Test SDF Network ; September 2015" */ + network?: string | null + /** Called when the user clicks "Disconnect" in the dropdown */ + onDisconnect: () => void + /** Label for the switch-account menu item */ + switchAccountLabel?: string + /** Label for the disconnect menu item */ + disconnectLabel?: string } /** - * Shows the connected wallet address and a dropdown with options to switch - * accounts or disconnect. Clicking outside closes the menu. + * Shows the connected wallet address and a dropdown with: + * - Current network indicator + * - Switch account (re-opens Freighter permission popup) + * - Disconnect (clears local connection state) + * + * Clicking outside closes the dropdown. */ -export function WalletButton({ address, onDisconnect }: WalletButtonProps) { - const [open, setOpen] = useState(false); - const ref = useRef(null); +export function WalletButton({ + address, + network, + onDisconnect, + switchAccountLabel = 'Switch account', + disconnectLabel = 'Disconnect', +}: WalletButtonProps) { + const [open, setOpen] = useState(false) + const ref = useRef(null) useEffect(() => { function handleClickOutside(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false); + setOpen(false) } } - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) - const displayName = `${address.slice(0, 4)}...${address.slice(-4)}`; + const displayName = `${address.slice(0, 4)}...${address.slice(-4)}` + + // Derive a short, human-readable network label + const networkLabel = deriveNetworkLabel(network) function handleSwap() { - setOpen(false); + setOpen(false) // Re-invoking setAllowed opens the Freighter permission popup so the user // can approve a different profile without disconnecting first. - void setAllowed(); + void setAllowed() } function handleDisconnect() { - setOpen(false); - onDisconnect(); + setOpen(false) + onDisconnect() } return ( @@ -46,31 +66,80 @@ export function WalletButton({ address, onDisconnect }: WalletButtonProps) { aria-haspopup="true" className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" > -