Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -31,11 +32,13 @@ export const metadata: Metadata = {
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
const { hasToken } = getAuthStatusFromCookie();

return (
<html lang="en" suppressHydrationWarning>
<body className="bg-slate-50 text-slate-950 transition-colors dark:bg-slate-950 dark:text-slate-100">
<ThemeProvider>
<WalletProvider>
<WalletProvider initialHasToken={hasToken}>
<ToastProvider>
<div className="min-h-screen bg-slate-50 text-slate-950 transition-colors dark:bg-slate-950 dark:text-slate-100">
<Navbar />
Expand Down
17 changes: 15 additions & 2 deletions apps/frontend/components/WalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -39,12 +41,22 @@ async function loadFreighter(): Promise<FreighterApi> {
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<string | null>(null);
const [freighterNetwork, setFreighterNetwork] = useState<string | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(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);

Expand Down Expand Up @@ -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 <WalletContext.Provider value={value}>{children}</WalletContext.Provider>;
Expand Down
16 changes: 16 additions & 0 deletions apps/frontend/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
const savedToken =
typeof window !== "undefined" ? window.localStorage.getItem(TOKEN_STORAGE_KEY) : null;
Expand Down Expand Up @@ -63,6 +77,7 @@ export async function getAccessToken(publicKey: string): Promise<string> {
}

window.localStorage.setItem(TOKEN_STORAGE_KEY, accessToken);
setAuthCookie(accessToken);
return accessToken;
}

Expand All @@ -72,6 +87,7 @@ export function clearAuthToken(): void {
}

window.localStorage.removeItem(TOKEN_STORAGE_KEY);
clearAuthCookie();
}

/**
Expand Down
26 changes: 26 additions & 0 deletions apps/frontend/lib/get-auth-status.server.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
Loading