import { useState } from 'react'; import Form from 'antd/es/form'; import Input from 'antd/es/input'; import Button from 'antd/es/button'; import Alert from 'antd/es/alert'; import Collapse from 'antd/es/collapse'; import Typography from 'antd/es/typography'; import message from 'antd/es/message'; import { Customer, updateMyName, changeMyPassword, changeMyEmail } from './customerApi'; const { Title, Paragraph } = Typography; type Props = Readonly<{ customer: Customer; // Re-reads the session so the rest of the account view stops showing the old // name and the old verification state. onChanged: () => void; }>; // Changing an email address or a password has a consequence the customer cannot // see from the form: the new address needs verifying and the old one is told, // and a password change signs other devices out. Both are stated above the // fields rather than reported after the fact, so the surprise arrives while // there is still a chance to back out. export default function AccountDetails({ customer, onChanged }: Props) { const [nameError, setNameError] = useState(null); const [emailError, setEmailError] = useState(null); const [passwordError, setPasswordError] = useState(null); const [busy, setBusy] = useState<'name' | 'email' | 'password' | null>(null); const [passwordForm] = Form.useForm(); const [emailForm] = Form.useForm(); // A customer who signed up with Google has none, which changes the wording, // the button, and whether a current-password field exists at all (#344). const hasPassword = customer.has_password; async function saveName(values: { firstName: string; lastName: string }) { setBusy('name'); setNameError(null); try { await updateMyName(values.firstName, values.lastName); onChanged(); message.success('Name updated'); } catch (err) { setNameError((err as Error).message); } finally { setBusy(null); } } async function saveEmail(values: { emailPassword: string; newEmail: string }) { setBusy('email'); setEmailError(null); try { await changeMyEmail(values.emailPassword, values.newEmail); onChanged(); emailForm.resetFields(); message.success('Email changed. Check the new address for a verification link.'); } catch (err) { setEmailError((err as Error).message); } finally { setBusy(null); } } async function savePassword(values: { currentPassword?: string; newPassword: string }) { setBusy('password'); setPasswordError(null); try { await changeMyPassword(values.currentPassword ?? '', values.newPassword); // Clearing the fields matters more than anything else here, since they // hold both passwords. Setting a first one does refresh, because // has_password has just changed and this panel renders from it. passwordForm.resetFields(); if (hasPassword) { message.success('Password changed. Other devices have been signed out.'); } else { onChanged(); message.success('Password set. You can now sign in with it as well as with Google.'); } } catch (err) { setPasswordError((err as Error).message); } finally { setBusy(null); } } return (
Your details {nameError && }
{/* Collapsed by default. Both are rare, deliberate actions, and leaving them expanded would push order history and the account controls below the fold for everyone who never uses them. */} Your new address needs verifying before it can be used to sign in or reset your password. We will also tell {customer.email} that the address was changed. {emailError && ( )}
{/* Asked for because a live session alone is not enough to move the address a password reset would be sent to. */}
) }, { key: 'password', // Named for what it is for this customer. Offering to change a // password to somebody who signed up with Google and has never had // one is a dead end (#344). label: hasPassword ? 'Change your password' : 'Set a password', children: ( <> {hasPassword ? 'Signing in elsewhere will end. You will stay signed in on this device.' : 'You signed up without a password. Setting one gives you a second way in, alongside the accounts listed below.'} {passwordError && ( )}
{/* Absent, not disabled, for an account that has none. The server branches on the stored hash rather than on anything sent, so there is nothing for this field to carry. */} {hasPassword && ( )} ({ validator: (_, value) => !value || getFieldValue('newPassword') === value ? Promise.resolve() : Promise.reject(new Error('The passwords do not match')) }) ]} >
) } ]} />
); }