Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
597216a
feat: add password pattern constants and types
sricharan-varanasi Apr 9, 2026
74ab16e
feat: add checkPassword utility and password constants
sricharan-varanasi Apr 9, 2026
bf968de
test: add unit tests for checkPassword utility
sricharan-varanasi Apr 9, 2026
778998d
feat: add i18n translation keys for password requirements
sricharan-varanasi Apr 9, 2026
91df53f
feat: create PasswordRequirementsTooltip component
sricharan-varanasi Apr 9, 2026
ca4aab7
feat: update Zod schemas with strict password validation
sricharan-varanasi Apr 9, 2026
1b77fdc
feat: enforce legacy 6-char minimum on login password
sricharan-varanasi Apr 9, 2026
ba3ab44
feat: integrate PasswordRequirementsTooltip into forms
sricharan-varanasi Apr 9, 2026
e3d36f4
refactor: convert password schemas to functions with i18n
sricharan-varanasi Apr 9, 2026
2814b45
fix: move tooltip icon inside password input box
sricharan-varanasi Apr 9, 2026
3e96322
feat: add password validation with clearErrors
sricharan-varanasi Apr 9, 2026
c738dee
fix: use NFKC normalization per RFC 8265 PRECIS
sricharan-varanasi Apr 10, 2026
be31add
Use Intl.Segmenter for grapheme-aware password length
sricharan-varanasi Apr 10, 2026
5ba1bac
fix: include modifier letters in caseless letter regexp
sricharan-varanasi Apr 10, 2026
efd86d5
Remove client-side password validation on login form
divbzero Apr 10, 2026
ebcdb6f
Ditto for old password on change password form
divbzero Apr 10, 2026
a9123ff
Avoid .trim() on password input fields
divbzero Apr 10, 2026
14d8202
Check password requirements in parallel
divbzero Apr 10, 2026
a65f411
Translate error messages in Input UI component
divbzero Apr 12, 2026
d5773bf
feat: updated UI to match design specs
adeiji Apr 14, 2026
82a6486
fix: updated gap of box to match other forms
adeiji Apr 14, 2026
28e51de
feat: add showError prop to toggle input error visibility
adeiji Apr 14, 2026
989d7db
fix: changed filenames to match with the ui specifications
adeiji Apr 14, 2026
8b3415c
feat: updated password requirements section to match design specs
adeiji Apr 14, 2026
488e3c3
feat: Update UI to match new UI specs and delayed actions
adeiji Apr 16, 2026
a8b2734
fix: added missed french translation for password requirements met
adeiji Apr 16, 2026
87148e6
fix: input shows red outline until password passes validation
adeiji Apr 16, 2026
84530f1
fix: Fixed UI bug where visual error outline for input was not matchi…
adeiji Apr 17, 2026
6623154
fix: Show password error message when password is empty and the user …
adeiji Apr 20, 2026
3b38302
fix: error color for title of password requirements checklist and sho…
adeiji Apr 20, 2026
ffbe696
fix: fixed bug where error message was showing for passwords with onl…
adeiji Apr 20, 2026
8598151
fix: using string interpolation for password min length and min chara…
adeiji Apr 20, 2026
a2c1370
fix: fixed tests and lint issues
adeiji Apr 21, 2026
5b225b2
fix: fixed bug M2-10652 and moved functionality into PasswordRequirem…
adeiji Apr 21, 2026
74ac481
fix: fixed issue M2-10653 - Allow ZWJ and pictographic-adjacent space…
adeiji Apr 21, 2026
7ae5ed9
fix: Removed combined emoji support
adeiji Apr 22, 2026
1956ac5
feat: Add emoji detection to password validation
adeiji Apr 22, 2026
e041cf2
fix: Update MUI theme to use custom error color for input validation …
adeiji Apr 23, 2026
0c5977e
fix: Prevent submission error from being cleared by debounce effect
adeiji Apr 23, 2026
ca5f0a0
fix: dont clear errors on empty password user has already submitted
adeiji Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions src/features/ChangePassword/model/schema.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import { z } from 'zod';

import { Dictionary, stringContainsSpaces } from '~/shared/utils';
import { ACCOUNT_PASSWORD_MIN_LENGTH, ACCOUNT_PASSWORD_MIN_CHAR_TYPES } from '~/shared/constants';
import { Dictionary } from '~/shared/utils';
import { checkPassword } from '~/shared/utils/passwordValidation';

export const ChangePasswordSchema = z
.object({
old: z.string().trim().min(6, { message: Dictionary.validation.password.minLength }),
new: z
.string()
.trim()
.min(6, { message: Dictionary.validation.password.minLength })
.refine((value) => !stringContainsSpaces(value), {
message: Dictionary.validation.password.shouldNotContainSpaces,
}),
confirm: z
.string()
.trim()
.min(6, { message: Dictionary.validation.password.minLength })
.refine((value) => !stringContainsSpaces(value), {
message: Dictionary.validation.password.shouldNotContainSpaces,
}),
old: z.string().min(1, { message: Dictionary.validation.password.required }),
new: z.string().superRefine((value, ctx) => {
const result = checkPassword(value);
if (!result.meetsLength)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.minLength,
params: { chars: ACCOUNT_PASSWORD_MIN_LENGTH },
});
if (!result.hasNoSpaces)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.blankSpaces,
});
if (!result.meetsCharTypeRequirement)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.characterTypes,
params: { types: ACCOUNT_PASSWORD_MIN_CHAR_TYPES },
});
}),
confirm: z.string().min(1, { message: Dictionary.validation.password.required }),
})
.refine((data) => data.new === data.confirm, {
message: Dictionary.validation.password.notMatch,
Expand Down
57 changes: 42 additions & 15 deletions src/features/ChangePassword/ui/ChangePasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { useEffect } from 'react';

import { useWatch } from 'react-hook-form';

import { useChangePasswordTranslation } from '../lib/useChangePasswordTranslation';
import { ChangePasswordSchema, TChangePassword } from '../model/schema';

Expand All @@ -10,6 +14,7 @@
DisplaySystemMessage,
Input,
PasswordIcon,
PasswordRequirementsSection,
} from '~/shared/ui';
import { useCustomForm, usePasswordType } from '~/shared/utils';

Expand All @@ -27,10 +32,26 @@
const [confirmNewPasswordType, onConfirmNewPasswordIconClick] = usePasswordType();

const form = useCustomForm(
{ defaultValues: { old: '', new: '', confirm: '' } },
{ defaultValues: { old: '', new: '', confirm: '' }, mode: 'onTouched' },
ChangePasswordSchema,
);
const { handleSubmit, reset } = form;
const { handleSubmit, reset, trigger, clearErrors } = form;

const newPasswordValue = useWatch({ control: form.control, name: 'new' });

useEffect(() => {
clearErrors('new');

if (!newPasswordValue) {
return;
}

const timer = setTimeout(async () => {
await trigger('new');
}, 500);

return () => clearTimeout(timer);
}, [newPasswordValue, trigger, clearErrors]);

const {
mutate: updatePassword,
Expand Down Expand Up @@ -68,19 +89,25 @@
/>
}
/>
<Input
id="change-password-form-new-password"
type={newPasswordType}
name="new"
placeholder={t('newPassword') || ''}
autoComplete="new-password"
Icon={
<PasswordIcon
isSecure={newPasswordType === 'password'}
onClick={onNewPasswordIconClick}
/>
}
/>
<PasswordRequirementsSection password={newPasswordValue || ''}>

Check failure on line 92 in src/features/ChangePassword/ui/ChangePasswordForm.tsx

View workflow job for this annotation

GitHub Actions / build

Property 'delayMs' is missing in type '{ children: Element; password: string; }' but required in type 'PasswordRequirementsSectionProps'.
<Input
id="change-password-form-new-password"
type={newPasswordType}
name="new"
placeholder={t('newPassword') || ''}
autoComplete="new-password"
showError={false}
Icon={
<>
<PasswordIcon
isSecure={newPasswordType === 'password'}
onClick={onNewPasswordIconClick}
/>
</>
}
/>
</PasswordRequirementsSection>

<Input
id="change-password-form-confirm-password"
type={confirmNewPasswordType}
Expand Down
2 changes: 1 addition & 1 deletion src/features/ChangePassword/ui/style.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.change-password-form-container {
max-width: 400px !important;
padding: 10px;
padding: 20px;
margin-top: 20px;
background-color: white;
box-shadow: 0 0 7px 0 #80808036;
Expand Down
1 change: 1 addition & 0 deletions src/features/Login/model/login.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import { Dictionary } from '~/shared/utils';
export const LoginSchema = BaseUserSchema.pick({ email: true }).extend({
password: z.string().min(1, Dictionary.validation.password.required),
});

export type TLoginForm = z.infer<typeof LoginSchema>;
39 changes: 24 additions & 15 deletions src/features/RecoveryPassword/model/schema.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { z } from 'zod';

import { Dictionary, stringContainsSpaces } from '~/shared/utils';
import { ACCOUNT_PASSWORD_MIN_LENGTH, ACCOUNT_PASSWORD_MIN_CHAR_TYPES } from '~/shared/constants';
import { Dictionary } from '~/shared/utils';
import { checkPassword } from '~/shared/utils/passwordValidation';

export const RecoveryPasswordSchema = z
.object({
new: z
.string()
.trim()
.min(6, { message: Dictionary.validation.password.minLength })
.refine((value) => !stringContainsSpaces(value), {
message: Dictionary.validation.password.shouldNotContainSpaces,
}),
confirm: z
.string()
.trim()
.min(6, { message: Dictionary.validation.password.minLength })
.refine((value) => !stringContainsSpaces(value), {
message: Dictionary.validation.password.shouldNotContainSpaces,
}),
new: z.string().superRefine((value, ctx) => {
const result = checkPassword(value);
if (!result.meetsLength)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.minLength,
params: { chars: ACCOUNT_PASSWORD_MIN_LENGTH },
});
if (!result.hasNoSpaces)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.blankSpaces,
});
if (!result.meetsCharTypeRequirement)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.characterTypes,
params: { types: ACCOUNT_PASSWORD_MIN_CHAR_TYPES },
});
}),
confirm: z.string().min(1, { message: Dictionary.validation.password.required }),
})
.refine((data) => data.new === data.confirm, {
message: Dictionary.validation.password.notMatch,
Expand Down
73 changes: 57 additions & 16 deletions src/features/RecoveryPassword/ui/RecoveryPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useEffect, useState } from 'react';

import { useWatch } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';

import { useRecoveryPasswordTranslation } from '../lib/useRecoveryPasswordTranslation';
import { RecoveryPassword, RecoveryPasswordSchema } from '../model/schema';

import { useApproveRecoveryPasswordMutation } from '~/entities/user';
import { DEFAULT_PASSWORD_CHECKLIST_DEBOUNCE_MS } from '~/shared/constants';
import ROUTES from '~/shared/constants/routes';
import { Box } from '~/shared/ui';
import {
Expand All @@ -13,6 +17,7 @@
DisplaySystemMessage,
Input,
PasswordIcon,
PasswordRequirementsSection,
} from '~/shared/ui';
import { useCustomForm, usePasswordType } from '~/shared/utils';

Expand All @@ -26,8 +31,30 @@
const navigate = useNavigate();
const { t } = useRecoveryPasswordTranslation();

const form = useCustomForm({ defaultValues: { new: '', confirm: '' } }, RecoveryPasswordSchema);
const { handleSubmit, reset } = form;
const form = useCustomForm(
{ defaultValues: { new: '', confirm: '' }, mode: 'onTouched' },
RecoveryPasswordSchema,
);
const { handleSubmit, reset, trigger, clearErrors } = form;

const newPasswordValue = useWatch({ control: form.control, name: 'new' });

const [isFirstTimeTyping, setIsFirstTimeTyping] = useState<boolean>(true);

useEffect(() => {
const timer = setTimeout(async () => {
if (!newPasswordValue) {
clearErrors('new');
return;
}

if (!isFirstTimeTyping) {
await trigger('new');
}
}, DEFAULT_PASSWORD_CHECKLIST_DEBOUNCE_MS);

return () => clearTimeout(timer);
}, [newPasswordValue, trigger, clearErrors]);

Check warning on line 57 in src/features/RecoveryPassword/ui/RecoveryPasswordForm.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'isFirstTimeTyping'. Either include it or remove the dependency array

const {
mutate: approveRecoveryPassword,
Expand Down Expand Up @@ -63,20 +90,34 @@
<p>{title}</p>
</Box>

<Box display="flex" flex={1} gap={2} flexDirection="column">
<Input
id="recovery-password-new-password"
type={newPasswordType}
name="new"
placeholder={t('newPassword') || ''}
autoComplete="new-password"
Icon={
<PasswordIcon
isSecure={newPasswordType === 'password'}
onClick={onNewPasswordIconClick}
/>
}
/>
<Box display="flex" flex={1} gap="24px" flexDirection="column">
<PasswordRequirementsSection
password={newPasswordValue || ''}
delayMs={DEFAULT_PASSWORD_CHECKLIST_DEBOUNCE_MS}
>
<Input
id="recovery-password-new-password"
type={newPasswordType}
name="new"
placeholder={t('newPassword') || ''}
autoComplete="new-password"
showError={false}
onBlur={() => {
if (isFirstTimeTyping) {
trigger('new');

Check warning on line 107 in src/features/RecoveryPassword/ui/RecoveryPasswordForm.tsx

View workflow job for this annotation

GitHub Actions / Lint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
setIsFirstTimeTyping(false);
}}
Icon={
<>
<PasswordIcon
isSecure={newPasswordType === 'password'}
onClick={onNewPasswordIconClick}
/>
</>
}
/>
</PasswordRequirementsSection>
<Input
id="recovery-password-confirm-new-password"
type={confirmNewPasswordType}
Expand Down
29 changes: 25 additions & 4 deletions src/features/Signup/model/signup.schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { z } from 'zod';

import { BaseUserSchema } from '~/entities/user';
import { ACCOUNT_PASSWORD_MIN_LENGTH, ACCOUNT_PASSWORD_MIN_CHAR_TYPES } from '~/shared/constants';
import { Dictionary } from '~/shared/utils';

export type TSignupForm = z.infer<typeof SignupFormSchema>;
import { checkPassword } from '~/shared/utils/passwordValidation';

export const SignupFormSchema = BaseUserSchema.pick({
email: true,
Expand All @@ -13,10 +13,31 @@ export const SignupFormSchema = BaseUserSchema.pick({
.extend({
firstName: z.string().trim().min(1, Dictionary.validation.firstName.required),
lastName: z.string().trim().min(1, Dictionary.validation.lastName.required),
password: z.string().trim().min(6, Dictionary.validation.password.minLength),
confirmPassword: z.string().trim().min(6, Dictionary.validation.password.minLength),
password: z.string().superRefine((value, ctx) => {
const result = checkPassword(value);
if (!result.meetsLength)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.minLength,
params: { chars: ACCOUNT_PASSWORD_MIN_LENGTH },
});
if (!result.hasNoSpaces)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.blankSpaces,
});
if (!result.meetsCharTypeRequirement)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: Dictionary.validation.password.characterTypes,
params: { types: ACCOUNT_PASSWORD_MIN_CHAR_TYPES },
});
}),
confirmPassword: z.string().min(1, Dictionary.validation.password.required),
})
.refine((data) => data.confirmPassword === data.password, {
message: Dictionary.validation.password.notMatch,
path: ['confirmPassword'],
});

export type TSignupForm = z.infer<typeof SignupFormSchema>;
Loading
Loading