Skip to content

Commit 8650ed1

Browse files
dmoernerclaude
andcommitted
fix(ui): render a message for password errors the UI does not map
We pushed a new password error from the backend with the intention that it would be passed through to our SDKs. However, password fields have special behavior that override the generic error builder and do not fallback to the apierror from the backend. Two layers to the fix: 1. Specific: handle `form_password_matches_identifier`, and add the `unstable__errors.form_password_matches_identifier` key with an en-US string mirroring the server copy. 2. General: filter to codes present in `mapComplexityErrors` and, when none remain, fall back to `longMessage || message`. This restores the passthrough for any future unrecognized code, and stops an unmapped code arriving alongside a mapped one from contributing a blank list item. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
1 parent 4b2b688 commit 8650ed1

5 files changed

Lines changed: 81 additions & 3 deletions

File tree

‎.changeset/great-pots-tickle.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/localizations': patch
3+
'@clerk/shared': patch
4+
'@clerk/ui': patch
5+
---
6+
7+
Display a proper message when a password is rejected for matching one of the account's identifiers. Previously this error rendered as the incomplete sentence "Your password must contain ." on sign-up, reset password, and the user profile password form. The new message is available under the `unstable__errors.form_password_matches_identifier` localization key, and any password error the UI does not recognize now falls back to the message returned by the API instead of an empty sentence.

‎packages/localizations/src/en-US.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,6 +1787,8 @@ export const enUS: LocalizationResource = {
17871787
form_password_compromised__sign_in: undefined,
17881788
form_password_incorrect: undefined,
17891789
form_password_length_too_short: 'Your password is too short. It must be at least 8 characters long.',
1790+
form_password_matches_identifier:
1791+
'Password cannot match your email address, phone number or username. For account safety, please use a different password.',
17901792
form_password_not_strong_enough: 'Your password is not strong enough.',
17911793
form_password_or_identifier_incorrect: undefined,
17921794
form_password_pwned:

‎packages/shared/src/types/localization.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2158,6 +2158,7 @@ type UnstableErrors = WithParamName<{
21582158
form_password_pwned: LocalizationValue;
21592159
form_password_pwned__sign_in: LocalizationValue;
21602160
form_new_password_matches_current: LocalizationValue;
2161+
form_password_matches_identifier: LocalizationValue;
21612162
/** @deprecated Use `form_password_compromised__sign_in` instead */
21622163
form_password_untrusted__sign_in: LocalizationValue;
21632164
form_password_compromised__sign_in: LocalizationValue;

‎packages/ui/src/utils/__tests__/passwordUtils.test.tsx‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,65 @@ describe('createPasswordError() constructs error that password', () => {
161161
);
162162
});
163163

164+
it('matches one of the account identifiers', async () => {
165+
const { wrapper: Wrapper } = await createFixtures();
166+
167+
const wrapperBefore = ({ children }) => (
168+
<Wrapper>
169+
<OptionsProvider value={{ localization: {} }}>{children}</OptionsProvider>
170+
</Wrapper>
171+
);
172+
173+
const { result } = renderHook(() => useLocalizations(), { wrapper: wrapperBefore });
174+
175+
const res = createPasswordError(
176+
[{ code: 'form_password_matches_identifier', message: 'server message' }],
177+
createLocalizationConfig(result.current.t),
178+
);
179+
expect(res).toBe(
180+
'Password cannot match your email address, phone number or username. For account safety, please use a different password.',
181+
);
182+
});
183+
184+
it('falls back to the server message for a code with no complexity mapping', async () => {
185+
const { wrapper: Wrapper } = await createFixtures();
186+
187+
const wrapperBefore = ({ children }) => (
188+
<Wrapper>
189+
<OptionsProvider value={{ localization: {} }}>{children}</OptionsProvider>
190+
</Wrapper>
191+
);
192+
193+
const { result } = renderHook(() => useLocalizations(), { wrapper: wrapperBefore });
194+
195+
const res = createPasswordError(
196+
[{ code: 'form_password_some_future_rule', message: 'Server explains the rule.' }],
197+
createLocalizationConfig(result.current.t),
198+
);
199+
expect(res).toBe('Server explains the rule.');
200+
});
201+
202+
it('ignores unmapped codes that accompany a mapped one', async () => {
203+
const { wrapper: Wrapper } = await createFixtures();
204+
205+
const wrapperBefore = ({ children }) => (
206+
<Wrapper>
207+
<OptionsProvider value={{ localization: {} }}>{children}</OptionsProvider>
208+
</Wrapper>
209+
);
210+
211+
const { result } = renderHook(() => useLocalizations(), { wrapper: wrapperBefore });
212+
213+
const res = createPasswordError(
214+
[
215+
{ code: 'form_password_no_uppercase', message: '' },
216+
{ code: 'form_password_some_future_rule', message: 'Server explains the rule.' },
217+
],
218+
createLocalizationConfig(result.current.t),
219+
);
220+
expect(res).toBe('Your password must contain an uppercase letter.');
221+
});
222+
164223
//
165224
// zxcvbn
166225
//

‎packages/ui/src/utils/passwordUtils.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export const createPasswordError = (errors: ClerkAPIError[], localizationConfig:
4040
if (
4141
errors?.[0]?.code === 'form_password_size_in_bytes_exceeded' ||
4242
errors?.[0]?.code === 'form_password_pwned' ||
43-
errors?.[0]?.code === 'form_new_password_matches_current'
43+
errors?.[0]?.code === 'form_new_password_matches_current' ||
44+
errors?.[0]?.code === 'form_password_matches_identifier'
4445
) {
4546
return `${t(localizationKeys(`unstable__errors.${errors?.[0]?.code}` as any)) || errors?.[0]?.message}`;
4647
}
@@ -58,8 +59,16 @@ export const createPasswordError = (errors: ClerkAPIError[], localizationConfig:
5859
// show min length error first by itself
5960
const minLenErrors = errors.filter(e => e.code === 'form_password_length_too_short');
6061

61-
const message = (minLenErrors.length ? minLenErrors : errors).map((s: any) => {
62-
const localizedKey = (mapComplexityErrors(passwordSettings) as any)[s.code];
62+
const complexityErrors = mapComplexityErrors(passwordSettings);
63+
const knownErrors = (minLenErrors.length ? minLenErrors : errors).filter(e => e.code in complexityErrors);
64+
65+
// an unmapped code would otherwise render the prefix with an empty list, eg "Your password must contain ."
66+
if (!knownErrors.length) {
67+
return errors?.[0]?.longMessage || errors?.[0]?.message || '';
68+
}
69+
70+
const message = knownErrors.map((s: any) => {
71+
const localizedKey = (complexityErrors as any)[s.code];
6372

6473
if (Array.isArray(localizedKey)) {
6574
const [lk, attr, val] = localizedKey;

0 commit comments

Comments
 (0)