Skip to content

Commit

Permalink
Refactor redirect guards for tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
LauraBeatris committed Feb 20, 2025
1 parent e6c10fa commit 1120aa0
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 26 deletions.
45 changes: 37 additions & 8 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {
RedirectOptions,
Resources,
SDKMetadata,
SessionTask,
SetActiveParams,
SignedInSessionResource,
SignInProps,
Expand All @@ -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,
Expand All @@ -89,11 +91,11 @@ import {
isError,
isOrganizationId,
isRedirectForFAPIInitiatedFlow,
isSignedInAndSingleSessionModeEnabled,
noOrganizationExists,
noUserExists,
removeClerkQueryParam,
requiresUserInput,
sessionExistsAndSingleSessionModeEnabled,
stripOrigin,
windowNavigate,
} from '../utils';
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -1004,6 +1012,8 @@ export class Clerk implements ClerkInterface {
return;
}

console.log({ to });

/**
* Trigger all navigation listeners. In order for modal UI components to close.
*/
Expand Down Expand Up @@ -1037,6 +1047,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);
};
Expand Down Expand Up @@ -1090,6 +1101,7 @@ export class Clerk implements ClerkInterface {
}

public buildAfterSignInUrl({ params }: { params?: URLSearchParams } = {}): string {
console.log('build after sign in');
return this.buildUrlWithAuth(new RedirectUrls(this.#options, {}, params).getAfterSignInUrl());
}

Expand All @@ -1114,13 +1126,18 @@ export class Clerk implements ClerkInterface {
return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true });
}

public buildTasksUrl(options?: { initialValues?: Record<string, string> }): 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 {
Expand Down Expand Up @@ -1244,6 +1261,13 @@ export class Clerk implements ClerkInterface {
return;
};

public redirectToTask = async (...options: Parameters<typeof this.buildTasksUrl>): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.buildTasksUrl(...options));
}
return;
};

public handleEmailLinkVerification = async (
params: HandleEmailLinkVerificationParams,
customNavigate?: (to: string) => Promise<unknown>,
Expand Down Expand Up @@ -2085,6 +2109,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
Expand Down
4 changes: 3 additions & 1 deletion packages/clerk-js/src/core/events.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -13,6 +14,7 @@ type TokenUpdatePayload = { token: TokenResource | null };
type EventPayload = {
[events.TokenUpdate]: TokenUpdatePayload;
[events.UserSignOut]: null;
[events.NewSessionTask]: SignedInSessionResource;
};

const createEventBus = () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
18 changes: 12 additions & 6 deletions packages/clerk-js/src/ui/common/withRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,8 +60,11 @@ export const withRedirectToAfterSignIn = <P extends AvailableComponentProps>(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);
};
Expand All @@ -79,8 +82,11 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(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);
};
Expand All @@ -93,7 +99,7 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
export const withRedirectToHomeSingleSessionGuard = <P extends AvailableComponentProps>(Component: ComponentType<P>) =>
withRedirect(
Component,
sessionExistsAndSingleSessionModeEnabled,
isSignedInAndSingleSessionModeEnabled,
({ environment }) => environment.displayConfig.homeUrl,
warnings.cannotRenderComponentWhenSessionExists,
);
4 changes: 2 additions & 2 deletions packages/clerk-js/src/ui/components/Task/Task.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,7 +19,7 @@ const TaskRegistry: Record<SessionTask['key'], ComponentType> = {
export function Task(): React.ReactNode {
const session = useSessionContext();

if (!session?.hasTask) {
if (!session?.currentTask) {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/components/Task/useTaskRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Task } from './Task';
export function useTaskRoute(): ComponentProps<typeof Route> | null {
const session = useSessionContext();

if (!session?.hasTask) {
if (!session?.currentTask) {
return null;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/clerk-js/src/utils/componentGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand Down
6 changes: 2 additions & 4 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -479,7 +479,7 @@ export interface Clerk {
/**
* Returns the url where a custom task page is rendered.
*/
buildTasksUrl(opts?: { initialValues?: Record<string, string> }): string;
buildTasksUrl(options: { task?: SessionTask; origin?: 'SignIn' | 'SignUp' }): string;

/**
*
Expand Down Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export interface SessionResource extends ClerkResource {
lastActiveAt: Date;
actor: ActJWTClaim | null;
tasks: Array<SessionTask> | null;
hasTask: boolean;
currentTask?: SessionTask;
user: UserResource | null;
publicUserData: PublicUserData;
end: () => Promise<SessionResource>;
Expand Down

0 comments on commit 1120aa0

Please sign in to comment.