From 27e3600ccd37cc8fec8afe16b3e43ab1932aa717 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:43:35 -0300 Subject: [PATCH 01/10] Do not display organization list for force-an-org without memberships --- .../OrganizationList/OrganizationListPage.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx index 87e0958bfe4..2fd8d7f7046 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx @@ -1,4 +1,4 @@ -import { useOrganizationList, useUser } from '@clerk/shared/react'; +import { useOrganizationList, useSessionContext, useUser } from '@clerk/shared/react'; import { useContext, useState } from 'react'; import { Action, Actions } from '@/ui/elements/Actions'; @@ -117,6 +117,8 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole const { navigateAfterCreateOrganization, skipInvitationScreen, hideSlug } = useOrganizationListContext(); const [isCreateOrganizationFlow, setCreateOrganizationFlow] = useState(!showListInitially); const sessionTasksContext = useContext(SessionTasksContext); + const session = useSessionContext(); + return ( <> {!isCreateOrganizationFlow && ( @@ -135,7 +137,18 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }} skipInvitationScreen={skipInvitationScreen} navigateAfterCreateOrganization={org => - navigateAfterCreateOrganization(org).then(() => setCreateOrganizationFlow(false)) + navigateAfterCreateOrganization(org).then(() => { + const isForceOrganizationSelectionFlow = sessionTasksContext && session?.currentTask.key === 'org'; + + // During a force organization selection flow, keep displaying the creation form in a loading state + // rather than showing the organization list. This allows the client-side navigation to complete + // before transitioning away from this view. + if (isForceOrganizationSelectionFlow) { + return; + } + + setCreateOrganizationFlow(false); + }) } onCancel={ showListInitially && isCreateOrganizationFlow ? () => setCreateOrganizationFlow(false) : undefined From 264af78bdb307543959e3df718ef91f4ea5ea285 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:39:31 -0300 Subject: [PATCH 02/10] Extract force-an-org flow to separate component --- .../OrganizationList/OrganizationListPage.tsx | 144 +++++++++++++----- 1 file changed, 102 insertions(+), 42 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx index 2fd8d7f7046..04181aba6a9 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx @@ -1,4 +1,5 @@ -import { useOrganizationList, useSessionContext, useUser } from '@clerk/shared/react'; +import { useOrganizationList, useUser } from '@clerk/shared/react'; +import type { PropsWithChildren } from 'react'; import { useContext, useState } from 'react'; import { Action, Actions } from '@/ui/elements/Actions'; @@ -77,47 +78,45 @@ 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); const { hidePersonal } = useOrganizationListContext(); + const sessionTasksContext = useContext(SessionTasksContext); + if (sessionTasksContext) { + return ; + } + return ( - - ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}> - ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error} - {isLoading && ( - ({ - height: '100%', - minHeight: t.sizes.$60, - })} - > - - - )} - - {!isLoading && } - - - + + {isLoading && ( + ({ + height: '100%', + minHeight: t.sizes.$60, + })} + > + + + )} + + {!isLoading && } + ); }); const OrganizationListFlows = ({ showListInitially }: { showListInitially: boolean }) => { const { navigateAfterCreateOrganization, skipInvitationScreen, hideSlug } = useOrganizationListContext(); const [isCreateOrganizationFlow, setCreateOrganizationFlow] = useState(!showListInitially); - const sessionTasksContext = useContext(SessionTasksContext); - const session = useSessionContext(); return ( <> @@ -133,22 +132,10 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole > - navigateAfterCreateOrganization(org).then(() => { - const isForceOrganizationSelectionFlow = sessionTasksContext && session?.currentTask.key === 'org'; - - // During a force organization selection flow, keep displaying the creation form in a loading state - // rather than showing the organization list. This allows the client-side navigation to complete - // before transitioning away from this view. - if (isForceOrganizationSelectionFlow) { - return; - } - - setCreateOrganizationFlow(false); - }) + navigateAfterCreateOrganization(org).then(() => setCreateOrganizationFlow(false)) } onCancel={ showListInitially && isCreateOrganizationFlow ? () => setCreateOrganizationFlow(false) : undefined @@ -240,3 +227,76 @@ const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void > ); }; + +const ForceOrganizationSelectionFlow = () => { + const sessionTasksContext = useContext(SessionTasksContext); + const { navigateAfterCreateOrganization, hideSlug } = useOrganizationListContext(); + const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); + + const [isNavigating, setIsNavigating] = useState(false); + const isLoading = isNavigating || !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); + + const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(!userMemberships?.data?.length); + + if (isLoading) { + return ( + + ({ + height: '100%', + minHeight: t.sizes.$60, + })} + > + + + + ); + } + + return ( + + {isCreateOrganizationFlow ? ( + ({ + padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`, + })} + > + { + setIsNavigating(true); + return navigateAfterCreateOrganization(org); + }} + hideSlug={hideSlug} + /> + + ) : ( + setIsCreateOrganizationFlow(true)} /> + )} + + ); +}; + +const FlowCard = ({ children }: PropsWithChildren) => { + const card = useCardState(); + + return ( + + ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}> + ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error} + {children} + + + + ); +}; From c35c6ec2620ad0c4f05dfbdc3b75901c5fb3e918 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:30:14 -0300 Subject: [PATCH 03/10] Add changeset --- .changeset/common-mice-ring.md | 5 ++ .../OrganizationList/OrganizationListPage.tsx | 89 +++++++++---------- 2 files changed, 47 insertions(+), 47 deletions(-) create mode 100644 .changeset/common-mice-ring.md diff --git a/.changeset/common-mice-ring.md b/.changeset/common-mice-ring.md new file mode 100644 index 00000000000..5363e043892 --- /dev/null +++ b/.changeset/common-mice-ring.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Prevent organization list from displaying after creating an organization through the force organization selection flow diff --git a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx index 04181aba6a9..18b0283d6b6 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx @@ -21,6 +21,7 @@ import { organizationListParams } from './utils'; const useOrganizationListInView = () => { const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; const { ref } = useInView({ threshold: 0, @@ -39,6 +40,7 @@ const useOrganizationListInView = () => { }); return { + isLoading, userMemberships, userInvitations, userSuggestions, @@ -78,38 +80,22 @@ const CreateOrganizationButton = ({ }; export const OrganizationListPage = withCardStateProvider(() => { - const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); - const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; - const hasAnyData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - + const { isLoading, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); const { hidePersonal } = useOrganizationListContext(); + const hasOrganizationResources = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); const sessionTasksContext = useContext(SessionTasksContext); if (sessionTasksContext) { - return ; + return ; } return ( - {isLoading && ( - ({ - height: '100%', - minHeight: t.sizes.$60, - })} - > - - + {isLoading ? ( + + ) : ( + )} - - {!isLoading && } ); }); @@ -151,10 +137,9 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void }) => { const environment = useEnvironment(); - const { ref, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); + const { ref, isLoading, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); const { hidePersonal } = useOrganizationListContext(); - const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; const hasNextPage = userMemberships?.hasNextPage || userInvitations?.hasNextPage || userSuggestions?.hasNextPage; const onCreateOrganizationClick = () => { @@ -228,34 +213,19 @@ const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void ); }; -const ForceOrganizationSelectionFlow = () => { +const ForceOrganizationSelectionFlow = ({ hasOrganizationResources }: { hasOrganizationResources: boolean }) => { const sessionTasksContext = useContext(SessionTasksContext); const { navigateAfterCreateOrganization, hideSlug } = useOrganizationListContext(); - const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); + const { isLoading: isLoadingOrganizationResources } = useOrganizationListInView(); - const [isNavigating, setIsNavigating] = useState(false); - const isLoading = isNavigating || !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - - const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(!userMemberships?.data?.length); + const [isNavigatingAfterOrgCreation, setIsNavigatingAfterOrgCreation] = useState(false); + const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(!hasOrganizationResources); + const isLoading = isNavigatingAfterOrgCreation || isLoadingOrganizationResources; if (isLoading) { return ( - ({ - height: '100%', - minHeight: t.sizes.$60, - })} - > - - + ); } @@ -274,9 +244,16 @@ const ForceOrganizationSelectionFlow = () => { startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }} skipInvitationScreen navigateAfterCreateOrganization={org => { - setIsNavigating(true); + // During a force organization selection flow, keep displaying the creation form in a loading state. + // This allows the client-side navigation to complete before transitioning away from this view. + setIsNavigatingAfterOrgCreation(true); return navigateAfterCreateOrganization(org); }} + onCancel={ + isCreateOrganizationFlow && hasOrganizationResources + ? () => setIsCreateOrganizationFlow(false) + : undefined + } hideSlug={hideSlug} /> @@ -300,3 +277,21 @@ const FlowCard = ({ children }: PropsWithChildren) => { ); }; + +const FlowLoadingState = () => ( + ({ + height: '100%', + minHeight: t.sizes.$60, + })} + > + + +); From 008771978e6285152d19bbee623d917f0dab5423 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Sun, 15 Jun 2025 18:33:54 -0300 Subject: [PATCH 04/10] Bump `clerk.js` max bundle size --- .../ui/components/OrganizationList/OrganizationListPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx index 18b0283d6b6..54af70d6a44 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx @@ -94,7 +94,7 @@ export const OrganizationListPage = withCardStateProvider(() => { {isLoading ? ( ) : ( - + )} ); @@ -219,7 +219,7 @@ const ForceOrganizationSelectionFlow = ({ hasOrganizationResources }: { hasOrgan const { isLoading: isLoadingOrganizationResources } = useOrganizationListInView(); const [isNavigatingAfterOrgCreation, setIsNavigatingAfterOrgCreation] = useState(false); - const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(!hasOrganizationResources); + const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(() => !hasOrganizationResources); const isLoading = isNavigatingAfterOrgCreation || isLoadingOrganizationResources; if (isLoading) { From d51718a60c88c53546fbc36be3eb9002366cde71 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 16 Jun 2025 19:52:12 -0300 Subject: [PATCH 05/10] Extract task flow to internal component instead of `OrganizationList` --- .../OrganizationList/OrganizationListPage.tsx | 139 +++++------------- .../src/ui/components/SessionTasks/index.tsx | 19 +-- .../tasks/ForceOrganizationSelection.tsx | 113 ++++++++++++++ ...ganizationList.ts => OrganizationList.tsx} | 2 +- 4 files changed, 152 insertions(+), 121 deletions(-) create mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx rename packages/clerk-js/src/ui/contexts/components/{OrganizationList.ts => OrganizationList.tsx} (98%) diff --git a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx index 54af70d6a44..6bf1dcc6c1f 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx @@ -1,6 +1,5 @@ import { useOrganizationList, useUser } from '@clerk/shared/react'; -import type { PropsWithChildren } from 'react'; -import { useContext, useState } from 'react'; +import { useState } from 'react'; import { Action, Actions } from '@/ui/elements/Actions'; import { Card } from '@/ui/elements/Card'; @@ -8,7 +7,6 @@ import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { useEnvironment, useOrganizationListContext } from '../../contexts'; -import { SessionTasksContext } from '../../contexts/components/SessionTasks'; import { Box, Col, descriptors, Flex, localizationKeys, Spinner } from '../../customizables'; import { useInView } from '../../hooks'; import { Add } from '../../icons'; @@ -21,7 +19,6 @@ import { organizationListParams } from './utils'; const useOrganizationListInView = () => { const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); - const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; const { ref } = useInView({ threshold: 0, @@ -40,7 +37,6 @@ const useOrganizationListInView = () => { }); return { - isLoading, userMemberships, userInvitations, userSuggestions, @@ -80,30 +76,45 @@ const CreateOrganizationButton = ({ }; export const OrganizationListPage = withCardStateProvider(() => { - const { isLoading, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); - const { hidePersonal } = useOrganizationListContext(); - const hasOrganizationResources = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); + const card = useCardState(); + const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasAnyData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - const sessionTasksContext = useContext(SessionTasksContext); - if (sessionTasksContext) { - return ; - } + const { hidePersonal } = useOrganizationListContext(); return ( - - {isLoading ? ( - - ) : ( - - )} - + + ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}> + ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error} + {isLoading && ( + ({ + height: '100%', + minHeight: t.sizes.$60, + })} + > + + + )} + + {!isLoading && } + + + ); }); const OrganizationListFlows = ({ showListInitially }: { showListInitially: boolean }) => { const { navigateAfterCreateOrganization, skipInvitationScreen, hideSlug } = useOrganizationListContext(); const [isCreateOrganizationFlow, setCreateOrganizationFlow] = useState(!showListInitially); - return ( <> {!isCreateOrganizationFlow && ( @@ -134,12 +145,13 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole ); }; -const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void }) => { +export const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void }) => { const environment = useEnvironment(); - const { ref, isLoading, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); + const { ref, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); const { hidePersonal } = useOrganizationListContext(); + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; const hasNextPage = userMemberships?.hasNextPage || userInvitations?.hasNextPage || userSuggestions?.hasNextPage; const onCreateOrganizationClick = () => { @@ -212,86 +224,3 @@ const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void > ); }; - -const ForceOrganizationSelectionFlow = ({ hasOrganizationResources }: { hasOrganizationResources: boolean }) => { - const sessionTasksContext = useContext(SessionTasksContext); - const { navigateAfterCreateOrganization, hideSlug } = useOrganizationListContext(); - const { isLoading: isLoadingOrganizationResources } = useOrganizationListInView(); - - const [isNavigatingAfterOrgCreation, setIsNavigatingAfterOrgCreation] = useState(false); - const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(() => !hasOrganizationResources); - - const isLoading = isNavigatingAfterOrgCreation || isLoadingOrganizationResources; - if (isLoading) { - return ( - - - - ); - } - - return ( - - {isCreateOrganizationFlow ? ( - ({ - padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`, - })} - > - { - // During a force organization selection flow, keep displaying the creation form in a loading state. - // This allows the client-side navigation to complete before transitioning away from this view. - setIsNavigatingAfterOrgCreation(true); - return navigateAfterCreateOrganization(org); - }} - onCancel={ - isCreateOrganizationFlow && hasOrganizationResources - ? () => setIsCreateOrganizationFlow(false) - : undefined - } - hideSlug={hideSlug} - /> - - ) : ( - setIsCreateOrganizationFlow(true)} /> - )} - - ); -}; - -const FlowCard = ({ children }: PropsWithChildren) => { - const card = useCardState(); - - return ( - - ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}> - ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error} - {children} - - - - ); -}; - -const FlowLoadingState = () => ( - ({ - height: '100%', - minHeight: t.sizes.$60, - })} - > - - -); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 31bc9b41ade..17376a0282f 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -1,6 +1,5 @@ import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; -import type { SessionTask } from '@clerk/types'; import { useCallback, useContext, useEffect } from 'react'; import { Card } from '@/ui/elements/Card'; @@ -8,13 +7,10 @@ import { withCardStateProvider } from '@/ui/elements/contexts'; import { LoadingCardContainer } from '@/ui/elements/LoadingCard'; import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; -import { OrganizationListContext, SignInContext, SignUpContext } from '../../../ui/contexts'; -import { - SessionTasksContext as SessionTasksContext, - useSessionTasksContext, -} from '../../contexts/components/SessionTasks'; +import { SignInContext, SignUpContext } from '../../../ui/contexts'; +import { SessionTasksContext, useSessionTasksContext } from '../../contexts/components/SessionTasks'; import { Route, Switch, useRouter } from '../../router'; -import { OrganizationList } from '../OrganizationList'; +import { ForceOrganizationSelectionTask } from './tasks/ForceOrganizationSelection'; const SessionTasksStart = withCardStateProvider(() => { const clerk = useClerk(); @@ -43,14 +39,7 @@ function SessionTaskRoutes(): JSX.Element { return ( - - - + diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx new file mode 100644 index 00000000000..07eca495e82 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -0,0 +1,113 @@ +import { useOrganizationList } from '@clerk/shared/react/index'; +import type { ComponentType, PropsWithChildren } from 'react'; +import { useContext, useState } from 'react'; + +import { OrganizationListContext, useOrganizationListContext } from '@/ui/contexts'; +import { SessionTasksContext } from '@/ui/contexts/components/SessionTasks'; +import { Card } from '@/ui/elements/Card'; + +import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; +import { descriptors, Flex, localizationKeys, Spinner } from '../../../customizables'; +import { CreateOrganizationForm } from '../../CreateOrganization/CreateOrganizationForm'; +import { OrganizationListPageList } from '../../OrganizationList/OrganizationListPage'; +import { organizationListParams } from '../../OrganizationSwitcher/utils'; + +function ForceOrganizationSelectionFlows() { + const sessionTasksContext = useContext(SessionTasksContext); + const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + const { navigateAfterCreateOrganization, hideSlug } = useOrganizationListContext(); + + const isLoadingResources = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); + + const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(false); + const [isNavigatingAfterOrgCreation, setIsNavigatingAfterOrgCreation] = useState(false); + + const CreateOrganizationFlow = ( + { + // During a force organization selection flow, keep displaying the creation form in a loading state. + // This allows the client-side navigation to complete before transitioning away from this view. + setIsNavigatingAfterOrgCreation(true); + return navigateAfterCreateOrganization(org); + }} + onCancel={isCreateOrganizationFlow && hasData ? () => setIsCreateOrganizationFlow(false) : undefined} + hideSlug={hideSlug} + /> + ); + + const isLoading = isNavigatingAfterOrgCreation || isLoadingResources; + if (isLoading) { + return ( + + + + ); + } + + if (!hasData) { + return {CreateOrganizationFlow}; + } + + return ( + + {isCreateOrganizationFlow ? ( + <>{CreateOrganizationFlow}> + ) : ( + setIsCreateOrganizationFlow(true)} /> + )} + + ); +} + +const FlowCard = ({ children }: PropsWithChildren) => { + const card = useCardState(); + + return ( + + + {card.error} + {children} + + + + ); +}; + +const FlowLoadingState = () => ( + + + +); + +const withOrganizationListContext = (WrappedComponent: ComponentType) => { + return (props: P) => ( + + + + ); +}; + +/** + * Renders the force organization selection flow as part of session tasks + * @internal + */ +export const ForceOrganizationSelectionTask = withCardStateProvider( + withOrganizationListContext(ForceOrganizationSelectionFlows), +); diff --git a/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts b/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx similarity index 98% rename from packages/clerk-js/src/ui/contexts/components/OrganizationList.ts rename to packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx index 0c07c4161f4..932e98be727 100644 --- a/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts +++ b/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx @@ -1,7 +1,7 @@ import type { OrganizationResource, UserResource } from '@clerk/types'; import { createContext, useContext } from 'react'; -import { useEnvironment } from '../../contexts'; +import { useEnvironment } from '..'; import { useRouter } from '../../router'; import type { OrganizationListCtx } from '../../types'; import { populateParamFromObject } from '../utils'; From efa1b2d9e2a2179976fe685e5567730c8cee45ba Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:21:20 -0300 Subject: [PATCH 06/10] Introduce separate pages and states --- .../CreateOrganizationForm.tsx | 13 +- .../src/ui/components/SessionTasks/index.tsx | 26 +++- .../tasks/ForceOrganizationSelection.tsx | 143 ++++++++++-------- ...ganizationList.tsx => OrganizationList.ts} | 2 +- packages/clerk-js/src/ui/types.ts | 12 +- 5 files changed, 119 insertions(+), 77 deletions(-) rename packages/clerk-js/src/ui/contexts/components/{OrganizationList.tsx => OrganizationList.ts} (98%) diff --git a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx index d23faa00a6b..e832ab82f08 100644 --- a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx +++ b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx @@ -1,7 +1,8 @@ import { useOrganization, useOrganizationList } from '@clerk/shared/react'; import type { CreateOrganizationParams, OrganizationResource } from '@clerk/types'; -import React from 'react'; +import React, { useContext } from 'react'; +import { SessionTasksContext } from '@/ui/contexts/components/SessionTasks'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { FormButtonContainer } from '@/ui/elements/FormButtons'; @@ -23,7 +24,7 @@ import { organizationListParams } from '../OrganizationSwitcher/utils'; type CreateOrganizationFormProps = { skipInvitationScreen: boolean; - navigateAfterCreateOrganization: (organization: OrganizationResource) => Promise; + navigateAfterCreateOrganization?: (organization: OrganizationResource) => Promise; onCancel?: () => void; onComplete?: () => void; flow: 'default' | 'organizationList'; @@ -37,6 +38,7 @@ type CreateOrganizationFormProps = { export const CreateOrganizationForm = withCardStateProvider((props: CreateOrganizationFormProps) => { const card = useCardState(); const wizard = useWizard({ onNextStep: () => card.setError(undefined) }); + const sessionTasksContext = useContext(SessionTasksContext); const lastCreatedOrganizationRef = React.useRef(null); const { createOrganization, isLoaded, setActive, userMemberships } = useOrganizationList({ @@ -87,6 +89,11 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani void userMemberships.revalidate?.(); + if (sessionTasksContext) { + await sessionTasksContext.nextTask(); + return; + } + if (props.skipInvitationScreen ?? organization.maxAllowedMemberships === 1) { return completeFlow(); } @@ -100,7 +107,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani const completeFlow = () => { // We are confident that lastCreatedOrganizationRef.current will never be null // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - void props.navigateAfterCreateOrganization(lastCreatedOrganizationRef.current!); + void props.navigateAfterCreateOrganization?.(lastCreatedOrganizationRef.current!); props.onComplete?.(); }; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 17376a0282f..38f85dd1516 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -12,7 +12,7 @@ import { SessionTasksContext, useSessionTasksContext } from '../../contexts/comp import { Route, Switch, useRouter } from '../../router'; import { ForceOrganizationSelectionTask } from './tasks/ForceOrganizationSelection'; -const SessionTasksStart = withCardStateProvider(() => { +const SessionTasksStart = () => { const clerk = useClerk(); const { navigate } = useRouter(); const { redirectUrlComplete } = useSessionTasksContext(); @@ -33,7 +33,7 @@ const SessionTasksStart = withCardStateProvider(() => { ); -}); +}; function SessionTaskRoutes(): JSX.Element { return ( @@ -51,7 +51,7 @@ function SessionTaskRoutes(): JSX.Element { /** * @internal */ -export function SessionTask(): JSX.Element { +export const SessionTask = withCardStateProvider(() => { const clerk = useClerk(); const { navigate } = useRouter(); const signInContext = useContext(SignInContext); @@ -71,14 +71,24 @@ export function SessionTask(): JSX.Element { clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); }, [clerk, navigate, redirectUrlComplete]); - const nextTask = useCallback( - () => clerk.__experimental_navigateToTask({ redirectUrlComplete }), - [clerk, redirectUrlComplete], - ); + const nextTask = useCallback(() => { + return clerk.__experimental_navigateToTask({ redirectUrlComplete }); + }, [clerk, redirectUrlComplete]); + + if (!clerk.session?.currentTask) { + return ( + + + + + + + ); + } return ( ); -} +}); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index 07eca495e82..ef89b762a76 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -1,46 +1,19 @@ import { useOrganizationList } from '@clerk/shared/react/index'; -import type { ComponentType, PropsWithChildren } from 'react'; -import { useContext, useState } from 'react'; +import type { ComponentProps, PropsWithChildren } from 'react'; +import { useEffect, useState } from 'react'; -import { OrganizationListContext, useOrganizationListContext } from '@/ui/contexts'; -import { SessionTasksContext } from '@/ui/contexts/components/SessionTasks'; +import { OrganizationListContext } from '@/ui/contexts'; import { Card } from '@/ui/elements/Card'; - import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; -import { descriptors, Flex, localizationKeys, Spinner } from '../../../customizables'; + +import { Box, descriptors, Flex, localizationKeys, Spinner } from '../../../customizables'; import { CreateOrganizationForm } from '../../CreateOrganization/CreateOrganizationForm'; import { OrganizationListPageList } from '../../OrganizationList/OrganizationListPage'; import { organizationListParams } from '../../OrganizationSwitcher/utils'; -function ForceOrganizationSelectionFlows() { - const sessionTasksContext = useContext(SessionTasksContext); - const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); - const { navigateAfterCreateOrganization, hideSlug } = useOrganizationListContext(); - - const isLoadingResources = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; - const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - - const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(false); - const [isNavigatingAfterOrgCreation, setIsNavigatingAfterOrgCreation] = useState(false); - - const CreateOrganizationFlow = ( - { - // During a force organization selection flow, keep displaying the creation form in a loading state. - // This allows the client-side navigation to complete before transitioning away from this view. - setIsNavigatingAfterOrgCreation(true); - return navigateAfterCreateOrganization(org); - }} - onCancel={isCreateOrganizationFlow && hasData ? () => setIsCreateOrganizationFlow(false) : undefined} - hideSlug={hideSlug} - /> - ); +const ForceOrganizationSelectionFlows = () => { + const { isLoading, hasData, currentFlow, setCurrentFlow } = useForceOrganizationSelectionFlows(); - const isLoading = isNavigatingAfterOrgCreation || isLoadingResources; if (isLoading) { return ( @@ -49,28 +22,71 @@ function ForceOrganizationSelectionFlows() { ); } - if (!hasData) { - return {CreateOrganizationFlow}; + if (hasData && currentFlow !== 'create-organization') { + return ; } + return ; +}; + +const OrganizationSelectionPage = ({ setCurrentFlow }: CommonPageProps) => { + const [showCreateOrganizationForm, setShowCreateOrganizationForm] = useState(false); + + useEffect(() => { + setCurrentFlow('organization-selection'); + }, [setCurrentFlow]); + + if (showCreateOrganizationForm) { + return ; + } + + return ( + + + setShowCreateOrganizationForm(true)} /> + + + ); +}; + +const CreateOrganizationPage = ({ + onCancel, + setCurrentFlow, +}: CommonPageProps & Pick, 'onCancel'>) => { + useEffect(() => { + setCurrentFlow('create-organization'); + }, [setCurrentFlow]); + return ( - {isCreateOrganizationFlow ? ( - <>{CreateOrganizationFlow}> - ) : ( - setIsCreateOrganizationFlow(true)} /> - )} + ({ + padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`, + })} + > + + ); -} +}; const FlowCard = ({ children }: PropsWithChildren) => { const card = useCardState(); return ( - - {card.error} + ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}> + ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error} {children} @@ -82,6 +98,10 @@ const FlowLoadingState = () => ( ({ + height: '100%', + minHeight: t.sizes.$60, + })} > ( ); -const withOrganizationListContext = (WrappedComponent: ComponentType) => { - return (props: P) => ( - - - - ); +type Flow = 'create-organization' | 'organization-selection'; + +type CommonPageProps = { + setCurrentFlow: React.Dispatch>; +}; + +const useForceOrganizationSelectionFlows = () => { + const [currentFlow, setCurrentFlow] = useState(); + const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); + + return { + currentFlow, + setCurrentFlow, + hasData, + isLoading, + }; }; /** - * Renders the force organization selection flow as part of session tasks * @internal */ -export const ForceOrganizationSelectionTask = withCardStateProvider( - withOrganizationListContext(ForceOrganizationSelectionFlows), -); +export const ForceOrganizationSelectionTask = withCardStateProvider(ForceOrganizationSelectionFlows); diff --git a/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx b/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts similarity index 98% rename from packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx rename to packages/clerk-js/src/ui/contexts/components/OrganizationList.ts index 932e98be727..0c07c4161f4 100644 --- a/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx +++ b/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts @@ -1,7 +1,7 @@ import type { OrganizationResource, UserResource } from '@clerk/types'; import { createContext, useContext } from 'react'; -import { useEnvironment } from '..'; +import { useEnvironment } from '../../contexts'; import { useRouter } from '../../router'; import type { OrganizationListCtx } from '../../types'; import { populateParamFromObject } from '../utils'; diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 04cf9ee6366..65972f8d53f 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -23,18 +23,18 @@ import type { } from '@clerk/types'; export type { + __internal_OAuthConsentProps, + __internal_UserVerificationProps, + CreateOrganizationProps, GoogleOneTapProps, + OrganizationListProps, + OrganizationProfileProps, + OrganizationSwitcherProps, SignInProps, SignUpProps, UserButtonProps, UserProfileProps, - OrganizationSwitcherProps, - OrganizationProfileProps, - CreateOrganizationProps, - OrganizationListProps, WaitlistProps, - __internal_UserVerificationProps, - __internal_OAuthConsentProps, }; export type AvailableComponentProps = From 211271426fca243585e17a21e88238cf65c13942 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:34:36 -0300 Subject: [PATCH 07/10] Do not set transitive state on navigation --- packages/clerk-js/src/core/clerk.ts | 2 -- packages/clerk-js/src/ui/types.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d67438da176..f46fa09c0f5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1301,8 +1301,6 @@ export class Clerk implements ClerkInterface { const tracker = createBeforeUnloadTracker(this.#options.standardBrowser); const defaultRedirectUrlComplete = this.client?.signUp ? this.buildAfterSignUpUrl() : this.buildAfterSignInUrl(); - this.#setTransitiveState(); - await tracker.track(async () => { await this.navigate(redirectUrlComplete ?? defaultRedirectUrlComplete); }); diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 65972f8d53f..04cf9ee6366 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -23,18 +23,18 @@ import type { } from '@clerk/types'; export type { - __internal_OAuthConsentProps, - __internal_UserVerificationProps, - CreateOrganizationProps, GoogleOneTapProps, - OrganizationListProps, - OrganizationProfileProps, - OrganizationSwitcherProps, SignInProps, SignUpProps, UserButtonProps, UserProfileProps, + OrganizationSwitcherProps, + OrganizationProfileProps, + CreateOrganizationProps, + OrganizationListProps, WaitlistProps, + __internal_UserVerificationProps, + __internal_OAuthConsentProps, }; export type AvailableComponentProps = From 190f4024918981b64a627cd4268f0f0492d14ae5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:10:27 -0300 Subject: [PATCH 08/10] Improve redirection logic --- packages/clerk-js/bundlewatch.config.json | 8 +++---- .../src/ui/components/SessionTasks/index.tsx | 21 ++++++++++++++----- .../tasks/ForceOrganizationSelection.tsx | 2 ++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index d044e48372b..a5e9ec5b3b0 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,8 +1,8 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "610.32kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "70.2KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" }, + { "path": "./dist/clerk.js", "maxSize": "612kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, { "path": "./dist/ui-common*.js", "maxSize": "108.75KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, @@ -25,6 +25,6 @@ { "path": "./dist/paymentSources*.js", "maxSize": "9.17KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, { "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" }, - { "path": "./dist/sessionTasks*.js", "maxSize": "1KB" } + { "path": "./dist/sessionTasks*.js", "maxSize": "1.5KB" } ] } diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 38f85dd1516..ab7f9191a06 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -1,6 +1,6 @@ import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; -import { useCallback, useContext, useEffect } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; @@ -56,23 +56,34 @@ export const SessionTask = withCardStateProvider(() => { const { navigate } = useRouter(); const signInContext = useContext(SignInContext); const signUpContext = useContext(SignUpContext); + const [isNavigatingToTask, setIsNavigatingToTask] = useState(false); const redirectUrlComplete = signInContext?.afterSignInUrl ?? signUpContext?.afterSignUpUrl ?? clerk?.buildAfterSignInUrl(); + // If there are no pending tasks, navigate away from the tasks flow. + // This handles cases where a user with an active session returns to the tasks URL, + // for example by using browser back navigation. Since there are no pending tasks, + // we redirect them to their intended destination. useEffect(() => { - const task = clerk.session?.currentTask; + if (isNavigatingToTask) { + return; + } - if (!task) { + // Tasks can only exist on pending sessions, but we check both conditions + // here to be defensive and ensure proper redirection + const task = clerk.session?.currentTask; + if (!task || clerk.session?.status === 'active') { void navigate(redirectUrlComplete); return; } clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); - }, [clerk, navigate, redirectUrlComplete]); + }, [clerk, navigate, isNavigatingToTask, redirectUrlComplete]); const nextTask = useCallback(() => { - return clerk.__experimental_navigateToTask({ redirectUrlComplete }); + setIsNavigatingToTask(true); + return clerk.__experimental_navigateToTask({ redirectUrlComplete }).finally(() => setIsNavigatingToTask(false)); }, [clerk, redirectUrlComplete]); if (!clerk.session?.currentTask) { diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index ef89b762a76..748707139b7 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -22,6 +22,8 @@ const ForceOrganizationSelectionFlows = () => { ); } + // Do not render the organization selection flow when organization memberships + // get invalidated after the create organization mutation if (hasData && currentFlow !== 'create-organization') { return ; } From a00c9f456e46bd7d4a59914a61ce3442084e1b40 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:48:06 -0300 Subject: [PATCH 09/10] Simplify conditions to render UI on mount --- .changeset/chatty-llamas-wink.md | 4 + .../tasks/ForceOrganizationSelection.tsx | 86 +++++++------------ 2 files changed, 35 insertions(+), 55 deletions(-) create mode 100644 .changeset/chatty-llamas-wink.md diff --git a/.changeset/chatty-llamas-wink.md b/.changeset/chatty-llamas-wink.md new file mode 100644 index 00000000000..be979bb770a --- /dev/null +++ b/.changeset/chatty-llamas-wink.md @@ -0,0 +1,4 @@ +--- +'@clerk/shared': patch +'@clerk/clerk-react': patch +--- diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index 748707139b7..a8ff0cfaef7 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -1,6 +1,6 @@ import { useOrganizationList } from '@clerk/shared/react/index'; -import type { ComponentProps, PropsWithChildren } from 'react'; -import { useEffect, useState } from 'react'; +import type { PropsWithChildren } from 'react'; +import { useState } from 'react'; import { OrganizationListContext } from '@/ui/contexts'; import { Card } from '@/ui/elements/Card'; @@ -11,8 +11,13 @@ import { CreateOrganizationForm } from '../../CreateOrganization/CreateOrganizat import { OrganizationListPageList } from '../../OrganizationList/OrganizationListPage'; import { organizationListParams } from '../../OrganizationSwitcher/utils'; -const ForceOrganizationSelectionFlows = () => { - const { isLoading, hasData, currentFlow, setCurrentFlow } = useForceOrganizationSelectionFlows(); +/** + * @internal + */ +export const ForceOrganizationSelectionTask = withCardStateProvider(() => { + const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); if (isLoading) { return ( @@ -22,26 +27,16 @@ const ForceOrganizationSelectionFlows = () => { ); } - // Do not render the organization selection flow when organization memberships - // get invalidated after the create organization mutation - if (hasData && currentFlow !== 'create-organization') { - return ; + if (hasData) { + return ; } - return ; -}; + return ; +}); -const OrganizationSelectionPage = ({ setCurrentFlow }: CommonPageProps) => { +const OrganizationSelectionPage = () => { const [showCreateOrganizationForm, setShowCreateOrganizationForm] = useState(false); - useEffect(() => { - setCurrentFlow('organization-selection'); - }, [setCurrentFlow]); - - if (showCreateOrganizationForm) { - return ; - } - return ( { }} > - setShowCreateOrganizationForm(true)} /> + {showCreateOrganizationForm ? ( + ({ + padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`, + })} + > + setShowCreateOrganizationForm(false)} + /> + + ) : ( + setShowCreateOrganizationForm(true)} /> + )} ); }; -const CreateOrganizationPage = ({ - onCancel, - setCurrentFlow, -}: CommonPageProps & Pick, 'onCancel'>) => { - useEffect(() => { - setCurrentFlow('create-organization'); - }, [setCurrentFlow]); - +const CreateOrganizationPage = () => { return ( @@ -112,29 +114,3 @@ const FlowLoadingState = () => ( /> ); - -type Flow = 'create-organization' | 'organization-selection'; - -type CommonPageProps = { - setCurrentFlow: React.Dispatch>; -}; - -const useForceOrganizationSelectionFlows = () => { - const [currentFlow, setCurrentFlow] = useState(); - const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); - - const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; - const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - - return { - currentFlow, - setCurrentFlow, - hasData, - isLoading, - }; -}; - -/** - * @internal - */ -export const ForceOrganizationSelectionTask = withCardStateProvider(ForceOrganizationSelectionFlows); From e01d4f536b6c226473170ae9a6e3236cb92353cb Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:13:46 -0300 Subject: [PATCH 10/10] fix: Revalidate auth state for Next.js server --- .changeset/chatty-llamas-wink.md | 4 ---- packages/clerk-js/bundlewatch.config.json | 4 ++-- packages/clerk-js/src/core/clerk.ts | 22 ++++++++++++++++++- .../unstable/page-objects/sessionTask.ts | 1 - 4 files changed, 23 insertions(+), 8 deletions(-) delete mode 100644 .changeset/chatty-llamas-wink.md diff --git a/.changeset/chatty-llamas-wink.md b/.changeset/chatty-llamas-wink.md deleted file mode 100644 index be979bb770a..00000000000 --- a/.changeset/chatty-llamas-wink.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -'@clerk/shared': patch -'@clerk/clerk-react': patch ---- diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index a5e9ec5b3b0..b6e1054e5d4 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -3,8 +3,8 @@ { "path": "./dist/clerk.js", "maxSize": "612kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" }, - { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "108.75KB" }, + { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "110KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f46fa09c0f5..7dc496acdd1 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1283,7 +1283,16 @@ export class Clerk implements ClerkInterface { }; public __experimental_navigateToTask = async ({ redirectUrlComplete }: NextTaskParams = {}): Promise => { - const session = await this.session?.reload(); + /** + * Invalidate previously cache pages with auth state before navigating + */ + const onBeforeSetActive: SetActiveHook = + typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function' + ? window.__unstable__onBeforeSetActive + : noop; + await onBeforeSetActive(); + + const session = this.session; if (!session || !this.environment) { return; } @@ -1311,6 +1320,17 @@ export class Clerk implements ClerkInterface { this.#setAccessors(session); this.#emit(); + + /** + * Invoke the Next.js middleware to synchronize server and client state after resolving a session task. + * This ensures that any server-side logic depending on the session status (like middleware-based + * redirects or protected routes) correctly reflects the updated client authentication state. + */ + const onAfterSetActive: SetActiveHook = + typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function' + ? window.__unstable__onAfterSetActive + : noop; + await onAfterSetActive(); }; public addListener = (listener: ListenerCallback): UnsubscribeCallback => { diff --git a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts index c5721982326..6295e6ffbd5 100644 --- a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts +++ b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts @@ -12,7 +12,6 @@ export const createSessionTaskComponentPageObject = (testArgs: { page: EnhancedP const createOrganizationButton = page.getByRole('button', { name: /create organization/i }); await expect(createOrganizationButton).toBeVisible(); - expect(page.url()).toContain('add-organization'); await page.locator('input[name=name]').fill(fakeOrganization.name); await page.locator('input[name=slug]').fill(fakeOrganization.slug);
(WrappedComponent: ComponentType
) => { + return (props: P) => ( + + + + ); +}; + +/** + * Renders the force organization selection flow as part of session tasks + * @internal + */ +export const ForceOrganizationSelectionTask = withCardStateProvider( + withOrganizationListContext(ForceOrganizationSelectionFlows), +); diff --git a/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts b/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx similarity index 98% rename from packages/clerk-js/src/ui/contexts/components/OrganizationList.ts rename to packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx index 0c07c4161f4..932e98be727 100644 --- a/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts +++ b/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx @@ -1,7 +1,7 @@ import type { OrganizationResource, UserResource } from '@clerk/types'; import { createContext, useContext } from 'react'; -import { useEnvironment } from '../../contexts'; +import { useEnvironment } from '..'; import { useRouter } from '../../router'; import type { OrganizationListCtx } from '../../types'; import { populateParamFromObject } from '../utils'; From efa1b2d9e2a2179976fe685e5567730c8cee45ba Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:21:20 -0300 Subject: [PATCH 06/10] Introduce separate pages and states --- .../CreateOrganizationForm.tsx | 13 +- .../src/ui/components/SessionTasks/index.tsx | 26 +++- .../tasks/ForceOrganizationSelection.tsx | 143 ++++++++++-------- ...ganizationList.tsx => OrganizationList.ts} | 2 +- packages/clerk-js/src/ui/types.ts | 12 +- 5 files changed, 119 insertions(+), 77 deletions(-) rename packages/clerk-js/src/ui/contexts/components/{OrganizationList.tsx => OrganizationList.ts} (98%) diff --git a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx index d23faa00a6b..e832ab82f08 100644 --- a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx +++ b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx @@ -1,7 +1,8 @@ import { useOrganization, useOrganizationList } from '@clerk/shared/react'; import type { CreateOrganizationParams, OrganizationResource } from '@clerk/types'; -import React from 'react'; +import React, { useContext } from 'react'; +import { SessionTasksContext } from '@/ui/contexts/components/SessionTasks'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { FormButtonContainer } from '@/ui/elements/FormButtons'; @@ -23,7 +24,7 @@ import { organizationListParams } from '../OrganizationSwitcher/utils'; type CreateOrganizationFormProps = { skipInvitationScreen: boolean; - navigateAfterCreateOrganization: (organization: OrganizationResource) => Promise; + navigateAfterCreateOrganization?: (organization: OrganizationResource) => Promise; onCancel?: () => void; onComplete?: () => void; flow: 'default' | 'organizationList'; @@ -37,6 +38,7 @@ type CreateOrganizationFormProps = { export const CreateOrganizationForm = withCardStateProvider((props: CreateOrganizationFormProps) => { const card = useCardState(); const wizard = useWizard({ onNextStep: () => card.setError(undefined) }); + const sessionTasksContext = useContext(SessionTasksContext); const lastCreatedOrganizationRef = React.useRef(null); const { createOrganization, isLoaded, setActive, userMemberships } = useOrganizationList({ @@ -87,6 +89,11 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani void userMemberships.revalidate?.(); + if (sessionTasksContext) { + await sessionTasksContext.nextTask(); + return; + } + if (props.skipInvitationScreen ?? organization.maxAllowedMemberships === 1) { return completeFlow(); } @@ -100,7 +107,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani const completeFlow = () => { // We are confident that lastCreatedOrganizationRef.current will never be null // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - void props.navigateAfterCreateOrganization(lastCreatedOrganizationRef.current!); + void props.navigateAfterCreateOrganization?.(lastCreatedOrganizationRef.current!); props.onComplete?.(); }; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 17376a0282f..38f85dd1516 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -12,7 +12,7 @@ import { SessionTasksContext, useSessionTasksContext } from '../../contexts/comp import { Route, Switch, useRouter } from '../../router'; import { ForceOrganizationSelectionTask } from './tasks/ForceOrganizationSelection'; -const SessionTasksStart = withCardStateProvider(() => { +const SessionTasksStart = () => { const clerk = useClerk(); const { navigate } = useRouter(); const { redirectUrlComplete } = useSessionTasksContext(); @@ -33,7 +33,7 @@ const SessionTasksStart = withCardStateProvider(() => { ); -}); +}; function SessionTaskRoutes(): JSX.Element { return ( @@ -51,7 +51,7 @@ function SessionTaskRoutes(): JSX.Element { /** * @internal */ -export function SessionTask(): JSX.Element { +export const SessionTask = withCardStateProvider(() => { const clerk = useClerk(); const { navigate } = useRouter(); const signInContext = useContext(SignInContext); @@ -71,14 +71,24 @@ export function SessionTask(): JSX.Element { clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); }, [clerk, navigate, redirectUrlComplete]); - const nextTask = useCallback( - () => clerk.__experimental_navigateToTask({ redirectUrlComplete }), - [clerk, redirectUrlComplete], - ); + const nextTask = useCallback(() => { + return clerk.__experimental_navigateToTask({ redirectUrlComplete }); + }, [clerk, redirectUrlComplete]); + + if (!clerk.session?.currentTask) { + return ( + + + + + + + ); + } return ( ); -} +}); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index 07eca495e82..ef89b762a76 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -1,46 +1,19 @@ import { useOrganizationList } from '@clerk/shared/react/index'; -import type { ComponentType, PropsWithChildren } from 'react'; -import { useContext, useState } from 'react'; +import type { ComponentProps, PropsWithChildren } from 'react'; +import { useEffect, useState } from 'react'; -import { OrganizationListContext, useOrganizationListContext } from '@/ui/contexts'; -import { SessionTasksContext } from '@/ui/contexts/components/SessionTasks'; +import { OrganizationListContext } from '@/ui/contexts'; import { Card } from '@/ui/elements/Card'; - import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; -import { descriptors, Flex, localizationKeys, Spinner } from '../../../customizables'; + +import { Box, descriptors, Flex, localizationKeys, Spinner } from '../../../customizables'; import { CreateOrganizationForm } from '../../CreateOrganization/CreateOrganizationForm'; import { OrganizationListPageList } from '../../OrganizationList/OrganizationListPage'; import { organizationListParams } from '../../OrganizationSwitcher/utils'; -function ForceOrganizationSelectionFlows() { - const sessionTasksContext = useContext(SessionTasksContext); - const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); - const { navigateAfterCreateOrganization, hideSlug } = useOrganizationListContext(); - - const isLoadingResources = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; - const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - - const [isCreateOrganizationFlow, setIsCreateOrganizationFlow] = useState(false); - const [isNavigatingAfterOrgCreation, setIsNavigatingAfterOrgCreation] = useState(false); - - const CreateOrganizationFlow = ( - { - // During a force organization selection flow, keep displaying the creation form in a loading state. - // This allows the client-side navigation to complete before transitioning away from this view. - setIsNavigatingAfterOrgCreation(true); - return navigateAfterCreateOrganization(org); - }} - onCancel={isCreateOrganizationFlow && hasData ? () => setIsCreateOrganizationFlow(false) : undefined} - hideSlug={hideSlug} - /> - ); +const ForceOrganizationSelectionFlows = () => { + const { isLoading, hasData, currentFlow, setCurrentFlow } = useForceOrganizationSelectionFlows(); - const isLoading = isNavigatingAfterOrgCreation || isLoadingResources; if (isLoading) { return ( @@ -49,28 +22,71 @@ function ForceOrganizationSelectionFlows() { ); } - if (!hasData) { - return {CreateOrganizationFlow}; + if (hasData && currentFlow !== 'create-organization') { + return ; } + return ; +}; + +const OrganizationSelectionPage = ({ setCurrentFlow }: CommonPageProps) => { + const [showCreateOrganizationForm, setShowCreateOrganizationForm] = useState(false); + + useEffect(() => { + setCurrentFlow('organization-selection'); + }, [setCurrentFlow]); + + if (showCreateOrganizationForm) { + return ; + } + + return ( + + + setShowCreateOrganizationForm(true)} /> + + + ); +}; + +const CreateOrganizationPage = ({ + onCancel, + setCurrentFlow, +}: CommonPageProps & Pick, 'onCancel'>) => { + useEffect(() => { + setCurrentFlow('create-organization'); + }, [setCurrentFlow]); + return ( - {isCreateOrganizationFlow ? ( - <>{CreateOrganizationFlow}> - ) : ( - setIsCreateOrganizationFlow(true)} /> - )} + ({ + padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`, + })} + > + + ); -} +}; const FlowCard = ({ children }: PropsWithChildren) => { const card = useCardState(); return ( - - {card.error} + ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}> + ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error} {children} @@ -82,6 +98,10 @@ const FlowLoadingState = () => ( ({ + height: '100%', + minHeight: t.sizes.$60, + })} > ( ); -const withOrganizationListContext = (WrappedComponent: ComponentType) => { - return (props: P) => ( - - - - ); +type Flow = 'create-organization' | 'organization-selection'; + +type CommonPageProps = { + setCurrentFlow: React.Dispatch>; +}; + +const useForceOrganizationSelectionFlows = () => { + const [currentFlow, setCurrentFlow] = useState(); + const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); + + return { + currentFlow, + setCurrentFlow, + hasData, + isLoading, + }; }; /** - * Renders the force organization selection flow as part of session tasks * @internal */ -export const ForceOrganizationSelectionTask = withCardStateProvider( - withOrganizationListContext(ForceOrganizationSelectionFlows), -); +export const ForceOrganizationSelectionTask = withCardStateProvider(ForceOrganizationSelectionFlows); diff --git a/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx b/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts similarity index 98% rename from packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx rename to packages/clerk-js/src/ui/contexts/components/OrganizationList.ts index 932e98be727..0c07c4161f4 100644 --- a/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx +++ b/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts @@ -1,7 +1,7 @@ import type { OrganizationResource, UserResource } from '@clerk/types'; import { createContext, useContext } from 'react'; -import { useEnvironment } from '..'; +import { useEnvironment } from '../../contexts'; import { useRouter } from '../../router'; import type { OrganizationListCtx } from '../../types'; import { populateParamFromObject } from '../utils'; diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 04cf9ee6366..65972f8d53f 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -23,18 +23,18 @@ import type { } from '@clerk/types'; export type { + __internal_OAuthConsentProps, + __internal_UserVerificationProps, + CreateOrganizationProps, GoogleOneTapProps, + OrganizationListProps, + OrganizationProfileProps, + OrganizationSwitcherProps, SignInProps, SignUpProps, UserButtonProps, UserProfileProps, - OrganizationSwitcherProps, - OrganizationProfileProps, - CreateOrganizationProps, - OrganizationListProps, WaitlistProps, - __internal_UserVerificationProps, - __internal_OAuthConsentProps, }; export type AvailableComponentProps = From 211271426fca243585e17a21e88238cf65c13942 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:34:36 -0300 Subject: [PATCH 07/10] Do not set transitive state on navigation --- packages/clerk-js/src/core/clerk.ts | 2 -- packages/clerk-js/src/ui/types.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d67438da176..f46fa09c0f5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1301,8 +1301,6 @@ export class Clerk implements ClerkInterface { const tracker = createBeforeUnloadTracker(this.#options.standardBrowser); const defaultRedirectUrlComplete = this.client?.signUp ? this.buildAfterSignUpUrl() : this.buildAfterSignInUrl(); - this.#setTransitiveState(); - await tracker.track(async () => { await this.navigate(redirectUrlComplete ?? defaultRedirectUrlComplete); }); diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 65972f8d53f..04cf9ee6366 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -23,18 +23,18 @@ import type { } from '@clerk/types'; export type { - __internal_OAuthConsentProps, - __internal_UserVerificationProps, - CreateOrganizationProps, GoogleOneTapProps, - OrganizationListProps, - OrganizationProfileProps, - OrganizationSwitcherProps, SignInProps, SignUpProps, UserButtonProps, UserProfileProps, + OrganizationSwitcherProps, + OrganizationProfileProps, + CreateOrganizationProps, + OrganizationListProps, WaitlistProps, + __internal_UserVerificationProps, + __internal_OAuthConsentProps, }; export type AvailableComponentProps = From 190f4024918981b64a627cd4268f0f0492d14ae5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:10:27 -0300 Subject: [PATCH 08/10] Improve redirection logic --- packages/clerk-js/bundlewatch.config.json | 8 +++---- .../src/ui/components/SessionTasks/index.tsx | 21 ++++++++++++++----- .../tasks/ForceOrganizationSelection.tsx | 2 ++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index d044e48372b..a5e9ec5b3b0 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,8 +1,8 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "610.32kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "70.2KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" }, + { "path": "./dist/clerk.js", "maxSize": "612kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, { "path": "./dist/ui-common*.js", "maxSize": "108.75KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, @@ -25,6 +25,6 @@ { "path": "./dist/paymentSources*.js", "maxSize": "9.17KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, { "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" }, - { "path": "./dist/sessionTasks*.js", "maxSize": "1KB" } + { "path": "./dist/sessionTasks*.js", "maxSize": "1.5KB" } ] } diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 38f85dd1516..ab7f9191a06 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -1,6 +1,6 @@ import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; -import { useCallback, useContext, useEffect } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; @@ -56,23 +56,34 @@ export const SessionTask = withCardStateProvider(() => { const { navigate } = useRouter(); const signInContext = useContext(SignInContext); const signUpContext = useContext(SignUpContext); + const [isNavigatingToTask, setIsNavigatingToTask] = useState(false); const redirectUrlComplete = signInContext?.afterSignInUrl ?? signUpContext?.afterSignUpUrl ?? clerk?.buildAfterSignInUrl(); + // If there are no pending tasks, navigate away from the tasks flow. + // This handles cases where a user with an active session returns to the tasks URL, + // for example by using browser back navigation. Since there are no pending tasks, + // we redirect them to their intended destination. useEffect(() => { - const task = clerk.session?.currentTask; + if (isNavigatingToTask) { + return; + } - if (!task) { + // Tasks can only exist on pending sessions, but we check both conditions + // here to be defensive and ensure proper redirection + const task = clerk.session?.currentTask; + if (!task || clerk.session?.status === 'active') { void navigate(redirectUrlComplete); return; } clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); - }, [clerk, navigate, redirectUrlComplete]); + }, [clerk, navigate, isNavigatingToTask, redirectUrlComplete]); const nextTask = useCallback(() => { - return clerk.__experimental_navigateToTask({ redirectUrlComplete }); + setIsNavigatingToTask(true); + return clerk.__experimental_navigateToTask({ redirectUrlComplete }).finally(() => setIsNavigatingToTask(false)); }, [clerk, redirectUrlComplete]); if (!clerk.session?.currentTask) { diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index ef89b762a76..748707139b7 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -22,6 +22,8 @@ const ForceOrganizationSelectionFlows = () => { ); } + // Do not render the organization selection flow when organization memberships + // get invalidated after the create organization mutation if (hasData && currentFlow !== 'create-organization') { return ; } From a00c9f456e46bd7d4a59914a61ce3442084e1b40 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:48:06 -0300 Subject: [PATCH 09/10] Simplify conditions to render UI on mount --- .changeset/chatty-llamas-wink.md | 4 + .../tasks/ForceOrganizationSelection.tsx | 86 +++++++------------ 2 files changed, 35 insertions(+), 55 deletions(-) create mode 100644 .changeset/chatty-llamas-wink.md diff --git a/.changeset/chatty-llamas-wink.md b/.changeset/chatty-llamas-wink.md new file mode 100644 index 00000000000..be979bb770a --- /dev/null +++ b/.changeset/chatty-llamas-wink.md @@ -0,0 +1,4 @@ +--- +'@clerk/shared': patch +'@clerk/clerk-react': patch +--- diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index 748707139b7..a8ff0cfaef7 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -1,6 +1,6 @@ import { useOrganizationList } from '@clerk/shared/react/index'; -import type { ComponentProps, PropsWithChildren } from 'react'; -import { useEffect, useState } from 'react'; +import type { PropsWithChildren } from 'react'; +import { useState } from 'react'; import { OrganizationListContext } from '@/ui/contexts'; import { Card } from '@/ui/elements/Card'; @@ -11,8 +11,13 @@ import { CreateOrganizationForm } from '../../CreateOrganization/CreateOrganizat import { OrganizationListPageList } from '../../OrganizationList/OrganizationListPage'; import { organizationListParams } from '../../OrganizationSwitcher/utils'; -const ForceOrganizationSelectionFlows = () => { - const { isLoading, hasData, currentFlow, setCurrentFlow } = useForceOrganizationSelectionFlows(); +/** + * @internal + */ +export const ForceOrganizationSelectionTask = withCardStateProvider(() => { + const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); if (isLoading) { return ( @@ -22,26 +27,16 @@ const ForceOrganizationSelectionFlows = () => { ); } - // Do not render the organization selection flow when organization memberships - // get invalidated after the create organization mutation - if (hasData && currentFlow !== 'create-organization') { - return ; + if (hasData) { + return ; } - return ; -}; + return ; +}); -const OrganizationSelectionPage = ({ setCurrentFlow }: CommonPageProps) => { +const OrganizationSelectionPage = () => { const [showCreateOrganizationForm, setShowCreateOrganizationForm] = useState(false); - useEffect(() => { - setCurrentFlow('organization-selection'); - }, [setCurrentFlow]); - - if (showCreateOrganizationForm) { - return ; - } - return ( { }} > - setShowCreateOrganizationForm(true)} /> + {showCreateOrganizationForm ? ( + ({ + padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`, + })} + > + setShowCreateOrganizationForm(false)} + /> + + ) : ( + setShowCreateOrganizationForm(true)} /> + )} ); }; -const CreateOrganizationPage = ({ - onCancel, - setCurrentFlow, -}: CommonPageProps & Pick, 'onCancel'>) => { - useEffect(() => { - setCurrentFlow('create-organization'); - }, [setCurrentFlow]); - +const CreateOrganizationPage = () => { return ( @@ -112,29 +114,3 @@ const FlowLoadingState = () => ( /> ); - -type Flow = 'create-organization' | 'organization-selection'; - -type CommonPageProps = { - setCurrentFlow: React.Dispatch>; -}; - -const useForceOrganizationSelectionFlows = () => { - const [currentFlow, setCurrentFlow] = useState(); - const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); - - const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; - const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - - return { - currentFlow, - setCurrentFlow, - hasData, - isLoading, - }; -}; - -/** - * @internal - */ -export const ForceOrganizationSelectionTask = withCardStateProvider(ForceOrganizationSelectionFlows); From e01d4f536b6c226473170ae9a6e3236cb92353cb Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:13:46 -0300 Subject: [PATCH 10/10] fix: Revalidate auth state for Next.js server --- .changeset/chatty-llamas-wink.md | 4 ---- packages/clerk-js/bundlewatch.config.json | 4 ++-- packages/clerk-js/src/core/clerk.ts | 22 ++++++++++++++++++- .../unstable/page-objects/sessionTask.ts | 1 - 4 files changed, 23 insertions(+), 8 deletions(-) delete mode 100644 .changeset/chatty-llamas-wink.md diff --git a/.changeset/chatty-llamas-wink.md b/.changeset/chatty-llamas-wink.md deleted file mode 100644 index be979bb770a..00000000000 --- a/.changeset/chatty-llamas-wink.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -'@clerk/shared': patch -'@clerk/clerk-react': patch ---- diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index a5e9ec5b3b0..b6e1054e5d4 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -3,8 +3,8 @@ { "path": "./dist/clerk.js", "maxSize": "612kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" }, - { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "108.75KB" }, + { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "110KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f46fa09c0f5..7dc496acdd1 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1283,7 +1283,16 @@ export class Clerk implements ClerkInterface { }; public __experimental_navigateToTask = async ({ redirectUrlComplete }: NextTaskParams = {}): Promise => { - const session = await this.session?.reload(); + /** + * Invalidate previously cache pages with auth state before navigating + */ + const onBeforeSetActive: SetActiveHook = + typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function' + ? window.__unstable__onBeforeSetActive + : noop; + await onBeforeSetActive(); + + const session = this.session; if (!session || !this.environment) { return; } @@ -1311,6 +1320,17 @@ export class Clerk implements ClerkInterface { this.#setAccessors(session); this.#emit(); + + /** + * Invoke the Next.js middleware to synchronize server and client state after resolving a session task. + * This ensures that any server-side logic depending on the session status (like middleware-based + * redirects or protected routes) correctly reflects the updated client authentication state. + */ + const onAfterSetActive: SetActiveHook = + typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function' + ? window.__unstable__onAfterSetActive + : noop; + await onAfterSetActive(); }; public addListener = (listener: ListenerCallback): UnsubscribeCallback => { diff --git a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts index c5721982326..6295e6ffbd5 100644 --- a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts +++ b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts @@ -12,7 +12,6 @@ export const createSessionTaskComponentPageObject = (testArgs: { page: EnhancedP const createOrganizationButton = page.getByRole('button', { name: /create organization/i }); await expect(createOrganizationButton).toBeVisible(); - expect(page.url()).toContain('add-organization'); await page.locator('input[name=name]').fill(fakeOrganization.name); await page.locator('input[name=slug]').fill(fakeOrganization.slug);
) => { - return (props: P) => ( - - - - ); +type Flow = 'create-organization' | 'organization-selection'; + +type CommonPageProps = { + setCurrentFlow: React.Dispatch>; +}; + +const useForceOrganizationSelectionFlows = () => { + const [currentFlow, setCurrentFlow] = useState(); + const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); + + return { + currentFlow, + setCurrentFlow, + hasData, + isLoading, + }; }; /** - * Renders the force organization selection flow as part of session tasks * @internal */ -export const ForceOrganizationSelectionTask = withCardStateProvider( - withOrganizationListContext(ForceOrganizationSelectionFlows), -); +export const ForceOrganizationSelectionTask = withCardStateProvider(ForceOrganizationSelectionFlows); diff --git a/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx b/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts similarity index 98% rename from packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx rename to packages/clerk-js/src/ui/contexts/components/OrganizationList.ts index 932e98be727..0c07c4161f4 100644 --- a/packages/clerk-js/src/ui/contexts/components/OrganizationList.tsx +++ b/packages/clerk-js/src/ui/contexts/components/OrganizationList.ts @@ -1,7 +1,7 @@ import type { OrganizationResource, UserResource } from '@clerk/types'; import { createContext, useContext } from 'react'; -import { useEnvironment } from '..'; +import { useEnvironment } from '../../contexts'; import { useRouter } from '../../router'; import type { OrganizationListCtx } from '../../types'; import { populateParamFromObject } from '../utils'; diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 04cf9ee6366..65972f8d53f 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -23,18 +23,18 @@ import type { } from '@clerk/types'; export type { + __internal_OAuthConsentProps, + __internal_UserVerificationProps, + CreateOrganizationProps, GoogleOneTapProps, + OrganizationListProps, + OrganizationProfileProps, + OrganizationSwitcherProps, SignInProps, SignUpProps, UserButtonProps, UserProfileProps, - OrganizationSwitcherProps, - OrganizationProfileProps, - CreateOrganizationProps, - OrganizationListProps, WaitlistProps, - __internal_UserVerificationProps, - __internal_OAuthConsentProps, }; export type AvailableComponentProps = From 211271426fca243585e17a21e88238cf65c13942 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:34:36 -0300 Subject: [PATCH 07/10] Do not set transitive state on navigation --- packages/clerk-js/src/core/clerk.ts | 2 -- packages/clerk-js/src/ui/types.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d67438da176..f46fa09c0f5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1301,8 +1301,6 @@ export class Clerk implements ClerkInterface { const tracker = createBeforeUnloadTracker(this.#options.standardBrowser); const defaultRedirectUrlComplete = this.client?.signUp ? this.buildAfterSignUpUrl() : this.buildAfterSignInUrl(); - this.#setTransitiveState(); - await tracker.track(async () => { await this.navigate(redirectUrlComplete ?? defaultRedirectUrlComplete); }); diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 65972f8d53f..04cf9ee6366 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -23,18 +23,18 @@ import type { } from '@clerk/types'; export type { - __internal_OAuthConsentProps, - __internal_UserVerificationProps, - CreateOrganizationProps, GoogleOneTapProps, - OrganizationListProps, - OrganizationProfileProps, - OrganizationSwitcherProps, SignInProps, SignUpProps, UserButtonProps, UserProfileProps, + OrganizationSwitcherProps, + OrganizationProfileProps, + CreateOrganizationProps, + OrganizationListProps, WaitlistProps, + __internal_UserVerificationProps, + __internal_OAuthConsentProps, }; export type AvailableComponentProps = From 190f4024918981b64a627cd4268f0f0492d14ae5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:10:27 -0300 Subject: [PATCH 08/10] Improve redirection logic --- packages/clerk-js/bundlewatch.config.json | 8 +++---- .../src/ui/components/SessionTasks/index.tsx | 21 ++++++++++++++----- .../tasks/ForceOrganizationSelection.tsx | 2 ++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index d044e48372b..a5e9ec5b3b0 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,8 +1,8 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "610.32kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "70.2KB" }, - { "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" }, + { "path": "./dist/clerk.js", "maxSize": "612kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" }, + { "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, { "path": "./dist/ui-common*.js", "maxSize": "108.75KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, @@ -25,6 +25,6 @@ { "path": "./dist/paymentSources*.js", "maxSize": "9.17KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, { "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" }, - { "path": "./dist/sessionTasks*.js", "maxSize": "1KB" } + { "path": "./dist/sessionTasks*.js", "maxSize": "1.5KB" } ] } diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 38f85dd1516..ab7f9191a06 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -1,6 +1,6 @@ import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; -import { useCallback, useContext, useEffect } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; @@ -56,23 +56,34 @@ export const SessionTask = withCardStateProvider(() => { const { navigate } = useRouter(); const signInContext = useContext(SignInContext); const signUpContext = useContext(SignUpContext); + const [isNavigatingToTask, setIsNavigatingToTask] = useState(false); const redirectUrlComplete = signInContext?.afterSignInUrl ?? signUpContext?.afterSignUpUrl ?? clerk?.buildAfterSignInUrl(); + // If there are no pending tasks, navigate away from the tasks flow. + // This handles cases where a user with an active session returns to the tasks URL, + // for example by using browser back navigation. Since there are no pending tasks, + // we redirect them to their intended destination. useEffect(() => { - const task = clerk.session?.currentTask; + if (isNavigatingToTask) { + return; + } - if (!task) { + // Tasks can only exist on pending sessions, but we check both conditions + // here to be defensive and ensure proper redirection + const task = clerk.session?.currentTask; + if (!task || clerk.session?.status === 'active') { void navigate(redirectUrlComplete); return; } clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); - }, [clerk, navigate, redirectUrlComplete]); + }, [clerk, navigate, isNavigatingToTask, redirectUrlComplete]); const nextTask = useCallback(() => { - return clerk.__experimental_navigateToTask({ redirectUrlComplete }); + setIsNavigatingToTask(true); + return clerk.__experimental_navigateToTask({ redirectUrlComplete }).finally(() => setIsNavigatingToTask(false)); }, [clerk, redirectUrlComplete]); if (!clerk.session?.currentTask) { diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index ef89b762a76..748707139b7 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -22,6 +22,8 @@ const ForceOrganizationSelectionFlows = () => { ); } + // Do not render the organization selection flow when organization memberships + // get invalidated after the create organization mutation if (hasData && currentFlow !== 'create-organization') { return ; } From a00c9f456e46bd7d4a59914a61ce3442084e1b40 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:48:06 -0300 Subject: [PATCH 09/10] Simplify conditions to render UI on mount --- .changeset/chatty-llamas-wink.md | 4 + .../tasks/ForceOrganizationSelection.tsx | 86 +++++++------------ 2 files changed, 35 insertions(+), 55 deletions(-) create mode 100644 .changeset/chatty-llamas-wink.md diff --git a/.changeset/chatty-llamas-wink.md b/.changeset/chatty-llamas-wink.md new file mode 100644 index 00000000000..be979bb770a --- /dev/null +++ b/.changeset/chatty-llamas-wink.md @@ -0,0 +1,4 @@ +--- +'@clerk/shared': patch +'@clerk/clerk-react': patch +--- diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx index 748707139b7..a8ff0cfaef7 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/ForceOrganizationSelection.tsx @@ -1,6 +1,6 @@ import { useOrganizationList } from '@clerk/shared/react/index'; -import type { ComponentProps, PropsWithChildren } from 'react'; -import { useEffect, useState } from 'react'; +import type { PropsWithChildren } from 'react'; +import { useState } from 'react'; import { OrganizationListContext } from '@/ui/contexts'; import { Card } from '@/ui/elements/Card'; @@ -11,8 +11,13 @@ import { CreateOrganizationForm } from '../../CreateOrganization/CreateOrganizat import { OrganizationListPageList } from '../../OrganizationList/OrganizationListPage'; import { organizationListParams } from '../../OrganizationSwitcher/utils'; -const ForceOrganizationSelectionFlows = () => { - const { isLoading, hasData, currentFlow, setCurrentFlow } = useForceOrganizationSelectionFlows(); +/** + * @internal + */ +export const ForceOrganizationSelectionTask = withCardStateProvider(() => { + const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); + const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; + const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); if (isLoading) { return ( @@ -22,26 +27,16 @@ const ForceOrganizationSelectionFlows = () => { ); } - // Do not render the organization selection flow when organization memberships - // get invalidated after the create organization mutation - if (hasData && currentFlow !== 'create-organization') { - return ; + if (hasData) { + return ; } - return ; -}; + return ; +}); -const OrganizationSelectionPage = ({ setCurrentFlow }: CommonPageProps) => { +const OrganizationSelectionPage = () => { const [showCreateOrganizationForm, setShowCreateOrganizationForm] = useState(false); - useEffect(() => { - setCurrentFlow('organization-selection'); - }, [setCurrentFlow]); - - if (showCreateOrganizationForm) { - return ; - } - return ( { }} > - setShowCreateOrganizationForm(true)} /> + {showCreateOrganizationForm ? ( + ({ + padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`, + })} + > + setShowCreateOrganizationForm(false)} + /> + + ) : ( + setShowCreateOrganizationForm(true)} /> + )} ); }; -const CreateOrganizationPage = ({ - onCancel, - setCurrentFlow, -}: CommonPageProps & Pick, 'onCancel'>) => { - useEffect(() => { - setCurrentFlow('create-organization'); - }, [setCurrentFlow]); - +const CreateOrganizationPage = () => { return ( @@ -112,29 +114,3 @@ const FlowLoadingState = () => ( /> ); - -type Flow = 'create-organization' | 'organization-selection'; - -type CommonPageProps = { - setCurrentFlow: React.Dispatch>; -}; - -const useForceOrganizationSelectionFlows = () => { - const [currentFlow, setCurrentFlow] = useState(); - const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams); - - const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; - const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); - - return { - currentFlow, - setCurrentFlow, - hasData, - isLoading, - }; -}; - -/** - * @internal - */ -export const ForceOrganizationSelectionTask = withCardStateProvider(ForceOrganizationSelectionFlows); From e01d4f536b6c226473170ae9a6e3236cb92353cb Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:13:46 -0300 Subject: [PATCH 10/10] fix: Revalidate auth state for Next.js server --- .changeset/chatty-llamas-wink.md | 4 ---- packages/clerk-js/bundlewatch.config.json | 4 ++-- packages/clerk-js/src/core/clerk.ts | 22 ++++++++++++++++++- .../unstable/page-objects/sessionTask.ts | 1 - 4 files changed, 23 insertions(+), 8 deletions(-) delete mode 100644 .changeset/chatty-llamas-wink.md diff --git a/.changeset/chatty-llamas-wink.md b/.changeset/chatty-llamas-wink.md deleted file mode 100644 index be979bb770a..00000000000 --- a/.changeset/chatty-llamas-wink.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -'@clerk/shared': patch -'@clerk/clerk-react': patch ---- diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index a5e9ec5b3b0..b6e1054e5d4 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -3,8 +3,8 @@ { "path": "./dist/clerk.js", "maxSize": "612kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" }, - { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "108.75KB" }, + { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "110KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f46fa09c0f5..7dc496acdd1 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1283,7 +1283,16 @@ export class Clerk implements ClerkInterface { }; public __experimental_navigateToTask = async ({ redirectUrlComplete }: NextTaskParams = {}): Promise => { - const session = await this.session?.reload(); + /** + * Invalidate previously cache pages with auth state before navigating + */ + const onBeforeSetActive: SetActiveHook = + typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function' + ? window.__unstable__onBeforeSetActive + : noop; + await onBeforeSetActive(); + + const session = this.session; if (!session || !this.environment) { return; } @@ -1311,6 +1320,17 @@ export class Clerk implements ClerkInterface { this.#setAccessors(session); this.#emit(); + + /** + * Invoke the Next.js middleware to synchronize server and client state after resolving a session task. + * This ensures that any server-side logic depending on the session status (like middleware-based + * redirects or protected routes) correctly reflects the updated client authentication state. + */ + const onAfterSetActive: SetActiveHook = + typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function' + ? window.__unstable__onAfterSetActive + : noop; + await onAfterSetActive(); }; public addListener = (listener: ListenerCallback): UnsubscribeCallback => { diff --git a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts index c5721982326..6295e6ffbd5 100644 --- a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts +++ b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts @@ -12,7 +12,6 @@ export const createSessionTaskComponentPageObject = (testArgs: { page: EnhancedP const createOrganizationButton = page.getByRole('button', { name: /create organization/i }); await expect(createOrganizationButton).toBeVisible(); - expect(page.url()).toContain('add-organization'); await page.locator('input[name=name]').fill(fakeOrganization.name); await page.locator('input[name=slug]').fill(fakeOrganization.slug);