From 9f9e6408c45cce6ee3e5d5896cf4ddff7cf84289 Mon Sep 17 00:00:00 2001 From: Allison Date: Tue, 28 Jul 2026 12:18:51 +0000 Subject: [PATCH] fix: asset issuer display helper, reduced-motion support, and Freighter timeout/retry (closes #23, closes #36, closes #63) --- app/globals.css | 16 +++++++++ app/tools/freighter-connect/page.tsx | 50 +++++++++++++++++++++++----- app/tools/trustline-checker/page.tsx | 31 +++++++++++++++-- lib/stellar/trustline.ts | 4 +++ 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/app/globals.css b/app/globals.css index dac0abb9..3a432e3e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -62,3 +62,19 @@ button, textarea { font: inherit; } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + + body::before, + body::after { + animation: none !important; + transition: none !important; + } +} diff --git a/app/tools/freighter-connect/page.tsx b/app/tools/freighter-connect/page.tsx index 8dd648f0..718aa2cf 100644 --- a/app/tools/freighter-connect/page.tsx +++ b/app/tools/freighter-connect/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/Button"; import { Card } from "@/components/ui/Card"; import { CharacterPanel } from "@/components/ui/CharacterPanel"; @@ -18,6 +18,8 @@ declare global { } } +const CONNECTION_TIMEOUT_MS = 15_000; + function normalizeFreighterNetwork(value: string) { const normalized = value.toLowerCase(); @@ -33,7 +35,9 @@ export default function FreighterConnectPage() { const [connected, setConnected] = useState(false); const [publicKey, setPublicKey] = useState(""); const [walletNetwork, setWalletNetwork] = useState(""); + const [connecting, setConnecting] = useState(false); const [message, setMessage] = useState({ type: "info" as "info" | "success" | "warning" | "error", text: "The wallet mascot is listening for Freighter in this browser." }); + const connectionId = useRef(0); const walletNetworkKind = walletNetwork ? normalizeFreighterNetwork(walletNetwork) : ""; const networkMismatch = walletNetworkKind !== "" && walletNetworkKind !== "unknown" && walletNetworkKind !== network; @@ -82,25 +86,55 @@ export default function FreighterConnectPage() { }; }, []); - async function connect() { + const connect = useCallback(async () => { + if (connecting) return; + if (!window.freighterApi?.getPublicKey) { setMessage({ type: "warning", text: "The wallet mascot cannot reach the Freighter API in this browser." }); return; } + const id = ++connectionId.current; + setConnecting(true); + setMessage({ type: "info", text: "The wallet mascot is reaching out to Freighter..." }); + try { - const key = await window.freighterApi.getPublicKey(); + const key = await Promise.race([ + window.freighterApi.getPublicKey(), + new Promise((_, reject) => + setTimeout(() => reject(new Error("TIMEOUT")), CONNECTION_TIMEOUT_MS) + ) + ]); + + if (id !== connectionId.current) return; + const nextWalletNetwork = window.freighterApi.getNetwork ? await window.freighterApi.getNetwork().catch(() => "") : walletNetwork; + + if (id !== connectionId.current) return; + setPublicKey(key); setConnected(true); setWalletNetwork(nextWalletNetwork); setMessage({ type: "success", text: "The wallet mascot received the Freighter public key." }); - } catch { - setMessage({ type: "error", text: "Connection request was rejected or could not be completed." }); + } catch (error) { + if (id !== connectionId.current) return; + + if (error instanceof Error && error.message === "TIMEOUT") { + setMessage({ + type: "error", + text: "Connection timed out. Freighter did not respond within 15 seconds. Make sure the extension is unlocked and try again." + }); + } else { + setMessage({ type: "error", text: "Connection request was rejected or could not be completed." }); + } + } finally { + if (id === connectionId.current) { + setConnecting(false); + } } - } + }, [connecting, walletNetwork]); return (
@@ -111,8 +145,8 @@ export default function FreighterConnectPage() { description="The wallet mascot watches for Freighter, asks for a public key, and explains what happened without asking for secrets." /> - {publicKey ? (
diff --git a/app/tools/trustline-checker/page.tsx b/app/tools/trustline-checker/page.tsx index 9fd4095f..398f7c2d 100644 --- a/app/tools/trustline-checker/page.tsx +++ b/app/tools/trustline-checker/page.tsx @@ -8,8 +8,9 @@ import { CharacterPanel } from "@/components/ui/CharacterPanel"; import { Input } from "@/components/ui/Input"; import { StatusMessage } from "@/components/ui/StatusMessage"; import { AddressInput } from "@/components/stellar/AddressInput"; +import { CopyableValue } from "@/components/stellar/CopyableValue"; import { useNetwork } from "@/components/stellar/NetworkProvider"; -import { checkTrustline } from "@/lib/stellar/trustline"; +import { checkTrustline, type TrustlineCheck } from "@/lib/stellar/trustline"; export default function TrustlineCheckerPage() { const { network } = useNetwork(); @@ -17,6 +18,7 @@ export default function TrustlineCheckerPage() { const [assetCode, setAssetCode] = useState(""); const [issuer, setIssuer] = useState(""); const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); const [message, setMessage] = useState({ type: "info" as "info" | "success" | "warning" | "error", text: "The trust inspector needs an account, asset code, and issuer to look for the handshake." }); async function handleSubmit(event: React.FormEvent) { @@ -24,8 +26,9 @@ export default function TrustlineCheckerPage() { setLoading(true); try { - const result = await checkTrustline(account, assetCode, issuer, network); - setMessage({ type: result.exists ? "success" : "warning", text: result.message }); + const check = await checkTrustline(account, assetCode, issuer, network); + setResult(check); + setMessage({ type: check.exists ? "success" : "warning", text: check.message }); } catch (error) { setMessage({ type: "error", text: error instanceof Error ? error.message : "Unexpected error." }); } finally { @@ -55,6 +58,28 @@ export default function TrustlineCheckerPage() { + {result ? ( + +
+

Checked asset identity

+
+
+

Asset code

+

{result.assetCode}

+
+
+

Issuer

+

+ +

+
+
+

+ Checked on Stellar {network} +

+
+
+ ) : null} {message.type === "error" && message.text.includes("Account not found on Stellar testnet") ? (