From 7b08e8eb8ecac3dc2dcbc92206848ee42a2b08e0 Mon Sep 17 00:00:00 2001 From: Alexander Saiannyi Date: Fri, 29 Mar 2024 12:13:36 +0100 Subject: [PATCH] refactoring change password form --- .../submit-customer-change-password-form.ts | 10 +- .../_components/change-password-form.tsx | 256 ++++++++++++++++++ .../account/change-password/page.tsx | 26 ++ .../app/[locale]/(default)/account/page.tsx | 23 +- .../_components/change-password-form.tsx | 144 ++-------- .../app/[locale]/(default)/login/page.tsx | 6 +- .../mutations/submit-change-password.ts | 26 +- .../submit-customer-change-password.ts | 20 +- 8 files changed, 335 insertions(+), 176 deletions(-) rename apps/core/app/[locale]/(default)/{login => account/change-password}/_actions/submit-customer-change-password-form.ts (82%) create mode 100644 apps/core/app/[locale]/(default)/account/change-password/_components/change-password-form.tsx create mode 100644 apps/core/app/[locale]/(default)/account/change-password/page.tsx diff --git a/apps/core/app/[locale]/(default)/login/_actions/submit-customer-change-password-form.ts b/apps/core/app/[locale]/(default)/account/change-password/_actions/submit-customer-change-password-form.ts similarity index 82% rename from apps/core/app/[locale]/(default)/login/_actions/submit-customer-change-password-form.ts rename to apps/core/app/[locale]/(default)/account/change-password/_actions/submit-customer-change-password-form.ts index f1e04f6d9..76e8a87b7 100644 --- a/apps/core/app/[locale]/(default)/login/_actions/submit-customer-change-password-form.ts +++ b/apps/core/app/[locale]/(default)/account/change-password/_actions/submit-customer-change-password-form.ts @@ -1,11 +1,9 @@ 'use server'; -import { ZodError } from 'zod'; +import { z } from 'zod'; -import { - CustomerChangePasswordSchema, - submitCustomerChangePassword, -} from '~/client/mutations/submit-customer-change-password'; +import { CustomerChangePasswordSchema } from '~/client/mutations/submit-change-password'; +import { submitCustomerChangePassword } from '~/client/mutations/submit-customer-change-password'; export interface State { status: 'idle' | 'error' | 'success'; @@ -37,7 +35,7 @@ export const submitCustomerChangePasswordForm = async ( message: response.errors.map((error) => error.message).join('\n'), }; } catch (error: unknown) { - if (error instanceof ZodError) { + if (error instanceof z.ZodError) { return { status: 'error', message: error.issues diff --git a/apps/core/app/[locale]/(default)/account/change-password/_components/change-password-form.tsx b/apps/core/app/[locale]/(default)/account/change-password/_components/change-password-form.tsx new file mode 100644 index 000000000..17ef6a7fa --- /dev/null +++ b/apps/core/app/[locale]/(default)/account/change-password/_components/change-password-form.tsx @@ -0,0 +1,256 @@ +'use client'; + +import { Button } from '@bigcommerce/components/button'; +import { + Field, + FieldControl, + FieldLabel, + FieldMessage, + Form, + FormSubmit, +} from '@bigcommerce/components/form'; +import { Input } from '@bigcommerce/components/input'; +import { Message } from '@bigcommerce/components/message'; +import { Loader2 as Spinner } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { ChangeEvent, useRef, useState } from 'react'; +import { useFormState, useFormStatus } from 'react-dom'; +import { z } from 'zod'; + +import { CustomerChangePasswordSchema } from '~/client/mutations/submit-change-password'; +import { logout } from '~/components/header/_actions/logout'; +import { useRouter } from '~/navigation'; + +import { submitCustomerChangePasswordForm } from '../_actions/submit-customer-change-password-form'; + +type Passwords = z.infer; + +const validateAgainstConfirmPassword = ({ + newPassword, + confirmPassword, +}: { + newPassword: Passwords['newPassword']; + confirmPassword: Passwords['confirmPassword']; +}): boolean => newPassword === confirmPassword; + +const validateAgainstCurrentPassword = ({ + newPassword, + currentPassword, +}: { + newPassword: Passwords['newPassword']; + currentPassword: Passwords['currentPassword']; +}): boolean => newPassword !== currentPassword; + +export const validatePasswords = ( + validationField: 'new-password' | 'confirm-password', + formData?: FormData, +) => { + if (!formData) { + return false; + } + + if (validationField === 'new-password') { + return CustomerChangePasswordSchema.omit({ confirmPassword: true }) + .refine(validateAgainstCurrentPassword) + .safeParse({ + currentPassword: formData.get('current-password'), + newPassword: formData.get('new-password'), + }).success; + } + + return CustomerChangePasswordSchema.refine(validateAgainstConfirmPassword).safeParse({ + currentPassword: formData.get('current-password'), + newPassword: formData.get('new-password'), + confirmPassword: formData.get('confirm-password'), + }).success; +}; + +const SubmitButton = () => { + const { pending } = useFormStatus(); + const t = useTranslations('Account.SubmitChangePassword'); + + return ( + + ); +}; + +export const ChangePasswordForm = () => { + const router = useRouter(); + const form = useRef(null); + const t = useTranslations('Account.ChangePassword'); + const [state, formAction] = useFormState(submitCustomerChangePasswordForm, { + status: 'idle', + message: '', + }); + + const [isCurrentPasswordValid, setIsCurrentPasswordValid] = useState(true); + const [isNewPasswordValid, setIsNewPasswordValid] = useState(true); + const [isConfirmPasswordValid, setIsConfirmPasswordValid] = useState(true); + + let messageText = ''; + + if (state.status === 'error') { + messageText = state.message; + } + + if (state.status === 'success') { + messageText = t('successMessage'); + } + + const handleCurrentPasswordChange = (e: ChangeEvent) => + setIsCurrentPasswordValid(!e.target.validity.valueMissing); + const handleNewPasswordChange = (e: ChangeEvent) => { + let formData; + + if (e.target.form) { + formData = new FormData(e.target.form); + } + + const isValid = validatePasswords('new-password', formData); + + setIsNewPasswordValid(isValid); + }; + const handleConfirmPasswordValidation = (e: ChangeEvent) => { + let formData; + + if (e.target.form) { + formData = new FormData(e.target.form); + } + + const isValid = validatePasswords('confirm-password', formData); + + setIsConfirmPasswordValid(isValid); + }; + + if (state.status === 'success') { + setTimeout(() => { + void logout(); + router.push('/login'); + }, 2000); + } + + return ( + <> + {(state.status === 'error' || state.status === 'success') && ( + +

{messageText}

+
+ )} + +
+ + + {t('currentPasswordLabel')} + + + + + + {t('notEmptyMessage')} + + + + + {t('newPasswordLabel')} + + + + + + {t('notEmptyMessage')} + + { + const currentPasswordValue = formData.get('current-password'); + const isMatched = currentPasswordValue === newPasswordValue; + + setIsNewPasswordValid(!isMatched); + + return isMatched; + }} + > + {t('newPasswordValidationMessage')} + + + + + {t('confirmPasswordLabel')} + + + + + + {t('notEmptyMessage')} + + { + const newPassword = formData.get('new-password'); + const isMatched = confirmPassword === newPassword; + + setIsConfirmPasswordValid(isMatched); + + return !isMatched; + }} + > + {t('confirmPasswordValidationMessage')} + + + + + +
+ + ); +}; diff --git a/apps/core/app/[locale]/(default)/account/change-password/page.tsx b/apps/core/app/[locale]/(default)/account/change-password/page.tsx new file mode 100644 index 000000000..db4586f6c --- /dev/null +++ b/apps/core/app/[locale]/(default)/account/change-password/page.tsx @@ -0,0 +1,26 @@ +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages, getTranslations } from 'next-intl/server'; + +import { LocaleType } from '~/i18n'; + +import { ChangePasswordForm } from './_components/change-password-form'; + +interface Props { + params: { + locale: LocaleType; + }; +} + +export default async function ChangePasswordPage({ params: { locale } }: Props) { + const messages = await getMessages({ locale }); + const t = await getTranslations({ locale, namespace: 'Account.Home' }); + + return ( +
+

{t('changePassword')}

+ + + +
+ ); +} diff --git a/apps/core/app/[locale]/(default)/account/page.tsx b/apps/core/app/[locale]/(default)/account/page.tsx index 266ea89c6..f7260d170 100644 --- a/apps/core/app/[locale]/(default)/account/page.tsx +++ b/apps/core/app/[locale]/(default)/account/page.tsx @@ -1,15 +1,12 @@ import { BookUser, Eye, Gift, Mail, Package, Settings } from 'lucide-react'; import { redirect } from 'next/navigation'; -import { NextIntlClientProvider } from 'next-intl'; -import { getMessages, getTranslations } from 'next-intl/server'; +import { getTranslations } from 'next-intl/server'; import { ReactNode } from 'react'; import { auth } from '~/auth'; import { Link } from '~/components/link'; import { LocaleType } from '~/i18n'; -import { ChangePasswordForm } from '../login/_components/change-password-form'; - interface AccountItem { children: ReactNode; description?: string; @@ -36,32 +33,16 @@ interface Props { params: { locale: LocaleType; }; - searchParams: { - action?: 'reset_password'; - }; } -export default async function AccountPage({ params: { locale }, searchParams: { action } }: Props) { +export default async function AccountPage({ params: { locale } }: Props) { const session = await auth(); const t = await getTranslations({ locale, namespace: 'Account.Home' }); - const messages = await getMessages({ locale }); - const Account = messages.Account ?? {}; if (!session) { redirect('/login'); } - if (action === 'reset_password') { - return ( -
-

{t('changePassword')}

- - - -
- ); - } - return (

{t('heading')}

diff --git a/apps/core/app/[locale]/(default)/login/_components/change-password-form.tsx b/apps/core/app/[locale]/(default)/login/_components/change-password-form.tsx index 15ce118a8..61977785d 100644 --- a/apps/core/app/[locale]/(default)/login/_components/change-password-form.tsx +++ b/apps/core/app/[locale]/(default)/login/_components/change-password-form.tsx @@ -16,16 +16,13 @@ import { useTranslations } from 'next-intl'; import { ChangeEvent, useRef, useState } from 'react'; import { useFormState, useFormStatus } from 'react-dom'; -import { logout } from '~/components/header/_actions/logout'; import { useRouter } from '~/navigation'; import { submitChangePasswordForm } from '../_actions/submit-change-password-form'; -import { submitCustomerChangePasswordForm } from '../_actions/submit-customer-change-password-form'; interface Props { - customerId?: number; - customerToken?: string; - isLoggedIn: boolean; + customerId: number; + customerToken: string; } const SubmitButton = () => { @@ -54,20 +51,18 @@ const SubmitButton = () => { ); }; -export const ChangePasswordForm = ({ customerId, customerToken, isLoggedIn }: Props) => { +export const ChangePasswordForm = ({ customerId, customerToken }: Props) => { const form = useRef(null); - const t = useTranslations('Account.ChangePassword'); const router = useRouter(); - const submitFormAction = isLoggedIn ? submitCustomerChangePasswordForm : submitChangePasswordForm; - const [state, formAction] = useFormState(submitFormAction, { + const [state, formAction] = useFormState(submitChangePasswordForm, { status: 'idle', message: '', }); - const [isCurrentPasswordValid, setIsCurrentPasswordValid] = useState(true); - const [isNewPasswordValid, setIsNewPasswordValid] = useState(true); + const [newPassword, setNewPasssword] = useState(''); const [isConfirmPasswordValid, setIsConfirmPasswordValid] = useState(true); + const t = useTranslations('Account.ChangePassword'); let messageText = ''; if (state.status === 'error') { @@ -78,46 +73,18 @@ export const ChangePasswordForm = ({ customerId, customerToken, isLoggedIn }: Pr messageText = t('successMessage'); } - const handleCurrentPasswordChange = (e: ChangeEvent) => - setIsCurrentPasswordValid(!e.target.validity.valueMissing); - const handleNewPasswordChange = (e: ChangeEvent) => { - let currentPasswordValue: FormDataEntryValue | null = null; - let isValid = true; - const newPasswordValue = e.target.value; - - if (e.target.form) { - currentPasswordValue = new FormData(e.target.form).get('current-password'); - } - - if (isLoggedIn) { - isValid = !e.target.validity.valueMissing && newPasswordValue !== currentPasswordValue; - } else { - isValid = !e.target.validity.valueMissing; - } - - setIsNewPasswordValid(isValid); - }; + const handleNewPasswordChange = (e: ChangeEvent) => + setNewPasssword(e.target.value); const handleConfirmPasswordValidation = (e: ChangeEvent) => { - let newPasswordValue: FormDataEntryValue | null = null; - const confirmPasswordValue = e.target.value; + const confirmPassword = e.target.value; - if (e.target.form) { - newPasswordValue = new FormData(e.target.form).get('new-password'); - } - - setIsConfirmPasswordValid( - confirmPasswordValue.length > 0 && newPasswordValue === confirmPasswordValue, - ); + return setIsConfirmPasswordValid(confirmPassword === newPassword); }; - if (state.status === 'success' && !isLoggedIn) { + if (state.status === 'success') { setTimeout(() => router.push('/login'), 2000); } - if (state.status === 'success' && isLoggedIn) { - void logout(); - } - return ( <> {(state.status === 'error' || state.status === 'success') && ( @@ -127,44 +94,16 @@ export const ChangePasswordForm = ({ customerId, customerToken, isLoggedIn }: Pr )}
- {Boolean(customerId) && ( - - - - - - )} - {Boolean(customerToken) && ( - - - - - - )} - {isLoggedIn && ( - - - {t('currentPasswordLabel')} - - - - - - {t('notEmptyMessage')} - - - )} + + + + + + + + + + {t('newPasswordLabel')} @@ -174,34 +113,13 @@ export const ChangePasswordForm = ({ customerId, customerToken, isLoggedIn }: Pr autoComplete="none" id="new-password" onChange={handleNewPasswordChange} - onInvalid={handleNewPasswordChange} required type="password" - variant={!isNewPasswordValid || state.status === 'error' ? 'error' : undefined} + variant={state.status === 'error' ? 'error' : undefined} /> - - {t('notEmptyMessage')} - - {isLoggedIn && ( - { - const currentPasswordValue = formData.get('current-password'); - const isMatched = currentPasswordValue === newPasswordValue; - - setIsNewPasswordValid(!isMatched); - - return isMatched; - }} - > - {t('newPasswordValidationMessage')} - - )} + {t('confirmPasswordLabel')} @@ -219,24 +137,12 @@ export const ChangePasswordForm = ({ customerId, customerToken, isLoggedIn }: Pr - {t('notEmptyMessage')} - - { - const newPasswordValue = formData.get('new-password'); - const isMatched = confirmPasswordValue === newPasswordValue; - - setIsConfirmPasswordValid(isMatched); - - return !isMatched; - }} + match={(value: string) => value !== newPassword} > {t('confirmPasswordValidationMessage')} + diff --git a/apps/core/app/[locale]/(default)/login/page.tsx b/apps/core/app/[locale]/(default)/login/page.tsx index d389ba885..6d260fc70 100644 --- a/apps/core/app/[locale]/(default)/login/page.tsx +++ b/apps/core/app/[locale]/(default)/login/page.tsx @@ -39,11 +39,7 @@ export default async function Login({ params: { locale }, searchParams }: Props)

{t('changePasswordHeading')}

- +
); diff --git a/apps/core/client/mutations/submit-change-password.ts b/apps/core/client/mutations/submit-change-password.ts index 68b3d8d5f..f8d93b770 100644 --- a/apps/core/client/mutations/submit-change-password.ts +++ b/apps/core/client/mutations/submit-change-password.ts @@ -3,17 +3,25 @@ import { z } from 'zod'; import { client } from '..'; import { graphql } from '../graphql'; -export const ChangePasswordSchema = z - .object({ - customerId: z.string(), - customerToken: z.string(), - newPassword: z.string(), - confirmPassword: z.string(), - }) - .required(); +const ChangePasswordFieldsSchema = z.object({ + customerId: z.string(), + customerToken: z.string(), + currentPassword: z.string().min(1), + newPassword: z.string().min(1), + confirmPassword: z.string().min(1), +}); + +export const CustomerChangePasswordSchema = ChangePasswordFieldsSchema.omit({ + customerId: true, + customerToken: true, +}); + +export const ChangePasswordSchema = ChangePasswordFieldsSchema.omit({ + currentPassword: true, +}).required(); interface SubmitChangePassword { - newPassword: z.infer['newPassword']; + newPassword: z.infer['newPassword']; token: string; customerEntityId: number; } diff --git a/apps/core/client/mutations/submit-customer-change-password.ts b/apps/core/client/mutations/submit-customer-change-password.ts index ddbdd1b20..8a20100e2 100644 --- a/apps/core/client/mutations/submit-customer-change-password.ts +++ b/apps/core/client/mutations/submit-customer-change-password.ts @@ -1,21 +1,7 @@ -import { z } from 'zod'; - import { getSessionCustomerId } from '~/auth'; import { client } from '..'; -import { graphql } from '../graphql'; - -export const CustomerChangePasswordSchema = z - .object({ - currentPassword: z.string(), - newPassword: z.string(), - confirmPassword: z.string(), - }) - .required(); - -const Input = CustomerChangePasswordSchema.omit({ confirmPassword: true }); - -type SubmitCustomerChangePassword = z.infer; +import { graphql, VariablesOf } from '../graphql'; const SUBMIT_CUSTOMER_CHANGE_PASSWORD_MUTATION = graphql(` mutation CustomerChangePassword($input: ChangePasswordInput!) { @@ -41,10 +27,12 @@ const SUBMIT_CUSTOMER_CHANGE_PASSWORD_MUTATION = graphql(` } `); +type Variables = VariablesOf; + export const submitCustomerChangePassword = async ({ currentPassword, newPassword, -}: SubmitCustomerChangePassword) => { +}: Variables['input']) => { const customerId = await getSessionCustomerId(); const variables = { input: {