feat(auth): open sign-in and registration as modals over the page behind (#50)
/login, /register and /forgot-password rendered bare cards with no site header. They linked to each other and nowhere else, so a customer who clicked Log in from the storefront and changed their mind had no way back except the browser's back button. All four auth routes are now modals over the page the customer was already on, reusing the backdrop-location arrangement from #51: a direct visit or a link from an email opens over the storefront, so closing always lands somewhere real. They remain real routes, because /reset-password links to /login and customers may have bookmarks. Signing in or registering now returns the customer to the page behind, signed in, rather than moving them to /account. Someone who signs in while browsing wants to carry on browsing, and this is already how the cart and favorites prompts behave when they resume an interrupted action. The larger half of this is removing the duplication. Signing in existed twice — as these routes and again inside the prompt shown when a signed-out visitor adds to the cart or favorites something — and the two had already drifted. There were three different wordings of the marketing consent in circulation: the register page's, a shorter one in the prompt, and the string the server actually stores. The server keeps that text verbatim so the consent record says what the customer saw, which none of the three did. Both callers now render one shared AuthForm whose checkbox is the exact string the server records, and a test asserts that wording so it cannot drift again silently. Steps within the auth flow replace rather than push, so switching between tabs or stepping to password recovery leaves the whole detour as a single history entry and closing returns to where it started instead of walking back through every tab that was looked at. The privacy policy link opens in a new tab: following it in place would discard a part-filled signup form, and /privacy still has no way back of its own until #52. Test changes follow from the destination change rather than being incidental. Nineteen assertions across seven specs waited for /account after signing in; they now assert the header shows a signed-in customer, which is the condition actually being waited for. Modal submits are scoped to their dialog, because the storefront behind now offers a Log in button of its own and an unscoped locator matched both. Assertions that follow a server round-trip were given a realistic timeout — the 5s default is too tight for a bcrypt hash plus re-rendering the storefront behind the modal. Verified with 83 end-to-end tests, all passing, and type checking clean. No backend changes. Closes #50
This commit is contained in:
+36
-12
@@ -6,8 +6,7 @@ import { ConfigProvider, theme as antdTheme } from 'antd';
|
||||
import 'antd/dist/reset.css';
|
||||
import App from './App';
|
||||
import Admin from './admin/Admin';
|
||||
import Login from './customer/Login';
|
||||
import Register from './customer/Register';
|
||||
import AuthRouteModal from './customer/AuthRouteModal';
|
||||
import Account from './customer/Account';
|
||||
import PrivacyPolicy from './customer/PrivacyPolicy';
|
||||
import VerifyEmail from './customer/VerifyEmail';
|
||||
@@ -32,6 +31,12 @@ const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
||||
// case, and a modal floating on nothing has nowhere to close back to.
|
||||
const STOREFRONT_BACKDROP: Partial<Location> = { pathname: '/', search: '', hash: '' };
|
||||
|
||||
// Routes presented as a modal over the page the customer was already on rather
|
||||
// than as a page of their own. Each stays a real, linkable URL — bookmarkable,
|
||||
// refreshable, and closed by the browser's Back button — while never being
|
||||
// somewhere with no way out.
|
||||
const MODAL_ROUTES = ['/account', '/login', '/register', '/forgot-password', '/reset-password'];
|
||||
|
||||
// Respects the OS-level "reduce motion" accessibility setting by turning off
|
||||
// antd's transitions. Beyond the accessibility win, animated popups are a
|
||||
// standing source of flake in end-to-end tests, which drive the app with this
|
||||
@@ -59,36 +64,55 @@ function AppRoutes() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const state = location.state as { background?: Location } | null;
|
||||
const isAccount = location.pathname === '/account';
|
||||
const modalPath = MODAL_ROUTES.includes(location.pathname) ? location.pathname : null;
|
||||
|
||||
// In-app navigation names the page to render behind. Anything else — a
|
||||
// bookmark, an email link, the redirect after registering — falls back to the
|
||||
// storefront, so closing always lands somewhere real.
|
||||
// bookmark, an email link, a link in a password reset message — falls back to
|
||||
// the storefront, so closing always lands somewhere real.
|
||||
const background = state?.background;
|
||||
const backdrop = isAccount ? background ?? { ...location, ...STOREFRONT_BACKDROP } : location;
|
||||
const backdrop = modalPath ? background ?? { ...location, ...STOREFRONT_BACKDROP } : location;
|
||||
|
||||
function closeAccount() {
|
||||
function closeModal() {
|
||||
// Back, when there is somewhere to go back to, so closing the modal and
|
||||
// pressing Back do the same thing and neither leaves a dead entry behind.
|
||||
if (background) navigate(-1);
|
||||
else navigate('/', { replace: true });
|
||||
}
|
||||
|
||||
// Steps within the auth flow replace rather than push, so the whole detour
|
||||
// stays a single history entry and closing returns to where it started
|
||||
// instead of walking back through every tab that was looked at.
|
||||
function goWithinAuth(path: string) {
|
||||
navigate(path, { state: background ? { background } : undefined, replace: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes location={backdrop as Location}>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/cart" element={<Cart />} />
|
||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
</Routes>
|
||||
{/* Rendered outside the Routes above, which are showing the backdrop. */}
|
||||
{isAccount && <Account onClose={closeAccount} />}
|
||||
{modalPath === '/account' && <Account onClose={closeModal} />}
|
||||
{modalPath === '/login' && (
|
||||
<AuthRouteModal mode="login" onClose={closeModal} onNavigate={goWithinAuth} />
|
||||
)}
|
||||
{modalPath === '/register' && (
|
||||
<AuthRouteModal mode="register" onClose={closeModal} onNavigate={goWithinAuth} />
|
||||
)}
|
||||
{modalPath === '/forgot-password' && (
|
||||
<ForgotPassword onClose={closeModal} onBackToSignIn={() => goWithinAuth('/login')} />
|
||||
)}
|
||||
{modalPath === '/reset-password' && (
|
||||
<ResetPassword
|
||||
onClose={closeModal}
|
||||
onRequestNewLink={() => goWithinAuth('/forgot-password')}
|
||||
onBackToSignIn={() => goWithinAuth('/login')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user