Skip to content

fix(clerk-js): Add error handling for setActive with stale organization data #6550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 15, 2025
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