Skip to content
Merged
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
39 changes: 8 additions & 31 deletions components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,9 @@ import { ShortcutHelpProvider } from "@/lib/context/ShortcutHelpContext";
import LayoutWrapper from "@/components/LayoutWrapper";
import ToastRegion from "@/components/ToastRegion";
import SessionExpiryProvider from "@/components/SessionExpiryProvider";
import ShortcutHelpModal from "@/components/ShortcutHelpModal";
import UnhandledRejectionListener from "@/components/UnhandledRejectionListener";
import { apiClient } from "@/lib/client/apiClient";

/**
* Lazy-loaded: CommandPalette pulls in its own icon set (13 lucide-react
* icons) and renders `null` until the user opens it (Cmd/Ctrl+K), but was
* previously a static import here -- mounted on every route via `Providers`,
* its icons shipped in the initial bundle on every page load regardless of
* whether the palette was ever opened.
*/
const CommandPalette = lazy(() => import("@/components/CommandPalette"));

/** Keeps the API client's authorization header aligned with wallet state. */
function ApiClientAuthBridge() {
const { account, isConnected } = useWallet();

useEffect(() => {
apiClient.setAuthToken(isConnected ? account?.address : null);
}, [account?.address, isConnected]);

return null;
}
import CommandPalette from "@/components/CommandPalette";
import DevRequestIdDisplay from "@/components/DevRequestIdDisplay";
import DevResetHandler from "@/components/dev/DevResetHandler";

/**
* Client-side provider boundary for the app.
Expand All @@ -56,14 +36,11 @@ export default function Providers({ children }: { children: ReactNode }) {
<TelemetryProvider>
<AsyncOperationsProvider>
<SessionExpiryProvider>
<ShortcutHelpProvider>
<LayoutWrapper>{children}</LayoutWrapper>
<ToastRegion />
<Suspense fallback={null}>
<CommandPalette />
</Suspense>
<ShortcutHelpModal />
</ShortcutHelpProvider>
<LayoutWrapper>{children}</LayoutWrapper>
<ToastRegion />
<CommandPalette />
<DevRequestIdDisplay />
<DevResetHandler />
</SessionExpiryProvider>
</AsyncOperationsProvider>
</TelemetryProvider>
Expand Down
36 changes: 36 additions & 0 deletions components/dev/DevResetHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { useEffect, Suspense } from "react";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { DEV_RESET_QUERY_PARAM, resetLocalStorage } from "@/lib/dev/resetLocalStorage";

/** Watches for `?dev-reset` (added by visiting the URL the `npm run
* dev:reset` script prints), clears every app-owned `localStorage` key,
* then strips the param so a page refresh doesn't clear storage again. */
function DevResetHandlerInner() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();

useEffect(() => {
if (!searchParams.has(DEV_RESET_QUERY_PARAM)) return;

resetLocalStorage();

const remaining = new URLSearchParams(searchParams);
remaining.delete(DEV_RESET_QUERY_PARAM);
const query = remaining.toString();
router.replace(query ? `${pathname}?${query}` : pathname);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);

return null;
}

export default function DevResetHandler() {
return (
<Suspense fallback={null}>
<DevResetHandlerInner />
</Suspense>
);
}
31 changes: 31 additions & 0 deletions lib/dev/resetLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { THEME_STORAGE_KEY } from "@/lib/config/theme";
import { DEV_MODE_STORAGE_KEY, DEV_MODE_LATEST_REQUEST_ID_KEY } from "@/lib/config/developer";

/** Query param that triggers `resetLocalStorage()` on load -- see
* `components/dev/DevResetHandler.tsx`. Paired with the `npm run dev:reset`
* script, which just prints the URL to visit (a Node script has no way to
* reach into a running browser's `localStorage` directly). */
export const DEV_RESET_QUERY_PARAM = "dev-reset";

/** Every `localStorage` key this app owns. Kept as one list so `dev:reset`
* clears the whole set instead of whichever ones someone remembered --
* add new app-level persisted keys here when they're introduced. */
export const DEV_RESET_LOCAL_STORAGE_KEYS = [
THEME_STORAGE_KEY,
"display-density",
"remitwise_whats_new_last_seen",
DEV_MODE_STORAGE_KEY,
DEV_MODE_LATEST_REQUEST_ID_KEY,
] as const;

/** Clears every key in {@link DEV_RESET_LOCAL_STORAGE_KEYS} so a developer
* can get back to a fresh first-visit client state (theme, density,
* "what's new" seen-state, dev mode) without manually clearing browser
* storage. Safe to call on the server -- it's a no-op without `window`. */
export function resetLocalStorage(): void {
if (typeof window === "undefined") return;

for (const key of DEV_RESET_LOCAL_STORAGE_KEYS) {
window.localStorage.removeItem(key);
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"generate:types": "openapi-typescript ./openapi.yaml --output ./src/api/types.ts",
"prebuild": "npm run generate:types",
"dev": "next dev",
"mock:anchor": "node scripts/mock-anchor-server.mjs",
"dev:reset": "node scripts/dev-reset.mjs",
"build": "next build --webpack",
"start": "next start",
"check:img-alt": "node scripts/check-img-alt.js",
Expand Down
9 changes: 9 additions & 0 deletions scripts/dev-reset.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env node
// Prints the URL that clears the app's localStorage. A Node script has no
// way to reach into a running browser's storage directly, so `dev:reset`
// points you at the client-side handler in components/dev/DevResetHandler.tsx
// instead of pretending to do the clearing itself.
const port = process.env.PORT || 3000;
const url = `http://localhost:${port}/?dev-reset`;

console.log(`\nVisit this URL in your browser to clear the app's localStorage:\n\n ${url}\n`);
35 changes: 35 additions & 0 deletions tests/unit/dev/resetLocalStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from "vitest";
import {
DEV_RESET_LOCAL_STORAGE_KEYS,
resetLocalStorage,
} from "@/lib/dev/resetLocalStorage";

describe("resetLocalStorage", () => {
beforeEach(() => {
localStorage.clear();
});

it("clears every app-owned localStorage key", () => {
for (const key of DEV_RESET_LOCAL_STORAGE_KEYS) {
localStorage.setItem(key, "some-value");
}

resetLocalStorage();

for (const key of DEV_RESET_LOCAL_STORAGE_KEYS) {
expect(localStorage.getItem(key)).toBeNull();
}
});

it("leaves keys it doesn't own untouched", () => {
localStorage.setItem("some-other-apps-key", "keep-me");

resetLocalStorage();

expect(localStorage.getItem("some-other-apps-key")).toBe("keep-me");
});

it("does nothing when localStorage already has no matching keys", () => {
expect(() => resetLocalStorage()).not.toThrow();
});
});
Loading