diff --git a/.changeset/mosaic-add-contact-empty-error.md b/.changeset/mosaic-add-contact-empty-error.md new file mode 100644 index 00000000000..0486c63b88b --- /dev/null +++ b/.changeset/mosaic-add-contact-empty-error.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': patch +--- + +Show the generic error message in the `UserProfile` add email and add phone dialogs when sending a verification fails with a Clerk response that carries no error details. Previously these dialogs showed no message. diff --git a/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx index bd739117a6b..b9185a45480 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx @@ -1,15 +1,18 @@ import type { EmailAddressJSON, EnterpriseAccountConnectionJSON } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; import { describe, expect, it, vi } from 'vitest'; import { demandReverificationOnce, type FakeFapiSeed, + fapiUrl, holdRequests, serveFapi, VERIFICATION_CODE, verifyEmailOutOfBand, + worker, } from '../../../../__tests__/feature/fake-fapi'; import { fapiClient, @@ -83,6 +86,21 @@ describe('the user profile email section', () => { expect(emailRow()).not.toHaveTextContent('Unverified'); }); + it('says something went wrong when sending the code fails without details', async () => { + const { actor } = await renderSection(signedInWithEmails([])); + worker.use( + http.post(fapiUrl('/v1/me/email_addresses/:id/prepare_verification'), () => + HttpResponse.json({ errors: [] }, { status: 500 }), + ), + ); + + await actor.click(screen.getByRole('button', { name: 'Add email' })); + await actor.type(screen.getByRole('textbox', { name: 'Email' }), 'new@example.com'); + await actor.click(screen.getByRole('button', { name: 'Continue' })); + + expect(await screen.findByText('Something went wrong. Please try again.')).toBeInTheDocument(); + }); + it('holds the code field until the code has been sent', async () => { const { actor } = await renderSection(signedInWithEmails([])); const prepare = holdRequests('post', '/v1/me/email_addresses/:id/prepare_verification'); diff --git a/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx index 5bc04540616..4abe0f4d86b 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx @@ -1,14 +1,17 @@ import type { EnterpriseAccountConnectionJSON, PhoneNumberJSON } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; import { describe, expect, it } from 'vitest'; import { demandReverificationOnce, type FakeFapiSeed, + fapiUrl, holdRequests, serveFapi, VERIFICATION_CODE, + worker, } from '../../../../__tests__/feature/fake-fapi'; import type { FapiAttributeOverrides } from '../../../../__tests__/feature/fapi'; import { @@ -125,6 +128,19 @@ describe('the user profile phone section', () => { expect(screen.getByText('+49')).toBeInTheDocument(); }); + it('says something went wrong when sending the code fails without details', async () => { + const { actor } = await renderSection(); + worker.use( + http.post(fapiUrl('/v1/me/phone_numbers/:id/prepare_verification'), () => + HttpResponse.json({ errors: [] }, { status: 500 }), + ), + ); + + await addPhone(actor, '8015550100'); + + expect(await screen.findByText('Something went wrong. Please try again.')).toBeInTheDocument(); + }); + it('keeps the dialog open and says the code was wrong', async () => { const { actor } = await renderSection(); diff --git a/packages/mosaic/src/utils/__tests__/errors.test.tsx b/packages/mosaic/src/utils/__tests__/errors.test.tsx index 9c8ce5c9fd4..7c4eb880b63 100644 --- a/packages/mosaic/src/utils/__tests__/errors.test.tsx +++ b/packages/mosaic/src/utils/__tests__/errors.test.tsx @@ -4,7 +4,7 @@ import type { ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { isLocalizableError, MosaicLocalizationProvider, resolveLocalization, useErrorText } from '../../localization'; -import { save, SaveError, toLocalizableError, toLocalizableErrors } from '../errors'; +import { save, SaveError, toFormError, toLocalizableError, toLocalizableErrors, UNEXPECTED_ERROR } from '../errors'; function errorText() { const wrapper = ({ children }: { children: ReactNode }) => ( @@ -32,6 +32,12 @@ describe('save', () => { }); }); +describe('toFormError', () => { + it('reads a save that carried nothing as the generic error', () => { + expect(toFormError(new SaveError({})).global).toEqual(UNEXPECTED_ERROR); + }); +}); + describe('toLocalizableError', () => { it('reads the global error a save left, code and all', () => { const cause = new SaveError({ global: { code: 'action_blocked', message: 'Raw server sentence.' } }); diff --git a/packages/mosaic/src/utils/errors.ts b/packages/mosaic/src/utils/errors.ts index b30ce0eb457..fe4c771a3ec 100644 --- a/packages/mosaic/src/utils/errors.ts +++ b/packages/mosaic/src/utils/errors.ts @@ -126,7 +126,7 @@ export function toLocalizableErrors(cause: unknown): ErrorDescription[] { export function toFormError(cause: unknown): FormError { if (cause instanceof SaveError) { const { global, fields } = cause.formError; - return { global, fields }; + return global || fields ? { global, fields } : { global: UNEXPECTED_ERROR }; } console.error('[Clerk] Could not localize error', cause); return { global: UNEXPECTED_ERROR };