fix(web): dashboard quick-actions use SPA soft-nav so the session survives

The wallet keeps `isUnlocked` + the plaintext mnemonic in memory ONLY (never
persisted — see the auth store's partialize), so ANY full-document reload wipes
the session and bounces the user back to onboarding. On mobile this made the
wallet unusable: every full-reload dashboard action effectively logged you out.

- Portfolio dashboard: render the Send / Cross-Chain / Earn / Manage Keys
  quick-action cards as react-router <Link> (client-side soft-nav) instead of
  full-reload <a href>. A tap pushState-navigates and the in-memory unlock +
  mnemonic survive.
- Audit for other session-dropping full-reload links: Callback's "Back to
  wallet" recovery link was a raw <a href="/"> — converted to <Link to="/">.

Verified at 390x844 (iPhone-class): the 4 cards render with no horizontal
overflow (documentElement.scrollWidth == 390); tapping Send soft-navigates to
/send with NO document reload (a page sentinel + the zustand auth store both
survive, isUnlocked stays true); a full-reload navigation (the prior behavior)
wipes the in-memory session — confirming the fix.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-18 10:43:17 -07:00
co-authored by Hanzo Dev
parent 83e2706fb6
commit 1ddc0df258
2 changed files with 47 additions and 4 deletions
+3 -3
View File
@@ -10,7 +10,7 @@
* GPL-3.0-or-later — inherited from the wallet monorepo.
*/
import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { Link, useNavigate } from "react-router-dom"
import { useBrand } from "../../hooks/useBrand"
import { useSession } from "../../store/session"
@@ -43,9 +43,9 @@ export default function Callback(): React.JSX.Element {
<>
<h1 style={title}>Sign-in failed</h1>
<p style={msg}>{error}</p>
<a href="/" style={link}>
<Link to="/" replace style={link}>
Back to {brand.walletName || "wallet"}
</a>
</Link>
</>
) : (
<>
+44 -1
View File
@@ -18,7 +18,7 @@
* placeholders rather than crashing the screen.
*/
import { lazy, Suspense, useMemo } from "react"
import { useNavigate } from "react-router-dom"
import { Link, useNavigate } from "react-router-dom"
import { mnemonicToAccount, type Address } from "viem/accounts"
import { Button, Card, Text, XStack, YStack } from "@hanzo/gui"
import { useAuth } from "../../store/auth"
@@ -67,6 +67,35 @@ function useActiveChainId(): number {
return 96369
}
/**
* Dashboard quick actions. Each is a react-router <Link> (SPA soft-nav) — NOT
* an <a href>. The unlock state + plaintext mnemonic live ONLY in memory (the
* auth store never persists them), so a full document reload wipes them and
* bounces the user back to onboarding. Soft-nav keeps the session alive.
*/
const QUICK_ACTIONS: ReadonlyArray<{ to: string; label: string; icon: string }> = [
{ to: "/send", label: "Send", icon: "↑" },
{ to: "/bridge", label: "Cross-Chain", icon: "⇄" },
{ to: "/stake", label: "Earn", icon: "%" },
{ to: "/settings/security", label: "Manage Keys", icon: "⚿" },
]
const quickActionStyle: React.CSSProperties = {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 6,
minWidth: 0,
padding: "14px 6px",
borderRadius: 12,
border: "1px solid var(--surface3, #222)",
background: "var(--surface2, #111)",
color: "var(--neutral1, #fff)",
textDecoration: "none",
textAlign: "center",
}
export default function Portfolio() {
const navigate = useNavigate()
const isUnlocked = useAuth((s) => s.isUnlocked)
@@ -119,6 +148,20 @@ export default function Portfolio() {
</YStack>
</Card>
<nav
aria-label="Quick actions"
style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 8 }}
>
{QUICK_ACTIONS.map((a) => (
<Link key={a.to} to={a.to} style={quickActionStyle}>
<span aria-hidden="true" style={{ fontSize: 20, lineHeight: 1 }}>
{a.icon}
</span>
<span style={{ fontSize: 12, fontWeight: 600 }}>{a.label}</span>
</Link>
))}
</nav>
<Suspense fallback={null}>
<ChainSwitcher />
</Suspense>