From 6622e0a46271fc1f4a5822ec066c1e5c77c9546a Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:40:52 -0300 Subject: [PATCH 01/21] Add `tasks` on `Session` resource --- packages/types/src/index.ts | 1 + packages/types/src/tasks.ts | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 packages/types/src/tasks.ts diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ea41eaa12aa..b122e4cce9d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -65,3 +65,4 @@ export * from './customMenuItems'; export * from './samlConnection'; export * from './waitlist'; export * from './snapshots'; +export * from './tasks'; diff --git a/packages/types/src/tasks.ts b/packages/types/src/tasks.ts new file mode 100644 index 00000000000..068ebd3e9b7 --- /dev/null +++ b/packages/types/src/tasks.ts @@ -0,0 +1,3 @@ +export interface SessionTasks { + orgs: Record; +} From 7d2d43d553029b0c5f93eef323782398d6c9ff9a Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:09:00 -0300 Subject: [PATCH 02/21] Add changeset --- packages/types/src/index.ts | 1 - packages/types/src/tasks.ts | 3 --- 2 files changed, 4 deletions(-) delete mode 100644 packages/types/src/tasks.ts diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b122e4cce9d..ea41eaa12aa 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -65,4 +65,3 @@ export * from './customMenuItems'; export * from './samlConnection'; export * from './waitlist'; export * from './snapshots'; -export * from './tasks'; diff --git a/packages/types/src/tasks.ts b/packages/types/src/tasks.ts deleted file mode 100644 index 068ebd3e9b7..00000000000 --- a/packages/types/src/tasks.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface SessionTasks { - orgs: Record; -} From d4f072e67f5f002243503ad69ad1a5e3bb37bcf0 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:59:12 -0300 Subject: [PATCH 03/21] Update redirect guard to explict check for `active` Previously, all session statuses were leading to a redirect to after sign-in URL Now, when the session gets set with a `pending` status, it won't redirect to after sign in URL, but instead display the pending task route --- packages/clerk-js/src/utils/componentGuards.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts index f0084a6a47d..273e7d446b3 100644 --- a/packages/clerk-js/src/utils/componentGuards.ts +++ b/packages/clerk-js/src/utils/componentGuards.ts @@ -7,7 +7,7 @@ export type ComponentGuard = ( ) => boolean; export const sessionExistsAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { - return !!(clerk.session && environment?.authConfig.singleSessionMode); + return !!(clerk.session?.status === 'active' && environment?.authConfig.singleSessionMode); }; export const noUserExists: ComponentGuard = clerk => { From e307f3af0b8e9139fd1d314f88e0c0054f88d405 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:09:50 -0300 Subject: [PATCH 04/21] Display pending task routes --- .../ui/components/PendingTask/PendingTask.tsx | 50 +++++++++++++++++++ .../src/ui/components/PendingTask/index.ts | 1 + .../src/ui/components/SignIn/SignIn.tsx | 2 + .../src/ui/components/SignUp/SignUp.tsx | 2 + 4 files changed, 55 insertions(+) create mode 100644 packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx create mode 100644 packages/clerk-js/src/ui/components/PendingTask/index.ts diff --git a/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx b/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx new file mode 100644 index 00000000000..7f46e52f584 --- /dev/null +++ b/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx @@ -0,0 +1,50 @@ +import { useSessionContext } from '@clerk/shared/react/index'; +import type { SessionTasks } from '@clerk/types'; +import type { ComponentType } from 'react'; +import { withRedirectToAfterSignIn } from 'ui/common'; + +import { Route } from '../../../ui/router'; +import { OrganizationList } from '../OrganizationList'; + +const paths: Record = { + orgs: 'select-organization', +}; + +const TaskRegistry: Record = { + orgs: OrganizationList, +}; + +function usePendingTask() { + const session = useSessionContext(); + + if (!session) { + return null; + } + + const [pendingTask] = Object.entries(session.tasks ?? {}).map(([task, value]) => ({ + task, + ...value, + })); + + return pendingTask; +} + +export function _PendingTask() { + const pendingTask = usePendingTask(); + + if (!pendingTask) { + return null; + } + + const Task = TaskRegistry['orgs']; + // @ts-ignore + const path = paths[pendingTask.task]; + + return ( + + + + ); +} + +export const PendingTask = withRedirectToAfterSignIn(_PendingTask); diff --git a/packages/clerk-js/src/ui/components/PendingTask/index.ts b/packages/clerk-js/src/ui/components/PendingTask/index.ts new file mode 100644 index 00000000000..fd98317a8c6 --- /dev/null +++ b/packages/clerk-js/src/ui/components/PendingTask/index.ts @@ -0,0 +1 @@ +export * from './PendingTask'; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index a767b6dbf1e..c5ffbb7ebb5 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -14,6 +14,7 @@ import { } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; +import { PendingTask } from '../PendingTask'; import { SignUpContinue } from '../SignUp/SignUpContinue'; import { SignUpSSOCallback } from '../SignUp/SignUpSSOCallback'; import { SignUpStart } from '../SignUp/SignUpStart'; @@ -138,6 +139,7 @@ function SignInRoutes(): JSX.Element { + ); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 430ae2f53a9..88035c000e6 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -6,6 +6,7 @@ import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowC import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; +import { PendingTask } from '../PendingTask'; import { SignUpContinue } from './SignUpContinue'; import { SignUpSSOCallback } from './SignUpSSOCallback'; import { SignUpStart } from './SignUpStart'; @@ -80,6 +81,7 @@ function SignUpRoutes(): JSX.Element { + ); From 0d0a71718dd81799203f32c072d209de1a7d7bf5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:57:36 -0300 Subject: [PATCH 05/21] Update handling for array of tasks --- .../src/ui/components/PendingTask/PendingTask.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx b/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx index 7f46e52f584..e8d2e537e91 100644 --- a/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx +++ b/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx @@ -1,16 +1,16 @@ import { useSessionContext } from '@clerk/shared/react/index'; -import type { SessionTasks } from '@clerk/types'; +import type { SessionTask } from '@clerk/types'; import type { ComponentType } from 'react'; import { withRedirectToAfterSignIn } from 'ui/common'; import { Route } from '../../../ui/router'; import { OrganizationList } from '../OrganizationList'; -const paths: Record = { +const paths: Record = { orgs: 'select-organization', }; -const TaskRegistry: Record = { +const TaskRegistry: Record = { orgs: OrganizationList, }; @@ -21,10 +21,7 @@ function usePendingTask() { return null; } - const [pendingTask] = Object.entries(session.tasks ?? {}).map(([task, value]) => ({ - task, - ...value, - })); + const [pendingTask] = session.tasks ?? []; return pendingTask; } @@ -37,8 +34,7 @@ export function _PendingTask() { } const Task = TaskRegistry['orgs']; - // @ts-ignore - const path = paths[pendingTask.task]; + const path = paths[pendingTask.key]; return ( From f44a7ec7fdc7d0f0eb28d253c854ca1087e7ba37 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 18 Feb 2025 19:16:07 -0300 Subject: [PATCH 06/21] Fix session task key --- .changeset/old-cherries-laugh.md | 6 +++ .../clerk-js/src/core/resources/Session.ts | 4 ++ packages/clerk-js/src/ui/common/tasks.ts | 8 ++++ .../ui/components/PendingTask/PendingTask.tsx | 46 ------------------- .../src/ui/components/PendingTask/index.ts | 1 - .../src/ui/components/SignIn/SignIn.tsx | 12 ++++- .../src/ui/components/SignUp/SignUp.tsx | 12 ++++- .../clerk-js/src/ui/components/Task/Task.tsx | 30 ++++++++++++ .../Task/__tests__/PendingTask.spec.ts | 1 + .../__tests__/usePendingTaskRoute.spec.ts | 1 + .../src/ui/components/Task/useTaskRoute.tsx | 25 ++++++++++ .../clerk-js/src/utils/componentGuards.ts | 2 +- packages/types/src/session.ts | 3 +- 13 files changed, 98 insertions(+), 53 deletions(-) create mode 100644 .changeset/old-cherries-laugh.md create mode 100644 packages/clerk-js/src/ui/common/tasks.ts delete mode 100644 packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx delete mode 100644 packages/clerk-js/src/ui/components/PendingTask/index.ts create mode 100644 packages/clerk-js/src/ui/components/Task/Task.tsx create mode 100644 packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts create mode 100644 packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts create mode 100644 packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx diff --git a/.changeset/old-cherries-laugh.md b/.changeset/old-cherries-laugh.md new file mode 100644 index 00000000000..91fa8dfad0c --- /dev/null +++ b/.changeset/old-cherries-laugh.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Display pending task on `SignIn` and `SignUp` components diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index d10845636f2..67b4d6c52e5 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -303,4 +303,8 @@ export class Session extends BaseResource implements SessionResource { return token.getRawString() || null; }); } + + get hasTask() { + return (this.tasks ?? []).length > 0; + } } diff --git a/packages/clerk-js/src/ui/common/tasks.ts b/packages/clerk-js/src/ui/common/tasks.ts new file mode 100644 index 00000000000..0c775e20316 --- /dev/null +++ b/packages/clerk-js/src/ui/common/tasks.ts @@ -0,0 +1,8 @@ +import type { SessionTask } from '@clerk/types'; + +/** + * @internal + */ +export const sessionTaskRoutePaths: Record = { + org: 'select-organization', +}; diff --git a/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx b/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx deleted file mode 100644 index e8d2e537e91..00000000000 --- a/packages/clerk-js/src/ui/components/PendingTask/PendingTask.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useSessionContext } from '@clerk/shared/react/index'; -import type { SessionTask } from '@clerk/types'; -import type { ComponentType } from 'react'; -import { withRedirectToAfterSignIn } from 'ui/common'; - -import { Route } from '../../../ui/router'; -import { OrganizationList } from '../OrganizationList'; - -const paths: Record = { - orgs: 'select-organization', -}; - -const TaskRegistry: Record = { - orgs: OrganizationList, -}; - -function usePendingTask() { - const session = useSessionContext(); - - if (!session) { - return null; - } - - const [pendingTask] = session.tasks ?? []; - - return pendingTask; -} - -export function _PendingTask() { - const pendingTask = usePendingTask(); - - if (!pendingTask) { - return null; - } - - const Task = TaskRegistry['orgs']; - const path = paths[pendingTask.key]; - - return ( - - - - ); -} - -export const PendingTask = withRedirectToAfterSignIn(_PendingTask); diff --git a/packages/clerk-js/src/ui/components/PendingTask/index.ts b/packages/clerk-js/src/ui/components/PendingTask/index.ts deleted file mode 100644 index fd98317a8c6..00000000000 --- a/packages/clerk-js/src/ui/components/PendingTask/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './PendingTask'; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index c5ffbb7ebb5..65105d231ad 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -14,12 +14,12 @@ import { } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; -import { PendingTask } from '../PendingTask'; import { SignUpContinue } from '../SignUp/SignUpContinue'; import { SignUpSSOCallback } from '../SignUp/SignUpSSOCallback'; import { SignUpStart } from '../SignUp/SignUpStart'; import { SignUpVerifyEmail } from '../SignUp/SignUpVerifyEmail'; import { SignUpVerifyPhone } from '../SignUp/SignUpVerifyPhone'; +import { useTaskRoute } from '../Task/useTaskRoute'; import { ResetPassword } from './ResetPassword'; import { ResetPasswordSuccess } from './ResetPasswordSuccess'; import { SignInAccountSwitcher } from './SignInAccountSwitcher'; @@ -39,6 +39,7 @@ function RedirectToSignIn() { function SignInRoutes(): JSX.Element { const signInContext = useSignInContext(); const signUpContext = useSignUpContext(); + const taskRoute = useTaskRoute(); return ( @@ -133,13 +134,20 @@ function SignInRoutes(): JSX.Element { )} + {taskRoute && ( + // todo - fix routing underlying issue, do not use index + // for some reason, a redirection to sign-in is happening even when `setActive` navigates to `select-organization` + + )} - ); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 88035c000e6..5c3a5363ed8 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -6,7 +6,7 @@ import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowC import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; -import { PendingTask } from '../PendingTask'; +import { useTaskRoute } from '../Task/useTaskRoute'; import { SignUpContinue } from './SignUpContinue'; import { SignUpSSOCallback } from './SignUpSSOCallback'; import { SignUpStart } from './SignUpStart'; @@ -23,6 +23,7 @@ function RedirectToSignUp() { function SignUpRoutes(): JSX.Element { const signUpContext = useSignUpContext(); + const taskRoute = useTaskRoute(); return ( @@ -75,13 +76,20 @@ function SignUpRoutes(): JSX.Element { + {taskRoute && ( + + )} - ); diff --git a/packages/clerk-js/src/ui/components/Task/Task.tsx b/packages/clerk-js/src/ui/components/Task/Task.tsx new file mode 100644 index 00000000000..fbfd1016b05 --- /dev/null +++ b/packages/clerk-js/src/ui/components/Task/Task.tsx @@ -0,0 +1,30 @@ +import { useSessionContext } from '@clerk/shared/react/index'; +import type { SessionTask } from '@clerk/types'; +import type { ComponentType } from 'react'; + +import { OrganizationListContext } from '../../contexts'; +import { OrganizationList } from '../OrganizationList'; + +const TaskRegistry: Record = { + org: () => ( + + + + ), +}; + +/** + * @internal + */ +export function Task(): React.ReactNode { + const session = useSessionContext(); + + if (!session?.hasTask) { + return null; + } + + const [task] = session.tasks ?? []; + const Content = TaskRegistry[task.key]; + + return ; +} diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts b/packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts new file mode 100644 index 00000000000..65b3dba385b --- /dev/null +++ b/packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts @@ -0,0 +1 @@ +// todo diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts b/packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts new file mode 100644 index 00000000000..65b3dba385b --- /dev/null +++ b/packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts @@ -0,0 +1 @@ +// todo diff --git a/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx b/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx new file mode 100644 index 00000000000..0a7f905be26 --- /dev/null +++ b/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx @@ -0,0 +1,25 @@ +import { useSessionContext } from '@clerk/shared/react/index'; +import type { Route } from '@clerk/shared/router'; +import type { ComponentProps } from 'react'; + +import { sessionTaskRoutePaths } from '../../common/tasks'; +import { Task } from './Task'; + +/** + * Maps a session task key to routing props and content + * @internal + */ +export function useTaskRoute(): ComponentProps | null { + const session = useSessionContext(); + + if (!session?.hasTask) { + return null; + } + + const [task] = session.tasks ?? []; + + return { + children: , + path: sessionTaskRoutePaths[task.key], + }; +} diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts index 273e7d446b3..cea9944da74 100644 --- a/packages/clerk-js/src/utils/componentGuards.ts +++ b/packages/clerk-js/src/utils/componentGuards.ts @@ -7,7 +7,7 @@ export type ComponentGuard = ( ) => boolean; export const sessionExistsAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { - return !!(clerk.session?.status === 'active' && environment?.authConfig.singleSessionMode); + return !!(clerk.isSignedIn && !clerk.session?.hasTask && environment?.authConfig.singleSessionMode); }; export const noUserExists: ComponentGuard = clerk => { diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index db4772c3813..87ed3571a14 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -104,6 +104,7 @@ export interface SessionResource extends ClerkResource { lastActiveAt: Date; actor: ActJWTClaim | null; tasks: Array | null; + hasTask: boolean; user: UserResource | null; publicUserData: PublicUserData; end: () => Promise; @@ -197,7 +198,7 @@ export interface PublicUserData { } export interface SessionTask { - key: 'orgs'; + key: 'org'; } export type GetTokenOptions = { From 43beb76649b26e1caf5a59efaf47e72d25a69761 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:24:12 -0300 Subject: [PATCH 07/21] Implement unit tests for Task component --- .../clerk-js/src/ui/components/Task/Task.tsx | 2 +- .../Task/__tests__/PendingTask.spec.ts | 1 - .../components/Task/__tests__/Task.test.tsx | 137 ++++++++++++++++++ .../__tests__/usePendingTaskRoute.spec.ts | 1 - .../src/ui/utils/test/fixtureHelpers.ts | 4 +- 5 files changed, 141 insertions(+), 4 deletions(-) delete mode 100644 packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts create mode 100644 packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx delete mode 100644 packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts diff --git a/packages/clerk-js/src/ui/components/Task/Task.tsx b/packages/clerk-js/src/ui/components/Task/Task.tsx index fbfd1016b05..b4f027807fa 100644 --- a/packages/clerk-js/src/ui/components/Task/Task.tsx +++ b/packages/clerk-js/src/ui/components/Task/Task.tsx @@ -26,5 +26,5 @@ export function Task(): React.ReactNode { const [task] = session.tasks ?? []; const Content = TaskRegistry[task.key]; - return ; + return Content ? : null; } diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts b/packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts deleted file mode 100644 index 65b3dba385b..00000000000 --- a/packages/clerk-js/src/ui/components/Task/__tests__/PendingTask.spec.ts +++ /dev/null @@ -1 +0,0 @@ -// todo diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx b/packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx new file mode 100644 index 00000000000..8c8d36196d4 --- /dev/null +++ b/packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx @@ -0,0 +1,137 @@ +import { render, waitFor } from '../../../../testUtils'; +import { createFakeUserOrganizationMembership } from '../../../../ui/components/OrganizationSwitcher/__tests__/utlis'; +import { bindCreateFixtures } from '../../../utils/test/createFixtures'; +import { Task } from '../Task'; + +describe('Task', () => { + describe.each(['SignIn', 'SignUp'] satisfies Array[0]>)( + 'after %s flow', + flow => { + const { createFixtures } = bindCreateFixtures(flow); + + describe('with session task', () => { + it('renders the component', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + session: { + status: 'pending', + tasks: [{ key: 'org' }], + }, + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Org1', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const { queryByRole } = render(, { wrapper }); + await waitFor(() => { + expect(queryByRole('heading', { name: /choose an organization/i })).toBeInTheDocument(); + }); + }); + }); + + describe('without session task', () => { + it('does not render the component', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + session: { + status: 'active', + }, + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Org1', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const { queryByRole } = render(, { wrapper }); + await waitFor(() => { + expect(queryByRole('heading', { name: /choose an organization/i })).not.toBeInTheDocument(); + }); + }); + }); + + describe('with invalid session key', () => { + it('does not render the component', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + session: { + status: 'pending', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + tasks: [{ key: 'invalid' }], + }, + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Org1', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const { queryByRole } = render(, { wrapper }); + await waitFor(() => { + expect(queryByRole('heading', { name: /choose an organization/i })).not.toBeInTheDocument(); + }); + }); + }); + }, + ); +}); diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts b/packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts deleted file mode 100644 index 65b3dba385b..00000000000 --- a/packages/clerk-js/src/ui/components/Task/__tests__/usePendingTaskRoute.spec.ts +++ /dev/null @@ -1 +0,0 @@ -// todo diff --git a/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts b/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts index 618589791b0..b2bb53695e7 100644 --- a/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts +++ b/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts @@ -49,6 +49,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => { external_accounts?: Array>; saml_accounts?: Array>; organization_memberships?: Array; + session?: Partial; }; const createPublicUserData = (params: WithUserParams) => { @@ -62,7 +63,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => { } as PublicUserDataJSON; }; - const withUser = (params: WithUserParams) => { + const withUser = ({ session: sessionOverrides, ...params }: WithUserParams) => { baseClient.sessions = baseClient.sessions || []; // set the first organization as active @@ -87,6 +88,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => { last_active_token: { jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NzU4NzY3OTAsImRhdGEiOiJmb29iYXIiLCJpYXQiOjE2NzU4NzY3MzB9.Z1BC47lImYvaAtluJlY-kBo0qOoAk42Xb-gNrB2SxJg', }, + ...sessionOverrides, } as SessionJSON; baseClient.sessions.push(session); }; From 46fe2b802688e2231b727182de3534ab9bc03ad0 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:38:39 -0300 Subject: [PATCH 08/21] Add unit tests for `useTaskRoute` --- .changeset/old-cherries-laugh.md | 2 +- .../src/ui/components/SignIn/SignIn.tsx | 9 +- .../src/ui/components/SignUp/SignUp.tsx | 9 +- .../components/Task/__tests__/Task.test.tsx | 6 +- .../Task/__tests__/useTaskRoute.test.tsx | 173 ++++++++++++++++++ .../src/ui/components/Task/useTaskRoute.tsx | 2 +- 6 files changed, 180 insertions(+), 21 deletions(-) create mode 100644 packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx diff --git a/.changeset/old-cherries-laugh.md b/.changeset/old-cherries-laugh.md index 91fa8dfad0c..c3530d1861b 100644 --- a/.changeset/old-cherries-laugh.md +++ b/.changeset/old-cherries-laugh.md @@ -3,4 +3,4 @@ '@clerk/types': patch --- -Display pending task on `SignIn` and `SignUp` components +Display after-auth tasks on `SignIn` and `SignUp` components diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 65105d231ad..ff92a24035b 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -134,14 +134,7 @@ function SignInRoutes(): JSX.Element { )} - {taskRoute && ( - // todo - fix routing underlying issue, do not use index - // for some reason, a redirection to sign-in is happening even when `setActive` navigates to `select-organization` - - )} + {taskRoute && } diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 5c3a5363ed8..6b92c4c52d1 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -76,14 +76,7 @@ function SignUpRoutes(): JSX.Element { - {taskRoute && ( - - )} + {taskRoute && } diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx b/packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx index 8c8d36196d4..7c20395b8c8 100644 --- a/packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx +++ b/packages/clerk-js/src/ui/components/Task/__tests__/Task.test.tsx @@ -9,7 +9,7 @@ describe('Task', () => { flow => { const { createFixtures } = bindCreateFixtures(flow); - describe('with session task', () => { + describe('with task', () => { it('renders the component', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); @@ -50,7 +50,7 @@ describe('Task', () => { }); }); - describe('without session task', () => { + describe('without task', () => { it('does not render the component', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); @@ -90,7 +90,7 @@ describe('Task', () => { }); }); - describe('with invalid session key', () => { + describe('with invalid key', () => { it('does not render the component', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx b/packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx new file mode 100644 index 00000000000..5a74351acdd --- /dev/null +++ b/packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx @@ -0,0 +1,173 @@ +import { render, waitFor } from '../../../../testUtils'; +import { HashRouter, Route, Switch } from '../../../../ui/router'; +import { bindCreateFixtures } from '../../../utils/test/createFixtures'; +import { createFakeUserOrganizationMembership } from '../../OrganizationSwitcher/__tests__/utlis'; +import { useTaskRoute } from '../useTaskRoute'; + +const oldWindowLocation = window.location; +const setWindowOrigin = (origin: string) => { + // @ts-ignore + delete window.location; + // the URL interface is very similar to window.location + // we use it to easily mock the location methods in tests + (window.location as any) = new URL(origin); +}; + +const MockRoute = (): JSX.Element => { + const taskRoute = useTaskRoute(); + + return ( + + + {taskRoute?.children} + + + ); +}; + +describe('useTaskRoute', () => { + describe.each(['SignIn', 'SignUp'] satisfies Array[0]>)( + 'after %s flow', + flow => { + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + window.location = oldWindowLocation; + }); + + const { createFixtures } = bindCreateFixtures(flow); + + describe('with task', () => { + it('renders the component', async () => { + setWindowOrigin('http://dashboard.example.com/#/select-organization'); + + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + session: { + status: 'pending', + tasks: [{ key: 'org' }], + }, + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Org1', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const { queryByRole } = render(, { wrapper }); + await waitFor(() => { + expect(queryByRole('heading', { name: /choose an organization/i })).toBeInTheDocument(); + }); + }); + }); + + describe('without task', () => { + it('does not render the component', async () => { + setWindowOrigin('http://dashboard.example.com/#/select-organization'); + + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + session: { + status: 'active', + }, + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Org1', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const { queryByRole } = render(, { wrapper }); + await waitFor(() => { + expect(queryByRole('heading', { name: /choose an organization/i })).not.toBeInTheDocument(); + }); + }); + }); + + describe('with invalid key', () => { + it('does not render the component', async () => { + setWindowOrigin('http://dashboard.example.com/#/select-organization'); + + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + session: { + status: 'pending', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + tasks: [{ key: 'invalid' }], + }, + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Org1', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const { queryByRole } = render(, { wrapper }); + await waitFor(() => { + expect(queryByRole('heading', { name: /choose an organization/i })).not.toBeInTheDocument(); + }); + }); + }); + }, + ); +}); diff --git a/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx b/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx index 0a7f905be26..2153b65138f 100644 --- a/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx +++ b/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx @@ -6,7 +6,7 @@ import { sessionTaskRoutePaths } from '../../common/tasks'; import { Task } from './Task'; /** - * Maps a session task key to routing props and content + * Maps a task key to routing props and content * @internal */ export function useTaskRoute(): ComponentProps | null { From 9234c79f457395d2ccecfaa1095ea126ca3dcafe Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 20 Feb 2025 09:05:50 -0300 Subject: [PATCH 09/21] Add option for a custom tasks URL to cover custom flow --- packages/clerk-js/src/core/clerk.ts | 9 +++++++++ packages/clerk-js/src/utils/componentGuards.ts | 2 +- packages/types/src/clerk.ts | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 7470b340788..1b017113ad9 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1114,6 +1114,15 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } + public buildTasksUrl(options?: { initialValues?: Record }): string { + if (!this.environment) { + return ''; + } + const taskUrl = this.#options['tasksUrl']; + const initValues = new URLSearchParams(options?.initialValues || {}); + return buildURL({ base: taskUrl, hashSearchParams: [initValues] }, { stringify: true }); + } + public buildAfterMultiSessionSingleSignOutUrl(): string { if (!this.#options.afterMultiSessionSingleSignOutUrl) { return this.buildUrlWithAuth( diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts index cea9944da74..2552f347c48 100644 --- a/packages/clerk-js/src/utils/componentGuards.ts +++ b/packages/clerk-js/src/utils/componentGuards.ts @@ -7,7 +7,7 @@ export type ComponentGuard = ( ) => boolean; export const sessionExistsAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { - return !!(clerk.isSignedIn && !clerk.session?.hasTask && environment?.authConfig.singleSessionMode); + return !!(clerk.isSignedIn && environment?.authConfig.singleSessionMode); }; export const noUserExists: ComponentGuard = clerk => { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 1296685e084..60f5a9f8e5d 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -476,6 +476,11 @@ export interface Clerk { */ buildWaitlistUrl(opts?: { initialValues?: Record }): string; + /** + * Returns the url where a custom task page is rendered. + */ + buildTasksUrl(opts?: { initialValues?: Record }): string; + /** * * Redirects to the provided url after decorating it with the auth token for development instances. @@ -765,6 +770,8 @@ export type ClerkOptions = ClerkOptionsNavigation & sdkMetadata?: SDKMetadata; /** This URL will be used for any redirects that might happen and needs to point to your primary application on the client-side. This option is optional for production instances and required for development instances. */ waitlistUrl?: string; + /** This URL will be used for any redirects that might happen and needs to point to your primary application on the client-side. This option is optional and defaults to `signInUrl` */ + tasksUrl?: string; /** * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. */ From d38dbbc03d1ad00ba346e726c358c544055224bc Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:20:53 -0300 Subject: [PATCH 10/21] Refactor redirect guards for tasks --- packages/clerk-js/src/core/clerk.ts | 42 +++++++++++++++---- packages/clerk-js/src/core/events.ts | 4 +- .../clerk-js/src/core/resources/Session.ts | 4 +- .../clerk-js/src/ui/common/withRedirect.tsx | 18 +++++--- .../clerk-js/src/ui/components/Task/Task.tsx | 4 +- .../src/ui/components/Task/useTaskRoute.tsx | 2 +- .../clerk-js/src/utils/componentGuards.ts | 7 +++- packages/types/src/clerk.ts | 6 +-- packages/types/src/session.ts | 2 +- 9 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 1b017113ad9..8b2fd422ed8 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -45,6 +45,7 @@ import type { RedirectOptions, Resources, SDKMetadata, + SessionTask, SetActiveParams, SignedInSessionResource, SignInProps, @@ -66,6 +67,7 @@ import type { Web3Provider, } from '@clerk/types'; +import { sessionTaskRoutePaths } from '../ui/common/tasks'; import type { MountComponentRenderer } from '../ui/Components'; import { ALLOWED_PROTOCOLS, @@ -89,11 +91,11 @@ import { isError, isOrganizationId, isRedirectForFAPIInitiatedFlow, + isSignedInAndSingleSessionModeEnabled, noOrganizationExists, noUserExists, removeClerkQueryParam, requiresUserInput, - sessionExistsAndSingleSessionModeEnabled, stripOrigin, windowNavigate, } from '../utils'; @@ -427,7 +429,7 @@ export class Clerk implements ClerkInterface { public openSignIn = (props?: SignInProps): void => { this.assertComponentsReady(this.#componentControls); - if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) { + if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: 'cannot_render_single_session_enabled', @@ -481,7 +483,7 @@ export class Clerk implements ClerkInterface { public openSignUp = (props?: SignUpProps): void => { this.assertComponentsReady(this.#componentControls); - if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) { + if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: 'cannot_render_single_session_enabled', @@ -930,6 +932,11 @@ export class Clerk implements ClerkInterface { eventBus.dispatch(events.TokenUpdate, { token: null }); } + const hasNewTask = newSession && !!newSession?.tasks; + if (hasNewTask && newSession) { + eventBus.dispatch(events.NewSessionTask, newSession); + } + //2. If there's a beforeEmit, typically we're navigating. Emit the session as // undefined, then wait for beforeEmit to complete before emitting the new session. // When undefined, neither SignedIn nor SignedOut renders, which avoids flickers or @@ -946,6 +953,7 @@ export class Clerk implements ClerkInterface { beforeUnloadTracker?.stopTracking(); } + // todo -> how to handle navigation to after sign-in? if (redirectUrl && !beforeEmit) { beforeUnloadTracker?.startTracking(); this.#setTransitiveState(); @@ -1037,6 +1045,7 @@ export class Clerk implements ClerkInterface { ...(options?.metadata ? { __internal_metadata: options?.metadata } : {}), windowNavigate, }; + console.log(stripOrigin(toURL)); // React router only wants the path, search or hash portion. return await customNavigate(stripOrigin(toURL), metadata); }; @@ -1114,13 +1123,18 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - public buildTasksUrl(options?: { initialValues?: Record }): string { - if (!this.environment) { + public buildTasksUrl({ task, origin }: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string { + if (!task) { return ''; } - const taskUrl = this.#options['tasksUrl']; - const initValues = new URLSearchParams(options?.initialValues || {}); - return buildURL({ base: taskUrl, hashSearchParams: [initValues] }, { stringify: true }); + + const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; + const referrerIsSignUpUrl = signUpUrl && window.location.href.startsWith(signUpUrl); + + const originWithDefault = origin ?? (referrerIsSignUpUrl ? 'SignUp' : 'SignIn'); + const defaultUrlByOrigin = originWithDefault === 'SignIn' ? this.#options.signInUrl : this.#options.signUpUrl; + + return buildURL({ base: `${defaultUrlByOrigin}/${sessionTaskRoutePaths[task.key]}` }, { stringify: true }); } public buildAfterMultiSessionSingleSignOutUrl(): string { @@ -1244,6 +1258,13 @@ export class Clerk implements ClerkInterface { return; }; + public redirectToTask = async (...options: Parameters): Promise => { + if (inBrowser()) { + return this.navigate(this.buildTasksUrl(...options)); + } + return; + }; + public handleEmailLinkVerification = async ( params: HandleEmailLinkVerificationParams, customNavigate?: (to: string) => Promise, @@ -2085,6 +2106,11 @@ export class Clerk implements ClerkInterface { eventBus.on(events.UserSignOut, () => { this.#broadcastChannel?.postMessage({ type: 'signout' }); }); + + eventBus.on(events.NewSessionTask, session => { + console.log('new task 🍪'); + void this.redirectToTask({ task: session.currentTask }); + }); }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts index 7401dd91370..aa7c8bb0306 100644 --- a/packages/clerk-js/src/core/events.ts +++ b/packages/clerk-js/src/core/events.ts @@ -1,8 +1,9 @@ -import type { TokenResource } from '@clerk/types'; +import type { SignedInSessionResource, TokenResource } from '@clerk/types'; export const events = { TokenUpdate: 'token:update', UserSignOut: 'user:signOut', + NewSessionTask: 'sessionTask:new', } as const; type ClerkEvent = (typeof events)[keyof typeof events]; @@ -13,6 +14,7 @@ type TokenUpdatePayload = { token: TokenResource | null }; type EventPayload = { [events.TokenUpdate]: TokenUpdatePayload; [events.UserSignOut]: null; + [events.NewSessionTask]: SignedInSessionResource; }; const createEventBus = () => { diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 67b4d6c52e5..4ac8f4ad4a6 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -304,7 +304,7 @@ export class Session extends BaseResource implements SessionResource { }); } - get hasTask() { - return (this.tasks ?? []).length > 0; + get currentTask(): SessionTask | undefined { + return (this.tasks ?? [])[0]; } } diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index 43ec9b95172..9107a3ee301 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { warnings } from '../../core/warnings'; import type { ComponentGuard } from '../../utils'; -import { sessionExistsAndSingleSessionModeEnabled } from '../../utils'; +import { isSignedInAndSingleSessionModeEnabled } from '../../utils'; import { useEnvironment, useOptions, useSignInContext, useSignUpContext } from '../contexts'; import { useRouter } from '../router'; import type { AvailableComponentProps } from '../types'; @@ -60,8 +60,11 @@ export const withRedirectToAfterSignIn =

(Com const signInCtx = useSignInContext(); return withRedirect( Component, - sessionExistsAndSingleSessionModeEnabled, - ({ clerk }) => signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), + isSignedInAndSingleSessionModeEnabled, + ({ clerk }) => + clerk.session?.currentTask + ? clerk.buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignIn' }) + : signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignInComponentWhenSessionExists, )(props); }; @@ -79,8 +82,11 @@ export const withRedirectToAfterSignUp =

(Com const signUpCtx = useSignUpContext(); return withRedirect( Component, - sessionExistsAndSingleSessionModeEnabled, - ({ clerk }) => signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(), + isSignedInAndSingleSessionModeEnabled, + ({ clerk }) => + clerk.session?.currentTask + ? clerk.buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignUp' }) + : signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignUpComponentWhenSessionExists, )(props); }; @@ -93,7 +99,7 @@ export const withRedirectToAfterSignUp =

(Com export const withRedirectToHomeSingleSessionGuard =

(Component: ComponentType

) => withRedirect( Component, - sessionExistsAndSingleSessionModeEnabled, + isSignedInAndSingleSessionModeEnabled, ({ environment }) => environment.displayConfig.homeUrl, warnings.cannotRenderComponentWhenSessionExists, ); diff --git a/packages/clerk-js/src/ui/components/Task/Task.tsx b/packages/clerk-js/src/ui/components/Task/Task.tsx index b4f027807fa..0fb4445db2e 100644 --- a/packages/clerk-js/src/ui/components/Task/Task.tsx +++ b/packages/clerk-js/src/ui/components/Task/Task.tsx @@ -1,6 +1,6 @@ import { useSessionContext } from '@clerk/shared/react/index'; import type { SessionTask } from '@clerk/types'; -import type { ComponentType } from 'react'; +import { type ComponentType } from 'react'; import { OrganizationListContext } from '../../contexts'; import { OrganizationList } from '../OrganizationList'; @@ -19,7 +19,7 @@ const TaskRegistry: Record = { export function Task(): React.ReactNode { const session = useSessionContext(); - if (!session?.hasTask) { + if (!session?.currentTask) { return null; } diff --git a/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx b/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx index 2153b65138f..d15cb8eb993 100644 --- a/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx +++ b/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx @@ -12,7 +12,7 @@ import { Task } from './Task'; export function useTaskRoute(): ComponentProps | null { const session = useSessionContext(); - if (!session?.hasTask) { + if (!session?.currentTask) { return null; } diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts index 2552f347c48..cfc68da013e 100644 --- a/packages/clerk-js/src/utils/componentGuards.ts +++ b/packages/clerk-js/src/utils/componentGuards.ts @@ -6,7 +6,12 @@ export type ComponentGuard = ( options?: ClerkOptions, ) => boolean; -export const sessionExistsAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { +// todo -> add tests +// is signed in, and single session mode enabled, and active session -> redirect to after sign in +// is signed in, and single session mode, and pending session => redirect to task +// is signed in, and multi session mode, and active session -> let it be +// is signed in, and multi session mode, and pending session -> let it be +export const isSignedInAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { return !!(clerk.isSignedIn && environment?.authConfig.singleSessionMode); }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 60f5a9f8e5d..cfef7cada76 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -31,7 +31,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; -import type { SignedInSessionResource } from './session'; +import type { SessionTask, SignedInSessionResource } from './session'; import type { SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; @@ -479,7 +479,7 @@ export interface Clerk { /** * Returns the url where a custom task page is rendered. */ - buildTasksUrl(opts?: { initialValues?: Record }): string; + buildTasksUrl(options: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string; /** * @@ -770,8 +770,6 @@ export type ClerkOptions = ClerkOptionsNavigation & sdkMetadata?: SDKMetadata; /** This URL will be used for any redirects that might happen and needs to point to your primary application on the client-side. This option is optional for production instances and required for development instances. */ waitlistUrl?: string; - /** This URL will be used for any redirects that might happen and needs to point to your primary application on the client-side. This option is optional and defaults to `signInUrl` */ - tasksUrl?: string; /** * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. */ diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 87ed3571a14..ac809ce0d7c 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -104,7 +104,7 @@ export interface SessionResource extends ClerkResource { lastActiveAt: Date; actor: ActJWTClaim | null; tasks: Array | null; - hasTask: boolean; + currentTask?: SessionTask; user: UserResource | null; publicUserData: PublicUserData; end: () => Promise; From 1dc30d545d6afe5b740d5d3b301e1aa7a0bf64b1 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:40:22 -0300 Subject: [PATCH 11/21] Does not trigger `redirectUrl` logic --- .changeset/old-cherries-laugh.md | 2 +- packages/clerk-js/src/core/clerk.ts | 26 +++++++++++++------------- packages/react/src/isomorphicClerk.ts | 10 ++++++++++ packages/types/src/clerk.ts | 5 +++++ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.changeset/old-cherries-laugh.md b/.changeset/old-cherries-laugh.md index c3530d1861b..ee1f5487332 100644 --- a/.changeset/old-cherries-laugh.md +++ b/.changeset/old-cherries-laugh.md @@ -3,4 +3,4 @@ '@clerk/types': patch --- -Display after-auth tasks on `SignIn` and `SignUp` components +Navigate to after-auth tasks diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 8b2fd422ed8..e1e266bfb64 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -43,9 +43,9 @@ import type { PublicKeyCredentialWithAuthenticatorAssertionResponse, PublicKeyCredentialWithAuthenticatorAttestationResponse, RedirectOptions, + RedirectToTasksUrlOptions, Resources, SDKMetadata, - SessionTask, SetActiveParams, SignedInSessionResource, SignInProps, @@ -891,6 +891,12 @@ export class Clerk implements ClerkInterface { let newSession = session === undefined ? this.session : session; + // todo - handle the transition on token poller + const hasNewTask = newSession && !!newSession?.tasks; + if (hasNewTask && newSession) { + eventBus.dispatch(events.NewSessionTask, newSession); + } + // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. // We now want to set the last active organization id on that session (if it exists). @@ -932,11 +938,6 @@ export class Clerk implements ClerkInterface { eventBus.dispatch(events.TokenUpdate, { token: null }); } - const hasNewTask = newSession && !!newSession?.tasks; - if (hasNewTask && newSession) { - eventBus.dispatch(events.NewSessionTask, newSession); - } - //2. If there's a beforeEmit, typically we're navigating. Emit the session as // undefined, then wait for beforeEmit to complete before emitting the new session. // When undefined, neither SignedIn nor SignedOut renders, which avoids flickers or @@ -953,8 +954,7 @@ export class Clerk implements ClerkInterface { beforeUnloadTracker?.stopTracking(); } - // todo -> how to handle navigation to after sign-in? - if (redirectUrl && !beforeEmit) { + if (redirectUrl && !beforeEmit && !hasNewTask) { beforeUnloadTracker?.startTracking(); this.#setTransitiveState(); @@ -1045,7 +1045,6 @@ export class Clerk implements ClerkInterface { ...(options?.metadata ? { __internal_metadata: options?.metadata } : {}), windowNavigate, }; - console.log(stripOrigin(toURL)); // React router only wants the path, search or hash portion. return await customNavigate(stripOrigin(toURL), metadata); }; @@ -1123,7 +1122,8 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - public buildTasksUrl({ task, origin }: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string { + // todo(fix sign up navigation) + public buildTasksUrl({ task, origin }: RedirectToTasksUrlOptions): string { if (!task) { return ''; } @@ -1258,9 +1258,9 @@ export class Clerk implements ClerkInterface { return; }; - public redirectToTask = async (...options: Parameters): Promise => { + public redirectToTasks = async (options: RedirectToTasksUrlOptions): Promise => { if (inBrowser()) { - return this.navigate(this.buildTasksUrl(...options)); + return this.navigate(this.buildTasksUrl(options)); } return; }; @@ -2109,7 +2109,7 @@ export class Clerk implements ClerkInterface { eventBus.on(events.NewSessionTask, session => { console.log('new task 🍪'); - void this.redirectToTask({ task: session.currentTask }); + void this.redirectToTasks({ task: session.currentTask }); }); }; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 85202601e3d..8f5fb70f7a5 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -26,6 +26,7 @@ import type { OrganizationResource, OrganizationSwitcherProps, RedirectOptions, + RedirectToTasksUrlOptions, SetActiveParams, SignInProps, SignInRedirectOptions, @@ -333,6 +334,15 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + buildTasksUrl = (opts: RedirectToTasksUrlOptions): string | void => { + const callback = () => this.clerkjs?.buildTasksUrl(opts) || ''; + if (this.clerkjs && this.#loaded) { + return callback(); + } else { + this.premountMethodCalls.set('buildTasksUrl', callback); + } + }; + handleUnauthenticated = async () => { const callback = () => this.clerkjs?.handleUnauthenticated(); if (this.clerkjs && this.#loaded) { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index cfef7cada76..6569b382576 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -877,6 +877,11 @@ export type SignUpRedirectOptions = RedirectOptions & initialValues?: SignUpInitialValues; }; +export type RedirectToTasksUrlOptions = { + task?: SessionTask; + origin?: 'SignIn' | 'SignUp'; +}; + export type SetActiveParams = { /** * The session resource or session id (string version) to be set on the client. From 36e0c919814b133fca4079edd00293d27de7a1d3 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:37:36 -0300 Subject: [PATCH 12/21] Trigger navigation on client-piggybacking --- packages/clerk-js/src/core/clerk.ts | 38 +++++++++++++------ packages/clerk-js/src/core/events.ts | 2 + .../clerk-js/src/ui/common/withRedirect.tsx | 4 +- .../clerk-js/src/ui/router/BaseRouter.tsx | 1 + packages/react/src/isomorphicClerk.ts | 6 +-- packages/types/src/clerk.ts | 4 +- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index e1e266bfb64..ed97e1eaed5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -891,11 +891,9 @@ export class Clerk implements ClerkInterface { let newSession = session === undefined ? this.session : session; - // todo - handle the transition on token poller - const hasNewTask = newSession && !!newSession?.tasks; - if (hasNewTask && newSession) { - eventBus.dispatch(events.NewSessionTask, newSession); - } + const isResolvingSessionTasks = + !!newSession?.currentTask || + window.location.href.includes(this.internal__buildTasksUrl({ task: newSession?.currentTask })); // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. @@ -954,7 +952,7 @@ export class Clerk implements ClerkInterface { beforeUnloadTracker?.stopTracking(); } - if (redirectUrl && !beforeEmit && !hasNewTask) { + if (redirectUrl && !beforeEmit && !isResolvingSessionTasks) { beforeUnloadTracker?.startTracking(); this.#setTransitiveState(); @@ -1012,6 +1010,8 @@ export class Clerk implements ClerkInterface { return; } + console.log({ to }); + /** * Trigger all navigation listeners. In order for modal UI components to close. */ @@ -1122,19 +1122,18 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - // todo(fix sign up navigation) - public buildTasksUrl({ task, origin }: RedirectToTasksUrlOptions): string { + public internal__buildTasksUrl({ task, origin }: RedirectToTasksUrlOptions): string { if (!task) { return ''; } const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; - const referrerIsSignUpUrl = signUpUrl && window.location.href.startsWith(signUpUrl); + const referrerIsSignUpUrl = signUpUrl && window.location.href.includes(signUpUrl); const originWithDefault = origin ?? (referrerIsSignUpUrl ? 'SignUp' : 'SignIn'); const defaultUrlByOrigin = originWithDefault === 'SignIn' ? this.#options.signInUrl : this.#options.signUpUrl; - return buildURL({ base: `${defaultUrlByOrigin}/${sessionTaskRoutePaths[task.key]}` }, { stringify: true }); + return buildURL({ base: defaultUrlByOrigin, hashPath: sessionTaskRoutePaths[task.key] }, { stringify: true }); } public buildAfterMultiSessionSingleSignOutUrl(): string { @@ -1260,7 +1259,7 @@ export class Clerk implements ClerkInterface { public redirectToTasks = async (options: RedirectToTasksUrlOptions): Promise => { if (inBrowser()) { - return this.navigate(this.buildTasksUrl(options)); + return this.navigate(this.internal__buildTasksUrl(options)); } return; }; @@ -1758,11 +1757,21 @@ export class Clerk implements ClerkInterface { if (this.session) { const session = this.#getSessionFromClient(this.session.id); + const hasResolvedPreviousTask = this.session.currentTask != session?.currentTask; + // Note: this might set this.session to null this.#setAccessors(session); // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. eventBus.dispatch(events.TokenUpdate, { token: this.session?.lastActiveToken }); + + // Any FAPI call could lead to a task being unsatisfied such as app owners + // actions therefore the check must be done on client piggybacking + if (session?.currentTask) { + eventBus.dispatch(events.NewSessionTask, session); + } else if (session && hasResolvedPreviousTask) { + eventBus.dispatch(events.ResolvedSessionTask, session); + } } this.#emit(); @@ -2108,9 +2117,14 @@ export class Clerk implements ClerkInterface { }); eventBus.on(events.NewSessionTask, session => { - console.log('new task 🍪'); + console.log('new session task'); void this.redirectToTasks({ task: session.currentTask }); }); + + eventBus.on(events.ResolvedSessionTask, () => { + console.log('resolved task'); + void this.redirectToAfterSignIn(); + }); }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts index aa7c8bb0306..fb436223c49 100644 --- a/packages/clerk-js/src/core/events.ts +++ b/packages/clerk-js/src/core/events.ts @@ -4,6 +4,7 @@ export const events = { TokenUpdate: 'token:update', UserSignOut: 'user:signOut', NewSessionTask: 'sessionTask:new', + ResolvedSessionTask: 'sessionTask:resolve', } as const; type ClerkEvent = (typeof events)[keyof typeof events]; @@ -15,6 +16,7 @@ type EventPayload = { [events.TokenUpdate]: TokenUpdatePayload; [events.UserSignOut]: null; [events.NewSessionTask]: SignedInSessionResource; + [events.ResolvedSessionTask]: SignedInSessionResource; }; const createEventBus = () => { diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index 9107a3ee301..24b6b88cfcc 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -63,7 +63,7 @@ export const withRedirectToAfterSignIn =

(Com isSignedInAndSingleSessionModeEnabled, ({ clerk }) => clerk.session?.currentTask - ? clerk.buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignIn' }) + ? clerk.internal__buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignIn' }) : signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignInComponentWhenSessionExists, )(props); @@ -85,7 +85,7 @@ export const withRedirectToAfterSignUp =

(Com isSignedInAndSingleSessionModeEnabled, ({ clerk }) => clerk.session?.currentTask - ? clerk.buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignUp' }) + ? clerk.internal__buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignUp' }) : signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignUpComponentWhenSessionExists, )(props); diff --git a/packages/clerk-js/src/ui/router/BaseRouter.tsx b/packages/clerk-js/src/ui/router/BaseRouter.tsx index 1874116aa04..de123d74286 100644 --- a/packages/clerk-js/src/ui/router/BaseRouter.tsx +++ b/packages/clerk-js/src/ui/router/BaseRouter.tsx @@ -116,6 +116,7 @@ export const BaseRouter = ({ toURL.search = stringifyQueryParams(toQueryParams); } + console.log({ toURL }); const internalNavRes = await internalNavigate(toURL, { metadata: { navigationType: 'internal' } }); setRouteParts({ path: toURL.pathname, queryString: toURL.search }); return internalNavRes; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 8f5fb70f7a5..c7d11daaf53 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -334,12 +334,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - buildTasksUrl = (opts: RedirectToTasksUrlOptions): string | void => { - const callback = () => this.clerkjs?.buildTasksUrl(opts) || ''; + internal__buildTasksUrl = (opts: RedirectToTasksUrlOptions): string | void => { + const callback = () => this.clerkjs?.internal__buildTasksUrl(opts) || ''; if (this.clerkjs && this.#loaded) { return callback(); } else { - this.premountMethodCalls.set('buildTasksUrl', callback); + this.premountMethodCalls.set('internal__buildTasksUrl', callback); } }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 6569b382576..611bd5bdadd 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -477,9 +477,9 @@ export interface Clerk { buildWaitlistUrl(opts?: { initialValues?: Record }): string; /** - * Returns the url where a custom task page is rendered. + * Returns the url where a task page is rendered. */ - buildTasksUrl(options: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string; + internal__buildTasksUrl(options: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string; /** * From f1e64f1d1980f4dcbb38c915a21347233b8807a3 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:57:13 -0500 Subject: [PATCH 13/21] Set tasks URL on the sign-in/sign-up context --- packages/clerk-js/src/core/clerk.ts | 4 ++-- packages/clerk-js/src/ui/common/withRedirect.tsx | 8 ++------ packages/clerk-js/src/ui/contexts/components/SignIn.ts | 6 ++++++ packages/clerk-js/src/ui/contexts/components/SignUp.ts | 6 ++++++ packages/clerk-js/src/ui/router/BaseRouter.tsx | 1 - packages/clerk-js/src/utils/componentGuards.ts | 5 ----- packages/types/src/clerk.ts | 2 +- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index ed97e1eaed5..5e25dc34bb0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1010,7 +1010,7 @@ export class Clerk implements ClerkInterface { return; } - console.log({ to }); + console.log('Clerk.navigate is navigating to', { to }); /** * Trigger all navigation listeners. In order for modal UI components to close. @@ -1122,7 +1122,7 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - public internal__buildTasksUrl({ task, origin }: RedirectToTasksUrlOptions): string { + public internal__buildTasksUrl({ task = this.session?.currentTask, origin }: RedirectToTasksUrlOptions): string { if (!task) { return ''; } diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index 24b6b88cfcc..2bb41f7d4e6 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -62,9 +62,7 @@ export const withRedirectToAfterSignIn =

(Com Component, isSignedInAndSingleSessionModeEnabled, ({ clerk }) => - clerk.session?.currentTask - ? clerk.internal__buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignIn' }) - : signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), + clerk.session?.currentTask ? signInCtx.tasksUrl : signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignInComponentWhenSessionExists, )(props); }; @@ -84,9 +82,7 @@ export const withRedirectToAfterSignUp =

(Com Component, isSignedInAndSingleSessionModeEnabled, ({ clerk }) => - clerk.session?.currentTask - ? clerk.internal__buildTasksUrl({ task: clerk.session?.currentTask, origin: 'SignUp' }) - : signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), + clerk.session?.currentTask ? signUpCtx.tasksUrl : signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignUpComponentWhenSessionExists, )(props); }; diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 9bfdad969b2..6f748fae8bc 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -26,6 +26,7 @@ export type SignInContextType = SignInCtx & { emailLinkRedirectUrl: string; ssoCallbackUrl: string; isCombinedFlow: boolean; + tasksUrl: string; }; export const SignInContext = createContext(null); @@ -85,6 +86,10 @@ export const useSignInContext = (): SignInContextType => { signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); waitlistUrl = buildURL({ base: waitlistUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); + const tasksUrl = clerk.internal__buildTasksUrl({ + origin: 'SignIn', + }); + const authQueryString = redirectUrls.toSearchParams().toString(); const emailLinkRedirectUrl = buildRedirectUrl({ @@ -124,6 +129,7 @@ export const useSignInContext = (): SignInContextType => { ssoCallbackUrl, navigateAfterSignIn, signUpContinueUrl, + tasksUrl, queryParams, initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams }, authQueryString, diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index 32b4e4794c0..97803e27b1b 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -25,6 +25,7 @@ export type SignUpContextType = SignUpCtx & { isCombinedFlow: boolean; emailLinkRedirectUrl: string; ssoCallbackUrl: string; + tasksUrl: string; }; export const SignUpContext = createContext(null); @@ -83,6 +84,10 @@ export const useSignUpContext = (): SignUpContextType => { signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); waitlistUrl = buildURL({ base: waitlistUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); + const tasksUrl = clerk.internal__buildTasksUrl({ + origin: 'SignUp', + }); + const authQueryString = redirectUrls.toSearchParams().toString(); const emailLinkRedirectUrl = @@ -118,6 +123,7 @@ export const useSignUpContext = (): SignUpContextType => { afterSignInUrl, emailLinkRedirectUrl, ssoCallbackUrl, + tasksUrl, navigateAfterSignUp, queryParams, initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams }, diff --git a/packages/clerk-js/src/ui/router/BaseRouter.tsx b/packages/clerk-js/src/ui/router/BaseRouter.tsx index de123d74286..1874116aa04 100644 --- a/packages/clerk-js/src/ui/router/BaseRouter.tsx +++ b/packages/clerk-js/src/ui/router/BaseRouter.tsx @@ -116,7 +116,6 @@ export const BaseRouter = ({ toURL.search = stringifyQueryParams(toQueryParams); } - console.log({ toURL }); const internalNavRes = await internalNavigate(toURL, { metadata: { navigationType: 'internal' } }); setRouteParts({ path: toURL.pathname, queryString: toURL.search }); return internalNavRes; diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts index cfc68da013e..3349869c807 100644 --- a/packages/clerk-js/src/utils/componentGuards.ts +++ b/packages/clerk-js/src/utils/componentGuards.ts @@ -6,11 +6,6 @@ export type ComponentGuard = ( options?: ClerkOptions, ) => boolean; -// todo -> add tests -// is signed in, and single session mode enabled, and active session -> redirect to after sign in -// is signed in, and single session mode, and pending session => redirect to task -// is signed in, and multi session mode, and active session -> let it be -// is signed in, and multi session mode, and pending session -> let it be export const isSignedInAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { return !!(clerk.isSignedIn && environment?.authConfig.singleSessionMode); }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 611bd5bdadd..7f4d597a514 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -477,7 +477,7 @@ export interface Clerk { buildWaitlistUrl(opts?: { initialValues?: Record }): string; /** - * Returns the url where a task page is rendered. + * Returns the url where tasks get displayed. It defaults to `signInUrl` or `signUpUrl`. */ internal__buildTasksUrl(options: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string; From 9a5415dfcb8058cb4ed96b96383aaa0e3b807110 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:57:17 -0500 Subject: [PATCH 14/21] Introduce separate redirect guards for tasks --- packages/clerk-js/src/core/warnings.ts | 4 ++ .../clerk-js/src/ui/common/withRedirect.tsx | 46 +++++++++++++++++-- .../SignIn/SignInAccountSwitcher.tsx | 6 ++- .../ui/components/SignIn/SignInFactorOne.tsx | 6 ++- .../ui/components/SignIn/SignInFactorTwo.tsx | 6 ++- .../components/SignIn/SignInSSOCallback.tsx | 4 +- .../src/ui/components/SignIn/SignInStart.tsx | 11 ++++- .../components/SignUp/SignUpEmailLinkCard.tsx | 2 + .../components/SignUp/SignUpSSOCallback.tsx | 4 +- .../src/ui/components/SignUp/SignUpStart.tsx | 6 ++- .../components/SignUp/SignUpVerifyEmail.tsx | 2 + .../clerk-js/src/utils/componentGuards.ts | 4 ++ 12 files changed, 82 insertions(+), 19 deletions(-) diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts index 1fd500a1e91..52b35e39cb5 100644 --- a/packages/clerk-js/src/core/warnings.ts +++ b/packages/clerk-js/src/core/warnings.ts @@ -18,6 +18,10 @@ const warnings = { 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the value set in `afterSignUp` URL instead.', cannotRenderSignInComponentWhenSessionExists: 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `afterSignIn` URL instead.', + cannotRenderSignInComponentWithPendingTasks: + 'The component cannot render when a user has pending tasks to resolve, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `tasksUrl` instead.', + cannotRenderSignUpComponentWithPendingTasks: + 'The component cannot render when a user has pending tasks to resolve, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `tasksUrl` instead.', cannotRenderComponentWhenUserDoesNotExist: ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', cannotRenderComponentWhenOrgDoesNotExist: ` cannot render unless an organization is active. Since no organization is currently active, this is no-op.`, diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index 2bb41f7d4e6..d698d348a79 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { warnings } from '../../core/warnings'; import type { ComponentGuard } from '../../utils'; -import { isSignedInAndSingleSessionModeEnabled } from '../../utils'; +import { hasPendingTasksAndSingleSessionModeEnabled, isSignedInAndSingleSessionModeEnabled } from '../../utils'; import { useEnvironment, useOptions, useSignInContext, useSignUpContext } from '../contexts'; import { useRouter } from '../router'; import type { AvailableComponentProps } from '../types'; @@ -52,6 +52,25 @@ export function withRedirect

( return HOC; } +export const withRedirectToTasksAfterSignIn =

(Component: ComponentType

) => { + const displayName = Component.displayName || Component.name || 'Component'; + Component.displayName = displayName; + + const HOC = (props: P) => { + const signInCtx = useSignInContext(); + return withRedirect( + Component, + hasPendingTasksAndSingleSessionModeEnabled, + () => signInCtx.tasksUrl, + warnings.cannotRenderSignInComponentWithPendingTasks, + )(props); + }; + + HOC.displayName = `withRedirectToTasksAfterSignIn(${displayName})`; + + return HOC; +}; + export const withRedirectToAfterSignIn =

(Component: ComponentType

) => { const displayName = Component.displayName || Component.name || 'Component'; Component.displayName = displayName; @@ -61,8 +80,7 @@ export const withRedirectToAfterSignIn =

(Com return withRedirect( Component, isSignedInAndSingleSessionModeEnabled, - ({ clerk }) => - clerk.session?.currentTask ? signInCtx.tasksUrl : signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), + ({ clerk }) => signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignInComponentWhenSessionExists, )(props); }; @@ -81,8 +99,7 @@ export const withRedirectToAfterSignUp =

(Com return withRedirect( Component, isSignedInAndSingleSessionModeEnabled, - ({ clerk }) => - clerk.session?.currentTask ? signUpCtx.tasksUrl : signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), + ({ clerk }) => signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignUpComponentWhenSessionExists, )(props); }; @@ -92,6 +109,25 @@ export const withRedirectToAfterSignUp =

(Com return HOC; }; +export const withRedirectToTasksAfterSignUp =

(Component: ComponentType

) => { + const displayName = Component.displayName || Component.name || 'Component'; + Component.displayName = displayName; + + const HOC = (props: P) => { + const signInCtx = useSignUpContext(); + return withRedirect( + Component, + hasPendingTasksAndSingleSessionModeEnabled, + () => signInCtx.tasksUrl, + warnings.cannotRenderSignUpComponentWithPendingTasks, + )(props); + }; + + HOC.displayName = `withRedirectToTasksAfterSignIn(${displayName})`; + + return HOC; +}; + export const withRedirectToHomeSingleSessionGuard =

(Component: ComponentType

) => withRedirect( Component, diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx index f5f8e6d0ee3..da00068e722 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx @@ -1,4 +1,4 @@ -import { withRedirectToAfterSignIn } from '../../common'; +import { withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; import { useEnvironment, useSignInContext, useSignOutContext } from '../../contexts'; import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; import { Action, Actions, Card, Header, PreviewButton, UserPreview, withCardStateProvider } from '../../elements'; @@ -120,4 +120,6 @@ const _SignInAccountSwitcher = () => { ); }; -export const SignInAccountSwitcher = withRedirectToAfterSignIn(withCardStateProvider(_SignInAccountSwitcher)); +export const SignInAccountSwitcher = withRedirectToTasksAfterSignIn( + withRedirectToAfterSignIn(withCardStateProvider(_SignInAccountSwitcher)), +); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx index 880f429039f..a5aa7d19a98 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx @@ -1,7 +1,7 @@ import type { SignInFactor } from '@clerk/types'; import React from 'react'; -import { withRedirectToAfterSignIn } from '../../common'; +import { withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; import { useCoreSignIn, useEnvironment } from '../../contexts'; import { ErrorCard, LoadingCard, useCardState, withCardStateProvider } from '../../elements'; import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies'; @@ -210,4 +210,6 @@ export function _SignInFactorOne(): JSX.Element { } } -export const SignInFactorOne = withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne)); +export const SignInFactorOne = withRedirectToTasksAfterSignIn( + withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne)), +); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx index f19b71a13cb..0e1d72bed59 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx @@ -1,7 +1,7 @@ import type { SignInFactor } from '@clerk/types'; import React from 'react'; -import { withRedirectToAfterSignIn } from '../../common'; +import { withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; import { useCoreSignIn } from '../../contexts'; import { LoadingCard, withCardStateProvider } from '../../elements'; import { SignInFactorTwoAlternativeMethods } from './SignInFactorTwoAlternativeMethods'; @@ -80,4 +80,6 @@ export function _SignInFactorTwo(): JSX.Element { } } -export const SignInFactorTwo = withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo)); +export const SignInFactorTwo = withRedirectToTasksAfterSignIn( + withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo)), +); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx index 07bfaed52d2..da903447ae3 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx @@ -1,3 +1,3 @@ -import { SSOCallback, withRedirectToAfterSignIn } from '../../common'; +import { SSOCallback, withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; -export const SignInSSOCallback = withRedirectToAfterSignIn(SSOCallback); +export const SignInSSOCallback = withRedirectToTasksAfterSignIn(withRedirectToAfterSignIn(SSOCallback)); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 88ceafd0341..23c014c118c 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -7,7 +7,12 @@ import { ERROR_CODES, SIGN_UP_MODES } from '../../../core/constants'; import { clerkInvalidFAPIResponse } from '../../../core/errors'; import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils'; import type { SignInStartIdentifier } from '../../common'; -import { getIdentifierControlDisplayValues, groupIdentifiers, withRedirectToAfterSignIn } from '../../common'; +import { + getIdentifierControlDisplayValues, + groupIdentifiers, + withRedirectToAfterSignIn, + withRedirectToTasksAfterSignIn, +} from '../../common'; import { buildSSOCallbackURL } from '../../common/redirects'; import { useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts'; import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; @@ -568,4 +573,6 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> }) ); }; -export const SignInStart = withRedirectToAfterSignIn(withCardStateProvider(_SignInStart)); +export const SignInStart = withRedirectToTasksAfterSignIn( + withRedirectToAfterSignIn(withCardStateProvider(_SignInStart)), +); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx index b1a9aec6c0d..9f49e130af4 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx @@ -13,6 +13,8 @@ import { handleError } from '../../utils'; import { completeSignUpFlow } from './util'; export const SignUpEmailLinkCard = () => { + console.log('rendering'); + const { t } = useLocalizations(); const signUp = useCoreSignUp(); const signUpContext = useSignUpContext(); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx index a72a27627fa..9f7812fedbc 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx @@ -1,3 +1,3 @@ -import { SSOCallback, withRedirectToAfterSignUp } from '../../common'; +import { SSOCallback, withRedirectToAfterSignUp, withRedirectToTasksAfterSignUp } from '../../common'; -export const SignUpSSOCallback = withRedirectToAfterSignUp(SSOCallback); +export const SignUpSSOCallback = withRedirectToTasksAfterSignUp(withRedirectToAfterSignUp(SSOCallback)); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 9ba89b02c85..1f154e482bf 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { ERROR_CODES, SIGN_UP_MODES } from '../../../core/constants'; import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils/getClerkQueryParam'; -import { withRedirectToAfterSignUp } from '../../common'; +import { withRedirectToAfterSignUp, withRedirectToTasksAfterSignUp } from '../../common'; import { SignInContext, useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; import { descriptors, Flex, Flow, localizationKeys, useAppearance, useLocalizations } from '../../customizables'; import { @@ -346,4 +346,6 @@ function _SignUpStart(): JSX.Element { ); } -export const SignUpStart = withRedirectToAfterSignUp(withCardStateProvider(_SignUpStart)); +export const SignUpStart = withRedirectToTasksAfterSignUp( + withRedirectToAfterSignUp(withCardStateProvider(_SignUpStart)), +); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx index 4057763d3e0..10e5a602dbe 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx @@ -4,6 +4,8 @@ import { SignUpEmailCodeCard } from './SignUpEmailCodeCard'; import { SignUpEmailLinkCard } from './SignUpEmailLinkCard'; export const SignUpVerifyEmail = withCardStateProvider(() => { + console.log('rendering'); + const { userSettings } = useEnvironment(); const { attributes } = userSettings; const emailLinkStrategyEnabled = attributes.email_address.verifications.includes('email_link'); diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts index 3349869c807..f3cb4d0b031 100644 --- a/packages/clerk-js/src/utils/componentGuards.ts +++ b/packages/clerk-js/src/utils/componentGuards.ts @@ -10,6 +10,10 @@ export const isSignedInAndSingleSessionModeEnabled: ComponentGuard = (clerk, env return !!(clerk.isSignedIn && environment?.authConfig.singleSessionMode); }; +export const hasPendingTasksAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { + return !!(clerk.session?.currentTask && environment?.authConfig.singleSessionMode); +}; + export const noUserExists: ComponentGuard = clerk => { return !clerk.user; }; From 1233d3bc6e74b6bbc7a447ce7c14b93c76b87a86 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:57:15 -0500 Subject: [PATCH 15/21] Add unit test for redirection to task --- .../core/__tests__/clerk.redirects.test.ts | 54 ++++++ packages/clerk-js/src/core/clerk.ts | 41 ++--- packages/clerk-js/src/core/warnings.ts | 6 +- packages/clerk-js/src/ui/common/tasks.ts | 6 +- .../clerk-js/src/ui/common/withRedirect.tsx | 30 +-- .../src/ui/components/SignIn/SignIn.tsx | 16 +- .../SignIn/SignInAccountSwitcher.tsx | 4 +- .../ui/components/SignIn/SignInFactorOne.tsx | 6 +- .../ui/components/SignIn/SignInFactorTwo.tsx | 6 +- .../components/SignIn/SignInSSOCallback.tsx | 4 +- .../src/ui/components/SignIn/SignInStart.tsx | 6 +- .../src/ui/components/SignUp/SignUp.tsx | 14 +- .../components/SignUp/SignUpSSOCallback.tsx | 4 +- .../src/ui/components/SignUp/SignUpStart.tsx | 6 +- .../Task/__tests__/useTaskRoute.test.tsx | 173 ------------------ .../clerk-js/src/ui/components/Task/index.ts | 1 + .../src/ui/components/Task/useTaskRoute.tsx | 25 --- .../src/ui/contexts/components/SignIn.ts | 6 - .../src/ui/contexts/components/SignUp.ts | 6 - packages/react/src/isomorphicClerk.ts | 7 +- packages/types/src/clerk.ts | 11 +- 21 files changed, 130 insertions(+), 302 deletions(-) delete mode 100644 packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx create mode 100644 packages/clerk-js/src/ui/components/Task/index.ts delete mode 100644 packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index c744fffd27f..2ffbe87a2d1 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -122,6 +122,7 @@ describe('Clerk singleton - Redirects', () => { afterEach(() => { mockEnvironmentFetch.mockRestore(); + mockClientFetch.mockRestore(); }); it('redirects to signInUrl for development instance', async () => { @@ -220,6 +221,7 @@ describe('Clerk singleton - Redirects', () => { afterEach(() => { mockEnvironmentFetch.mockRestore(); + mockClientFetch.mockRestore(); }); const host = 'http://another-test.host'; @@ -309,4 +311,56 @@ describe('Clerk singleton - Redirects', () => { expect(mockHref).toHaveBeenNthCalledWith(2, `${host}/?__clerk_db_jwt=deadbeef`); }); }); + + describe('on signed-in session with pending tasks', () => { + let clerkForProductionInstance: Clerk; + let clerkForDevelopmentInstance: Clerk; + + beforeEach(async () => { + mockEnvironmentFetch.mockReturnValue( + Promise.resolve({ + userSettings: mockUserSettings, + displayConfig: mockDisplayConfigWithDifferentOrigin, + isProduction: () => false, + isDevelopmentOrStaging: () => true, + }), + ); + + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [ + { + id: '1', + remove: jest.fn(), + status: 'pending', + currentTask: { key: 'org' }, + user: {}, + touch: jest.fn(() => Promise.resolve()), + getToken: jest.fn(), + lastActiveToken: { getRawString: () => 'mocked-token' }, + }, + ], + }), + ); + + clerkForProductionInstance = new Clerk(productionPublishableKey); + clerkForDevelopmentInstance = new Clerk(developmentPublishableKey); + + await clerkForProductionInstance.load(mockedLoadOptions); + await clerkForDevelopmentInstance.load(mockedLoadOptions); + }); + + afterEach(() => { + mockEnvironmentFetch.mockRestore(); + mockClientFetch.mockRestore(); + }); + + it('when session has tasks, redirect to tasks URL', async () => { + await clerkForDevelopmentInstance.redirectToTasks(); + + expect(mockNavigate).toHaveBeenCalledWith('/#/select-organization', { + windowNavigate: expect.any(Function), + }); + }); + }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 5e25dc34bb0..4a1cad60800 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -43,7 +43,6 @@ import type { PublicKeyCredentialWithAuthenticatorAssertionResponse, PublicKeyCredentialWithAuthenticatorAttestationResponse, RedirectOptions, - RedirectToTasksUrlOptions, Resources, SDKMetadata, SetActiveParams, @@ -67,7 +66,7 @@ import type { Web3Provider, } from '@clerk/types'; -import { sessionTaskRoutePaths } from '../ui/common/tasks'; +import { sessionTaskKeyToRoutePaths } from '../ui/common/tasks'; import type { MountComponentRenderer } from '../ui/Components'; import { ALLOWED_PROTOCOLS, @@ -891,9 +890,7 @@ export class Clerk implements ClerkInterface { let newSession = session === undefined ? this.session : session; - const isResolvingSessionTasks = - !!newSession?.currentTask || - window.location.href.includes(this.internal__buildTasksUrl({ task: newSession?.currentTask })); + const isResolvingSessionTasks = !!newSession?.currentTask || window.location.href.includes(this.buildTasksUrl()); // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. @@ -1122,18 +1119,14 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - public internal__buildTasksUrl({ task = this.session?.currentTask, origin }: RedirectToTasksUrlOptions): string { - if (!task) { + public buildTasksUrl(): string { + const currentTask = this.session?.currentTask; + + if (!currentTask) { return ''; } - const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; - const referrerIsSignUpUrl = signUpUrl && window.location.href.includes(signUpUrl); - - const originWithDefault = origin ?? (referrerIsSignUpUrl ? 'SignUp' : 'SignIn'); - const defaultUrlByOrigin = originWithDefault === 'SignIn' ? this.#options.signInUrl : this.#options.signUpUrl; - - return buildURL({ base: defaultUrlByOrigin, hashPath: sessionTaskRoutePaths[task.key] }, { stringify: true }); + return buildURL({ hashPath: sessionTaskKeyToRoutePaths[currentTask.key] }, { stringify: true }); } public buildAfterMultiSessionSingleSignOutUrl(): string { @@ -1257,9 +1250,9 @@ export class Clerk implements ClerkInterface { return; }; - public redirectToTasks = async (options: RedirectToTasksUrlOptions): Promise => { + public redirectToTasks = async (): Promise => { if (inBrowser()) { - return this.navigate(this.internal__buildTasksUrl(options)); + return this.navigate(this.buildTasksUrl()); } return; }; @@ -1757,6 +1750,10 @@ export class Clerk implements ClerkInterface { if (this.session) { const session = this.#getSessionFromClient(this.session.id); + // TODO - Resolve after-task redirection + // TODO - Resolve issue on closing modals on navigation within Clerk.navigate + // sign-in/select-organization -> /home + // sign-in -> sign-in/select-organization const hasResolvedPreviousTask = this.session.currentTask != session?.currentTask; // Note: this might set this.session to null @@ -1765,16 +1762,16 @@ export class Clerk implements ClerkInterface { // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. eventBus.dispatch(events.TokenUpdate, { token: this.session?.lastActiveToken }); - // Any FAPI call could lead to a task being unsatisfied such as app owners - // actions therefore the check must be done on client piggybacking + this.#emit(); + + // Tasks handling must be reactive on client piggybacking to support + // immediate instance-level configuration changes by app owners if (session?.currentTask) { eventBus.dispatch(events.NewSessionTask, session); } else if (session && hasResolvedPreviousTask) { eventBus.dispatch(events.ResolvedSessionTask, session); } } - - this.#emit(); }; get __unstable__environment(): EnvironmentResource | null | undefined { @@ -2116,9 +2113,9 @@ export class Clerk implements ClerkInterface { this.#broadcastChannel?.postMessage({ type: 'signout' }); }); - eventBus.on(events.NewSessionTask, session => { + eventBus.on(events.NewSessionTask, () => { console.log('new session task'); - void this.redirectToTasks({ task: session.currentTask }); + void this.redirectToTasks(); }); eventBus.on(events.ResolvedSessionTask, () => { diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts index 52b35e39cb5..2a70489cc78 100644 --- a/packages/clerk-js/src/core/warnings.ts +++ b/packages/clerk-js/src/core/warnings.ts @@ -18,10 +18,8 @@ const warnings = { 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the value set in `afterSignUp` URL instead.', cannotRenderSignInComponentWhenSessionExists: 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `afterSignIn` URL instead.', - cannotRenderSignInComponentWithPendingTasks: - 'The component cannot render when a user has pending tasks to resolve, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `tasksUrl` instead.', - cannotRenderSignUpComponentWithPendingTasks: - 'The component cannot render when a user has pending tasks to resolve, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `tasksUrl` instead.', + cannotRenderComponentWithPendingTasks: + 'The component cannot render when a user has pending tasks to resolve, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `tasksUrl` instead.', cannotRenderComponentWhenUserDoesNotExist: ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', cannotRenderComponentWhenOrgDoesNotExist: ` cannot render unless an organization is active. Since no organization is currently active, this is no-op.`, diff --git a/packages/clerk-js/src/ui/common/tasks.ts b/packages/clerk-js/src/ui/common/tasks.ts index 0c775e20316..e23b93a323e 100644 --- a/packages/clerk-js/src/ui/common/tasks.ts +++ b/packages/clerk-js/src/ui/common/tasks.ts @@ -1,8 +1,12 @@ import type { SessionTask } from '@clerk/types'; +export const sessionTaskRoutePaths = ['select-organization'] as const; + +type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number]; + /** * @internal */ -export const sessionTaskRoutePaths: Record = { +export const sessionTaskKeyToRoutePaths: Record = { org: 'select-organization', }; diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index d698d348a79..d6d62db4c65 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -52,21 +52,20 @@ export function withRedirect

( return HOC; } -export const withRedirectToTasksAfterSignIn =

(Component: ComponentType

) => { +export const withRedirectToTasks =

(Component: ComponentType

) => { const displayName = Component.displayName || Component.name || 'Component'; Component.displayName = displayName; const HOC = (props: P) => { - const signInCtx = useSignInContext(); return withRedirect( Component, hasPendingTasksAndSingleSessionModeEnabled, - () => signInCtx.tasksUrl, - warnings.cannotRenderSignInComponentWithPendingTasks, + ({ clerk }) => clerk.buildTasksUrl(), + warnings.cannotRenderComponentWithPendingTasks, )(props); }; - HOC.displayName = `withRedirectToTasksAfterSignIn(${displayName})`; + HOC.displayName = `withRedirectToTasks(${displayName})`; return HOC; }; @@ -99,7 +98,7 @@ export const withRedirectToAfterSignUp =

(Com return withRedirect( Component, isSignedInAndSingleSessionModeEnabled, - ({ clerk }) => signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), + ({ clerk }) => signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(), warnings.cannotRenderSignUpComponentWhenSessionExists, )(props); }; @@ -109,25 +108,6 @@ export const withRedirectToAfterSignUp =

(Com return HOC; }; -export const withRedirectToTasksAfterSignUp =

(Component: ComponentType

) => { - const displayName = Component.displayName || Component.name || 'Component'; - Component.displayName = displayName; - - const HOC = (props: P) => { - const signInCtx = useSignUpContext(); - return withRedirect( - Component, - hasPendingTasksAndSingleSessionModeEnabled, - () => signInCtx.tasksUrl, - warnings.cannotRenderSignUpComponentWithPendingTasks, - )(props); - }; - - HOC.displayName = `withRedirectToTasksAfterSignIn(${displayName})`; - - return HOC; -}; - export const withRedirectToHomeSingleSessionGuard =

(Component: ComponentType

) => withRedirect( Component, diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index ff92a24035b..4f68d66646c 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -2,6 +2,7 @@ import { useClerk } from '@clerk/shared/react'; import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; +import { sessionTaskRoutePaths } from '../../../ui/common/tasks'; import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions'; import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; import type { SignUpContextType } from '../../contexts'; @@ -19,7 +20,7 @@ import { SignUpSSOCallback } from '../SignUp/SignUpSSOCallback'; import { SignUpStart } from '../SignUp/SignUpStart'; import { SignUpVerifyEmail } from '../SignUp/SignUpVerifyEmail'; import { SignUpVerifyPhone } from '../SignUp/SignUpVerifyPhone'; -import { useTaskRoute } from '../Task/useTaskRoute'; +import { Task } from '../Task'; import { ResetPassword } from './ResetPassword'; import { ResetPasswordSuccess } from './ResetPasswordSuccess'; import { SignInAccountSwitcher } from './SignInAccountSwitcher'; @@ -39,7 +40,6 @@ function RedirectToSignIn() { function SignInRoutes(): JSX.Element { const signInContext = useSignInContext(); const signUpContext = useSignUpContext(); - const taskRoute = useTaskRoute(); return ( @@ -134,7 +134,17 @@ function SignInRoutes(): JSX.Element { )} - {taskRoute && } + + {sessionTaskRoutePaths.map(path => ( + !!clerk.session?.currentTask} + > + + + ))} + diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx index da00068e722..2ed47c1fa71 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx @@ -1,4 +1,4 @@ -import { withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; +import { withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; import { useEnvironment, useSignInContext, useSignOutContext } from '../../contexts'; import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; import { Action, Actions, Card, Header, PreviewButton, UserPreview, withCardStateProvider } from '../../elements'; @@ -120,6 +120,6 @@ const _SignInAccountSwitcher = () => { ); }; -export const SignInAccountSwitcher = withRedirectToTasksAfterSignIn( +export const SignInAccountSwitcher = withRedirectToTasks( withRedirectToAfterSignIn(withCardStateProvider(_SignInAccountSwitcher)), ); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx index a5aa7d19a98..6d2cb3a6722 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx @@ -1,7 +1,7 @@ import type { SignInFactor } from '@clerk/types'; import React from 'react'; -import { withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; +import { withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; import { useCoreSignIn, useEnvironment } from '../../contexts'; import { ErrorCard, LoadingCard, useCardState, withCardStateProvider } from '../../elements'; import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies'; @@ -210,6 +210,4 @@ export function _SignInFactorOne(): JSX.Element { } } -export const SignInFactorOne = withRedirectToTasksAfterSignIn( - withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne)), -); +export const SignInFactorOne = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne))); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx index 0e1d72bed59..7dad8e3452a 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx @@ -1,7 +1,7 @@ import type { SignInFactor } from '@clerk/types'; import React from 'react'; -import { withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; +import { withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; import { useCoreSignIn } from '../../contexts'; import { LoadingCard, withCardStateProvider } from '../../elements'; import { SignInFactorTwoAlternativeMethods } from './SignInFactorTwoAlternativeMethods'; @@ -80,6 +80,4 @@ export function _SignInFactorTwo(): JSX.Element { } } -export const SignInFactorTwo = withRedirectToTasksAfterSignIn( - withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo)), -); +export const SignInFactorTwo = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo))); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx index da903447ae3..a0d9c6a4b6a 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx @@ -1,3 +1,3 @@ -import { SSOCallback, withRedirectToAfterSignIn, withRedirectToTasksAfterSignIn } from '../../common'; +import { SSOCallback, withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; -export const SignInSSOCallback = withRedirectToTasksAfterSignIn(withRedirectToAfterSignIn(SSOCallback)); +export const SignInSSOCallback = withRedirectToTasks(withRedirectToAfterSignIn(SSOCallback)); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 23c014c118c..7ec0681ee8b 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -11,7 +11,7 @@ import { getIdentifierControlDisplayValues, groupIdentifiers, withRedirectToAfterSignIn, - withRedirectToTasksAfterSignIn, + withRedirectToTasks, } from '../../common'; import { buildSSOCallbackURL } from '../../common/redirects'; import { useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts'; @@ -573,6 +573,4 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> }) ); }; -export const SignInStart = withRedirectToTasksAfterSignIn( - withRedirectToAfterSignIn(withCardStateProvider(_SignInStart)), -); +export const SignInStart = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInStart))); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 6b92c4c52d1..24bcbbe41c1 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -2,11 +2,12 @@ import { useClerk } from '@clerk/shared/react'; import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; +import { sessionTaskRoutePaths } from '../../../ui/common/tasks'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; -import { useTaskRoute } from '../Task/useTaskRoute'; +import { Task } from '../Task'; import { SignUpContinue } from './SignUpContinue'; import { SignUpSSOCallback } from './SignUpSSOCallback'; import { SignUpStart } from './SignUpStart'; @@ -23,7 +24,6 @@ function RedirectToSignUp() { function SignUpRoutes(): JSX.Element { const signUpContext = useSignUpContext(); - const taskRoute = useTaskRoute(); return ( @@ -76,7 +76,15 @@ function SignUpRoutes(): JSX.Element { - {taskRoute && } + {sessionTaskRoutePaths.map(path => ( + !!clerk.session?.currentTask} + > + + + ))} diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx index 9f7812fedbc..6029b0e13fa 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx @@ -1,3 +1,3 @@ -import { SSOCallback, withRedirectToAfterSignUp, withRedirectToTasksAfterSignUp } from '../../common'; +import { SSOCallback, withRedirectToAfterSignUp, withRedirectToTasks } from '../../common'; -export const SignUpSSOCallback = withRedirectToTasksAfterSignUp(withRedirectToAfterSignUp(SSOCallback)); +export const SignUpSSOCallback = withRedirectToTasks(withRedirectToAfterSignUp(SSOCallback)); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 1f154e482bf..86969e4c436 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { ERROR_CODES, SIGN_UP_MODES } from '../../../core/constants'; import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils/getClerkQueryParam'; -import { withRedirectToAfterSignUp, withRedirectToTasksAfterSignUp } from '../../common'; +import { withRedirectToAfterSignUp, withRedirectToTasks } from '../../common'; import { SignInContext, useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; import { descriptors, Flex, Flow, localizationKeys, useAppearance, useLocalizations } from '../../customizables'; import { @@ -346,6 +346,4 @@ function _SignUpStart(): JSX.Element { ); } -export const SignUpStart = withRedirectToTasksAfterSignUp( - withRedirectToAfterSignUp(withCardStateProvider(_SignUpStart)), -); +export const SignUpStart = withRedirectToTasks(withRedirectToAfterSignUp(withCardStateProvider(_SignUpStart))); diff --git a/packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx b/packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx deleted file mode 100644 index 5a74351acdd..00000000000 --- a/packages/clerk-js/src/ui/components/Task/__tests__/useTaskRoute.test.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { render, waitFor } from '../../../../testUtils'; -import { HashRouter, Route, Switch } from '../../../../ui/router'; -import { bindCreateFixtures } from '../../../utils/test/createFixtures'; -import { createFakeUserOrganizationMembership } from '../../OrganizationSwitcher/__tests__/utlis'; -import { useTaskRoute } from '../useTaskRoute'; - -const oldWindowLocation = window.location; -const setWindowOrigin = (origin: string) => { - // @ts-ignore - delete window.location; - // the URL interface is very similar to window.location - // we use it to easily mock the location methods in tests - (window.location as any) = new URL(origin); -}; - -const MockRoute = (): JSX.Element => { - const taskRoute = useTaskRoute(); - - return ( - - - {taskRoute?.children} - - - ); -}; - -describe('useTaskRoute', () => { - describe.each(['SignIn', 'SignUp'] satisfies Array[0]>)( - 'after %s flow', - flow => { - afterEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - window.location = oldWindowLocation; - }); - - const { createFixtures } = bindCreateFixtures(flow); - - describe('with task', () => { - it('renders the component', async () => { - setWindowOrigin('http://dashboard.example.com/#/select-organization'); - - const { wrapper, fixtures } = await createFixtures(f => { - f.withOrganizations(); - f.withUser({ - email_addresses: ['test@clerk.com'], - create_organization_enabled: true, - session: { - status: 'pending', - tasks: [{ key: 'org' }], - }, - }); - }); - - fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationMembership({ - id: '1', - organization: { - id: '1', - name: 'Org1', - slug: 'org1', - membersCount: 1, - adminDeleteEnabled: false, - maxAllowedMemberships: 1, - pendingInvitationsCount: 1, - }, - }), - ], - total_count: 1, - }), - ); - - const { queryByRole } = render(, { wrapper }); - await waitFor(() => { - expect(queryByRole('heading', { name: /choose an organization/i })).toBeInTheDocument(); - }); - }); - }); - - describe('without task', () => { - it('does not render the component', async () => { - setWindowOrigin('http://dashboard.example.com/#/select-organization'); - - const { wrapper, fixtures } = await createFixtures(f => { - f.withOrganizations(); - f.withUser({ - email_addresses: ['test@clerk.com'], - create_organization_enabled: true, - session: { - status: 'active', - }, - }); - }); - - fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationMembership({ - id: '1', - organization: { - id: '1', - name: 'Org1', - slug: 'org1', - membersCount: 1, - adminDeleteEnabled: false, - maxAllowedMemberships: 1, - pendingInvitationsCount: 1, - }, - }), - ], - total_count: 1, - }), - ); - - const { queryByRole } = render(, { wrapper }); - await waitFor(() => { - expect(queryByRole('heading', { name: /choose an organization/i })).not.toBeInTheDocument(); - }); - }); - }); - - describe('with invalid key', () => { - it('does not render the component', async () => { - setWindowOrigin('http://dashboard.example.com/#/select-organization'); - - const { wrapper, fixtures } = await createFixtures(f => { - f.withOrganizations(); - f.withUser({ - email_addresses: ['test@clerk.com'], - create_organization_enabled: true, - session: { - status: 'pending', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - tasks: [{ key: 'invalid' }], - }, - }); - }); - - fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( - Promise.resolve({ - data: [ - createFakeUserOrganizationMembership({ - id: '1', - organization: { - id: '1', - name: 'Org1', - slug: 'org1', - membersCount: 1, - adminDeleteEnabled: false, - maxAllowedMemberships: 1, - pendingInvitationsCount: 1, - }, - }), - ], - total_count: 1, - }), - ); - - const { queryByRole } = render(, { wrapper }); - await waitFor(() => { - expect(queryByRole('heading', { name: /choose an organization/i })).not.toBeInTheDocument(); - }); - }); - }); - }, - ); -}); diff --git a/packages/clerk-js/src/ui/components/Task/index.ts b/packages/clerk-js/src/ui/components/Task/index.ts new file mode 100644 index 00000000000..e6118bd890e --- /dev/null +++ b/packages/clerk-js/src/ui/components/Task/index.ts @@ -0,0 +1 @@ +export { Task } from './Task'; diff --git a/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx b/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx deleted file mode 100644 index d15cb8eb993..00000000000 --- a/packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useSessionContext } from '@clerk/shared/react/index'; -import type { Route } from '@clerk/shared/router'; -import type { ComponentProps } from 'react'; - -import { sessionTaskRoutePaths } from '../../common/tasks'; -import { Task } from './Task'; - -/** - * Maps a task key to routing props and content - * @internal - */ -export function useTaskRoute(): ComponentProps | null { - const session = useSessionContext(); - - if (!session?.currentTask) { - return null; - } - - const [task] = session.tasks ?? []; - - return { - children: , - path: sessionTaskRoutePaths[task.key], - }; -} diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 6f748fae8bc..9bfdad969b2 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -26,7 +26,6 @@ export type SignInContextType = SignInCtx & { emailLinkRedirectUrl: string; ssoCallbackUrl: string; isCombinedFlow: boolean; - tasksUrl: string; }; export const SignInContext = createContext(null); @@ -86,10 +85,6 @@ export const useSignInContext = (): SignInContextType => { signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); waitlistUrl = buildURL({ base: waitlistUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); - const tasksUrl = clerk.internal__buildTasksUrl({ - origin: 'SignIn', - }); - const authQueryString = redirectUrls.toSearchParams().toString(); const emailLinkRedirectUrl = buildRedirectUrl({ @@ -129,7 +124,6 @@ export const useSignInContext = (): SignInContextType => { ssoCallbackUrl, navigateAfterSignIn, signUpContinueUrl, - tasksUrl, queryParams, initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams }, authQueryString, diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index 97803e27b1b..32b4e4794c0 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -25,7 +25,6 @@ export type SignUpContextType = SignUpCtx & { isCombinedFlow: boolean; emailLinkRedirectUrl: string; ssoCallbackUrl: string; - tasksUrl: string; }; export const SignUpContext = createContext(null); @@ -84,10 +83,6 @@ export const useSignUpContext = (): SignUpContextType => { signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); waitlistUrl = buildURL({ base: waitlistUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); - const tasksUrl = clerk.internal__buildTasksUrl({ - origin: 'SignUp', - }); - const authQueryString = redirectUrls.toSearchParams().toString(); const emailLinkRedirectUrl = @@ -123,7 +118,6 @@ export const useSignUpContext = (): SignUpContextType => { afterSignInUrl, emailLinkRedirectUrl, ssoCallbackUrl, - tasksUrl, navigateAfterSignUp, queryParams, initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams }, diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index c7d11daaf53..2bf9024d94a 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -26,7 +26,6 @@ import type { OrganizationResource, OrganizationSwitcherProps, RedirectOptions, - RedirectToTasksUrlOptions, SetActiveParams, SignInProps, SignInRedirectOptions, @@ -334,12 +333,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - internal__buildTasksUrl = (opts: RedirectToTasksUrlOptions): string | void => { - const callback = () => this.clerkjs?.internal__buildTasksUrl(opts) || ''; + buildTasksUrl = (): string | void => { + const callback = () => this.clerkjs?.buildTasksUrl() || ''; if (this.clerkjs && this.#loaded) { return callback(); } else { - this.premountMethodCalls.set('internal__buildTasksUrl', callback); + this.premountMethodCalls.set('buildTasksUrl', callback); } }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 7f4d597a514..4f3f134bdf7 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -31,7 +31,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; -import type { SessionTask, SignedInSessionResource } from './session'; +import type { SignedInSessionResource } from './session'; import type { SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; @@ -477,9 +477,9 @@ export interface Clerk { buildWaitlistUrl(opts?: { initialValues?: Record }): string; /** - * Returns the url where tasks get displayed. It defaults to `signInUrl` or `signUpUrl`. + * Returns the URL where tasks get displayed. It defaults to `signInUrl` or `signUpUrl` as the base route. */ - internal__buildTasksUrl(options: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string; + buildTasksUrl(): string; /** * @@ -877,11 +877,6 @@ export type SignUpRedirectOptions = RedirectOptions & initialValues?: SignUpInitialValues; }; -export type RedirectToTasksUrlOptions = { - task?: SessionTask; - origin?: 'SignIn' | 'SignUp'; -}; - export type SetActiveParams = { /** * The session resource or session id (string version) to be set on the client. From e15d124930f4dc65c19b4dccb55de849ed883038 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:19:22 -0500 Subject: [PATCH 16/21] Introduce skeleton for integration tests --- .changeset/old-cherries-laugh.md | 2 +- integration/tests/session-tasks.test.ts | 35 ++++++++++++ .../core/__tests__/clerk.redirects.test.ts | 54 ++++--------------- 3 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 integration/tests/session-tasks.test.ts diff --git a/.changeset/old-cherries-laugh.md b/.changeset/old-cherries-laugh.md index ee1f5487332..4873561a826 100644 --- a/.changeset/old-cherries-laugh.md +++ b/.changeset/old-cherries-laugh.md @@ -3,4 +3,4 @@ '@clerk/types': patch --- -Navigate to after-auth tasks +Navigate to session tasks diff --git a/integration/tests/session-tasks.test.ts b/integration/tests/session-tasks.test.ts new file mode 100644 index 00000000000..b582b5982a1 --- /dev/null +++ b/integration/tests/session-tasks.test.ts @@ -0,0 +1,35 @@ +import { appConfigs } from '../presets'; +import { testAgainstRunningApps } from '../testUtils'; + +// TODO ORGS-566 - write integration tests for after-auth flow +testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('after-auth flows @generic @nextjs', () => { + describe('after sign-in', () => { + // /sign-in -> /sign-in/select-organization + it.todo('navigates to tasks'); + + // /sign-in -> /sign-in/select-organization -> /app (after-sign-in URL) + it.todo('navigates to after-sign-in URL when tasks get resolved'); + + // with session status pending -> accesses /sign-in -> redirects to /sign-in/select-organization + it.todo('on single-session mode, sign-in redirects back to tasks when accessed with a pending session'); + }); + + describe('after sign-up', () => { + // /sign-up -> /sign-up/select-organization + it.todo('navigates to tasks'); + + // /sign-up -> /sign-up/select-organization -> /app/welcome (after-sign-up URL) + it.todo('navigates to after-sign-up URL when tasks get resolved'); + + // with session status pending -> accesses /sign-up -> redirects to /sign-up/select-organization + it.todo('on single-session mode, sign-up redirects back to tasks when accessed with a pending session'); + }); + + describe('middle app', () => { + // /my-dashboard/recipes -> /sign-in/select-organization + it.todo('on session transition to pending with tasks, redirects to tasks'); + + // /my-dashboard/recipes -> /sign-in/select-organization -> /my-dashboard/recipes + it.todo('navigates to middle app origin when tasks get resolved'); + }); +}); diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index 2ffbe87a2d1..b35569f0d57 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -312,55 +312,21 @@ describe('Clerk singleton - Redirects', () => { }); }); - describe('on signed-in session with pending tasks', () => { - let clerkForProductionInstance: Clerk; - let clerkForDevelopmentInstance: Clerk; - - beforeEach(async () => { - mockEnvironmentFetch.mockReturnValue( - Promise.resolve({ - userSettings: mockUserSettings, - displayConfig: mockDisplayConfigWithDifferentOrigin, - isProduction: () => false, - isDevelopmentOrStaging: () => true, - }), - ); - - mockClientFetch.mockReturnValue( - Promise.resolve({ - signedInSessions: [ - { - id: '1', - remove: jest.fn(), - status: 'pending', - currentTask: { key: 'org' }, - user: {}, - touch: jest.fn(() => Promise.resolve()), - getToken: jest.fn(), - lastActiveToken: { getRawString: () => 'mocked-token' }, - }, - ], - }), - ); - - clerkForProductionInstance = new Clerk(productionPublishableKey); - clerkForDevelopmentInstance = new Clerk(developmentPublishableKey); - - await clerkForProductionInstance.load(mockedLoadOptions); - await clerkForDevelopmentInstance.load(mockedLoadOptions); + describe('.redirectToTasks', () => { + describe('after sign-in with pending session', () => { + it.todo('redirects to tasks URL with after sign-in URL appended as query param'); }); - afterEach(() => { - mockEnvironmentFetch.mockRestore(); - mockClientFetch.mockRestore(); + describe('after sign-up with pending session', () => { + it.todo('redirects to tasks URL with after sign-up URL appended as query param'); }); - it('when session has tasks, redirect to tasks URL', async () => { - await clerkForDevelopmentInstance.redirectToTasks(); + describe('after sign-up with pending session', () => { + it.todo('redirects to tasks URL with after sign-up URL appended as query param'); + }); - expect(mockNavigate).toHaveBeenCalledWith('/#/select-organization', { - windowNavigate: expect.any(Function), - }); + describe('user already exists and session transitions from active to pending on middle app', () => { + it.todo('redirects to tasks URL with app origin appended as query param'); }); }); }); From b9f8870fb04f331fd2c17621822850d4734e3b94 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:49:02 -0500 Subject: [PATCH 17/21] Introduce new base skeleton for URL resolution --- integration/tests/session-tasks.test.ts | 4 +- .../core/__tests__/clerk.redirects.test.ts | 4 +- packages/clerk-js/src/core/clerk.ts | 67 +++++++++++++++---- packages/clerk-js/src/ui/common/tasks.ts | 2 +- .../components/SignUp/SignUpEmailLinkCard.tsx | 2 - .../components/SignUp/SignUpVerifyEmail.tsx | 2 - .../clerk-js/src/ui/router/BaseRouter.tsx | 1 + 7 files changed, 61 insertions(+), 21 deletions(-) diff --git a/integration/tests/session-tasks.test.ts b/integration/tests/session-tasks.test.ts index b582b5982a1..2ecf8b8da69 100644 --- a/integration/tests/session-tasks.test.ts +++ b/integration/tests/session-tasks.test.ts @@ -1,7 +1,7 @@ import { appConfigs } from '../presets'; import { testAgainstRunningApps } from '../testUtils'; -// TODO ORGS-566 - write integration tests for after-auth flow +// TODO ORGS-566 - Write integration tests for after-auth flow testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('after-auth flows @generic @nextjs', () => { describe('after sign-in', () => { // /sign-in -> /sign-in/select-organization @@ -25,7 +25,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('after-aut it.todo('on single-session mode, sign-up redirects back to tasks when accessed with a pending session'); }); - describe('middle app', () => { + describe('when user is using the app and session transitions to active to pending', () => { // /my-dashboard/recipes -> /sign-in/select-organization it.todo('on session transition to pending with tasks, redirects to tasks'); diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index b35569f0d57..31d1d7046ad 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -314,7 +314,7 @@ describe('Clerk singleton - Redirects', () => { describe('.redirectToTasks', () => { describe('after sign-in with pending session', () => { - it.todo('redirects to tasks URL with after sign-in URL appended as query param'); + it('redirects to tasks URL with after sign-in URL appended as query param'); }); describe('after sign-up with pending session', () => { @@ -325,7 +325,7 @@ describe('Clerk singleton - Redirects', () => { it.todo('redirects to tasks URL with after sign-up URL appended as query param'); }); - describe('user already exists and session transitions from active to pending on middle app', () => { + describe('from a protected route', () => { it.todo('redirects to tasks URL with app origin appended as query param'); }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 4a1cad60800..2d0ae596298 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1007,8 +1007,6 @@ export class Clerk implements ClerkInterface { return; } - console.log('Clerk.navigate is navigating to', { to }); - /** * Trigger all navigation listeners. In order for modal UI components to close. */ @@ -1042,6 +1040,7 @@ export class Clerk implements ClerkInterface { ...(options?.metadata ? { __internal_metadata: options?.metadata } : {}), windowNavigate, }; + // React router only wants the path, search or hash portion. return await customNavigate(stripOrigin(toURL), metadata); }; @@ -1126,7 +1125,54 @@ export class Clerk implements ClerkInterface { return ''; } - return buildURL({ hashPath: sessionTaskKeyToRoutePaths[currentTask.key] }, { stringify: true }); + const referrerIsTaskUrl = window.location.href.includes(sessionTaskKeyToRoutePaths[currentTask.key]); + if (referrerIsTaskUrl) { + return ''; + } + + const signInUrl = this.#options.signInUrl || this.environment?.displayConfig.signInUrl; + const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; + const referrerIsSignInUrl = signInUrl && window.location.href.includes(signInUrl); + const referrerIsSignUpUrl = signUpUrl && window.location.href.includes(signUpUrl); + + let redirectUrl = ''; + if (referrerIsSignInUrl) { + redirectUrl = this.buildAfterSignInUrl(); + } else if (referrerIsSignUpUrl) { + redirectUrl = this.buildAfterSignUpUrl(); + } else { + /** + * User already has a active session and gets transition to a pending status + * on the client update + * + * It could happen on instance configuration changes that lead to new tasks that + * need to get resolved by the user, eg: Force MFA + */ + // Preserves the origin path, eg: /my-app/recipes -> /sign-in/select-organization -> /my-app/recipes + redirectUrl = window.location.href; + } + + /** + * For after sign-in or after sign-up, it's agnostic to the original base path + * in order to avoid having to check for the referrer + * + * If it's coming from a protected route where the user already exists, then + * the base path becomes the sign in URL + */ + const shouldAppendBasePath = !referrerIsSignInUrl && !referrerIsSignUpUrl; + + return buildURL( + { + ...(shouldAppendBasePath + ? { + base: signInUrl, + } + : {}), + hashPath: sessionTaskKeyToRoutePaths[currentTask.key], + hashSearchParams: { redirect_url: redirectUrl }, + }, + { stringify: true }, + ); } public buildAfterMultiSessionSingleSignOutUrl(): string { @@ -1750,10 +1796,6 @@ export class Clerk implements ClerkInterface { if (this.session) { const session = this.#getSessionFromClient(this.session.id); - // TODO - Resolve after-task redirection - // TODO - Resolve issue on closing modals on navigation within Clerk.navigate - // sign-in/select-organization -> /home - // sign-in -> sign-in/select-organization const hasResolvedPreviousTask = this.session.currentTask != session?.currentTask; // Note: this might set this.session to null @@ -1762,8 +1804,6 @@ export class Clerk implements ClerkInterface { // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. eventBus.dispatch(events.TokenUpdate, { token: this.session?.lastActiveToken }); - this.#emit(); - // Tasks handling must be reactive on client piggybacking to support // immediate instance-level configuration changes by app owners if (session?.currentTask) { @@ -1771,6 +1811,8 @@ export class Clerk implements ClerkInterface { } else if (session && hasResolvedPreviousTask) { eventBus.dispatch(events.ResolvedSessionTask, session); } + + this.#emit(); } }; @@ -2114,13 +2156,14 @@ export class Clerk implements ClerkInterface { }); eventBus.on(events.NewSessionTask, () => { - console.log('new session task'); void this.redirectToTasks(); }); eventBus.on(events.ResolvedSessionTask, () => { - console.log('resolved task'); - void this.redirectToAfterSignIn(); + // `redirect_url` gets appended based on the origin (after sign-in vs after sign-up), + // if it gets accidentally deleted, then fallbacks to the sign in URL + const redirectUrl = new URLSearchParams(window.location.search).get('redirect_url') ?? this.buildSignInUrl(); + void this.navigate(redirectUrl); }); }; diff --git a/packages/clerk-js/src/ui/common/tasks.ts b/packages/clerk-js/src/ui/common/tasks.ts index e23b93a323e..d2465f6a4a3 100644 --- a/packages/clerk-js/src/ui/common/tasks.ts +++ b/packages/clerk-js/src/ui/common/tasks.ts @@ -8,5 +8,5 @@ type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number]; * @internal */ export const sessionTaskKeyToRoutePaths: Record = { - org: 'select-organization', + org: '/select-organization', }; diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx index 9f49e130af4..b1a9aec6c0d 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailLinkCard.tsx @@ -13,8 +13,6 @@ import { handleError } from '../../utils'; import { completeSignUpFlow } from './util'; export const SignUpEmailLinkCard = () => { - console.log('rendering'); - const { t } = useLocalizations(); const signUp = useCoreSignUp(); const signUpContext = useSignUpContext(); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx index 10e5a602dbe..4057763d3e0 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpVerifyEmail.tsx @@ -4,8 +4,6 @@ import { SignUpEmailCodeCard } from './SignUpEmailCodeCard'; import { SignUpEmailLinkCard } from './SignUpEmailLinkCard'; export const SignUpVerifyEmail = withCardStateProvider(() => { - console.log('rendering'); - const { userSettings } = useEnvironment(); const { attributes } = userSettings; const emailLinkStrategyEnabled = attributes.email_address.verifications.includes('email_link'); diff --git a/packages/clerk-js/src/ui/router/BaseRouter.tsx b/packages/clerk-js/src/ui/router/BaseRouter.tsx index 1874116aa04..cd67ae75853 100644 --- a/packages/clerk-js/src/ui/router/BaseRouter.tsx +++ b/packages/clerk-js/src/ui/router/BaseRouter.tsx @@ -96,6 +96,7 @@ export const BaseRouter = ({ const isCrossOrigin = toURL.origin !== window.location.origin; const isOutsideOfUIComponent = !toURL.pathname.startsWith('/' + basePath); + console.log({ isCrossOrigin, isOutsideOfUIComponent }); if (isOutsideOfUIComponent || isCrossOrigin) { const res = await clerkNavigate(toURL.href); From 22b4a4ab556457edfe7db1885a0bb309ca61cfb2 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:03:13 -0500 Subject: [PATCH 18/21] Add `with-session-tasks` to integration tests --- .changeset/old-cherries-laugh.md | 2 +- integration/.keys.json.sample | 4 + integration/presets/envs.ts | 8 ++ integration/presets/longRunningApps.ts | 5 ++ .../testUtils/testAgainstRunningApps.ts | 2 + integration/tests/session-tasks.test.ts | 63 +++++++++------- .../core/__tests__/clerk.redirects.test.ts | 18 ----- .../src/core/auth/AuthCookieService.ts | 13 ++++ packages/clerk-js/src/core/clerk.ts | 73 ++----------------- packages/clerk-js/src/core/events.ts | 6 +- .../clerk-js/src/core/resources/Session.ts | 1 + packages/clerk-js/src/ui/common/tasks.ts | 4 +- .../clerk-js/src/ui/common/withRedirect.tsx | 2 +- packages/react/src/isomorphicClerk.ts | 6 +- packages/types/src/clerk.ts | 9 ++- packages/types/src/jwt.ts | 7 ++ packages/types/src/jwtv2.ts | 6 ++ 17 files changed, 105 insertions(+), 124 deletions(-) diff --git a/.changeset/old-cherries-laugh.md b/.changeset/old-cherries-laugh.md index 4873561a826..d94e4755426 100644 --- a/.changeset/old-cherries-laugh.md +++ b/.changeset/old-cherries-laugh.md @@ -3,4 +3,4 @@ '@clerk/types': patch --- -Navigate to session tasks +Navigate to session task diff --git a/integration/.keys.json.sample b/integration/.keys.json.sample index caa70922c39..1aeddba9b72 100644 --- a/integration/.keys.json.sample +++ b/integration/.keys.json.sample @@ -46,5 +46,9 @@ "with-waitlist-mode": { "pk": "", "sk": "" + }, + "with-session-tasks": { + "pk": "", + "sk": "" } } diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 4fbf4d2bb22..77bf8f93507 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -42,6 +42,13 @@ const withEmailCodes = base .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk) .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); +const withSessionTasks = base + .clone() + .setId('withSessionTasks') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks').sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks').pk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); + const withEmailCodes_destroy_client = withEmailCodes .clone() .setEnvVariable('public', 'EXPERIMENTAL_PERSIST_CLIENT', 'false'); @@ -157,4 +164,5 @@ export const envs = { withSignInOrUpFlow, withSignInOrUpEmailLinksFlow, withSignInOrUpwithRestrictedModeFlow, + withSessionTasks, } as const; diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index d5573f015e0..f595591a3c3 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -37,6 +37,11 @@ export const createLongRunningApps = () => { config: next.appRouter, env: envs.withSignInOrUpEmailLinksFlow, }, + { + id: 'next.appRouter.withSessionTasks', + config: next.appRouter, + env: envs.withSessionTasks, + }, { id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart }, { id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes }, { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles }, diff --git a/integration/testUtils/testAgainstRunningApps.ts b/integration/testUtils/testAgainstRunningApps.ts index 2b8c67752d7..dd5502f88a7 100644 --- a/integration/testUtils/testAgainstRunningApps.ts +++ b/integration/testUtils/testAgainstRunningApps.ts @@ -24,6 +24,8 @@ const runningApps = (params: RunningAppsParams = {}) => { const withPattern = (params.withPattern || []).flat().filter(Boolean); const { appIds, appUrl, appPk, appSk, clerkApiUrl } = parseEnvOptions(); + console.log({ appIds }); + if (appIds.length) { // if appIds are provided, we only return the apps with the given ids const filter = app => (withEnv.length ? withEnv.includes(app.env) : true); diff --git a/integration/tests/session-tasks.test.ts b/integration/tests/session-tasks.test.ts index 2ecf8b8da69..726095c4b90 100644 --- a/integration/tests/session-tasks.test.ts +++ b/integration/tests/session-tasks.test.ts @@ -1,35 +1,42 @@ -import { appConfigs } from '../presets'; -import { testAgainstRunningApps } from '../testUtils'; - -// TODO ORGS-566 - Write integration tests for after-auth flow -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('after-auth flows @generic @nextjs', () => { - describe('after sign-in', () => { - // /sign-in -> /sign-in/select-organization - it.todo('navigates to tasks'); +import { expect, test } from '@playwright/test'; - // /sign-in -> /sign-in/select-organization -> /app (after-sign-in URL) - it.todo('navigates to after-sign-in URL when tasks get resolved'); - - // with session status pending -> accesses /sign-in -> redirects to /sign-in/select-organization - it.todo('on single-session mode, sign-in redirects back to tasks when accessed with a pending session'); +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils } from '../testUtils'; + +test.describe('session tasks flow @nextjs', () => { + test.describe.configure({ mode: 'serial' }); + let app: Application; + let fakeUser: FakeUser; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter.clone().commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withSessionTasks); + await app.dev(); + + const m = createTestUtils({ app }); + fakeUser = m.services.users.createFakeUser({ + withPhoneNumber: true, + withUsername: true, + }); + await m.services.users.createBapiUser(fakeUser); }); - describe('after sign-up', () => { - // /sign-up -> /sign-up/select-organization - it.todo('navigates to tasks'); - - // /sign-up -> /sign-up/select-organization -> /app/welcome (after-sign-up URL) - it.todo('navigates to after-sign-up URL when tasks get resolved'); - - // with session status pending -> accesses /sign-up -> redirects to /sign-up/select-organization - it.todo('on single-session mode, sign-up redirects back to tasks when accessed with a pending session'); + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); }); - describe('when user is using the app and session transitions to active to pending', () => { - // /my-dashboard/recipes -> /sign-in/select-organization - it.todo('on session transition to pending with tasks, redirects to tasks'); - - // /my-dashboard/recipes -> /sign-in/select-organization -> /my-dashboard/recipes - it.todo('navigates to middle app origin when tasks get resolved'); + test('on after sign-in, navigates to tasks', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.po.signIn.setPassword(fakeUser.password); + await u.po.signIn.continue(); + await u.po.expect.toBeSignedIn(); + await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); }); }); diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index 31d1d7046ad..8e405068e48 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -311,22 +311,4 @@ describe('Clerk singleton - Redirects', () => { expect(mockHref).toHaveBeenNthCalledWith(2, `${host}/?__clerk_db_jwt=deadbeef`); }); }); - - describe('.redirectToTasks', () => { - describe('after sign-in with pending session', () => { - it('redirects to tasks URL with after sign-in URL appended as query param'); - }); - - describe('after sign-up with pending session', () => { - it.todo('redirects to tasks URL with after sign-up URL appended as query param'); - }); - - describe('after sign-up with pending session', () => { - it.todo('redirects to tasks URL with after sign-up URL appended as query param'); - }); - - describe('from a protected route', () => { - it.todo('redirects to tasks URL with app origin appended as query param'); - }); - }); }); diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 4ed81e7fc75..0745fad29b1 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -62,6 +62,8 @@ export class AuthCookieService { this.setClientUatCookieForDevelopmentInstances(); }); + eventBus.on(events.NewSessionTask, () => this.handleSessionTasks()); + this.refreshTokenOnFocus(); this.startPollingForToken(); @@ -151,6 +153,7 @@ export class AuthCookieService { if (updateCookieImmediately) { this.updateSessionCookie(token); } + this.handleSessionTasks(); } catch (e) { return this.handleGetTokenError(e); } @@ -223,4 +226,14 @@ export class AuthCookieService { return this.clerk.organization?.id === activeOrganizationId; } + + private handleSessionTasks() { + const hasPendingStatus = this.clerk.session?.status === 'pending'; + + if (!hasPendingStatus) { + return; + } + + this.clerk.redirectToTask(); + } } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2d0ae596298..601203bdda3 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -890,8 +890,6 @@ export class Clerk implements ClerkInterface { let newSession = session === undefined ? this.session : session; - const isResolvingSessionTasks = !!newSession?.currentTask || window.location.href.includes(this.buildTasksUrl()); - // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. // We now want to set the last active organization id on that session (if it exists). @@ -949,7 +947,8 @@ export class Clerk implements ClerkInterface { beforeUnloadTracker?.stopTracking(); } - if (redirectUrl && !beforeEmit && !isResolvingSessionTasks) { + const hasSessionToResolve = newSession?.currentTask; + if (redirectUrl && !beforeEmit && !hasSessionToResolve) { beforeUnloadTracker?.startTracking(); this.#setTransitiveState(); @@ -1040,7 +1039,6 @@ export class Clerk implements ClerkInterface { ...(options?.metadata ? { __internal_metadata: options?.metadata } : {}), windowNavigate, }; - // React router only wants the path, search or hash portion. return await customNavigate(stripOrigin(toURL), metadata); }; @@ -1118,9 +1116,8 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - public buildTasksUrl(): string { + public buildTaskUrl(): string { const currentTask = this.session?.currentTask; - if (!currentTask) { return ''; } @@ -1130,46 +1127,13 @@ export class Clerk implements ClerkInterface { return ''; } - const signInUrl = this.#options.signInUrl || this.environment?.displayConfig.signInUrl; - const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; - const referrerIsSignInUrl = signInUrl && window.location.href.includes(signInUrl); - const referrerIsSignUpUrl = signUpUrl && window.location.href.includes(signUpUrl); - - let redirectUrl = ''; - if (referrerIsSignInUrl) { - redirectUrl = this.buildAfterSignInUrl(); - } else if (referrerIsSignUpUrl) { - redirectUrl = this.buildAfterSignUpUrl(); - } else { - /** - * User already has a active session and gets transition to a pending status - * on the client update - * - * It could happen on instance configuration changes that lead to new tasks that - * need to get resolved by the user, eg: Force MFA - */ - // Preserves the origin path, eg: /my-app/recipes -> /sign-in/select-organization -> /my-app/recipes - redirectUrl = window.location.href; - } - - /** - * For after sign-in or after sign-up, it's agnostic to the original base path - * in order to avoid having to check for the referrer - * - * If it's coming from a protected route where the user already exists, then - * the base path becomes the sign in URL - */ - const shouldAppendBasePath = !referrerIsSignInUrl && !referrerIsSignUpUrl; + const referrerIsSignUpUrl = window.location.href.startsWith(this.buildSignUpUrl()); return buildURL( { - ...(shouldAppendBasePath - ? { - base: signInUrl, - } - : {}), + // TODO ORGS-531 - Introduce `tasksUrl` as custom option + base: referrerIsSignUpUrl ? this.#options.signUpUrl : this.#options.signInUrl, hashPath: sessionTaskKeyToRoutePaths[currentTask.key], - hashSearchParams: { redirect_url: redirectUrl }, }, { stringify: true }, ); @@ -1296,9 +1260,9 @@ export class Clerk implements ClerkInterface { return; }; - public redirectToTasks = async (): Promise => { + public redirectToTask = async (): Promise => { if (inBrowser()) { - return this.navigate(this.buildTasksUrl()); + return this.navigate(this.buildTaskUrl()); } return; }; @@ -1796,22 +1760,12 @@ export class Clerk implements ClerkInterface { if (this.session) { const session = this.#getSessionFromClient(this.session.id); - const hasResolvedPreviousTask = this.session.currentTask != session?.currentTask; - // Note: this might set this.session to null this.#setAccessors(session); // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. eventBus.dispatch(events.TokenUpdate, { token: this.session?.lastActiveToken }); - // Tasks handling must be reactive on client piggybacking to support - // immediate instance-level configuration changes by app owners - if (session?.currentTask) { - eventBus.dispatch(events.NewSessionTask, session); - } else if (session && hasResolvedPreviousTask) { - eventBus.dispatch(events.ResolvedSessionTask, session); - } - this.#emit(); } }; @@ -2154,17 +2108,6 @@ export class Clerk implements ClerkInterface { eventBus.on(events.UserSignOut, () => { this.#broadcastChannel?.postMessage({ type: 'signout' }); }); - - eventBus.on(events.NewSessionTask, () => { - void this.redirectToTasks(); - }); - - eventBus.on(events.ResolvedSessionTask, () => { - // `redirect_url` gets appended based on the origin (after sign-in vs after sign-up), - // if it gets accidentally deleted, then fallbacks to the sign in URL - const redirectUrl = new URLSearchParams(window.location.search).get('redirect_url') ?? this.buildSignInUrl(); - void this.navigate(redirectUrl); - }); }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts index fb436223c49..1f2a26cc2d2 100644 --- a/packages/clerk-js/src/core/events.ts +++ b/packages/clerk-js/src/core/events.ts @@ -1,10 +1,9 @@ -import type { SignedInSessionResource, TokenResource } from '@clerk/types'; +import type { TokenResource } from '@clerk/types'; export const events = { TokenUpdate: 'token:update', UserSignOut: 'user:signOut', NewSessionTask: 'sessionTask:new', - ResolvedSessionTask: 'sessionTask:resolve', } as const; type ClerkEvent = (typeof events)[keyof typeof events]; @@ -15,8 +14,7 @@ type TokenUpdatePayload = { token: TokenResource | null }; type EventPayload = { [events.TokenUpdate]: TokenUpdatePayload; [events.UserSignOut]: null; - [events.NewSessionTask]: SignedInSessionResource; - [events.ResolvedSessionTask]: SignedInSessionResource; + [events.NewSessionTask]: null; }; const createEventBus = () => { diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 4ac8f4ad4a6..ee22e743446 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -299,6 +299,7 @@ export class Session extends BaseResource implements SessionResource { if (shouldDispatchTokenUpdate) { eventBus.dispatch(events.TokenUpdate, { token }); } + // Return null when raw string is empty to indicate that there it's signed-out return token.getRawString() || null; }); diff --git a/packages/clerk-js/src/ui/common/tasks.ts b/packages/clerk-js/src/ui/common/tasks.ts index d2465f6a4a3..7b81d88a00f 100644 --- a/packages/clerk-js/src/ui/common/tasks.ts +++ b/packages/clerk-js/src/ui/common/tasks.ts @@ -1,6 +1,6 @@ import type { SessionTask } from '@clerk/types'; -export const sessionTaskRoutePaths = ['select-organization'] as const; +export const sessionTaskRoutePaths = ['add-organization'] as const; type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number]; @@ -8,5 +8,5 @@ type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number]; * @internal */ export const sessionTaskKeyToRoutePaths: Record = { - org: '/select-organization', + org: 'add-organization', }; diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index d6d62db4c65..05f633168ea 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -60,7 +60,7 @@ export const withRedirectToTasks =

(Component return withRedirect( Component, hasPendingTasksAndSingleSessionModeEnabled, - ({ clerk }) => clerk.buildTasksUrl(), + ({ clerk }) => clerk.buildTaskUrl(), warnings.cannotRenderComponentWithPendingTasks, )(props); }; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 2bf9024d94a..c067022ab4b 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -333,12 +333,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - buildTasksUrl = (): string | void => { - const callback = () => this.clerkjs?.buildTasksUrl() || ''; + buildTaskUrl = (): string | void => { + const callback = () => this.clerkjs?.buildTaskUrl() || ''; if (this.clerkjs && this.#loaded) { return callback(); } else { - this.premountMethodCalls.set('buildTasksUrl', callback); + this.premountMethodCalls.set('buildTaskUrl', callback); } }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 4f3f134bdf7..b675449b048 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -477,9 +477,9 @@ export interface Clerk { buildWaitlistUrl(opts?: { initialValues?: Record }): string; /** - * Returns the URL where tasks get displayed. It defaults to `signInUrl` or `signUpUrl` as the base route. + * Returns the URL where session task get displayed. It appends to `signInUrl` or `signUpUrl` as the base route. */ - buildTasksUrl(): string; + buildTaskUrl(): string; /** * @@ -538,6 +538,11 @@ export interface Clerk { */ redirectToWaitlist: () => void; + /** + * Redirects to the URL to resolve a pending session task + */ + redirectToTaskResolution: () => void; + /** * Completes a Google One Tap redirection flow started by * {@link Clerk.authenticateWithGoogleOneTap} diff --git a/packages/types/src/jwt.ts b/packages/types/src/jwt.ts index 3b078fd2986..fea8008e263 100644 --- a/packages/types/src/jwt.ts +++ b/packages/types/src/jwt.ts @@ -1,3 +1,5 @@ +import type { SessionStatus } from 'session'; + import type { OrganizationCustomRoleKey } from './organizationMembership'; export interface JWT { @@ -46,6 +48,11 @@ export interface ClerkJWTClaims { */ sid: string; + /** + * Session status + */ + sts: SessionStatus; + /** * JWT Not Before - [RFC7519#section-4.1.5](https://tools.ietf.org/html/rfc7519#section-4.1.5). */ diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index 2ce8ed3b323..94d253d2456 100644 --- a/packages/types/src/jwtv2.ts +++ b/packages/types/src/jwtv2.ts @@ -1,4 +1,5 @@ import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership'; +import type { SessionStatus } from './session'; export interface Jwt { header: JwtHeader; @@ -56,6 +57,11 @@ export interface JwtPayload extends CustomJwtSessionClaims { */ sid: string; + /** + * Session status + */ + sts?: SessionStatus; + /** * JWT Not Before - [RFC7519#section-4.1.5](https://tools.ietf.org/html/rfc7519#section-4.1.5). */ From af7f79a59675b2ca6da85ab3323bde1930ff4c1c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:37:09 -0500 Subject: [PATCH 19/21] Implement integration tests --- .../testUtils/testAgainstRunningApps.ts | 2 - ....test.ts => session-tasks-sign-in.test.ts} | 15 ++++- .../tests/session-tasks-sign-up.test.ts | 56 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) rename integration/tests/{session-tasks.test.ts => session-tasks-sign-in.test.ts} (65%) create mode 100644 integration/tests/session-tasks-sign-up.test.ts diff --git a/integration/testUtils/testAgainstRunningApps.ts b/integration/testUtils/testAgainstRunningApps.ts index dd5502f88a7..2b8c67752d7 100644 --- a/integration/testUtils/testAgainstRunningApps.ts +++ b/integration/testUtils/testAgainstRunningApps.ts @@ -24,8 +24,6 @@ const runningApps = (params: RunningAppsParams = {}) => { const withPattern = (params.withPattern || []).flat().filter(Boolean); const { appIds, appUrl, appPk, appSk, clerkApiUrl } = parseEnvOptions(); - console.log({ appIds }); - if (appIds.length) { // if appIds are provided, we only return the apps with the given ids const filter = app => (withEnv.length ? withEnv.includes(app.env) : true); diff --git a/integration/tests/session-tasks.test.ts b/integration/tests/session-tasks-sign-in.test.ts similarity index 65% rename from integration/tests/session-tasks.test.ts rename to integration/tests/session-tasks-sign-in.test.ts index 726095c4b90..46475d34468 100644 --- a/integration/tests/session-tasks.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -5,7 +5,7 @@ import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils } from '../testUtils'; -test.describe('session tasks flow @nextjs', () => { +test.describe('session tasks sign in flow @nextjs', () => { test.describe.configure({ mode: 'serial' }); let app: Application; let fakeUser: FakeUser; @@ -39,4 +39,17 @@ test.describe('session tasks flow @nextjs', () => { await u.po.expect.toBeSignedIn(); await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); }); + + test('with pending tasks, redirects to tasks when accessing root sign in', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.po.signIn.setPassword(fakeUser.password); + await u.po.signIn.continue(); + await u.po.expect.toBeSignedIn(); + await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + await u.po.signIn.goTo(); + await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + }); }); diff --git a/integration/tests/session-tasks-sign-up.test.ts b/integration/tests/session-tasks-sign-up.test.ts new file mode 100644 index 00000000000..195591b3fdf --- /dev/null +++ b/integration/tests/session-tasks-sign-up.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import { createTestUtils } from '../testUtils'; + +test.describe('session tasks sign in flow @nextjs', () => { + test.describe.configure({ mode: 'serial' }); + let app: Application; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter.clone().commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withSessionTasks); + await app.dev(); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test('on after sign-up, navigates to tasks', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + const fakeUser = u.services.users.createFakeUser({ + fictionalEmail: true, + }); + await u.po.signUp.goTo(); + await u.po.signUp.signUpWithEmailAndPassword({ + email: fakeUser.email, + password: fakeUser.password, + }); + await u.po.signUp.enterTestOtpCode(); + await u.po.expect.toBeSignedIn(); + + await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + + await fakeUser.deleteIfExists(); + }); + + test('with pending tasks, redirects to tasks when accessing root sign in', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + const fakeUser = u.services.users.createFakeUser({ + fictionalEmail: true, + }); + await u.po.signUp.goTo(); + await u.po.signUp.signUpWithEmailAndPassword({ + email: fakeUser.email, + password: fakeUser.password, + }); + await u.po.signUp.enterTestOtpCode(); + await u.po.expect.toBeSignedIn(); + await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + await u.po.signIn.goTo(); + await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + }); +}); From 88184992f4dc1afdda9ac4167954d88da3e33634 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:08:29 -0500 Subject: [PATCH 20/21] Do not close modals on `Clerk.navigate` if the origin is not outisde component --- .changeset/old-cherries-laugh.md | 2 +- .../tests/session-tasks-multi-session.test.ts | 1 + .../tests/session-tasks-sign-in.test.ts | 19 +++++++- .../tests/session-tasks-sign-up.test.ts | 19 +++++++- .../core/__tests__/clerk.redirects.test.ts | 2 - .../src/core/auth/AuthCookieService.ts | 13 ------ packages/clerk-js/src/core/clerk.ts | 43 +++---------------- packages/clerk-js/src/core/events.ts | 2 - .../clerk-js/src/core/resources/Session.ts | 1 - packages/clerk-js/src/core/warnings.ts | 2 - .../clerk-js/src/ui/common/withRedirect.tsx | 33 +++++--------- .../src/ui/components/SignIn/SignIn.tsx | 3 -- .../SignIn/SignInAccountSwitcher.tsx | 6 +-- .../ui/components/SignIn/SignInFactorOne.tsx | 4 +- .../ui/components/SignIn/SignInFactorTwo.tsx | 4 +- .../components/SignIn/SignInSSOCallback.tsx | 4 +- .../src/ui/components/SignIn/SignInStart.tsx | 9 +--- .../components/SignUp/SignUpSSOCallback.tsx | 4 +- .../src/ui/components/SignUp/SignUpStart.tsx | 4 +- .../clerk-js/src/ui/components/Task/Task.tsx | 3 +- .../clerk-js/src/ui/router/BaseRouter.tsx | 1 - .../clerk-js/src/utils/componentGuards.ts | 8 +--- packages/react/src/isomorphicClerk.ts | 9 ---- packages/types/src/clerk.ts | 10 ----- 24 files changed, 72 insertions(+), 134 deletions(-) create mode 100644 integration/tests/session-tasks-multi-session.test.ts diff --git a/.changeset/old-cherries-laugh.md b/.changeset/old-cherries-laugh.md index d94e4755426..44ed51ac371 100644 --- a/.changeset/old-cherries-laugh.md +++ b/.changeset/old-cherries-laugh.md @@ -3,4 +3,4 @@ '@clerk/types': patch --- -Navigate to session task +Navigate to session tasks on after sign-in/sign-up diff --git a/integration/tests/session-tasks-multi-session.test.ts b/integration/tests/session-tasks-multi-session.test.ts new file mode 100644 index 00000000000..4b6ccccc84f --- /dev/null +++ b/integration/tests/session-tasks-multi-session.test.ts @@ -0,0 +1 @@ +// TODO - add tests diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts index 46475d34468..fcb1b4524fd 100644 --- a/integration/tests/session-tasks-sign-in.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -38,9 +38,18 @@ test.describe('session tasks sign in flow @nextjs', () => { await u.po.signIn.continue(); await u.po.expect.toBeSignedIn(); await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + expect(u.page.url()).toContain('/sign-in/add-organization'); }); - test('with pending tasks, redirects to tasks when accessing root sign in', async ({ page, context }) => { + test.fixme('redirects to after sign-in url when session tasks has been resolved', () => { + // todo + }); + + test.fixme('redirects to after sign-in url when accessing root sign in with a active session', { + // todo + }); + + test('redirects back to tasks when accessing root sign in', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.po.signIn.goTo(); await u.po.signIn.setIdentifier(fakeUser.email); @@ -49,7 +58,15 @@ test.describe('session tasks sign in flow @nextjs', () => { await u.po.signIn.continue(); await u.po.expect.toBeSignedIn(); await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + expect(u.page.url()).toContain('/sign-in/add-organization'); await u.po.signIn.goTo(); await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + expect(u.page.url()).toContain('/sign-in/add-organization'); + }); + + test('without a session, does not allow to access tasks', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goto('/sign-in/add-organization'); + expect(u.page.url()).not.toContain('/sign-in/add-organization'); }); }); diff --git a/integration/tests/session-tasks-sign-up.test.ts b/integration/tests/session-tasks-sign-up.test.ts index 195591b3fdf..f438f37f057 100644 --- a/integration/tests/session-tasks-sign-up.test.ts +++ b/integration/tests/session-tasks-sign-up.test.ts @@ -33,11 +33,20 @@ test.describe('session tasks sign in flow @nextjs', () => { await u.po.expect.toBeSignedIn(); await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + expect(u.page.url()).toContain('/sign-up/add-organization'); await fakeUser.deleteIfExists(); }); - test('with pending tasks, redirects to tasks when accessing root sign in', async ({ page, context }) => { + test.fixme('redirects to after sign-up url when session tasks has been resolved', () => { + // todo + }); + + test.fixme('redirects to after sign-up url when accessing root sign in with a active session', { + // todo + }); + + test('redirects back to tasks when accessing root sign in', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); const fakeUser = u.services.users.createFakeUser({ fictionalEmail: true, @@ -50,7 +59,15 @@ test.describe('session tasks sign in flow @nextjs', () => { await u.po.signUp.enterTestOtpCode(); await u.po.expect.toBeSignedIn(); await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + expect(u.page.url()).toContain('/sign-up/add-organization'); await u.po.signIn.goTo(); await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible(); + expect(u.page.url()).toContain('/sign-up/add-organization'); + }); + + test('without a session, does not allow to access tasks', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goto('/sign-up/add-organization'); + expect(u.page.url()).not.toContain('/sign-up/add-organization'); }); }); diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index 8e405068e48..c744fffd27f 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -122,7 +122,6 @@ describe('Clerk singleton - Redirects', () => { afterEach(() => { mockEnvironmentFetch.mockRestore(); - mockClientFetch.mockRestore(); }); it('redirects to signInUrl for development instance', async () => { @@ -221,7 +220,6 @@ describe('Clerk singleton - Redirects', () => { afterEach(() => { mockEnvironmentFetch.mockRestore(); - mockClientFetch.mockRestore(); }); const host = 'http://another-test.host'; diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 0745fad29b1..4ed81e7fc75 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -62,8 +62,6 @@ export class AuthCookieService { this.setClientUatCookieForDevelopmentInstances(); }); - eventBus.on(events.NewSessionTask, () => this.handleSessionTasks()); - this.refreshTokenOnFocus(); this.startPollingForToken(); @@ -153,7 +151,6 @@ export class AuthCookieService { if (updateCookieImmediately) { this.updateSessionCookie(token); } - this.handleSessionTasks(); } catch (e) { return this.handleGetTokenError(e); } @@ -226,14 +223,4 @@ export class AuthCookieService { return this.clerk.organization?.id === activeOrganizationId; } - - private handleSessionTasks() { - const hasPendingStatus = this.clerk.session?.status === 'pending'; - - if (!hasPendingStatus) { - return; - } - - this.clerk.redirectToTask(); - } } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 601203bdda3..de3cb5afecb 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -66,7 +66,6 @@ import type { Web3Provider, } from '@clerk/types'; -import { sessionTaskKeyToRoutePaths } from '../ui/common/tasks'; import type { MountComponentRenderer } from '../ui/Components'; import { ALLOWED_PROTOCOLS, @@ -90,11 +89,11 @@ import { isError, isOrganizationId, isRedirectForFAPIInitiatedFlow, - isSignedInAndSingleSessionModeEnabled, noOrganizationExists, noUserExists, removeClerkQueryParam, requiresUserInput, + sessionExistsAndSingleSessionModeEnabled, stripOrigin, windowNavigate, } from '../utils'; @@ -428,7 +427,7 @@ export class Clerk implements ClerkInterface { public openSignIn = (props?: SignInProps): void => { this.assertComponentsReady(this.#componentControls); - if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { + if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: 'cannot_render_single_session_enabled', @@ -482,7 +481,7 @@ export class Clerk implements ClerkInterface { public openSignUp = (props?: SignUpProps): void => { this.assertComponentsReady(this.#componentControls); - if (isSignedInAndSingleSessionModeEnabled(this, this.environment)) { + if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) { if (this.#instanceType === 'development') { throw new ClerkRuntimeError(warnings.cannotOpenSignInOrSignUp, { code: 'cannot_render_single_session_enabled', @@ -947,6 +946,8 @@ export class Clerk implements ClerkInterface { beforeUnloadTracker?.stopTracking(); } + // Overrides the default behavior of redirects to `afterSignInUrl` + // or `afterSignUpUrl` to redirect the user to their assigned tasks const hasSessionToResolve = newSession?.currentTask; if (redirectUrl && !beforeEmit && !hasSessionToResolve) { beforeUnloadTracker?.startTracking(); @@ -1116,29 +1117,6 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - public buildTaskUrl(): string { - const currentTask = this.session?.currentTask; - if (!currentTask) { - return ''; - } - - const referrerIsTaskUrl = window.location.href.includes(sessionTaskKeyToRoutePaths[currentTask.key]); - if (referrerIsTaskUrl) { - return ''; - } - - const referrerIsSignUpUrl = window.location.href.startsWith(this.buildSignUpUrl()); - - return buildURL( - { - // TODO ORGS-531 - Introduce `tasksUrl` as custom option - base: referrerIsSignUpUrl ? this.#options.signUpUrl : this.#options.signInUrl, - hashPath: sessionTaskKeyToRoutePaths[currentTask.key], - }, - { stringify: true }, - ); - } - public buildAfterMultiSessionSingleSignOutUrl(): string { if (!this.#options.afterMultiSessionSingleSignOutUrl) { return this.buildUrlWithAuth( @@ -1260,13 +1238,6 @@ export class Clerk implements ClerkInterface { return; }; - public redirectToTask = async (): Promise => { - if (inBrowser()) { - return this.navigate(this.buildTaskUrl()); - } - return; - }; - public handleEmailLinkVerification = async ( params: HandleEmailLinkVerificationParams, customNavigate?: (to: string) => Promise, @@ -1765,9 +1736,9 @@ export class Clerk implements ClerkInterface { // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. eventBus.dispatch(events.TokenUpdate, { token: this.session?.lastActiveToken }); - - this.#emit(); } + + this.#emit(); }; get __unstable__environment(): EnvironmentResource | null | undefined { diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts index 1f2a26cc2d2..7401dd91370 100644 --- a/packages/clerk-js/src/core/events.ts +++ b/packages/clerk-js/src/core/events.ts @@ -3,7 +3,6 @@ import type { TokenResource } from '@clerk/types'; export const events = { TokenUpdate: 'token:update', UserSignOut: 'user:signOut', - NewSessionTask: 'sessionTask:new', } as const; type ClerkEvent = (typeof events)[keyof typeof events]; @@ -14,7 +13,6 @@ type TokenUpdatePayload = { token: TokenResource | null }; type EventPayload = { [events.TokenUpdate]: TokenUpdatePayload; [events.UserSignOut]: null; - [events.NewSessionTask]: null; }; const createEventBus = () => { diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index ee22e743446..4ac8f4ad4a6 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -299,7 +299,6 @@ export class Session extends BaseResource implements SessionResource { if (shouldDispatchTokenUpdate) { eventBus.dispatch(events.TokenUpdate, { token }); } - // Return null when raw string is empty to indicate that there it's signed-out return token.getRawString() || null; }); diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts index 2a70489cc78..1fd500a1e91 100644 --- a/packages/clerk-js/src/core/warnings.ts +++ b/packages/clerk-js/src/core/warnings.ts @@ -18,8 +18,6 @@ const warnings = { 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the value set in `afterSignUp` URL instead.', cannotRenderSignInComponentWhenSessionExists: 'The component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `afterSignIn` URL instead.', - cannotRenderComponentWithPendingTasks: - 'The component cannot render when a user has pending tasks to resolve, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `tasksUrl` instead.', cannotRenderComponentWhenUserDoesNotExist: ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', cannotRenderComponentWhenOrgDoesNotExist: ` cannot render unless an organization is active. Since no organization is currently active, this is no-op.`, diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index 05f633168ea..a7a1e0fa8ab 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { warnings } from '../../core/warnings'; import type { ComponentGuard } from '../../utils'; -import { hasPendingTasksAndSingleSessionModeEnabled, isSignedInAndSingleSessionModeEnabled } from '../../utils'; +import { sessionExistsAndSingleSessionModeEnabled } from '../../utils'; import { useEnvironment, useOptions, useSignInContext, useSignUpContext } from '../contexts'; import { useRouter } from '../router'; import type { AvailableComponentProps } from '../types'; @@ -30,6 +30,11 @@ export function withRedirect

( const shouldRedirect = condition(clerk, environment, options); React.useEffect(() => { + // Blocks `afterSignInUrl` or `afterSignUpUrl` redirects on `SignIn/SignUp` render + if (clerk.session?.currentTask) { + return; + } + if (shouldRedirect) { if (warning && isDevelopmentFromPublishableKey(clerk.publishableKey)) { console.info(warning); @@ -38,7 +43,7 @@ export function withRedirect

( // eslint-disable-next-line @typescript-eslint/no-floating-promises navigate(redirectUrl({ clerk, environment, options })); } - }, []); + }, [clerk.session?.currentTask]); if (shouldRedirect) { return null; @@ -52,24 +57,6 @@ export function withRedirect

( return HOC; } -export const withRedirectToTasks =

(Component: ComponentType

) => { - const displayName = Component.displayName || Component.name || 'Component'; - Component.displayName = displayName; - - const HOC = (props: P) => { - return withRedirect( - Component, - hasPendingTasksAndSingleSessionModeEnabled, - ({ clerk }) => clerk.buildTaskUrl(), - warnings.cannotRenderComponentWithPendingTasks, - )(props); - }; - - HOC.displayName = `withRedirectToTasks(${displayName})`; - - return HOC; -}; - export const withRedirectToAfterSignIn =

(Component: ComponentType

) => { const displayName = Component.displayName || Component.name || 'Component'; Component.displayName = displayName; @@ -78,7 +65,7 @@ export const withRedirectToAfterSignIn =

(Com const signInCtx = useSignInContext(); return withRedirect( Component, - isSignedInAndSingleSessionModeEnabled, + sessionExistsAndSingleSessionModeEnabled, ({ clerk }) => signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), warnings.cannotRenderSignInComponentWhenSessionExists, )(props); @@ -97,7 +84,7 @@ export const withRedirectToAfterSignUp =

(Com const signUpCtx = useSignUpContext(); return withRedirect( Component, - isSignedInAndSingleSessionModeEnabled, + sessionExistsAndSingleSessionModeEnabled, ({ clerk }) => signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(), warnings.cannotRenderSignUpComponentWhenSessionExists, )(props); @@ -111,7 +98,7 @@ export const withRedirectToAfterSignUp =

(Com export const withRedirectToHomeSingleSessionGuard =

(Component: ComponentType

) => withRedirect( Component, - isSignedInAndSingleSessionModeEnabled, + sessionExistsAndSingleSessionModeEnabled, ({ environment }) => environment.displayConfig.homeUrl, warnings.cannotRenderComponentWhenSessionExists, ); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 4f68d66646c..3d8540f4cec 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -134,17 +134,14 @@ function SignInRoutes(): JSX.Element { )} - {sessionTaskRoutePaths.map(path => ( !!clerk.session?.currentTask} > ))} - diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx index 2ed47c1fa71..f5f8e6d0ee3 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx @@ -1,4 +1,4 @@ -import { withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; +import { withRedirectToAfterSignIn } from '../../common'; import { useEnvironment, useSignInContext, useSignOutContext } from '../../contexts'; import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; import { Action, Actions, Card, Header, PreviewButton, UserPreview, withCardStateProvider } from '../../elements'; @@ -120,6 +120,4 @@ const _SignInAccountSwitcher = () => { ); }; -export const SignInAccountSwitcher = withRedirectToTasks( - withRedirectToAfterSignIn(withCardStateProvider(_SignInAccountSwitcher)), -); +export const SignInAccountSwitcher = withRedirectToAfterSignIn(withCardStateProvider(_SignInAccountSwitcher)); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx index 6d2cb3a6722..880f429039f 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx @@ -1,7 +1,7 @@ import type { SignInFactor } from '@clerk/types'; import React from 'react'; -import { withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; +import { withRedirectToAfterSignIn } from '../../common'; import { useCoreSignIn, useEnvironment } from '../../contexts'; import { ErrorCard, LoadingCard, useCardState, withCardStateProvider } from '../../elements'; import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies'; @@ -210,4 +210,4 @@ export function _SignInFactorOne(): JSX.Element { } } -export const SignInFactorOne = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne))); +export const SignInFactorOne = withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne)); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx index 7dad8e3452a..f19b71a13cb 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwo.tsx @@ -1,7 +1,7 @@ import type { SignInFactor } from '@clerk/types'; import React from 'react'; -import { withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; +import { withRedirectToAfterSignIn } from '../../common'; import { useCoreSignIn } from '../../contexts'; import { LoadingCard, withCardStateProvider } from '../../elements'; import { SignInFactorTwoAlternativeMethods } from './SignInFactorTwoAlternativeMethods'; @@ -80,4 +80,4 @@ export function _SignInFactorTwo(): JSX.Element { } } -export const SignInFactorTwo = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo))); +export const SignInFactorTwo = withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo)); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx index a0d9c6a4b6a..07bfaed52d2 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInSSOCallback.tsx @@ -1,3 +1,3 @@ -import { SSOCallback, withRedirectToAfterSignIn, withRedirectToTasks } from '../../common'; +import { SSOCallback, withRedirectToAfterSignIn } from '../../common'; -export const SignInSSOCallback = withRedirectToTasks(withRedirectToAfterSignIn(SSOCallback)); +export const SignInSSOCallback = withRedirectToAfterSignIn(SSOCallback); diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 7ec0681ee8b..88ceafd0341 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -7,12 +7,7 @@ import { ERROR_CODES, SIGN_UP_MODES } from '../../../core/constants'; import { clerkInvalidFAPIResponse } from '../../../core/errors'; import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils'; import type { SignInStartIdentifier } from '../../common'; -import { - getIdentifierControlDisplayValues, - groupIdentifiers, - withRedirectToAfterSignIn, - withRedirectToTasks, -} from '../../common'; +import { getIdentifierControlDisplayValues, groupIdentifiers, withRedirectToAfterSignIn } from '../../common'; import { buildSSOCallbackURL } from '../../common/redirects'; import { useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts'; import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; @@ -573,4 +568,4 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> }) ); }; -export const SignInStart = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInStart))); +export const SignInStart = withRedirectToAfterSignIn(withCardStateProvider(_SignInStart)); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx index 6029b0e13fa..a72a27627fa 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSSOCallback.tsx @@ -1,3 +1,3 @@ -import { SSOCallback, withRedirectToAfterSignUp, withRedirectToTasks } from '../../common'; +import { SSOCallback, withRedirectToAfterSignUp } from '../../common'; -export const SignUpSSOCallback = withRedirectToTasks(withRedirectToAfterSignUp(SSOCallback)); +export const SignUpSSOCallback = withRedirectToAfterSignUp(SSOCallback); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 86969e4c436..9ba89b02c85 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { ERROR_CODES, SIGN_UP_MODES } from '../../../core/constants'; import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils/getClerkQueryParam'; -import { withRedirectToAfterSignUp, withRedirectToTasks } from '../../common'; +import { withRedirectToAfterSignUp } from '../../common'; import { SignInContext, useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; import { descriptors, Flex, Flow, localizationKeys, useAppearance, useLocalizations } from '../../customizables'; import { @@ -346,4 +346,4 @@ function _SignUpStart(): JSX.Element { ); } -export const SignUpStart = withRedirectToTasks(withRedirectToAfterSignUp(withCardStateProvider(_SignUpStart))); +export const SignUpStart = withRedirectToAfterSignUp(withCardStateProvider(_SignUpStart)); diff --git a/packages/clerk-js/src/ui/components/Task/Task.tsx b/packages/clerk-js/src/ui/components/Task/Task.tsx index 0fb4445db2e..75d5c89ed81 100644 --- a/packages/clerk-js/src/ui/components/Task/Task.tsx +++ b/packages/clerk-js/src/ui/components/Task/Task.tsx @@ -23,8 +23,7 @@ export function Task(): React.ReactNode { return null; } - const [task] = session.tasks ?? []; - const Content = TaskRegistry[task.key]; + const Content = TaskRegistry[session.currentTask.key]; return Content ? : null; } diff --git a/packages/clerk-js/src/ui/router/BaseRouter.tsx b/packages/clerk-js/src/ui/router/BaseRouter.tsx index cd67ae75853..1874116aa04 100644 --- a/packages/clerk-js/src/ui/router/BaseRouter.tsx +++ b/packages/clerk-js/src/ui/router/BaseRouter.tsx @@ -96,7 +96,6 @@ export const BaseRouter = ({ const isCrossOrigin = toURL.origin !== window.location.origin; const isOutsideOfUIComponent = !toURL.pathname.startsWith('/' + basePath); - console.log({ isCrossOrigin, isOutsideOfUIComponent }); if (isOutsideOfUIComponent || isCrossOrigin) { const res = await clerkNavigate(toURL.href); diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts index f3cb4d0b031..f0084a6a47d 100644 --- a/packages/clerk-js/src/utils/componentGuards.ts +++ b/packages/clerk-js/src/utils/componentGuards.ts @@ -6,12 +6,8 @@ export type ComponentGuard = ( options?: ClerkOptions, ) => boolean; -export const isSignedInAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { - return !!(clerk.isSignedIn && environment?.authConfig.singleSessionMode); -}; - -export const hasPendingTasksAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { - return !!(clerk.session?.currentTask && environment?.authConfig.singleSessionMode); +export const sessionExistsAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { + return !!(clerk.session && environment?.authConfig.singleSessionMode); }; export const noUserExists: ComponentGuard = clerk => { diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index c067022ab4b..85202601e3d 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -333,15 +333,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - buildTaskUrl = (): string | void => { - const callback = () => this.clerkjs?.buildTaskUrl() || ''; - if (this.clerkjs && this.#loaded) { - return callback(); - } else { - this.premountMethodCalls.set('buildTaskUrl', callback); - } - }; - handleUnauthenticated = async () => { const callback = () => this.clerkjs?.handleUnauthenticated(); if (this.clerkjs && this.#loaded) { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index b675449b048..1296685e084 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -476,11 +476,6 @@ export interface Clerk { */ buildWaitlistUrl(opts?: { initialValues?: Record }): string; - /** - * Returns the URL where session task get displayed. It appends to `signInUrl` or `signUpUrl` as the base route. - */ - buildTaskUrl(): string; - /** * * Redirects to the provided url after decorating it with the auth token for development instances. @@ -538,11 +533,6 @@ export interface Clerk { */ redirectToWaitlist: () => void; - /** - * Redirects to the URL to resolve a pending session task - */ - redirectToTaskResolution: () => void; - /** * Completes a Google One Tap redirection flow started by * {@link Clerk.authenticateWithGoogleOneTap} From be6c67e01b9164f0b4271fd232cb6d057c7a2d79 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:36:07 -0300 Subject: [PATCH 21/21] Add intermediary route for task resolution --- packages/clerk-js/src/core/clerk.ts | 34 +++++++++++++++++++ .../clerk-js/src/ui/common/TaskNavigation.tsx | 23 +++++++++++++ packages/clerk-js/src/ui/common/index.ts | 1 + packages/clerk-js/src/ui/common/tasks.ts | 11 +----- .../clerk-js/src/ui/common/withRedirect.tsx | 11 +++--- .../src/ui/components/SignIn/SignIn.tsx | 4 +++ .../SignIn/SignInTaskNavigation.tsx | 3 ++ .../elements/contexts/FlowMetadataContext.tsx | 1 + .../clerk-js/src/utils/componentGuards.ts | 2 +- packages/types/src/clerk.ts | 2 ++ 10 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 packages/clerk-js/src/ui/common/TaskNavigation.tsx create mode 100644 packages/clerk-js/src/ui/components/SignIn/SignInTaskNavigation.tsx diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index de3cb5afecb..fdc5b8a079f 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -23,6 +23,7 @@ import type { CreateOrganizationParams, CreateOrganizationProps, CredentialReturn, + CustomNavigation, DomainOrProxyUrl, EnvironmentJSON, EnvironmentJSONSnapshot, @@ -45,6 +46,7 @@ import type { RedirectOptions, Resources, SDKMetadata, + SessionTask, SetActiveParams, SignedInSessionResource, SignInProps, @@ -65,6 +67,7 @@ import type { WaitlistResource, Web3Provider, } from '@clerk/types'; +import type { SessionTaskRoutePath } from 'ui/common/tasks'; import type { MountComponentRenderer } from '../ui/Components'; import { @@ -1731,6 +1734,8 @@ export class Clerk implements ClerkInterface { if (this.session) { const session = this.#getSessionFromClient(this.session.id); + this.maybeNavigateToTaskResolution(this.navigate); + // Note: this might set this.session to null this.#setAccessors(session); @@ -2263,4 +2268,33 @@ export class Clerk implements ClerkInterface { return allowedProtocols; } + + maybeNavigateToTaskResolution(customNavigate?: (to: string) => Promise) { + if (!this.session?.currentTask || !inBrowser()) { + return; + } + + const isOnTaskResolutionPath = window.location.href.includes('navigate-to-task'); + if (isOnTaskResolutionPath) { + return; + } + + const url = buildURL({ base: `${this.#options.signInUrl}/navigate-to-task` }, { stringify: true }); + + void customNavigate?.(url); + } + + navigateToTaskPath(customNavigate?: CustomNavigation) { + if (!this.session?.currentTask || !inBrowser()) { + return; + } + + const taskKeyToRoutePaths: Record = { + org: 'add-organization', + }; + + const routePath = taskKeyToRoutePaths[this.session.currentTask.key]; + + void customNavigate?.(routePath); + } } diff --git a/packages/clerk-js/src/ui/common/TaskNavigation.tsx b/packages/clerk-js/src/ui/common/TaskNavigation.tsx new file mode 100644 index 00000000000..28b3cca241d --- /dev/null +++ b/packages/clerk-js/src/ui/common/TaskNavigation.tsx @@ -0,0 +1,23 @@ +import { Flow } from '../customizables'; +import { Card, LoadingCardContainer, withCardStateProvider } from '../elements'; + +export const TaskNavigation = withCardStateProvider(() => { + return ( + + + + ); +}); + +export const TaskNavigationCard = () => { + return ( + + + + + + + + + ); +}; diff --git a/packages/clerk-js/src/ui/common/index.ts b/packages/clerk-js/src/ui/common/index.ts index bea7cf9e442..2aade4fa1f7 100644 --- a/packages/clerk-js/src/ui/common/index.ts +++ b/packages/clerk-js/src/ui/common/index.ts @@ -13,6 +13,7 @@ export * from './QRCode'; export * from './redirects'; export * from './RemoveResourceForm'; export * from './SSOCallback'; +export * from './TaskNavigation'; export * from './verification'; export * from './withRedirect'; export * from './Wizard'; diff --git a/packages/clerk-js/src/ui/common/tasks.ts b/packages/clerk-js/src/ui/common/tasks.ts index 7b81d88a00f..3f64e32a293 100644 --- a/packages/clerk-js/src/ui/common/tasks.ts +++ b/packages/clerk-js/src/ui/common/tasks.ts @@ -1,12 +1,3 @@ -import type { SessionTask } from '@clerk/types'; - export const sessionTaskRoutePaths = ['add-organization'] as const; -type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number]; - -/** - * @internal - */ -export const sessionTaskKeyToRoutePaths: Record = { - org: 'add-organization', -}; +export type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number]; diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index a7a1e0fa8ab..a46a2eeb65f 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -28,10 +28,13 @@ export function withRedirect

( const environment = useEnvironment(); const options = useOptions(); - const shouldRedirect = condition(clerk, environment, options); + const hasTasksAndSingleSessionMode = clerk.session?.currentTask && environment?.authConfig.singleSessionMode; + const shouldRedirect = + // Overrides default redirect guards to not lead with race conditions on redirection for session tasks + hasTasksAndSingleSessionMode ? false : condition(clerk, environment, options); React.useEffect(() => { - // Blocks `afterSignInUrl` or `afterSignUpUrl` redirects on `SignIn/SignUp` render - if (clerk.session?.currentTask) { + if (hasTasksAndSingleSessionMode) { + void clerk.maybeNavigateToTaskResolution(navigate); return; } @@ -43,7 +46,7 @@ export function withRedirect

( // eslint-disable-next-line @typescript-eslint/no-floating-promises navigate(redirectUrl({ clerk, environment, options })); } - }, [clerk.session?.currentTask]); + }, [hasTasksAndSingleSessionMode]); if (shouldRedirect) { return null; diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 3d8540f4cec..89493cc0049 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -28,6 +28,7 @@ import { SignInFactorOne } from './SignInFactorOne'; import { SignInFactorTwo } from './SignInFactorTwo'; import { SignInSSOCallback } from './SignInSSOCallback'; import { SignInStart } from './SignInStart'; +import { SignInTaskNavigation } from './SignInTaskNavigation'; function RedirectToSignIn() { const clerk = useClerk(); @@ -134,6 +135,9 @@ function SignInRoutes(): JSX.Element { )} + + + {sessionTaskRoutePaths.map(path => ( boolean; export const sessionExistsAndSingleSessionModeEnabled: ComponentGuard = (clerk, environment) => { - return !!(clerk.session && environment?.authConfig.singleSessionMode); + return !!(clerk.isSignedIn && environment?.authConfig.singleSessionMode); }; export const noUserExists: ComponentGuard = clerk => { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 1296685e084..3f6a3ed87d4 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -560,6 +560,8 @@ export interface Clerk { customNavigate?: (to: string) => Promise, ) => Promise; + maybeNavigateToTaskResolution: (customNavigate?: (to: string) => Promise) => void; + /** * Authenticates user using their Metamask browser extension */