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:
@@ -0,0 +1,139 @@
|
||||
import { useState } from 'react';
|
||||
import Form from 'antd/es/form';
|
||||
import Input from 'antd/es/input';
|
||||
import Button from 'antd/es/button';
|
||||
import Checkbox from 'antd/es/checkbox';
|
||||
import Tabs from 'antd/es/tabs';
|
||||
import Alert from 'antd/es/alert';
|
||||
import Typography from 'antd/es/typography';
|
||||
import { registerCustomer, loginCustomer } from './customerApi';
|
||||
import { useCustomerAuth } from './CustomerAuthContext';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export type AuthMode = 'register' | 'login';
|
||||
|
||||
// Must stay identical to MARKETING_CONSENT_TEXT in backend/src/utils.ts, which
|
||||
// is what gets stored verbatim against the customer's consent record. The point
|
||||
// of storing it is that the record says what the customer actually saw, so a
|
||||
// label that differs from the stored string defeats the whole mechanism. Before
|
||||
// this was shared there were three wordings in play — this one, a shorter one in
|
||||
// the cart prompt, and the string the server actually recorded — and none of
|
||||
// them matched.
|
||||
export const MARKETING_CONSENT_TEXT =
|
||||
'I want to receive occasional emails about new one-of-a-kind items from Redefined Designs. I can unsubscribe at any time.';
|
||||
|
||||
interface Props {
|
||||
mode: AuthMode;
|
||||
onModeChange: (mode: AuthMode) => void;
|
||||
onForgotPassword: () => void;
|
||||
// Called once the session exists. What that means differs by caller: the
|
||||
// route closes back to the page behind it, while the cart and favorite
|
||||
// prompts resume the action the customer was interrupted doing.
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
// The one implementation of signing in and registering. It was previously
|
||||
// written twice — once as the /login and /register pages, once inside the
|
||||
// prompt shown when a signed-out visitor adds to the cart — which had already
|
||||
// drifted in consent wording and in which links each offered.
|
||||
export default function AuthForm({ mode, onModeChange, onForgotPassword, onSuccess }: Props) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { refresh } = useCustomerAuth();
|
||||
|
||||
async function submit(action: () => Promise<unknown>) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await action();
|
||||
refresh();
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <Alert type="error" showIcon message={error} style={{ marginBottom: 16 }} />}
|
||||
<Tabs
|
||||
activeKey={mode}
|
||||
onChange={(key) => {
|
||||
// A stale error from the other tab would read as though it applied to
|
||||
// the form now on screen.
|
||||
setError(null);
|
||||
onModeChange(key as AuthMode);
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
key: 'register',
|
||||
label: 'Create Account',
|
||||
children: (
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={(values) =>
|
||||
submit(() =>
|
||||
registerCustomer(values.email, values.password, values.name, !!values.marketingConsent)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Form.Item name="name" label="Name">
|
||||
<Input autoComplete="name" />
|
||||
</Form.Item>
|
||||
<Form.Item name="email" label="Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input autoComplete="email" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="Password"
|
||||
rules={[{ required: true, min: 8, message: 'At least 8 characters' }]}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
<Form.Item name="marketingConsent" valuePropName="checked" initialValue={false}>
|
||||
<Checkbox>{MARKETING_CONSENT_TEXT}</Checkbox>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||
Create account
|
||||
</Button>
|
||||
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 12 }}>
|
||||
{/* Opens in a new tab deliberately: following it in place would
|
||||
discard a part-filled signup form, and /privacy has no way
|
||||
back of its own yet (#52). */}
|
||||
By creating an account you agree to our{' '}
|
||||
<a href="/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.
|
||||
</Text>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'login',
|
||||
label: 'Log In',
|
||||
children: (
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={(values) => submit(() => loginCustomer(values.email, values.password))}
|
||||
>
|
||||
<Form.Item name="email" label="Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input autoComplete="email" />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="Password" rules={[{ required: true }]}>
|
||||
<Input.Password autoComplete="current-password" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||
Log in
|
||||
</Button>
|
||||
<Button type="link" style={{ paddingInline: 0, marginTop: 8 }} onClick={onForgotPassword}>
|
||||
Forgot password?
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Modal, Form, Input, Button, Checkbox, Tabs, Alert } from 'antd';
|
||||
import { registerCustomer, loginCustomer } from './customerApi';
|
||||
import { useCustomerAuth } from './CustomerAuthContext';
|
||||
import Modal from 'antd/es/modal';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import AuthForm, { AuthMode } from './AuthForm';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -9,40 +9,15 @@ interface Props {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
// The prompt shown when a signed-out visitor does something that needs an
|
||||
// account — adding to the cart, favoriting, or filtering by favorites. It is
|
||||
// only the framing: the form itself is shared with the /login and /register
|
||||
// routes, so the two cannot drift apart in validation, copy, or consent
|
||||
// wording again.
|
||||
export default function AuthPromptModal({ open, onClose, onSuccess }: Props) {
|
||||
const [tab, setTab] = useState<'register' | 'login'>('register');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const { refresh } = useCustomerAuth();
|
||||
|
||||
async function handleRegister(values: any) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await registerCustomer(values.email, values.password, values.name, !!values.marketingConsent);
|
||||
refresh();
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(values: any) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await loginCustomer(values.email, values.password);
|
||||
refresh();
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
const [mode, setMode] = useState<AuthMode>('register');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -50,50 +25,19 @@ export default function AuthPromptModal({ open, onClose, onSuccess }: Props) {
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
{error && <Alert type="error" message={error} style={{ marginBottom: 16 }} />}
|
||||
<Tabs
|
||||
activeKey={tab}
|
||||
onChange={(k) => setTab(k as 'register' | 'login')}
|
||||
items={[
|
||||
{
|
||||
key: 'register',
|
||||
label: 'Create Account',
|
||||
children: (
|
||||
<Form form={form} layout="vertical" onFinish={handleRegister}>
|
||||
<Form.Item name="name" label="Name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="email" label="Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="Password" rules={[{ required: true, min: 8, message: 'At least 8 characters' }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item name="marketingConsent" valuePropName="checked" initialValue={false}>
|
||||
<Checkbox>Send me occasional emails about new one-of-a-kind items.</Checkbox>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>Create account & continue</Button>
|
||||
</Form>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'login',
|
||||
label: 'Log In',
|
||||
children: (
|
||||
<Form layout="vertical" onFinish={handleLogin}>
|
||||
<Form.Item name="email" label="Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="Password" rules={[{ required: true }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>Log in & continue</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
]}
|
||||
<AuthForm
|
||||
mode={mode}
|
||||
onModeChange={setMode}
|
||||
// Recovery is a route rather than another tab, so leave this prompt and
|
||||
// open it over whatever is behind — the interrupted action is abandoned
|
||||
// either way, and stacking a second modal on this one would be worse.
|
||||
onForgotPassword={() => {
|
||||
onClose();
|
||||
navigate('/forgot-password', { state: { background: location } });
|
||||
}}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import Modal from 'antd/es/modal';
|
||||
import AuthForm, { AuthMode } from './AuthForm';
|
||||
|
||||
interface Props {
|
||||
mode: AuthMode;
|
||||
onClose: () => void;
|
||||
// Moving between the auth routes, supplied by the router so the rule about
|
||||
// keeping the whole detour to one history entry lives in one place.
|
||||
onNavigate: (path: string) => void;
|
||||
}
|
||||
|
||||
const TITLES: Record<AuthMode, string> = {
|
||||
register: 'Create an account',
|
||||
login: 'Log in'
|
||||
};
|
||||
|
||||
// /login and /register as modals over the page behind them, so a customer who
|
||||
// clicks Log in while browsing and changes their mind is not stranded. Both
|
||||
// stay real routes: /reset-password links to /login, and customers may have
|
||||
// bookmarks.
|
||||
export default function AuthRouteModal({ mode, onClose, onNavigate }: Props) {
|
||||
return (
|
||||
<Modal
|
||||
title={TITLES[mode]}
|
||||
open
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
style={{ maxWidth: 'calc(100vw - 32px)' }}
|
||||
destroyOnHidden
|
||||
>
|
||||
<AuthForm
|
||||
mode={mode}
|
||||
onModeChange={(next) => onNavigate(next === 'login' ? '/login' : '/register')}
|
||||
onForgotPassword={() => onNavigate('/forgot-password')}
|
||||
// Closing returns to the page behind, now signed in. Someone who signs
|
||||
// in while browsing wants to carry on browsing rather than be moved to
|
||||
// their account page.
|
||||
onSuccess={onClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -3,14 +3,20 @@ import Form from 'antd/es/form';
|
||||
import Input from 'antd/es/input';
|
||||
import Button from 'antd/es/button';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Card from 'antd/es/card';
|
||||
import Modal from 'antd/es/modal';
|
||||
import Alert from 'antd/es/alert';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { requestPasswordReset } from './customerApi';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
export default function ForgotPassword() {
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
// Steps back to sign-in without leaving a history entry behind, the same way
|
||||
// the auth modal switches between its own tabs.
|
||||
onBackToSignIn: () => void;
|
||||
}
|
||||
|
||||
export default function ForgotPassword({ onClose, onBackToSignIn }: Props) {
|
||||
const [sent, setSent] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -29,8 +35,14 @@ export default function ForgotPassword() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={{ maxWidth: 420, margin: '64px auto' }}>
|
||||
<Title level={3}>Reset your password</Title>
|
||||
<Modal
|
||||
title="Reset your password"
|
||||
open
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
style={{ maxWidth: 'calc(100vw - 32px)' }}
|
||||
destroyOnHidden
|
||||
>
|
||||
|
||||
{sent ? (
|
||||
<>
|
||||
@@ -43,7 +55,9 @@ export default function ForgotPassword() {
|
||||
description="If an account exists for that address, we've sent a link to reset the password. The link expires in one hour."
|
||||
/>
|
||||
<Paragraph style={{ marginTop: 16 }}>
|
||||
<Link to="/login">Back to sign in</Link>
|
||||
<Button type="link" style={{ paddingInline: 0 }} onClick={onBackToSignIn}>
|
||||
Back to sign in
|
||||
</Button>
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
@@ -67,10 +81,13 @@ export default function ForgotPassword() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Text type="secondary">
|
||||
Remembered it? <Link to="/login">Sign in</Link>
|
||||
Remembered it?{' '}
|
||||
<Button type="link" style={{ paddingInline: 0 }} onClick={onBackToSignIn}>
|
||||
Sign in
|
||||
</Button>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Form, Input, Button, Typography, Card, Alert } from 'antd';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { loginCustomer } from './customerApi';
|
||||
import { useCustomerAuth } from './CustomerAuthContext';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Login() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { refresh } = useCustomerAuth();
|
||||
|
||||
async function onFinish(values: any) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await loginCustomer(values.email, values.password);
|
||||
refresh();
|
||||
navigate('/account');
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 420, margin: '48px auto', padding: '0 16px' }}>
|
||||
<Card>
|
||||
<Title level={3}>Log in</Title>
|
||||
{error && <Alert type="error" message={error} style={{ marginBottom: 16 }} />}
|
||||
<Form layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item name="email" label="Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="Password" rules={[{ required: true }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>Log in</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Text type="secondary">
|
||||
<Link to="/forgot-password">Forgot password?</Link>
|
||||
<br />
|
||||
No account yet? <Link to="/register">Create one</Link>
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Form, Input, Button, Checkbox, Typography, Card, Alert } from 'antd';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { registerCustomer } from './customerApi';
|
||||
import { useCustomerAuth } from './CustomerAuthContext';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Register() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { refresh } = useCustomerAuth();
|
||||
|
||||
async function onFinish(values: any) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await registerCustomer(values.email, values.password, values.name, !!values.marketingConsent);
|
||||
refresh();
|
||||
navigate('/account');
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 420, margin: '48px auto', padding: '0 16px' }}>
|
||||
<Card>
|
||||
<Title level={3}>Create an account</Title>
|
||||
{error && <Alert type="error" message={error} style={{ marginBottom: 16 }} />}
|
||||
<Form layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item name="name" label="Name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="email" label="Email" rules={[{ required: true, type: 'email' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="Password" rules={[{ required: true, min: 8, message: 'At least 8 characters' }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item name="marketingConsent" valuePropName="checked" initialValue={false}>
|
||||
<Checkbox>
|
||||
Send me occasional emails about new one-of-a-kind items. I can unsubscribe at any time.
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block loading={loading}>Create account</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Text type="secondary">
|
||||
Already have an account? <Link to="/login">Log in</Link>
|
||||
</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
By creating an account you agree to our <Link to="/privacy">Privacy Policy</Link>.
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,28 +3,32 @@ import Form from 'antd/es/form';
|
||||
import Input from 'antd/es/input';
|
||||
import Button from 'antd/es/button';
|
||||
import Typography from 'antd/es/typography';
|
||||
import Card from 'antd/es/card';
|
||||
import Modal from 'antd/es/modal';
|
||||
import Alert from 'antd/es/alert';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { resetPassword } from './customerApi';
|
||||
import { useCustomerAuth } from './CustomerAuthContext';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
export default function ResetPassword() {
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onRequestNewLink: () => void;
|
||||
onBackToSignIn: () => void;
|
||||
}
|
||||
|
||||
export default function ResetPassword({ onClose, onRequestNewLink, onBackToSignIn }: Props) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get('token');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { refresh } = useCustomerAuth();
|
||||
|
||||
// A link without a token can't do anything, so say so rather than showing a
|
||||
// form that is guaranteed to fail on submit.
|
||||
if (!token) {
|
||||
return (
|
||||
<Card style={{ maxWidth: 420, margin: '64px auto' }}>
|
||||
<Title level={3}>Reset your password</Title>
|
||||
<Modal title="Reset your password" open onCancel={onClose} footer={null} destroyOnHidden>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
@@ -32,9 +36,11 @@ export default function ResetPassword() {
|
||||
description="It's missing its reset token. Request a new link and use the most recent email."
|
||||
/>
|
||||
<Paragraph style={{ marginTop: 16 }}>
|
||||
<Link to="/forgot-password">Request a new link</Link>
|
||||
<Button type="link" style={{ paddingInline: 0 }} onClick={onRequestNewLink}>
|
||||
Request a new link
|
||||
</Button>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,9 +50,10 @@ export default function ResetPassword() {
|
||||
try {
|
||||
await resetPassword(token as string, values.password);
|
||||
// The server signs the customer in as part of the reset, so pick up the
|
||||
// new session before navigating.
|
||||
// new session before closing. Closing lands on the storefront: the link
|
||||
// came from an email, so there is no page behind to return to.
|
||||
refresh();
|
||||
navigate('/account', { replace: true });
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
@@ -55,8 +62,14 @@ export default function ResetPassword() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={{ maxWidth: 420, margin: '64px auto' }}>
|
||||
<Title level={3}>Choose a new password</Title>
|
||||
<Modal
|
||||
title="Choose a new password"
|
||||
open
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
style={{ maxWidth: 'calc(100vw - 32px)' }}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Paragraph type="secondary">
|
||||
Signing in elsewhere will end — you'll stay signed in on this device.
|
||||
</Paragraph>
|
||||
@@ -91,7 +104,9 @@ export default function ResetPassword() {
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Link to="/login">Back to sign in</Link>
|
||||
</Card>
|
||||
<Button type="link" style={{ paddingInline: 0 }} onClick={onBackToSignIn}>
|
||||
Back to sign in
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user