From 841ffc1c1fa52e2a1469325adfa5703973154c88 Mon Sep 17 00:00:00 2001 From: Alexander Saiannyi Date: Thu, 7 Mar 2024 21:38:12 +0100 Subject: [PATCH] feat(core): add components for change password --- .changeset/stale-tigers-cheat.md | 5 + .../_actions/submit-change-password-form.ts | 57 +++++++ .../_components/change-password-form.tsx | 152 ++++++++++++++++++ .../app/[locale]/(default)/login/page.tsx | 35 +++- .../mutations/submit-change-password.ts | 12 +- apps/core/messages/en.json | 12 ++ .../components/src/components/form/form.tsx | 20 +-- 7 files changed, 271 insertions(+), 22 deletions(-) create mode 100644 .changeset/stale-tigers-cheat.md create mode 100644 apps/core/app/[locale]/(default)/login/_actions/submit-change-password-form.ts create mode 100644 apps/core/app/[locale]/(default)/login/_components/change-password-form.tsx diff --git a/.changeset/stale-tigers-cheat.md b/.changeset/stale-tigers-cheat.md new file mode 100644 index 000000000..2531bf723 --- /dev/null +++ b/.changeset/stale-tigers-cheat.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": minor +--- + +add components for change password diff --git a/apps/core/app/[locale]/(default)/login/_actions/submit-change-password-form.ts b/apps/core/app/[locale]/(default)/login/_actions/submit-change-password-form.ts new file mode 100644 index 000000000..0ea9add08 --- /dev/null +++ b/apps/core/app/[locale]/(default)/login/_actions/submit-change-password-form.ts @@ -0,0 +1,57 @@ +'use server'; + +import { ZodError } from 'zod'; + +import { + ChangePasswordSchema, + submitChangePassword, +} from '~/client/mutations/submit-change-password'; + +export interface State { + status: 'idle' | 'error' | 'success'; + message?: string; +} + +export const submitChangePasswordForm = async (_previousState: unknown, formData: FormData) => { + try { + const parsedData = ChangePasswordSchema.parse({ + customerId: formData.get('customer-id'), + customerToken: formData.get('customer-token'), + newPassword: formData.get('new-password'), + confirmPassword: formData.get('confirm-password'), + }); + + const response = await submitChangePassword({ + newPassword: parsedData.newPassword, + token: parsedData.customerToken, + customerEntityId: Number(parsedData.customerId), + }); + + if (response.customer.resetPassword.errors.length === 0) { + return { status: 'success', message: '' }; + } + + return { + status: 'error', + message: response.customer.resetPassword.errors.map((error) => error.message).join('\n'), + }; + } catch (error: unknown) { + if (error instanceof ZodError) { + return { + status: 'error', + message: error.issues + .map(({ path, message }) => `${path.toString()}: ${message}.`) + .join('\n'), + }; + } + + if (error instanceof Error) { + return { + status: 'error', + message: error.message, + }; + } + + return { status: 'error', message: 'Unknown error' }; + } +}; 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 new file mode 100644 index 000000000..61977785d --- /dev/null +++ b/apps/core/app/[locale]/(default)/login/_components/change-password-form.tsx @@ -0,0 +1,152 @@ +'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 { useRouter } from '~/navigation'; + +import { submitChangePasswordForm } from '../_actions/submit-change-password-form'; + +interface Props { + customerId: number; + customerToken: string; +} + +const SubmitButton = () => { + const { pending } = useFormStatus(); + const t = useTranslations('Account.SubmitChangePassword'); + + return ( + + ); +}; + +export const ChangePasswordForm = ({ customerId, customerToken }: Props) => { + const form = useRef(null); + const router = useRouter(); + const [state, formAction] = useFormState(submitChangePasswordForm, { + status: 'idle', + message: '', + }); + + const [newPassword, setNewPasssword] = useState(''); + const [isConfirmPasswordValid, setIsConfirmPasswordValid] = useState(true); + + const t = useTranslations('Account.ChangePassword'); + let messageText = ''; + + if (state.status === 'error') { + messageText = state.message; + } + + if (state.status === 'success') { + messageText = t('successMessage'); + } + + const handleNewPasswordChange = (e: ChangeEvent) => + setNewPasssword(e.target.value); + const handleConfirmPasswordValidation = (e: ChangeEvent) => { + const confirmPassword = e.target.value; + + return setIsConfirmPasswordValid(confirmPassword === newPassword); + }; + + if (state.status === 'success') { + setTimeout(() => router.push('/login'), 2000); + } + + return ( + <> + {(state.status === 'error' || state.status === 'success') && ( + +

{messageText}

+
+ )} + +
+ + + + + + + + + + + + + {t('newPasswordLabel')} + + + + + + + + + {t('confirmPasswordLabel')} + + + + + 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 977a33203..9c06fffe0 100644 --- a/apps/core/app/[locale]/(default)/login/page.tsx +++ b/apps/core/app/[locale]/(default)/login/page.tsx @@ -1,9 +1,11 @@ import { Button } from '@bigcommerce/components/button'; -import { NextIntlClientProvider, useMessages, useTranslations } from 'next-intl'; +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages, getTranslations } from 'next-intl/server'; import { Link } from '~/components/link'; import { LocaleType } from '~/i18n'; +import { ChangePasswordForm } from './_components/change-password-form'; import { LoginForm } from './_components/login-form'; export const metadata = { @@ -14,17 +16,38 @@ interface Props { params: { locale: LocaleType; }; + searchParams: { + [key: string]: string | string[] | undefined; + action?: 'create_account' | 'reset_password' | 'change_password'; + c?: string; + t?: string; + }; } -export default function Login({ params: { locale } }: Props) { - const messages = useMessages(); - const t = useTranslations('Account.Login'); +export default async function Login({ params: { locale }, searchParams }: Props) { + const messages = await getMessages({ locale }); + const Account = messages.Account ?? {}; + const t = await getTranslations({ locale, namespace: 'Account.Login' }); + const action = searchParams.action; + const customerId = searchParams.c; + const customerToken = searchParams.t; + + if (action === 'change_password' && customerId && customerToken) { + return ( +
+

{t('changePasswordHeading')}

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

{t('heading')}

+

{t('heading')}

- +
diff --git a/apps/core/client/mutations/submit-change-password.ts b/apps/core/client/mutations/submit-change-password.ts index 66f268c95..68b3d8d5f 100644 --- a/apps/core/client/mutations/submit-change-password.ts +++ b/apps/core/client/mutations/submit-change-password.ts @@ -3,10 +3,14 @@ import { z } from 'zod'; import { client } from '..'; import { graphql } from '../graphql'; -export const ChangePasswordSchema = z.object({ - newPassword: z.string(), - confirmPassword: z.string(), -}); +export const ChangePasswordSchema = z + .object({ + customerId: z.string(), + customerToken: z.string(), + newPassword: z.string(), + confirmPassword: z.string(), + }) + .required(); interface SubmitChangePassword { newPassword: z.infer['newPassword']; diff --git a/apps/core/messages/en.json b/apps/core/messages/en.json index 3928b61dd..03dd1d2cc 100644 --- a/apps/core/messages/en.json +++ b/apps/core/messages/en.json @@ -159,6 +159,8 @@ }, "Login": { "heading": "Log In", + "resetPasswordHeading": "Reset password", + "changePasswordHeading": "Change password", "Form": { "errorMessage": "Your email address or password is incorrect. Try signing in again or reset your password", "emailLabel": "Email", @@ -179,6 +181,16 @@ "wishlists": "Save items to your Wish List", "createLink": "Create Account " } + }, + "ChangePassword": { + "newPasswordLabel": "New password", + "confirmPasswordLabel": "Confirm password", + "confirmPasswordValidationMessage": "Entered passwords are mismatched. Please try again.", + "successMessage": "Password has been updated successfully!" + }, + "SubmitChangePassword": { + "spinnerText": "Submitting...", + "submitText": "Change password" } }, "NotFound": { diff --git a/packages/components/src/components/form/form.tsx b/packages/components/src/components/form/form.tsx index 999eb953d..95b37228c 100644 --- a/packages/components/src/components/form/form.tsx +++ b/packages/components/src/components/form/form.tsx @@ -45,18 +45,14 @@ const Field = forwardRef< Field.displayName = 'Field'; -interface FieldMessageProps - extends Omit, 'match'> { - match?: ValidationPattern; -} - -const FieldMessage = forwardRef, FieldMessageProps>( - ({ className, children, ...props }, ref) => ( - - {children} - - ), -); +const FieldMessage = forwardRef< + ElementRef, + ComponentPropsWithRef +>(({ className, children, ...props }, ref) => ( + + {children} + +)); FieldMessage.displayName = 'FieldMessage';