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;