Skip to content

Commit

Permalink
feat(auth): add reset password functionality (#98)
Browse files Browse the repository at this point in the history
* feat(auth): add reset password functionality

* Fixes
  • Loading branch information
domhhv authored Oct 12, 2024
1 parent 5ff7bec commit aceaeb9
Show file tree
Hide file tree
Showing 33 changed files with 381 additions and 162 deletions.
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
SUPABASE_URL=https://<your_supabase_url>.supabase.co
SUPABASE_ANON_KEY=<your_supabase_anon_key>
# These are received when running `yarn db:start` or `yarn db:status`
# and are only required for running the local Supabase instance.
# Habitrack UI is still runnable without these values.
SUPABASE_URL=https://<your-supabase-url>.supabase.co
SUPABASE_ANON_KEY=<your-supabase-anon_key>
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ supabase/.branches/
supabase/.temp/

# local env files
.env
.env.production
.env.development
.env.test
.env.production

# IDE
.idea
Expand Down
2 changes: 2 additions & 0 deletions globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare let SUPABASE_URL: string;
declare let SUPABASE_ANON_KEY: string;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"prettier": "3.1.1",
"prettier-plugin-tailwindcss": "^0.6.6",
"rollup-plugin-visualizer": "^5.12.0",
"supabase": "1.204.3",
"supabase": "1.206.0",
"tailwindcss": "^3.4.10",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3",
Expand Down
71 changes: 71 additions & 0 deletions src/components/common/PasswordInput/PasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Button, Input } from '@nextui-org/react';
import { Eye, EyeSlash } from '@phosphor-icons/react';
import React, { type ChangeEventHandler } from 'react';

type PasswordInputProps = {
variant?: 'flat' | 'bordered' | 'faded' | 'underlined';
value: string;
onChange: ChangeEventHandler<HTMLInputElement>;
label: string;
isDisabled: boolean;
onReset?: () => void;
testId?: string;
};

const PasswordInput = ({
variant = 'flat',
value,
onChange,
label,
isDisabled,
onReset,
testId = '',
}: PasswordInputProps) => {
const [isVisible, setIsVisible] = React.useState(false);

const toggleVisibility = () => {
setIsVisible((prev) => !prev);
};

return (
<Input
classNames={{
description: 'text-right',
}}
description={
onReset ? (
<Button
className="h-auto bg-transparent p-0 text-gray-400 hover:text-gray-700"
onClick={onReset}
disableAnimation
>
Forgot password?
</Button>
) : null
}
variant={variant}
value={value}
onChange={onChange}
label={label}
isDisabled={isDisabled}
type={isVisible ? 'text' : 'password'}
endContent={
<button
className="focus:outline-none"
type="button"
onClick={toggleVisibility}
aria-label="toggle password visibility"
>
{isVisible ? (
<EyeSlash className="pointer-events-none text-2xl text-default-400" />
) : (
<Eye className="pointer-events-none text-2xl text-default-400" />
)}
</button>
}
data-testid={testId}
/>
);
};

export default PasswordInput;
2 changes: 2 additions & 0 deletions src/components/common/PasswordInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './PasswordInput';
export { default as PasswordInput } from './PasswordInput';
1 change: 1 addition & 0 deletions src/components/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ConfirmDialog';
export * from './VisuallyHiddenInput';
export * from './PasswordInput';
4 changes: 2 additions & 2 deletions src/components/user-account/AccountPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ jest.mock('@supabase/auth-helpers-react', () => ({
useUser: jest.fn().mockReturnValue({ id: '123' }),
}));
jest.mock('@services');
jest.mock('./use-email-confirmed', () => ({
jest.mock('./use-auth-search-params', () => ({
__esModule: true,
useEmailConfirmed: jest.fn(),
useAuthSearchParams: jest.fn(),
}));

import { SnackbarProvider, UserAccountProvider } from '@context';
Expand Down
15 changes: 8 additions & 7 deletions src/components/user-account/AccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import clsx from 'clsx';
import React, { type FormEventHandler } from 'react';
import { twMerge } from 'tailwind-merge';

import PasswordInput from '../common/PasswordInput/PasswordInput';

import { useAccountPage } from './use-account-page';
import { useEmailConfirmed } from './use-email-confirmed';
import { useAuthSearchParams } from './use-auth-search-params';

const AccountPage = () => {
useEmailConfirmed();
useAuthSearchParams();

useDocumentTitle('My Account | Habitrack');

Expand Down Expand Up @@ -82,14 +84,13 @@ const AccountPage = () => {
/>
</div>
<div>
<Input
<PasswordInput
variant="bordered"
type="password"
value={password}
onChange={handlePasswordChange}
isDisabled={loading}
label="Set new password"
data-testid="password-input"
isDisabled={loading}
testId="password-input"
/>
</div>
<div>
Expand All @@ -109,7 +110,7 @@ const AccountPage = () => {
isLoading={loading}
color="primary"
>
Save
Submit
</Button>
</div>
</div>
Expand Down
14 changes: 11 additions & 3 deletions src/components/user-account/AuthForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import AuthForm from './AuthForm';

describe(AuthForm.name, () => {
it('should call onSubmit with username and password', async () => {
it('should call onSubmit with email and password', async () => {
const onSubmit = jest.fn();
const onCancel = jest.fn();
const disabled = false;
Expand All @@ -18,6 +18,8 @@ describe(AuthForm.name, () => {
onCancel={onCancel}
disabled={disabled}
submitButtonLabel={submitButtonLabel}
onModeChange={() => {}}
goBackToLogin={() => {}}
/>
</SnackbarProvider>
);
Expand Down Expand Up @@ -50,6 +52,8 @@ describe(AuthForm.name, () => {
onCancel={onCancel}
disabled={disabled}
submitButtonLabel={submitButtonLabel}
onModeChange={() => {}}
goBackToLogin={() => {}}
/>
</SnackbarProvider>
);
Expand Down Expand Up @@ -79,6 +83,8 @@ describe(AuthForm.name, () => {
onCancel={onCancel}
disabled={disabled}
submitButtonLabel={submitButtonLabel}
onModeChange={() => {}}
goBackToLogin={() => {}}
/>
</SnackbarProvider>
);
Expand All @@ -91,7 +97,7 @@ describe(AuthForm.name, () => {

await waitFor(() => {
expect(getByTestId('snackbar')).toBeDefined();
expect(getByTestId('snackbar').querySelector('p')?.innerHTML).toContain(
expect(getByTestId('snackbar-message')).toHaveTextContent(
'My error message'
);
});
Expand All @@ -111,6 +117,8 @@ describe(AuthForm.name, () => {
onCancel={onCancel}
disabled={disabled}
submitButtonLabel={submitButtonLabel}
onModeChange={() => {}}
goBackToLogin={() => {}}
/>
</SnackbarProvider>
);
Expand All @@ -123,7 +131,7 @@ describe(AuthForm.name, () => {

await waitFor(() => {
expect(getByTestId('snackbar')).toBeDefined();
expect(getByTestId('snackbar').querySelector('p')?.innerHTML).toContain(
expect(getByTestId('snackbar-message')).toHaveTextContent(
'Something went wrong'
);
});
Expand Down
58 changes: 48 additions & 10 deletions src/components/user-account/AuthForm.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { PasswordInput, type AuthMode } from '@components';
import { useSnackbar } from '@context';
import { useTextField } from '@hooks';
import { Input, Button } from '@nextui-org/react';
import clsx from 'clsx';
import React from 'react';

type AuthFormProps = {
onSubmit: (username: string, password: string, name: string) => void;
onSubmit: (email: string, password: string, name: string) => void;
onCancel: () => void;
disabled: boolean;
submitButtonLabel: string;
mode: 'login' | 'register';
mode: AuthMode;
onModeChange: (mode: AuthMode) => void;
goBackToLogin: () => void;
};

const AuthForm = ({
Expand All @@ -17,6 +21,8 @@ const AuthForm = ({
onCancel,
disabled,
mode,
onModeChange,
goBackToLogin,
}: AuthFormProps) => {
const [email, handleEmailChange, clearEmail] = useTextField();
const [name, handleNameChange, clearName] = useTextField();
Expand Down Expand Up @@ -46,15 +52,41 @@ const AuthForm = ({
onCancel();
};

const formClassName = clsx(mode === 'reset-password' && 'py-3');

return (
<form onSubmit={handleSubmit} data-testid="submit-form">
<form
onSubmit={handleSubmit}
data-testid="submit-form"
className={formClassName}
>
<div className="flex flex-col gap-4">
{mode === 'reset-password' && (
<p>
Enter your email address below and we will send you a link to reset
your password.
</p>
)}
<Input
value={email}
onChange={handleEmailChange}
type="email"
label="Email"
isDisabled={disabled}
classNames={{
description: 'text-right',
}}
description={
mode === 'reset-password' && (
<Button
className="h-auto bg-transparent p-0 text-gray-400 hover:text-gray-700"
onClick={goBackToLogin}
disableAnimation
>
Back to login
</Button>
)
}
/>
{mode === 'register' && (
<Input
Expand All @@ -64,13 +96,19 @@ const AuthForm = ({
isDisabled={disabled}
/>
)}
<Input
value={password}
onChange={handlePasswordChange}
label="Password"
type="password"
isDisabled={disabled}
/>
{['login', 'register'].includes(mode) && (
<PasswordInput
value={password}
onChange={handlePasswordChange}
label="Password"
isDisabled={disabled}
onReset={
mode === 'login'
? () => onModeChange('reset-password')
: undefined
}
/>
)}
</div>
<div className="mt-4 flex justify-end gap-2">
<Button onClick={handleCancel} isDisabled={disabled} variant="flat">
Expand Down
8 changes: 4 additions & 4 deletions src/components/user-account/AuthModalButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe(AuthModalButton.name, () => {
act(() => {
fireEvent.click(button);
});
const modal = getByText('Log in with a username and a password');
const modal = getByText('Log in with a email and a password');
expect(modal).toBeDefined();
});

Expand All @@ -58,7 +58,7 @@ describe(AuthModalButton.name, () => {
act(() => {
fireEvent.click(button);
});
const modal = queryByText('Log in with a username and a password');
const modal = queryByText('Log in with a email and a password');
expect(modal).toBeNull();
});

Expand All @@ -79,7 +79,7 @@ describe(AuthModalButton.name, () => {
act(() => {
fireEvent.click(registerTab);
});
const modal = getByText('Register with a username and a password');
const modal = getByText('Register with a email and a password');
expect(modal).toBeDefined();
});

Expand All @@ -98,7 +98,7 @@ describe(AuthModalButton.name, () => {
fireEvent.click(cancel);
});
await waitFor(() => {
const modal = queryByText('Log in with a username and a password');
const modal = queryByText('Log in with a email and a password');
expect(modal).toBeNull();
});
});
Expand Down
Loading

0 comments on commit aceaeb9

Please sign in to comment.