Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/hip-spiders-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fixes username form field errors to display messages according to the respective code sent in the error response.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { OAUTH_PROVIDERS } from '@clerk/types';
import { ClerkAPIResponseError } from '@clerk/shared/error';
import { OAUTH_PROVIDERS } from '@clerk/shared/oauth';
import { waitFor } from '@testing-library/dom';
import React from 'react';

import { render, screen } from '../../../../testUtils';
import { render, runFakeTimers, screen } from '../../../../testUtils';
import { bindCreateFixtures } from '../../../utils/test/createFixtures';
import { SignUpContinue } from '../SignUpContinue';

Expand Down Expand Up @@ -106,6 +108,86 @@ describe('SignUpContinue', () => {
screen.getByText(`Continue with ${name}`);
});

it('renders error for invalid username length', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withEmailAddress({ required: true });
f.withUsername({ required: true });
f.startSignUpWithEmailAddress({
emailVerificationStatus: 'verified',
});
});

fixtures.signUp.update.mockRejectedValue(
new ClerkAPIResponseError('Error', {
data: [
{
code: 'form_username_invalid_length',
long_message: 'some server error',
message: 'some server error',
meta: { param_name: 'username' },
},
],
status: 400,
}),
);

await runFakeTimers(async timers => {
const { userEvent } = render(<SignUpContinue />, { wrapper });
expect(screen.queryByText(/username/i)).toBeInTheDocument();
await userEvent.type(screen.getByLabelText(/username/i), 'clerkUser');
timers.runOnlyPendingTimers();
const button = screen.getByText('Continue');
await userEvent.click(button);
timers.runOnlyPendingTimers();

await waitFor(() => expect(fixtures.signUp.update).toHaveBeenCalled());
timers.runOnlyPendingTimers();
await waitFor(() =>
expect(screen.queryByText(/^Your username must be between 4 and 40 characters long./i)).toBeInTheDocument(),
);
});
});

it('renders error for existing username', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withEmailAddress({ required: true });
f.withUsername({ required: true });
f.startSignUpWithEmailAddress({
emailVerificationStatus: 'verified',
});
});

fixtures.signUp.update.mockRejectedValue(
new ClerkAPIResponseError('Error', {
data: [
{
code: 'form_identifier_exists',
long_message: 'some server error',
message: 'some server error',
meta: { param_name: 'username' },
},
],
status: 400,
}),
);

await runFakeTimers(async timers => {
const { userEvent } = render(<SignUpContinue />, { wrapper });
expect(screen.queryByText(/username/i)).toBeInTheDocument();
await userEvent.type(screen.getByLabelText(/username/i), 'clerkUser');
timers.runOnlyPendingTimers();
const button = screen.getByText('Continue');
await userEvent.click(button);
timers.runOnlyPendingTimers();

await waitFor(() => expect(fixtures.signUp.update).toHaveBeenCalled());
timers.runOnlyPendingTimers();
await waitFor(() =>
expect(screen.queryByText(/^This username is taken. Please try another./i)).toBeInTheDocument(),
);
});
});

describe('Sign in Link', () => {
it('Shows the Sign In message with the appropriate link', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
Expand Down
6 changes: 6 additions & 0 deletions packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ const createSignUpFixtureHelpers = (baseClient: ClientJSON) => {
status: emailVerificationStatus,
},
},
missing_fields: [],
} as SignUpJSON;
};

Expand Down Expand Up @@ -341,6 +342,11 @@ const createUserSettingsFixtureHelpers = (environment: EnvironmentJSON) => {
mode: SIGN_UP_MODES.PUBLIC,
};

us.username_settings = {
min_length: 4,
max_length: 40,
};

const emptyAttribute = {
first_factors: [],
second_factors: [],
Expand Down
27 changes: 18 additions & 9 deletions packages/clerk-js/src/ui/utils/usernameUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@ type LocalizationConfigProps = {
usernameSettings: Pick<UsernameSettingsData, 'max_length' | 'min_length'>;
};

export const createUsernameError = (errors: ClerkAPIError[], localizationConfig: LocalizationConfigProps) => {
const INVALID_LENGTH = 'form_username_invalid_length';

export const createUsernameError = (
errors: ClerkAPIError[],
localizationConfig: LocalizationConfigProps,
): ClerkAPIError | string | undefined => {
const { t, usernameSettings } = localizationConfig;

const clerkApiError = errors[0] as ClerkAPIError | undefined;

if (!localizationConfig) {
return errors[0].longMessage;
return clerkApiError;
}

const msg = t(
localizationKeys('unstable__errors.form_username_invalid_length', {
min_length: usernameSettings.min_length,
max_length: usernameSettings.max_length,
}),
);
if (clerkApiError?.code === INVALID_LENGTH) {
return t(
localizationKeys(`unstable__errors.${INVALID_LENGTH}`, {
min_length: usernameSettings.min_length,
max_length: usernameSettings.max_length,
}),
);
}

return msg;
return clerkApiError;
};