Skip to content
Merged
8 changes: 8 additions & 0 deletions .changeset/tall-ears-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/types': patch
---

Add error handling for `setActive` with stale organization data
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const CreateOrganizationButton = ({
};

export const OrganizationListPage = withCardStateProvider(() => {
const card = useCardState();
const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView();
const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading;
const hasAnyData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count);
Expand All @@ -59,7 +58,6 @@ export const OrganizationListPage = withCardStateProvider(() => {
return (
<Card.Root>
<Card.Content sx={t => ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}>
<Card.Alert sx={t => ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error}</Card.Alert>
{isLoading && (
<Flex
direction={'row'}
Expand All @@ -86,6 +84,7 @@ export const OrganizationListPage = withCardStateProvider(() => {
});

const OrganizationListFlows = ({ showListInitially }: { showListInitially: boolean }) => {
const card = useCardState();
const { navigateAfterCreateOrganization, skipInvitationScreen, hideSlug } = useOrganizationListContext();
const [isCreateOrganizationFlow, setCreateOrganizationFlow] = useState(!showListInitially);
return (
Expand All @@ -95,30 +94,35 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole
)}

{isCreateOrganizationFlow && (
<Box
sx={t => ({
padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`,
})}
>
<CreateOrganizationForm
flow='organizationList'
startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }}
skipInvitationScreen={skipInvitationScreen}
navigateAfterCreateOrganization={org =>
navigateAfterCreateOrganization(org).then(() => setCreateOrganizationFlow(false))
}
onCancel={
showListInitially && isCreateOrganizationFlow ? () => setCreateOrganizationFlow(false) : undefined
}
hideSlug={hideSlug}
/>
</Box>
<>
<Card.Alert sx={t => ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error}</Card.Alert>

<Box
sx={t => ({
padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`,
})}
>
<CreateOrganizationForm
flow='organizationList'
startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }}
skipInvitationScreen={skipInvitationScreen}
navigateAfterCreateOrganization={org =>
navigateAfterCreateOrganization(org).then(() => setCreateOrganizationFlow(false))
}
onCancel={
showListInitially && isCreateOrganizationFlow ? () => setCreateOrganizationFlow(false) : undefined
}
hideSlug={hideSlug}
/>
</Box>
</>
)}
</>
);
};

export const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void }) => {
const card = useCardState();
const environment = useEnvironment();

const { ref, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView();
Expand All @@ -128,6 +132,8 @@ export const OrganizationListPageList = (props: { onCreateOrganizationClick: ()
const hasNextPage = userMemberships?.hasNextPage || userInvitations?.hasNextPage || userSuggestions?.hasNextPage;

const onCreateOrganizationClick = () => {
// Clear error originated from the list when switching to form
card.setError(undefined);
props.onCreateOrganizationClick();
};

Expand All @@ -154,6 +160,7 @@ export const OrganizationListPageList = (props: { onCreateOrganizationClick: ()
})}
/>
</Header.Root>
<Card.Alert sx={t => ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error}</Card.Alert>
<Col elementDescriptor={descriptors.main}>
<PreviewListItems>
<Actions role='menu'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
import { useOrganizationList, useUser } from '@clerk/shared/react';
import type { OrganizationResource } from '@clerk/types';

import { isClerkAPIResponseError } from '@/index.headless';
import { sharedMainIdentifierSx } from '@/ui/common/organizations/OrganizationPreview';
import { localizationKeys, useLocalizations } from '@/ui/customizables';
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { OrganizationPreview } from '@/ui/elements/OrganizationPreview';
import { PersonalWorkspacePreview } from '@/ui/elements/PersonalWorkspacePreview';
import { handleError } from '@/ui/utils/errorHandler';

import { useOrganizationListContext } from '../../contexts';
import { localizationKeys } from '../../localization';
import { OrganizationListPreviewButton } from './shared';

export const MembershipPreview = withCardStateProvider((props: { organization: OrganizationResource }) => {
export const MembershipPreview = (props: { organization: OrganizationResource }) => {
const { user } = useUser();
const card = useCardState();
const { navigateAfterSelectOrganization } = useOrganizationListContext();
const { t } = useLocalizations();
const { isLoaded, setActive } = useOrganizationList();

if (!isLoaded) {
return null;
}

const handleOrganizationClicked = (organization: OrganizationResource) => {
return card.runAsync(async () => {
await setActive({
organization,
});
try {
await setActive({
organization,
});

await navigateAfterSelectOrganization(organization);
await navigateAfterSelectOrganization(organization);
} catch (err) {
if (!isClerkAPIResponseError(err)) {
handleError(err, [], card.setError);
return;
}

switch (err.errors?.[0]?.code) {
case 'organization_not_found_or_unauthorized':
case 'not_a_member_in_organization': {
if (user?.createOrganizationEnabled) {
card.setError(t(localizationKeys('unstable__errors.organization_not_found_or_unauthorized')));
} else {
card.setError(
t(
localizationKeys(
'unstable__errors.organization_not_found_or_unauthorized_with_create_organization_disabled',
),
),
);
}
break;
}
default: {
handleError(err, [], card.setError);
}
}
}
});
};

return (
<OrganizationListPreviewButton onClick={() => handleOrganizationClicked(props.organization)}>
<OrganizationPreview
Expand All @@ -36,7 +70,8 @@ export const MembershipPreview = withCardStateProvider((props: { organization: O
/>
</OrganizationListPreviewButton>
);
});
};

export const PersonalAccountPreview = withCardStateProvider(() => {
const card = useCardState();
const { hidePersonal, navigateAfterSelectPersonal } = useOrganizationListContext();
Expand Down
Loading
Loading