diff --git a/.changeset/wild-insects-design.md b/.changeset/wild-insects-design.md new file mode 100644 index 00000000000..b0051fad7ec --- /dev/null +++ b/.changeset/wild-insects-design.md @@ -0,0 +1,5 @@ +--- +"@clerk/ui": patch +--- + +Fixed unhandled TypeError when `unsafeMetadata` is passed to `` diff --git a/packages/ui/src/components/SignUp/SignUpStart.tsx b/packages/ui/src/components/SignUp/SignUpStart.tsx index 8400c8984ce..e719bbaf5c3 100644 --- a/packages/ui/src/components/SignUp/SignUpStart.tsx +++ b/packages/ui/src/components/SignUp/SignUpStart.tsx @@ -259,7 +259,15 @@ function SignUpStartInternal(): JSX.Element { }, [] as Array); if (unsafeMetadata) { - fieldsToSubmit.push({ id: 'unsafeMetadata', value: unsafeMetadata } as any); + const noop = () => {}; + fieldsToSubmit.push({ + id: 'unsafeMetadata', + value: unsafeMetadata, + clearFeedback: noop, + setValue: noop, + onChange: noop, + setError: noop, + } as any); } if (fields.ticket || hasExistingSignUpWithTicket) { diff --git a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx index e85ee5184fa..d5f4349e365 100644 --- a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx +++ b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx @@ -1,3 +1,4 @@ +import { ClerkAPIResponseError } from '@clerk/shared/error'; import { OAUTH_PROVIDERS } from '@clerk/shared/oauth'; import type { SignUpResource } from '@clerk/shared/types'; import { describe, expect, it, vi } from 'vitest'; @@ -522,4 +523,57 @@ describe('SignUpStart', () => { await waitFor(() => screen.getByText(/create your account/i)); }); }); + + describe('unsafeMetadata', () => { + it('does not throw when signUp.create rejects with an API error', async () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { href: 'http://localhost/sign-up' }, + }); + + let unhandledError: unknown = null; + const onUnhandledRejection = (reason: unknown) => { + unhandledError = reason; + }; + process.on('unhandledRejection', onUnhandledRejection); + + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withEmailAddress({ required: true }); + f.withPassword({ required: true }); + }); + fixtures.signUp.create.mockRejectedValueOnce( + new ClerkAPIResponseError('Error', { + data: [ + { + code: 'form_password_pwned', + long_message: 'Password has been found in an online data breach.', + message: 'Password has been found in an online data breach.', + meta: { param_name: 'password' }, + }, + ], + status: 422, + }), + ); + props.setProps({ unsafeMetadata: { foo: 'bar' } }); + + const { userEvent } = render( + + + , + { wrapper }, + ); + + await userEvent.type(screen.getByLabelText(/email address/i), 'test@example.com'); + await userEvent.type(screen.getByPlaceholderText(/create a password/i), 'password123'); + await userEvent.click(screen.getByText(/continue/i)); + + await waitFor(() => expect(fixtures.signUp.create).toHaveBeenCalled()); + await screen.findByTestId('form-feedback-error'); + // Flush pending microtasks so any unhandled rejection event has a chance to fire. + await new Promise(resolve => setTimeout(resolve, 0)); + + process.off('unhandledRejection', onUnhandledRejection); + expect(unhandledError).toBeNull(); + }); + }); });