diff --git a/apps/frontend/app/layout.tsx b/apps/frontend/app/layout.tsx index 1dfcf6e9..d7017c47 100644 --- a/apps/frontend/app/layout.tsx +++ b/apps/frontend/app/layout.tsx @@ -4,6 +4,7 @@ import { WalletProvider } from "../components/WalletContext"; import { ToastProvider } from "../components/toast/ToastProvider"; import Navbar from "./components/Navbar"; import { absoluteUrl, defaultDescription, siteName, siteUrl } from "./seo"; +import { getAuthStatusFromCookie } from "../lib/get-auth-status.server"; import "./globals.css"; export const metadata: Metadata = { @@ -31,11 +32,13 @@ export const metadata: Metadata = { }; export default function RootLayout({ children }: { children: React.ReactNode }) { + const { hasToken } = getAuthStatusFromCookie(); + return ( - +
diff --git a/apps/frontend/components/WalletContext.tsx b/apps/frontend/components/WalletContext.tsx index a50e7e5a..f7ab94fa 100644 --- a/apps/frontend/components/WalletContext.tsx +++ b/apps/frontend/components/WalletContext.tsx @@ -9,6 +9,8 @@ type WalletState = { targetNetwork: string; isConnecting: boolean; error: string | null; + /** Whether the server detected an auth-token cookie during SSR. */ + hasToken: boolean; /** Monotonically increasing version. Increments on every connect and * disconnect so dashboard components can key their data-fetches on it * and automatically refetch after a wallet change. */ @@ -39,12 +41,22 @@ async function loadFreighter(): Promise { return import("@stellar/freighter-api"); } -export function WalletProvider({ children }: { children: React.ReactNode }) { +export function WalletProvider({ + children, + initialHasToken = false, +}: { + children: React.ReactNode; + /** Whether the server detected an auth-token cookie during SSR. */ + initialHasToken?: boolean; +}) { const [publicKey, setPublicKey] = useState(null); const [freighterNetwork, setFreighterNetwork] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); const [dashboardVersion, setDashboardVersion] = useState(0); + // Use the server-hydrated value so the first paint shows the correct + // state — no "Connect wallet to view" flash when a token exists. + const [hasToken, setHasToken] = useState(initialHasToken); const targetNetwork = normalizeNetwork(process.env.NEXT_PUBLIC_STELLAR_NETWORK); @@ -120,11 +132,12 @@ export function WalletProvider({ children }: { children: React.ReactNode }) { targetNetwork, isConnecting, error, + hasToken, dashboardVersion, connect, disconnect, }), - [connect, disconnect, dashboardVersion, error, freighterNetwork, isConnecting, publicKey, targetNetwork], + [connect, disconnect, dashboardVersion, error, freighterNetwork, hasToken, isConnecting, publicKey, targetNetwork], ); return {children}; diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index 40934453..cfb5fc4d 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -14,6 +14,20 @@ type JwtPayload = { sub?: unknown; }; +const AUTH_TOKEN_COOKIE = "auth-token"; + +function setAuthCookie(token: string): void { + if (typeof document === "undefined") return; + // Set a non-HttpOnly cookie so the server can read it via `cookies()`. + // Path=/ ensures it's sent on every request. + document.cookie = `${AUTH_TOKEN_COOKIE}=${encodeURIComponent(token)}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax`; +} + +export function clearAuthCookie(): void { + if (typeof document === "undefined") return; + document.cookie = `${AUTH_TOKEN_COOKIE}=; path=/; max-age=0; SameSite=Lax`; +} + export async function getAccessToken(publicKey: string): Promise { const savedToken = typeof window !== "undefined" ? window.localStorage.getItem(TOKEN_STORAGE_KEY) : null; @@ -63,6 +77,7 @@ export async function getAccessToken(publicKey: string): Promise { } window.localStorage.setItem(TOKEN_STORAGE_KEY, accessToken); + setAuthCookie(accessToken); return accessToken; } @@ -72,6 +87,7 @@ export function clearAuthToken(): void { } window.localStorage.removeItem(TOKEN_STORAGE_KEY); + clearAuthCookie(); } /** diff --git a/apps/frontend/lib/get-auth-status.server.ts b/apps/frontend/lib/get-auth-status.server.ts new file mode 100644 index 00000000..220e1934 --- /dev/null +++ b/apps/frontend/lib/get-auth-status.server.ts @@ -0,0 +1,26 @@ +/** + * Server-only utility that reads the `auth-token` cookie so the initial + * render can show the correct connection state instead of a flash of + * "Connect wallet to view". + * + * This file uses Next.js `cookies()` — it MUST only be imported from + * Server Components (app directory), never from client components. + */ +import { cookies } from "next/headers"; + +export type AuthStatus = { + /** Whether a valid auth-token cookie was found */ + hasToken: boolean; + /** The raw token value, if present */ + token: string | null; +}; + +export function getAuthStatusFromCookie(): AuthStatus { + const cookieStore = cookies(); + const token = cookieStore.get("auth-token")?.value ?? null; + + return { + hasToken: token !== null, + token, + }; +} \ No newline at end of file