Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(clerk-js,types): Navigate to after-auth tasks #5187

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/old-cherries-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/types': patch
---

Navigate to after-auth tasks
57 changes: 53 additions & 4 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
PublicKeyCredentialWithAuthenticatorAssertionResponse,
PublicKeyCredentialWithAuthenticatorAttestationResponse,
RedirectOptions,
RedirectToTasksUrlOptions,
Resources,
SDKMetadata,
SetActiveParams,
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 @@ -889,6 +891,10 @@ 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 }));

// 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).
Expand Down Expand Up @@ -946,7 +952,7 @@ export class Clerk implements ClerkInterface {
beforeUnloadTracker?.stopTracking();
}

if (redirectUrl && !beforeEmit) {
if (redirectUrl && !beforeEmit && !isResolvingSessionTasks) {
beforeUnloadTracker?.startTracking();
this.#setTransitiveState();

Expand Down Expand Up @@ -1004,6 +1010,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 @@ -1114,6 +1122,20 @@ export class Clerk implements ClerkInterface {
return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true });
}

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.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 });
}

public buildAfterMultiSessionSingleSignOutUrl(): string {
if (!this.#options.afterMultiSessionSingleSignOutUrl) {
return this.buildUrlWithAuth(
Expand Down Expand Up @@ -1235,6 +1257,13 @@ export class Clerk implements ClerkInterface {
return;
};

public redirectToTasks = async (options: RedirectToTasksUrlOptions): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.internal__buildTasksUrl(options));
}
return;
};

public handleEmailLinkVerification = async (
params: HandleEmailLinkVerificationParams,
customNavigate?: (to: string) => Promise<unknown>,
Expand Down Expand Up @@ -1728,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();
Expand Down Expand Up @@ -2076,6 +2115,16 @@ export class Clerk implements ClerkInterface {
eventBus.on(events.UserSignOut, () => {
this.#broadcastChannel?.postMessage({ type: 'signout' });
});

eventBus.on(events.NewSessionTask, session => {
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
Expand Down
6 changes: 5 additions & 1 deletion packages/clerk-js/src/core/events.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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',
ResolvedSessionTask: 'sessionTask:resolve',
} as const;

type ClerkEvent = (typeof events)[keyof typeof events];
Expand All @@ -13,6 +15,8 @@ type TokenUpdatePayload = { token: TokenResource | null };
type EventPayload = {
[events.TokenUpdate]: TokenUpdatePayload;
[events.UserSignOut]: null;
[events.NewSessionTask]: SignedInSessionResource;
[events.ResolvedSessionTask]: SignedInSessionResource;
};

const createEventBus = () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,8 @@ export class Session extends BaseResource implements SessionResource {
return token.getRawString() || null;
});
}

get currentTask(): SessionTask | undefined {
return (this.tasks ?? [])[0];
}
}
8 changes: 8 additions & 0 deletions packages/clerk-js/src/ui/common/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { SessionTask } from '@clerk/types';

/**
* @internal
*/
export const sessionTaskRoutePaths: Record<SessionTask['key'], string> = {
org: 'select-organization',
};
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.internal__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.internal__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,
);
3 changes: 3 additions & 0 deletions packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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 { ResetPassword } from './ResetPassword';
import { ResetPasswordSuccess } from './ResetPasswordSuccess';
import { SignInAccountSwitcher } from './SignInAccountSwitcher';
Expand All @@ -38,6 +39,7 @@ function RedirectToSignIn() {
function SignInRoutes(): JSX.Element {
const signInContext = useSignInContext();
const signUpContext = useSignUpContext();
const taskRoute = useTaskRoute();

return (
<Flow.Root flow='signIn'>
Expand Down Expand Up @@ -132,6 +134,7 @@ function SignInRoutes(): JSX.Element {
</Route>
</Route>
)}
{taskRoute && <Route {...taskRoute} />}
<Route index>
<SignInStart />
</Route>
Expand Down
3 changes: 3 additions & 0 deletions packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { useTaskRoute } from '../Task/useTaskRoute';
import { SignUpContinue } from './SignUpContinue';
import { SignUpSSOCallback } from './SignUpSSOCallback';
import { SignUpStart } from './SignUpStart';
Expand All @@ -22,6 +23,7 @@ function RedirectToSignUp() {

function SignUpRoutes(): JSX.Element {
const signUpContext = useSignUpContext();
const taskRoute = useTaskRoute();

return (
<Flow.Root flow='signUp'>
Expand Down Expand Up @@ -74,6 +76,7 @@ function SignUpRoutes(): JSX.Element {
<SignUpContinue />
</Route>
</Route>
{taskRoute && <Route {...taskRoute} />}
<Route index>
<SignUpStart />
</Route>
Expand Down
30 changes: 30 additions & 0 deletions packages/clerk-js/src/ui/components/Task/Task.tsx
Original file line number Diff line number Diff line change
@@ -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<SessionTask['key'], ComponentType> = {
org: () => (
<OrganizationListContext.Provider value={{ componentName: 'OrganizationList', hidePersonal: true }}>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hidePersonal is being directly provided here, but this is going to be updated in a next PR.

We're going to include a property on Enviroment to indicate whether an after-auth task is enabled for instance.

For the use case of forcing to select an org, this is going to be checked once the environment loads, and the org components are going to have hidePersonal: true by default.

<OrganizationList />
</OrganizationListContext.Provider>
),
};

/**
* @internal
*/
export function Task(): React.ReactNode {
const session = useSessionContext();

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

const [task] = session.tasks ?? [];
const Content = TaskRegistry[task.key];

return Content ? <Content /> : null;
}
Loading
Loading