From 138b13d66e7e2157f48ec6011072a22af8a50747 Mon Sep 17 00:00:00 2001 From: 3m1n3nc3 Date: Mon, 29 Jun 2026 14:39:25 +0100 Subject: [PATCH 1/4] fix(frontend): mobile responsive audit and fixes Audit and fix mobile responsiveness across the priority pages (creator layout, mission board, connect-wallet, account-type) per [#247](https://github.com/3m1n3nc3/Quid/issues/247). - Sidebar: fix broken overlay (bg-opacity-50 is a no-op in Tailwind v4 -> bg-black/50), enlarge hamburger and nav-link tap targets to >=44px, cap drawer width at 80vw, drop dead xs:* class, add aria-label. - Creator layout: add min-w-0 / overflow-x-hidden so wide children can't cause horizontal scroll on mobile. - Connect-wallet: replace fixed w-80/w-72 widths with fluid w-full max-w-sm + page padding (safe below 375px), bump wallet rows to >=44px tap targets with role/tabindex, size the warning icon, remove unused framer-motion import. - Account-type (role-selection): make the Continue button full-width on mobile instead of a fixed px-24 that risked overflow. - Quests header: give the mobile hamburger a >=44px tap target + aria-label. Verified: production build passes, no horizontal scroll at 375px. --- .../src/app/(auth)/connect-wallet/page.tsx | 21 ++++++++++--------- frontend/src/app/creator/layout.tsx | 6 ++++-- frontend/src/components/creator/Sidebar.tsx | 9 ++++---- .../src/features/creators/QuestHeader.tsx | 6 +++++- .../features/onboarding/role-selection.tsx | 2 +- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/(auth)/connect-wallet/page.tsx b/frontend/src/app/(auth)/connect-wallet/page.tsx index 70b3899..1446552 100644 --- a/frontend/src/app/(auth)/connect-wallet/page.tsx +++ b/frontend/src/app/(auth)/connect-wallet/page.tsx @@ -5,7 +5,6 @@ import Image from "next/image"; import WalletKit from "@/lib/stellar-wallets-kit" import { useEffect, useMemo, useState } from "react"; import { ISupportedWallet } from "@creit-tech/stellar-wallets-kit"; -import { div } from "framer-motion/client"; import { useRouter } from "next/navigation"; import { OctagonAlert } from "lucide-react" @@ -38,12 +37,12 @@ export default function ConnectWalletPage() { } return ( -
+
-
+
Quid Logo -
+

Connect a wallet

Choose a wallet and Connect

-
+
{availableWallets.map((wallet, id) => { return ( -
@@ -94,8 +95,8 @@ export default function ConnectWalletPage() {
-
- +
+

Quid will never ask for your private keys or seed phrases. Only connect wallets you trust and control

diff --git a/frontend/src/app/creator/layout.tsx b/frontend/src/app/creator/layout.tsx index 06efb74..5531d92 100644 --- a/frontend/src/app/creator/layout.tsx +++ b/frontend/src/app/creator/layout.tsx @@ -6,9 +6,11 @@ export default function DashboardLayout({ children: React.ReactNode; }) { return ( -
+
-
{children}
+
+ {children} +
); } diff --git a/frontend/src/components/creator/Sidebar.tsx b/frontend/src/components/creator/Sidebar.tsx index 6e17a3d..ef14edd 100644 --- a/frontend/src/components/creator/Sidebar.tsx +++ b/frontend/src/components/creator/Sidebar.tsx @@ -14,7 +14,7 @@ export default function Sidebar() { const [isOpen, setIsOpen] = useState(false); const linkClasses = (href: string) => - `flex items-center gap-2 pl-3 py-2 transition-colors + `flex items-center gap-2 pl-3 py-2.5 min-h-11 transition-colors ${ pathname === href ? "border-l-[3px] border-[#9011FF] text-[#FFFFFF]" @@ -26,7 +26,8 @@ export default function Sidebar() { {/* Hamburger button - only visible on mobile */} @@ -34,7 +35,7 @@ export default function Sidebar() { {/* Overlay - only visible on mobile when sidebar is open */} {isOpen && (
setIsOpen(false)} /> )} @@ -42,7 +43,7 @@ export default function Sidebar() { {/* Sidebar */}
+ + Open Stellar Laboratory + + +
+
+
+
+ )} + + {/* Wallet Balance card */} +
+
+
+
+ + Wallet Balance +
+ +
+ + ${usdWhole} + + + .{usdFraction} + +
+ +
+
+ XLM + {loading ? ( + + ) : ( + {formatXlm(xlm)} XLM + )} +
+ + + +
+ {loading ? ( + + ) : usdc !== null ? ( + {formatXlm(usdc)} USDC + ) : ( + No USDC trustline + )} +
+
+
+ +
+ + +
+
+ +
+
+

+ Your earnings from completed quests and approved responses. +

+ {!loading && funded && ( + + )} +
+

+ Minimum withdrawal: ${MIN_WITHDRAWAL_USD} +

+
+
+ + {/* Transactions + Withdrawal method */} +
+ {/* Recent Transactions */} +
+

Recent Transactions

+
+ + + + + + + + + + + + {loading ? ( + Array.from({ length: 4 }).map((_, i) => ( + + + + )) + ) : transactions.length === 0 ? ( + + + + ) : ( + transactions.map((tx) => ( + + + + + + + + )) + )} + +
TypeFrom / ToAmountStatusDate
+ +
+ No transactions yet. +
+
+ + {tx.direction === "in" ? ( + + ) : ( + + )} + + + {tx.direction === "in" ? "Received" : "Sent"} + +
+
+ + {truncateKey(tx.counterparty)} + + + {tx.direction === "in" ? "+" : "-"} + {formatXlm(tx.amount)} {tx.assetCode} + + + {tx.successful ? "Successful" : "Failed"} + + + {timeAgo(tx.date)} +
+
+
+ + {/* Withdrawal Method */} +
+

Withdrawal Method

+
+
+
+ + + +
+

+ Stellar Wallet 1 (XLM) +

+
+ + {truncateKey(publicKey)} + + +
+
+
+ +
+
+ +
+
+
+
+ ); +} + +function WalletTopBar({ + onRefresh, + loading, +}: { + onRefresh: () => void; + loading: boolean; +}) { + return ( +
+
+

Wallet

+ +
+ + + + +
+ + $0 +
+ + +
+
+
+ ); } diff --git a/frontend/src/app/hooks/useWallet.ts b/frontend/src/app/hooks/useWallet.ts new file mode 100644 index 0000000..f8a3591 --- /dev/null +++ b/frontend/src/app/hooks/useWallet.ts @@ -0,0 +1,268 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useWalletProvider } from "@/context/WalletProvider"; + +/** + * Horizon endpoint. Defaults to the public testnet so unfunded accounts can be + * funded via Friendbot. Override with NEXT_PUBLIC_HORIZON_URL for other networks. + */ +export const HORIZON_URL = + process.env.NEXT_PUBLIC_HORIZON_URL ?? "https://horizon-testnet.stellar.org"; + +export const FRIENDBOT_URL = + process.env.NEXT_PUBLIC_FRIENDBOT_URL ?? "https://friendbot.stellar.org"; + +/** A single balance line item as returned by Horizon's `/accounts` endpoint. */ +export interface WalletBalance { + assetType: string; + assetCode: string; + assetIssuer?: string; + balance: string; +} + +/** A normalized payment, derived from Horizon's `/accounts/{id}/payments`. */ +export interface WalletTransaction { + id: string; + hash: string; + type: string; + direction: "in" | "out"; + counterparty: string; + amount: string; + assetCode: string; + date: string; + successful: boolean; +} + +interface HorizonBalance { + balance: string; + asset_type: string; + asset_code?: string; + asset_issuer?: string; +} + +interface HorizonPayment { + id: string; + type: string; + transaction_hash: string; + transaction_successful?: boolean; + created_at: string; + amount?: string; + asset_type?: string; + asset_code?: string; + from?: string; + to?: string; + funder?: string; + account?: string; + starting_balance?: string; +} + +export interface UseWalletResult { + /** Connected account public key (empty string when not connected). */ + publicKey: string; + isConnected: boolean; + /** All balances on the account, native first. */ + balances: WalletBalance[]; + /** Native XLM balance, or null when the account is unfunded. */ + xlm: string | null; + /** USDC balance, or null when the account holds no USDC trustline. */ + usdc: string | null; + /** Recent payments to/from the account, newest first. */ + transactions: WalletTransaction[]; + /** False when Horizon has no record of the account yet (needs funding). */ + funded: boolean; + loading: boolean; + error: string | null; + /** Re-fetch balances + transactions from Horizon without a page reload. */ + refreshBalances: () => void; + /** Fund the account on testnet via Friendbot, then refresh. */ + fundWithFriendbot: () => Promise; + funding: boolean; +} + +function parseBalances(raw: HorizonBalance[]): WalletBalance[] { + return raw + .map((b) => ({ + assetType: b.asset_type, + assetCode: b.asset_type === "native" ? "XLM" : b.asset_code ?? "", + assetIssuer: b.asset_issuer, + balance: b.balance, + })) + // Native asset first, then the rest in Horizon's order. + .sort((a, b) => + a.assetType === "native" ? -1 : b.assetType === "native" ? 1 : 0, + ); +} + +function parsePayments( + records: HorizonPayment[], + pk: string, +): WalletTransaction[] { + const out: WalletTransaction[] = []; + + for (const r of records) { + let amount: string | undefined; + let assetCode = "XLM"; + let direction: "in" | "out"; + let counterparty: string | undefined; + + if (r.type === "create_account") { + amount = r.starting_balance; + direction = r.account === pk ? "in" : "out"; + counterparty = direction === "in" ? r.funder : r.account; + } else if ( + r.type === "payment" || + r.type === "path_payment_strict_send" || + r.type === "path_payment_strict_receive" + ) { + amount = r.amount; + assetCode = r.asset_type === "native" ? "XLM" : r.asset_code ?? ""; + direction = r.to === pk ? "in" : "out"; + counterparty = direction === "in" ? r.from : r.to; + } else { + // Skip operation types we can't represent as a simple debit/credit. + continue; + } + + out.push({ + id: r.id, + hash: r.transaction_hash, + type: r.type, + direction, + counterparty: counterparty ?? "", + amount: amount ?? "0", + assetCode, + date: r.created_at, + successful: r.transaction_successful !== false, + }); + } + + return out; +} + +/** + * Reads the connected wallet's public key from the wallet provider and fetches + * its balances and recent payments from Horizon. Exposes XLM/USDC balances, a + * funded flag for unfunded testnet accounts, real transaction history, and a + * manual refresh. + */ +export function useWallet(): UseWalletResult { + const { publicKey, isConnected } = useWalletProvider(); + + const [balances, setBalances] = useState([]); + const [transactions, setTransactions] = useState([]); + const [funded, setFunded] = useState(true); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [funding, setFunding] = useState(false); + + const refreshBalances = useCallback(async () => { + if (!publicKey) { + setBalances([]); + setTransactions([]); + setFunded(true); + setError(null); + return; + } + + setLoading(true); + setError(null); + + try { + const accRes = await fetch(`${HORIZON_URL}/accounts/${publicKey}`); + + // Horizon returns 404 for accounts that exist as a keypair but have not + // been funded/created on the ledger yet. + if (accRes.status === 404) { + setBalances([]); + setTransactions([]); + setFunded(false); + return; + } + + if (!accRes.ok) { + throw new Error(`Horizon responded with ${accRes.status}`); + } + + const accData = await accRes.json(); + setBalances(parseBalances(accData.balances ?? [])); + setFunded(true); + + // Real transaction history via the account's payments endpoint + // (the `_links.payments` href on the account response). + const payRes = await fetch( + `${HORIZON_URL}/accounts/${publicKey}/payments?order=desc&limit=15`, + ); + if (payRes.ok) { + const payData = await payRes.json(); + const records: HorizonPayment[] = payData?._embedded?.records ?? []; + setTransactions(parsePayments(records, publicKey)); + } else { + setTransactions([]); + } + } catch (err) { + console.error("Failed to load wallet data:", err); + setError("Unable to load wallet data. Check your connection and retry."); + } finally { + setLoading(false); + } + }, [publicKey]); + + useEffect(() => { + refreshBalances(); + }, [refreshBalances]); + + const fundWithFriendbot = useCallback(async () => { + if (!publicKey) return; + setFunding(true); + setError(null); + try { + const res = await fetch(`${FRIENDBOT_URL}/?addr=${publicKey}`); + if (!res.ok) { + // Friendbot replies 400 "account already funded to starting balance" + // for accounts that already exist — surface that clearly. + let detail = `Friendbot responded with ${res.status}`; + try { + const body = await res.json(); + if (typeof body?.detail === "string") detail = body.detail; + } catch { + // non-JSON body; keep the status-based message + } + if (detail.toLowerCase().includes("already funded")) { + throw new Error( + "This account is already funded. Friendbot only funds new testnet accounts.", + ); + } + throw new Error(detail); + } + await refreshBalances(); + } catch (err) { + console.error("Friendbot funding failed:", err); + setError( + err instanceof Error + ? err.message + : "Friendbot funding failed. Try again in a moment.", + ); + } finally { + setFunding(false); + } + }, [publicKey, refreshBalances]); + + const find = (code: string) => + balances.find((b) => b.assetCode === code)?.balance ?? null; + + return { + publicKey, + isConnected, + balances, + xlm: find("XLM"), + usdc: find("USDC"), + transactions, + funded, + loading, + error, + refreshBalances, + fundWithFriendbot, + funding, + }; +} From 4334a51ee92c397afe3d767fcf66184bd8e7f0cf Mon Sep 17 00:00:00 2001 From: 3m1n3nc3 Date: Mon, 29 Jun 2026 15:47:28 +0100 Subject: [PATCH 3/4] refactor(frontend): wallet hook consumes shared provider balances After the upstream WalletProvider refactor, balances were being fetched twice (provider + creator wallet hook). Make the provider the single source of truth: - Derive XLM/USDC and the balances list from the context's balances instead of a separate /accounts fetch. - Keep transaction history and the unfunded flag from the account's /payments endpoint (404 => unfunded), which the provider doesn't expose. - refreshBalances() now refreshes the provider balances and the payments history together. No change to the wallet page API or behavior. --- frontend/src/app/hooks/useWallet.ts | 90 ++++++++++++++--------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/frontend/src/app/hooks/useWallet.ts b/frontend/src/app/hooks/useWallet.ts index e5a1b86..4f6a676 100644 --- a/frontend/src/app/hooks/useWallet.ts +++ b/frontend/src/app/hooks/useWallet.ts @@ -1,7 +1,10 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; -import { useWallet as useWalletContext } from "@/context/WalletProvider"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useWallet as useWalletContext, + type Balance, +} from "@/context/WalletProvider"; /** * Horizon endpoint. Defaults to the public testnet so unfunded accounts can be @@ -13,7 +16,7 @@ export const HORIZON_URL = export const FRIENDBOT_URL = process.env.NEXT_PUBLIC_FRIENDBOT_URL ?? "https://friendbot.stellar.org"; -/** A single balance line item as returned by Horizon's `/accounts` endpoint. */ +/** A single balance line item, normalized from the wallet context. */ export interface WalletBalance { assetType: string; assetCode: string; @@ -34,13 +37,6 @@ export interface WalletTransaction { successful: boolean; } -interface HorizonBalance { - balance: string; - asset_type: string; - asset_code?: string; - asset_issuer?: string; -} - interface HorizonPayment { id: string; type: string; @@ -80,7 +76,7 @@ export interface UseWalletResult { funding: boolean; } -function parseBalances(raw: HorizonBalance[]): WalletBalance[] { +function parseBalances(raw: Balance[]): WalletBalance[] { return raw .map((b) => ({ assetType: b.asset_type, @@ -88,7 +84,7 @@ function parseBalances(raw: HorizonBalance[]): WalletBalance[] { assetIssuer: b.asset_issuer, balance: b.balance, })) - // Native asset first, then the rest in Horizon's order. + // Native asset first, then the rest in the context's order. .sort((a, b) => a.assetType === "native" ? -1 : b.assetType === "native" ? 1 : 0, ); @@ -141,26 +137,37 @@ function parsePayments( } /** - * Reads the connected wallet's public key from the wallet provider and fetches - * its balances and recent payments from Horizon. Exposes XLM/USDC balances, a - * funded flag for unfunded testnet accounts, real transaction history, and a - * manual refresh. + * Wallet view-model for the creator wallet page. Balances come from the shared + * WalletProvider (single source of truth); this hook adds the pieces the page + * needs on top — normalized balances, real transaction history and an unfunded + * flag from Horizon's payments endpoint, plus Friendbot funding and refresh. */ export function useWallet(): UseWalletResult { - const { publicKey: contextPublicKey, connected } = useWalletContext(); + const { + publicKey: contextPublicKey, + connected, + balances: contextBalances, + refreshBalances: refreshContextBalances, + } = useWalletContext(); + const publicKey = contextPublicKey ?? ""; const isConnected = connected; - const [balances, setBalances] = useState([]); const [transactions, setTransactions] = useState([]); const [funded, setFunded] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [funding, setFunding] = useState(false); - const refreshBalances = useCallback(async () => { + const balances = useMemo( + () => parseBalances(contextBalances), + [contextBalances], + ); + + // Transaction history + funded status come from the account's payments + // endpoint, which 404s for accounts that have not been created on the ledger. + const fetchActivity = useCallback(async () => { if (!publicKey) { - setBalances([]); setTransactions([]); setFunded(true); setError(null); @@ -171,48 +178,39 @@ export function useWallet(): UseWalletResult { setError(null); try { - const accRes = await fetch(`${HORIZON_URL}/accounts/${publicKey}`); + const res = await fetch( + `${HORIZON_URL}/accounts/${publicKey}/payments?order=desc&limit=15`, + ); - // Horizon returns 404 for accounts that exist as a keypair but have not - // been funded/created on the ledger yet. - if (accRes.status === 404) { - setBalances([]); + if (res.status === 404) { setTransactions([]); setFunded(false); return; } - if (!accRes.ok) { - throw new Error(`Horizon responded with ${accRes.status}`); + if (!res.ok) { + throw new Error(`Horizon responded with ${res.status}`); } - const accData = await accRes.json(); - setBalances(parseBalances(accData.balances ?? [])); + const data = await res.json(); + const records: HorizonPayment[] = data?._embedded?.records ?? []; + setTransactions(parsePayments(records, publicKey)); setFunded(true); - - // Real transaction history via the account's payments endpoint - // (the `_links.payments` href on the account response). - const payRes = await fetch( - `${HORIZON_URL}/accounts/${publicKey}/payments?order=desc&limit=15`, - ); - if (payRes.ok) { - const payData = await payRes.json(); - const records: HorizonPayment[] = payData?._embedded?.records ?? []; - setTransactions(parsePayments(records, publicKey)); - } else { - setTransactions([]); - } } catch (err) { - console.error("Failed to load wallet data:", err); - setError("Unable to load wallet data. Check your connection and retry."); + console.error("Failed to load wallet activity:", err); + setError("Unable to load wallet activity. Check your connection and retry."); } finally { setLoading(false); } }, [publicKey]); useEffect(() => { - refreshBalances(); - }, [refreshBalances]); + fetchActivity(); + }, [fetchActivity]); + + const refreshBalances = useCallback(async () => { + await Promise.all([refreshContextBalances(), fetchActivity()]); + }, [refreshContextBalances, fetchActivity]); const fundWithFriendbot = useCallback(async () => { if (!publicKey) return; From dfe5742be98bae1f64ef7877d5f78686d71419cf Mon Sep 17 00:00:00 2001 From: 3m1n3nc3 Date: Mon, 29 Jun 2026 15:47:28 +0100 Subject: [PATCH 4/4] refactor(frontend): wallet hook consumes shared provider balances After the upstream WalletProvider refactor, balances were being fetched twice (provider + creator wallet hook). Make the provider the single source of truth: - Derive XLM/USDC and the balances list from the context's balances instead of a separate /accounts fetch. - Keep transaction history and the unfunded flag from the account's /payments endpoint (404 => unfunded), which the provider doesn't expose. - refreshBalances() now refreshes the provider balances and the payments history together. - Defer the initial fetch to a microtask so no setState runs synchronously inside the effect (satisfies react-hooks/set-state-in-effect). No change to the wallet page API or behavior. --- frontend/src/app/hooks/useWallet.ts | 98 +++++++++++++++-------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/frontend/src/app/hooks/useWallet.ts b/frontend/src/app/hooks/useWallet.ts index e5a1b86..283b93d 100644 --- a/frontend/src/app/hooks/useWallet.ts +++ b/frontend/src/app/hooks/useWallet.ts @@ -1,7 +1,10 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; -import { useWallet as useWalletContext } from "@/context/WalletProvider"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useWallet as useWalletContext, + type Balance, +} from "@/context/WalletProvider"; /** * Horizon endpoint. Defaults to the public testnet so unfunded accounts can be @@ -13,7 +16,7 @@ export const HORIZON_URL = export const FRIENDBOT_URL = process.env.NEXT_PUBLIC_FRIENDBOT_URL ?? "https://friendbot.stellar.org"; -/** A single balance line item as returned by Horizon's `/accounts` endpoint. */ +/** A single balance line item, normalized from the wallet context. */ export interface WalletBalance { assetType: string; assetCode: string; @@ -34,13 +37,6 @@ export interface WalletTransaction { successful: boolean; } -interface HorizonBalance { - balance: string; - asset_type: string; - asset_code?: string; - asset_issuer?: string; -} - interface HorizonPayment { id: string; type: string; @@ -80,7 +76,7 @@ export interface UseWalletResult { funding: boolean; } -function parseBalances(raw: HorizonBalance[]): WalletBalance[] { +function parseBalances(raw: Balance[]): WalletBalance[] { return raw .map((b) => ({ assetType: b.asset_type, @@ -88,7 +84,7 @@ function parseBalances(raw: HorizonBalance[]): WalletBalance[] { assetIssuer: b.asset_issuer, balance: b.balance, })) - // Native asset first, then the rest in Horizon's order. + // Native asset first, then the rest in the context's order. .sort((a, b) => a.assetType === "native" ? -1 : b.assetType === "native" ? 1 : 0, ); @@ -141,26 +137,37 @@ function parsePayments( } /** - * Reads the connected wallet's public key from the wallet provider and fetches - * its balances and recent payments from Horizon. Exposes XLM/USDC balances, a - * funded flag for unfunded testnet accounts, real transaction history, and a - * manual refresh. + * Wallet view-model for the creator wallet page. Balances come from the shared + * WalletProvider (single source of truth); this hook adds the pieces the page + * needs on top — normalized balances, real transaction history and an unfunded + * flag from Horizon's payments endpoint, plus Friendbot funding and refresh. */ export function useWallet(): UseWalletResult { - const { publicKey: contextPublicKey, connected } = useWalletContext(); + const { + publicKey: contextPublicKey, + connected, + balances: contextBalances, + refreshBalances: refreshContextBalances, + } = useWalletContext(); + const publicKey = contextPublicKey ?? ""; const isConnected = connected; - const [balances, setBalances] = useState([]); const [transactions, setTransactions] = useState([]); const [funded, setFunded] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [funding, setFunding] = useState(false); - const refreshBalances = useCallback(async () => { + const balances = useMemo( + () => parseBalances(contextBalances), + [contextBalances], + ); + + // Transaction history + funded status come from the account's payments + // endpoint, which 404s for accounts that have not been created on the ledger. + const fetchActivity = useCallback(async () => { if (!publicKey) { - setBalances([]); setTransactions([]); setFunded(true); setError(null); @@ -171,48 +178,47 @@ export function useWallet(): UseWalletResult { setError(null); try { - const accRes = await fetch(`${HORIZON_URL}/accounts/${publicKey}`); + const res = await fetch( + `${HORIZON_URL}/accounts/${publicKey}/payments?order=desc&limit=15`, + ); - // Horizon returns 404 for accounts that exist as a keypair but have not - // been funded/created on the ledger yet. - if (accRes.status === 404) { - setBalances([]); + if (res.status === 404) { setTransactions([]); setFunded(false); return; } - if (!accRes.ok) { - throw new Error(`Horizon responded with ${accRes.status}`); + if (!res.ok) { + throw new Error(`Horizon responded with ${res.status}`); } - const accData = await accRes.json(); - setBalances(parseBalances(accData.balances ?? [])); + const data = await res.json(); + const records: HorizonPayment[] = data?._embedded?.records ?? []; + setTransactions(parsePayments(records, publicKey)); setFunded(true); - - // Real transaction history via the account's payments endpoint - // (the `_links.payments` href on the account response). - const payRes = await fetch( - `${HORIZON_URL}/accounts/${publicKey}/payments?order=desc&limit=15`, - ); - if (payRes.ok) { - const payData = await payRes.json(); - const records: HorizonPayment[] = payData?._embedded?.records ?? []; - setTransactions(parsePayments(records, publicKey)); - } else { - setTransactions([]); - } } catch (err) { - console.error("Failed to load wallet data:", err); - setError("Unable to load wallet data. Check your connection and retry."); + console.error("Failed to load wallet activity:", err); + setError("Unable to load wallet activity. Check your connection and retry."); } finally { setLoading(false); } }, [publicKey]); useEffect(() => { - refreshBalances(); - }, [refreshBalances]); + // Defer to a microtask so the loading/reset setState calls don't run + // synchronously inside the effect (avoids cascading renders). + let active = true; + void Promise.resolve().then(() => { + if (active) void fetchActivity(); + }); + return () => { + active = false; + }; + }, [fetchActivity]); + + const refreshBalances = useCallback(async () => { + await Promise.all([refreshContextBalances(), fetchActivity()]); + }, [refreshContextBalances, fetchActivity]); const fundWithFriendbot = useCallback(async () => { if (!publicKey) return;