Skip to content

Commit

Permalink
feat(clerk-js,shared,types): Remove invalid email addresses from inpu…
Browse files Browse the repository at this point in the history
…t for Organization Invitation (#1869)

In invite members screen of the <OrganizationProfile /> component, consume
any invalid email addresses as they are returned in the API error and remove
them from the input automatically.
  • Loading branch information
chanioxaris authored Oct 16, 2023
1 parent 9b644d7 commit a46d6fe
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 6 deletions.
7 changes: 7 additions & 0 deletions .changeset/yellow-pumpkins-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/types': patch
---

In invite members screen of the <OrganizationProfile /> component, consume any invalid email addresses as they are returned in the API error and remove them from the input automatically.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -89,24 +89,29 @@ 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);
}
});
};

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 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<InviteMembersPage />, { 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(<InviteMembersPage />, { 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', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/errors/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface ClerkAPIError {
paramName?: string;
sessionId?: string;
emailAddresses?: string[];
identifiers?: string[];
zxcvbn?: {
suggestions: {
code: string;
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export interface ClerkAPIErrorJSON {
param_name?: string;
session_id?: string;
email_addresses?: string[];
identifiers?: string[];
zxcvbn?: {
suggestions: {
code: string;
Expand Down

0 comments on commit a46d6fe

Please sign in to comment.