fix(plugins): clear overlay account state on local board transitions

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-09-19 08:57:49 -07:00
1 parent 51eb1e1b4a
commit 3f1fbf39c8
3 files changed
+16 -3

No files matched your search

+2
View File
@@ -71,6 +71,8 @@ The `appShellOverlay` slot requires `ui.action.register`. It receives the usual
`PluginWidgetProps` context. It mounts once in both application shells and
survives route navigation. It is disposed when the account or selected company
changes, during onboarding, and on sign-out. It is not mounted on login pages.
Local-trusted mode has no login requirement: its sessionless board may mount
overlays, but transitions to or from an account still dispose the prior state.
The host positions contributions above the mobile navigation and stacks them
at the bottom right. Each plugin owns its launcher, panel, keyboard handling,
@@ -19,9 +19,9 @@ vi.mock("@/plugins/slots", () => ({
}));
let root: Root | undefined;
let container: HTMLDivElement;
function render() {
function render(localTrusted = false) {
if (!root) { container = document.createElement("div"); document.body.append(container); root = createRoot(container); }
flushSync(() => root!.render(<PluginAppShellOverlays />));
flushSync(() => root!.render(<PluginAppShellOverlays localTrusted={localTrusted} />));
}
afterEach(() => {
if (root) flushSync(() => root!.unmount());
@@ -45,4 +45,12 @@ describe("persistent app-shell plugin lifecycle", () => {
state.onboarding = true; render(); expect(state.mounts).toBe(0);
state.onboarding = false; state.failed = true; render(); expect(container.textContent).toBe("");
});
it("clears account state when returning to the sessionless local board", () => {
render(true); flushSync(() => container.querySelector("button")!.click());
state.settled = false; render(true); expect(container.textContent).toBe(""); expect(state.disposals).toBe(1);
state.userId = null; state.settled = true; render(true);
expect(container.textContent).toBe("empty"); expect(state.mounts).toBe(2);
flushSync(() => container.querySelector("button")!.click());
state.userId = "bob"; render(true); expect(container.textContent).toBe("empty"); expect(state.disposals).toBe(2);
});
});
+4 -1
View File
@@ -34,7 +34,10 @@ export function PluginAppShellOverlays({ localTrusted = false }: { localTrusted?
const { userId, settled } = useAccountIdentity();
const { selectedCompanyId, selectedCompany, loading } = useCompany();
const { onboardingOpen } = useDialogState();
const identity = localTrusted ? "local-board" : settled ? userId : null;
// Local-trusted instances intentionally have no login requirement. Still
// prefer any real account and wait for identity resolution so account changes
// cannot reuse the prior account's in-memory plugin state.
const identity = settled ? userId ?? (localTrusted ? "local-board" : null) : null;
if (!identity || loading || onboardingOpen) return null;
return (
<AppShellEntries