A Page Redirect Is Not a Substitute for an Inline Modal When the Page Has State
TLDR
Bug: clicking export on a standalone tool page redirected to the sign-in page, destroying all in-memory work. Cause: the auth modal only existed inside the shell component. Standalone pages had no modal, so they fell back to a redirect. Fix: extract the modal into a standalone script any page can load.
Symptom
A user opens a tool directly (no parent shell), does meaningful work, uploads a file, processes it, makes corrections, then clicks Export. The page navigates to /auth/login. When they sign in and return, the tool is empty. The file is gone. All work is lost.
The redirect was intentional. It was the only available auth path for standalone pages. The problem was not the redirect logic. The problem was that a redirect was the only option.
Why It Happens
Auth UI inside a shell component is not portable. The modal knows about the shell's state, uses the shell's element IDs, calls the shell's internal methods. None of that is available on a standalone page.
The standard response when a component is not portable is to add a fallback. The fallback here was window.location.href = '/auth/login'. This is the wrong fallback when the page has in-memory state worth preserving. A redirect is a full document replacement. Everything goes.
The correct response is to make the component portable.
The Fix
Extract the modal into a script with no dependency on its original host. The script injects the modal once into document.body on first call and exposes a stable API:
// before: modal lived inside the shell, standalone pages had no option
function _openAuthModal() {
// 140 lines of DOM construction tied to shell internals
// only callable when the shell was loaded
}
// fallback on standalone pages: window.location.href = '/auth/login'; // destroys everything
// after: portable modal component window.GxAuth.open({ context: 'Sign in to save and export your work.', onSignIn: (user) => resolve({ signedIn: true, tier: user.tier }), });
The shell replaced its 140-line DOM construction with a one-line call to the same component. Standalone pages load the same script and call the same API. The experience is identical in both environments.
All element IDs inside the component use a dedicated prefix to avoid collisions on pages where both the component and the shell are loaded simultaneously.
How to Prevent It
When a UI component that handles user state is only available in one context, the fallback for other contexts should not destroy that state. Before adding a redirect as a fallback, ask: does the current page have state the user would lose? If yes, the fallback is wrong.
The general pattern: if a component needs to work in more than one context, extract it before adding per-context fallbacks. Per-context fallbacks compound. A portable component does not.
The Generalizable Lesson
When a user action requires a modal and no modal is available, a redirect is not a modal. Extract the component. The redirect path is a symptom that portability was deferred too long.