Skip to content

Commit

Permalink
Add unit test for redirection to task
Browse files Browse the repository at this point in the history
  • Loading branch information
LauraBeatris committed Feb 24, 2025
1 parent 9a5415d commit 1233d3b
Show file tree
Hide file tree
Showing 21 changed files with 130 additions and 302 deletions.
54 changes: 54 additions & 0 deletions packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ describe('Clerk singleton - Redirects', () => {

afterEach(() => {
mockEnvironmentFetch.mockRestore();
mockClientFetch.mockRestore();
});

it('redirects to signInUrl for development instance', async () => {
Expand Down Expand Up @@ -220,6 +221,7 @@ describe('Clerk singleton - Redirects', () => {

afterEach(() => {
mockEnvironmentFetch.mockRestore();
mockClientFetch.mockRestore();
});

const host = 'http://another-test.host';
Expand Down Expand Up @@ -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),
});
});
});
});
41 changes: 19 additions & 22 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import type {
PublicKeyCredentialWithAuthenticatorAssertionResponse,
PublicKeyCredentialWithAuthenticatorAttestationResponse,
RedirectOptions,
RedirectToTasksUrlOptions,
Resources,
SDKMetadata,
SetActiveParams,
Expand All @@ -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,
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1257,9 +1250,9 @@ export class Clerk implements ClerkInterface {
return;
};

public redirectToTasks = async (options: RedirectToTasksUrlOptions): Promise<unknown> => {
public redirectToTasks = async (): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.internal__buildTasksUrl(options));
return this.navigate(this.buildTasksUrl());
}
return;
};
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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, () => {
Expand Down
6 changes: 2 additions & 4 deletions packages/clerk-js/src/core/warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ const warnings = {
'The <SignUp/> 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 <SignIn/> 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 <SignIn/> 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 <SignUp/> 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:
'<UserProfile/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
cannotRenderComponentWhenOrgDoesNotExist: `<OrganizationProfile/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
Expand Down
6 changes: 5 additions & 1 deletion packages/clerk-js/src/ui/common/tasks.ts
Original file line number Diff line number Diff line change
@@ -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<SessionTask['key'], string> = {
export const sessionTaskKeyToRoutePaths: Record<SessionTask['key'], SessionTaskRoutePath> = {
org: 'select-organization',
};
30 changes: 5 additions & 25 deletions packages/clerk-js/src/ui/common/withRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,20 @@ export function withRedirect<P extends AvailableComponentProps>(
return HOC;
}

export const withRedirectToTasksAfterSignIn = <P extends AvailableComponentProps>(Component: ComponentType<P>) => {
export const withRedirectToTasks = <P extends AvailableComponentProps>(Component: ComponentType<P>) => {
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;
};
Expand Down Expand Up @@ -99,7 +98,7 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
return withRedirect(
Component,
isSignedInAndSingleSessionModeEnabled,
({ clerk }) => signUpCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
({ clerk }) => signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
warnings.cannotRenderSignUpComponentWhenSessionExists,
)(props);
};
Expand All @@ -109,25 +108,6 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
return HOC;
};

export const withRedirectToTasksAfterSignUp = <P extends AvailableComponentProps>(Component: ComponentType<P>) => {
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 = <P extends AvailableComponentProps>(Component: ComponentType<P>) =>
withRedirect(
Component,
Expand Down
16 changes: 13 additions & 3 deletions packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -39,7 +40,6 @@ function RedirectToSignIn() {
function SignInRoutes(): JSX.Element {
const signInContext = useSignInContext();
const signUpContext = useSignUpContext();
const taskRoute = useTaskRoute();

return (
<Flow.Root flow='signIn'>
Expand Down Expand Up @@ -134,7 +134,17 @@ function SignInRoutes(): JSX.Element {
</Route>
</Route>
)}
{taskRoute && <Route {...taskRoute} />}

{sessionTaskRoutePaths.map(path => (
<Route
key={path}
path={path}
canActivate={clerk => !!clerk.session?.currentTask}
>
<Task />
</Route>
))}

<Route index>
<SignInStart />
</Route>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -120,6 +120,6 @@ const _SignInAccountSwitcher = () => {
</Flow.Part>
);
};
export const SignInAccountSwitcher = withRedirectToTasksAfterSignIn(
export const SignInAccountSwitcher = withRedirectToTasks(
withRedirectToAfterSignIn(withCardStateProvider(_SignInAccountSwitcher)),
);
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -210,6 +210,4 @@ export function _SignInFactorOne(): JSX.Element {
}
}

export const SignInFactorOne = withRedirectToTasksAfterSignIn(
withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne)),
);
export const SignInFactorOne = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorOne)));
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -80,6 +80,4 @@ export function _SignInFactorTwo(): JSX.Element {
}
}

export const SignInFactorTwo = withRedirectToTasksAfterSignIn(
withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo)),
);
export const SignInFactorTwo = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInFactorTwo)));
Original file line number Diff line number Diff line change
@@ -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));
6 changes: 2 additions & 4 deletions packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
getIdentifierControlDisplayValues,
groupIdentifiers,
withRedirectToAfterSignIn,
withRedirectToTasksAfterSignIn,
withRedirectToTasks,
} from '../../common';
import { buildSSOCallbackURL } from '../../common/redirects';
import { useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts';
Expand Down Expand Up @@ -573,6 +573,4 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> })
);
};

export const SignInStart = withRedirectToTasksAfterSignIn(
withRedirectToAfterSignIn(withCardStateProvider(_SignInStart)),
);
export const SignInStart = withRedirectToTasks(withRedirectToAfterSignIn(withCardStateProvider(_SignInStart)));
Loading

0 comments on commit 1233d3b

Please sign in to comment.