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 && ( +
+ + +
+ )} +
+ ); +} 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 ? ( + + ) : ( + + )}
)} diff --git a/hooks/useAccount.ts b/hooks/useAccount.ts index 0a52cad..8fb4d1e 100644 --- a/hooks/useAccount.ts +++ b/hooks/useAccount.ts @@ -1,49 +1,37 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { isConnected, getUserInfo } 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 -}; - -/** - * 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. - */ -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 interface AccountInfo { + address: string; + displayName: string; +} + +export function useAccount(): AccountInfo | null { + const [address, setAddress] = useState(null); + + const syncAccount = useCallback(async () => { + try { + const connected = await isConnected(); + if (!connected) { + setAddress(null); + return; + } + const user = await getUserInfo(); + setAddress(user?.publicKey ?? null); + } catch { + setAddress(null); + } }, []); - if (address) return addressToHistoricObject(address); - - return null; -}; + useEffect(() => { + syncAccount(); + const interval = setInterval(syncAccount, 2000); + return () => clearInterval(interval); + }, [syncAccount]); + + if (!address) return null; + return { + address, + displayName: `${address.slice(0, 4)}...${address.slice(-4)}`, + }; +}