From 91b2d6a0d960241c67745952ac264598fe9461c2 Mon Sep 17 00:00:00 2001 From: stephanieoghenemega-eng Date: Wed, 24 Jun 2026 16:16:58 +0100 Subject: [PATCH 1/5] fix(wallet): react to Freighter account changes without page refresh --- hooks/useAccount.ts | 78 ++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/hooks/useAccount.ts b/hooks/useAccount.ts index 0a52cad..0d2de97 100644 --- a/hooks/useAccount.ts +++ b/hooks/useAccount.ts @@ -1,49 +1,41 @@ -import { useEffect, useState } from "react"; -import { isConnected, getUserInfo } from "@stellar/freighter-api"; +import { useEffect, useState, useCallback } from "react"; +import { isConnected, getUserInfo, watchWallet } from "@stellar/freighter-api"; -let address: string; - -let addressLookup = (async () => { - if (await isConnected()) return getUserInfo() -})(); - -// returning the same object identity every time avoids unnecessary re-renders -const addressObject = { - address: '', - displayName: '', -}; - -const addressToHistoricObject = (address: string) => { - addressObject.address = address; - addressObject.displayName = `${address.slice(0, 4)}...${address.slice(-4)}`; - return addressObject -}; +export interface AccountInfo { + address: string; + displayName: string; +} /** - * Returns an object containing `address` and `displayName` properties, with - * the address fetched from Freighter's `getPublicKey` method in a - * render-friendly way. - * - * Before the address is fetched, returns null. - * - * Caches the result so that the Freighter lookup only happens once, no matter - * how many times this hook is called. - * - * NOTE: This does not update the return value if the user changes their - * Freighter settings; they will need to refresh the page. + * Returns the currently connected Freighter account, or `null` when no wallet + * is connected. Updates automatically when the user switches profiles inside + * the Freighter extension — no page refresh required. */ -export function useAccount(): typeof addressObject | null { - const [, setLoading] = useState(address === undefined); - - useEffect(() => { - if (address !== undefined) return; - - addressLookup - .then(user => { if (user) address = user.publicKey }) - .finally(() => { setLoading(false) }); +export function useAccount(): AccountInfo | null { + const [address, setAddress] = useState(null); + + const syncAccount = useCallback(async () => { + const connected = await isConnected(); + if (!connected) { + setAddress(null); + return; + } + const user = await getUserInfo(); + setAddress(user?.publicKey ?? null); }, []); - if (address) return addressToHistoricObject(address); - - return null; -}; + useEffect(() => { + syncAccount(); + + // watchWallet fires whenever the active Freighter account changes so we + // update state without requiring a page refresh. + const unsubscribe = watchWallet(() => { void syncAccount(); }); + return () => { unsubscribe?.(); }; + }, [syncAccount]); + + if (!address) return null; + return { + address, + displayName: `${address.slice(0, 4)}...${address.slice(-4)}`, + }; +} From 1393ed373a180c6dee7a3961ee68a12b65f148c3 Mon Sep 17 00:00:00 2001 From: stephanieoghenemega-eng Date: Wed, 24 Jun 2026 16:17:18 +0100 Subject: [PATCH 2/5] feat(wallet): add WalletButton component with disconnect and swap --- components/atoms/wallet-button/index.tsx | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 components/atoms/wallet-button/index.tsx diff --git a/components/atoms/wallet-button/index.tsx b/components/atoms/wallet-button/index.tsx new file mode 100644 index 0000000..0251c09 --- /dev/null +++ b/components/atoms/wallet-button/index.tsx @@ -0,0 +1,76 @@ +import { useState, useRef, useEffect } from 'react'; +import { setAllowed } from '@stellar/freighter-api'; + +interface WalletButtonProps { + address: string; + onDisconnect: () => void; +} + +/** + * Shows the connected wallet address and a dropdown with options to switch + * accounts or disconnect. Clicking outside closes the menu. + */ +export function WalletButton({ address, onDisconnect }: 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); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const displayName = `${address.slice(0, 4)}...${address.slice(-4)}`; + + function handleSwap() { + setOpen(false); + // Re-invoking setAllowed opens the Freighter permission popup so the user + // can approve a different profile without disconnecting first. + void setAllowed(); + } + + function handleDisconnect() { + setOpen(false); + onDisconnect(); + } + + return ( +
+ + + {open && ( +
+ + +
+ )} +
+ ); +} From 02c393287ec635fcea5453590089c08b88161463 Mon Sep 17 00:00:00 2001 From: stephanieoghenemega-eng Date: Wed, 24 Jun 2026 16:18:07 +0100 Subject: [PATCH 3/5] feat(navbar): show WalletButton when connected, ConnectButton when not --- components/organisms/navbar/index.tsx | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/components/organisms/navbar/index.tsx b/components/organisms/navbar/index.tsx index 0167f0f..bbd89e6 100644 --- a/components/organisms/navbar/index.tsx +++ b/components/organisms/navbar/index.tsx @@ -1,9 +1,12 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useTranslations } from 'next-intl'; import { ThemeToggle } from '../../atoms/theme-toggle'; import { LocaleSwitcher } from '../../atoms/locale-switcher'; +import { ConnectButton } from '../../atoms/connect-button'; +import { WalletButton } from '../../atoms/wallet-button'; +import { useAccount } from '../../../hooks/useAccount'; const NAV_LINKS = [ { key: 'home', href: '/' }, @@ -17,6 +20,13 @@ export function Navbar() { const [open, setOpen] = useState(false); const router = useRouter(); const t = useTranslations('Nav'); + const account = useAccount(); + + // useAccount state is driven by Freighter events; clearing it locally is + // enough to reflect a disconnect since there is no server-side session. + const [disconnected, setDisconnected] = useState(false); + const handleDisconnect = useCallback(() => setDisconnected(true), []); + const effectiveAccount = disconnected ? null : account; const toggle = () => setOpen((v) => !v); const close = () => setOpen(false); @@ -50,12 +60,11 @@ export function Navbar() {
- - {t('launchApp')} - + {effectiveAccount ? ( + + ) : ( + + )}
{/* Hamburger */} @@ -93,13 +102,11 @@ export function Navbar() {
- - {t('launchApp')} - + {effectiveAccount ? ( + + ) : ( + + )}
)} From 4f1017e847ccdfed654da57fa1a8cde9bc69ae83 Mon Sep 17 00:00:00 2001 From: stephanieoghenemega-eng Date: Wed, 24 Jun 2026 17:37:25 +0100 Subject: [PATCH 4/5] fix(wallet): poll Freighter every 2s instead of watchWallet (not in v1.5.1) --- hooks/useAccount.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/hooks/useAccount.ts b/hooks/useAccount.ts index 0d2de97..3947a50 100644 --- a/hooks/useAccount.ts +++ b/hooks/useAccount.ts @@ -1,36 +1,32 @@ import { useEffect, useState, useCallback } from "react"; -import { isConnected, getUserInfo, watchWallet } from "@stellar/freighter-api"; +import { isConnected, getUserInfo } from "@stellar/freighter-api"; export interface AccountInfo { address: string; displayName: string; } -/** - * Returns the currently connected Freighter account, or `null` when no wallet - * is connected. Updates automatically when the user switches profiles inside - * the Freighter extension — no page refresh required. - */ export function useAccount(): AccountInfo | null { const [address, setAddress] = useState(null); const syncAccount = useCallback(async () => { - const connected = await isConnected(); - if (!connected) { + try { + const connected = await isConnected(); + if (!connected) { + setAddress(null); + return; + } + const user = await getUserInfo(); + setAddress(user?.publicKey ?? null); + } catch { setAddress(null); - return; } - const user = await getUserInfo(); - setAddress(user?.publicKey ?? null); - }, []); + %}, []); useEffect(() => { syncAccount(); - - // watchWallet fires whenever the active Freighter account changes so we - // update state without requiring a page refresh. - const unsubscribe = watchWallet(() => { void syncAccount(); }); - return () => { unsubscribe?.(); }; + const interval = setInterval(syncAccount, 2000); + return () => clearInterval(interval); }, [syncAccount]); if (!address) return null; From 9ccaada3b6653b278c0ae78be660cfd3ff7b2a9a Mon Sep 17 00:00:00 2001 From: stephanieoghenemega-eng Date: Wed, 24 Jun 2026 17:50:03 +0100 Subject: [PATCH 5/5] fix(wallet): poll Freighter every 2s instead of watchWallet (not in v1.5.1) --- hooks/useAccount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useAccount.ts b/hooks/useAccount.ts index 3947a50..8fb4d1e 100644 --- a/hooks/useAccount.ts +++ b/hooks/useAccount.ts @@ -21,7 +21,7 @@ export function useAccount(): AccountInfo | null { } catch { setAddress(null); } - %}, []); + }, []); useEffect(() => { syncAccount();