diff --git a/.changeset/yellow-pumpkins-buy.md b/.changeset/yellow-pumpkins-buy.md new file mode 100644 index 00000000000..7e3060e8eee --- /dev/null +++ b/.changeset/yellow-pumpkins-buy.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/types': patch +--- + +In invite members screen of the component, consume any invalid email addresses as they are returned in the API error and remove them from the input automatically. diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx index 035ee83edd7..ce58e431cd6 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx @@ -1,5 +1,5 @@ import { isClerkAPIResponseError } from '@clerk/shared'; -import type { MembershipRole, OrganizationResource } from '@clerk/types'; +import type { ClerkAPIError, MembershipRole, OrganizationResource } from '@clerk/types'; import React from 'react'; import { Flex, Text } from '../../customizables'; @@ -89,17 +89,16 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { .inviteMembers({ emailAddresses: emailAddressField.value.split(','), role: roleField.value as MembershipRole }) .then(onSuccess) .catch(err => { + if (isClerkAPIResponseError(err)) { + removeInvalidEmails(err.errors[0]); + } + if (isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'duplicate_record') { const unlocalizedEmailsList = err.errors[0].meta?.emailAddresses || []; // Create a localized list of email addresses const localizedList = createListFormat(unlocalizedEmailsList, locale); setLocalizedEmails(localizedList); - - // Remove any invalid email address - const invalids = new Set(unlocalizedEmailsList); - const emails = emailAddressField.value.split(','); - emailAddressField.setValue(emails.filter(e => !invalids.has(e)).join(',')); } else { setLocalizedEmails(null); handleError(err, [], card.setError); @@ -107,6 +106,12 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { }); }; + const removeInvalidEmails = (err: ClerkAPIError) => { + const invalidEmails = new Set([...(err.meta?.emailAddresses ?? []), ...(err.meta?.identifiers ?? [])]); + const emails = emailAddressField.value.split(','); + emailAddressField.setValue(emails.filter(e => !invalidEmails.has(e)).join(',')); + }; + return ( <> {localizedEmails && ( diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx index d3c73689864..3fc76ba84fb 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx @@ -133,6 +133,65 @@ describe('InviteMembersPage', () => { ).toBeInTheDocument(), ); }); + + it('removes duplicate emails from input after error', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + organization_memberships: [{ name: 'Org1', role: 'admin' }], + }); + }); + + fixtures.clerk.organization?.inviteMembers.mockRejectedValueOnce( + new ClerkAPIResponseError('Error', { + data: [ + { + code: 'duplicate_record', + long_message: + 'There are already pending invitations for the following email addresses: invalid@clerk.dev', + message: 'duplicate invitation', + meta: { email_addresses: ['invalid@clerk.dev'] }, + }, + ], + status: 400, + }), + ); + const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + await userEvent.type(getByTestId('tag-input'), 'invalid@clerk.dev'); + await userEvent.click(getByRole('button', { name: 'Send invitations' })); + + expect(getByTestId('tag-input')).not.toHaveValue(); + }); + + it('removes blocked/not allowed emails from input after error', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + organization_memberships: [{ name: 'Org1', role: 'admin' }], + }); + }); + + fixtures.clerk.organization?.inviteMembers.mockRejectedValueOnce( + new ClerkAPIResponseError('Error', { + data: [ + { + code: 'not_allowed_access', + long_message: 'blocked@clerk.dev is not allowed to access this application.', + message: 'Access not allowed.', + meta: { identifiers: ['blocked@clerk.dev'] }, + }, + ], + status: 403, + }), + ); + const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + await userEvent.type(getByTestId('tag-input'), 'blocked@clerk.dev'); + await userEvent.click(getByRole('button', { name: 'Send invitations' })); + + expect(getByTestId('tag-input')).not.toHaveValue(); + }); }); describe('Navigation', () => { diff --git a/packages/shared/src/errors/Error.ts b/packages/shared/src/errors/Error.ts index 066dffa627e..7280057eed8 100644 --- a/packages/shared/src/errors/Error.ts +++ b/packages/shared/src/errors/Error.ts @@ -60,6 +60,7 @@ export function parseError(error: ClerkAPIErrorJSON): ClerkAPIError { paramName: error?.meta?.param_name, sessionId: error?.meta?.session_id, emailAddresses: error?.meta?.email_addresses, + identifiers: error?.meta?.identifiers, zxcvbn: error?.meta?.zxcvbn, }, }; diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 5796a98cbce..8146fe34ec0 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -9,6 +9,7 @@ export interface ClerkAPIError { paramName?: string; sessionId?: string; emailAddresses?: string[]; + identifiers?: string[]; zxcvbn?: { suggestions: { code: string; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 607ffa43baa..aeb5684239f 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -267,6 +267,7 @@ export interface ClerkAPIErrorJSON { param_name?: string; session_id?: string; email_addresses?: string[]; + identifiers?: string[]; zxcvbn?: { suggestions: { code: string;