Skip to content

Commit

Permalink
feat(core): add components for reset password
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-alexsaiannyi committed Mar 13, 2024
1 parent 569a42a commit 4b8b462
Show file tree
Hide file tree
Showing 8 changed files with 500 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use server';

import { z } from 'zod';

import {
ChangePasswordSchema,
submitChangePassword,
} from '~/client/mutations/submit-change-password';

interface SubmitChangePasswordForm {
formData: FormData;
customerId: number;
customerToken: string;
}

export const submitChangePasswordForm = async ({
formData,
customerId,
customerToken,
}: SubmitChangePasswordForm) => {
try {
const parsedData = ChangePasswordSchema.parse({
newPassword: formData.get('new-password'),
confirmPassword: formData.get('confirm-password'),
});

const response = await submitChangePassword({
newPassword: parsedData.newPassword,
token: customerToken,
customerEntityId: customerId,
});

if (response.customer.resetPassword.errors.length === 0) {
return { status: 'success', data: parsedData };
}

return {
status: 'failed',
error: response.customer.resetPassword.errors.map((error) => error.message).join('\n'),
};
} catch (error: unknown) {
if (error instanceof Error || error instanceof z.ZodError) {
return { status: 'failed', error: error.message };
}

return { status: 'failed', error: 'Unknown error' };
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use server';

import { z } from 'zod';

import { ResetPasswordSchema, submitResetPassword } from '~/client/mutations/submit-reset-password';

interface SubmitRessetPasswordForm {
formData: FormData;
reCaptchaToken: string;
}

export const submitResetPasswordForm = async ({
formData,
reCaptchaToken,
}: SubmitRessetPasswordForm) => {
try {
const parsedData = ResetPasswordSchema.parse({
email: formData.get('email'),
});

const response = await submitResetPassword({
email: parsedData.email,
reCaptchaToken,
});

if (response.customer.requestResetPassword.errors.length === 0) {
return { status: 'success', data: parsedData };
}

return {
status: 'failed',
error: response.customer.requestResetPassword.errors.map((error) => error.message).join('\n'),
};
} catch (error: unknown) {
if (error instanceof Error || error instanceof z.ZodError) {
return { status: 'failed', error: error.message };
}

return { status: 'failed', error: 'Unknown error' };
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
'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 { useFormStatus } from 'react-dom';

import { useRouter } from '~/navigation';

import { submitChangePasswordForm } from '../_actions/submit-change-password-form';

interface Props {
customerId: number;
customerToken: string;
}

interface FormStatus {
status: 'success' | 'error';
message: string;
}

const SubmitButton = () => {
const { pending } = useFormStatus();
const t = useTranslations('Account.SubmitChangePassword');

return (
<Button
className="relative w-fit items-center px-8 py-2"
data-button
disabled={pending}
variant="primary"
>
<>
{pending && (
<>
<span className="absolute z-10 flex h-full w-full items-center justify-center bg-gray-400">
<Spinner aria-hidden="true" className="animate-spin" />
</span>
<span className="sr-only">{t('spinnerText')}</span>
</>
)}
<span aria-hidden={pending}>{t('submitText')}</span>
</>
</Button>
);
};

export const ChangePasswordForm = ({ customerId, customerToken }: Props) => {
const form = useRef<HTMLFormElement>(null);
const router = useRouter();
const [formStatus, setFormStatus] = useState<FormStatus | null>(null);
const [newPassword, setNewPasssword] = useState('');
const [isNewPasswordValid, setIsNewPasswordValid] = useState(true);
const [isConfirmPasswordValid, setIsConfirmPasswordValid] = useState(true);

const t = useTranslations('Account.ChangePassword');

const handleNewPasswordValidation = (e: ChangeEvent<HTMLInputElement>) => {
const validationStatus = e.target.validity.patternMismatch;

setNewPasssword(e.target.value);

return setIsNewPasswordValid(!validationStatus);
};

const handleConfirmPasswordValidation = (e: ChangeEvent<HTMLInputElement>) => {
const confirmPassword = e.target.value;

return setIsConfirmPasswordValid(confirmPassword === newPassword);
};

const onSubmit = async (formData: FormData) => {
const submit = await submitChangePasswordForm({ formData, customerId, customerToken });

if (submit.status === 'success') {
form.current?.reset();
setFormStatus({
status: 'success',
message: t('successMessage'),
});

setTimeout(() => router.push('/login'), 3000);
}

if (submit.status === 'failed') {
setFormStatus({ status: 'error', message: submit.error ?? '' });
}
};

return (
<>
{formStatus && (
<Message className="mb-8 w-full text-gray-500" variant={formStatus.status}>
<p>{formStatus.message}</p>
</Message>
)}

<p className="mb-4 text-base">{t('description')}</p>

<Form action={onSubmit} className="mb-14 flex flex-col gap-4 md:py-4 lg:p-0" ref={form}>
<Field className="relative space-y-2 pb-7" name="new-password">
<FieldLabel htmlFor="new-password" isRequired={true}>
{t('newPasswordLabel')}
</FieldLabel>
<FieldControl asChild>
<Input
autoComplete="none"
id="new-password"
onChange={handleNewPasswordValidation}
onInvalid={handleNewPasswordValidation}
pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{7,}$"
required
type="password"
variant={!isNewPasswordValid ? 'error' : undefined}
/>
</FieldControl>
<FieldMessage
className="absolute inset-x-0 bottom-0 inline-flex w-full text-sm text-gray-500"
match="patternMismatch"
>
{t('newPasswordValidationMessage')}
</FieldMessage>
</Field>

<Field className="relative space-y-2 pb-7" name="confirm-password">
<FieldLabel htmlFor="confirm-password" isRequired={true}>
{t('confirmPasswordLabel')}
</FieldLabel>
<FieldControl asChild>
<Input
autoComplete="none"
id="confirm-password"
onChange={handleConfirmPasswordValidation}
onInvalid={handleConfirmPasswordValidation}
required
type="password"
variant={!isConfirmPasswordValid ? 'error' : undefined}
/>
</FieldControl>
<FieldMessage
className="text-red-200 absolute inset-x-0 bottom-0 inline-flex w-full text-sm"
match={(value: string) => value !== newPassword}
>
{t('confirmPasswordValidationMessage')}
</FieldMessage>
</Field>

<FormSubmit asChild>
<SubmitButton />
</FormSubmit>
</Form>
</>
);
};
Loading

0 comments on commit 4b8b462

Please sign in to comment.