diff --git a/.changeset/silent-terms-flow.md b/.changeset/silent-terms-flow.md new file mode 100644 index 00000000000..b20f0bc5aa5 --- /dev/null +++ b/.changeset/silent-terms-flow.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/react': patch +--- + +Expose `__experimental_Tasks` component for after-auth custom flows out of `SignIn/SignUp` components diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2af08c16035..dd51bdf5ef5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1037,6 +1037,29 @@ export class Clerk implements ClerkInterface { ); }; + public mountTasks = (node: HTMLDivElement): void => { + this.assertComponentsReady(this.#componentControls); + + void this.#componentControls.ensureMounted({ preloadHint: '__experimental_Tasks' }).then(controls => + controls.mountComponent({ + name: '__experimental_Tasks', + appearanceKey: 'tasks', + node, + }), + ); + + this.telemetry?.record(eventPrebuiltComponentMounted('__experimental_Tasks')); + }; + + public unmountTasks = (node: HTMLDivElement): void => { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted().then(controls => + controls.unmountComponent({ + node, + }), + ); + }; + /** * `setActive` can be used to set the active session and/or organization. */ diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 5aecb49e7e4..18f7f042a06 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -1,6 +1,5 @@ import { useClerk } from '@clerk/shared/react'; import { eventComponentMounted } from '@clerk/shared/telemetry'; -import type { SessionTask } from '@clerk/types'; import { useCallback, useContext, useEffect } from 'react'; import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; @@ -36,7 +35,7 @@ const SessionTasksStart = withCardStateProvider(() => { ); }); -function SessionTaskRoutes(): JSX.Element { +function SessionTasksRoutes(): JSX.Element { return ( @@ -56,10 +55,7 @@ function SessionTaskRoutes(): JSX.Element { ); } -/** - * @internal - */ -export function SessionTask(): JSX.Element { +export function __experimental_Tasks(): JSX.Element { const clerk = useClerk(); const { navigate } = useRouter(); const signInContext = useContext(SignInContext); @@ -86,7 +82,7 @@ export function SessionTask(): JSX.Element { return ( - + ); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index a0f8b078514..178dbfeeb51 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -2,7 +2,7 @@ import { useClerk } from '@clerk/shared/react'; import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; -import { SessionTasks as LazySessionTasks } from '../../../ui/lazyModules/components'; +import { __experimental_Tasks as LazyTasks } from '../../../ui/lazyModules/components'; import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions'; import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; import { @@ -131,7 +131,7 @@ function SignInRoutes(): JSX.Element { - + @@ -143,7 +143,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 8668ee07dc9..217dcdbdc58 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -2,7 +2,7 @@ import { useClerk } from '@clerk/shared/react'; import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; -import { SessionTasks as LazySessionTasks } from '../../../ui/lazyModules/components'; +import { __experimental_Tasks as LazyTasks } from '../../../ui/lazyModules/components'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; @@ -85,7 +85,7 @@ function SignUpRoutes(): JSX.Element { - + diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts index eebba1e0a69..829fa147376 100644 --- a/packages/clerk-js/src/ui/lazyModules/components.ts +++ b/packages/clerk-js/src/ui/lazyModules/components.ts @@ -102,8 +102,8 @@ export const PlanDetails = lazy(() => componentImportPaths.PlanDetails().then(module => ({ default: module.PlanDetails })), ); -export const SessionTasks = lazy(() => - componentImportPaths.SessionTasks().then(module => ({ default: module.SessionTask })), +export const __experimental_Tasks = lazy(() => + componentImportPaths.SessionTasks().then(module => ({ default: module.__experimental_Tasks })), ); export const preloadComponent = async (component: unknown) => { @@ -133,6 +133,7 @@ export const ClerkComponents = { PricingTable, Checkout, PlanDetails, + __experimental_Tasks, }; export type ClerkComponentName = keyof typeof ClerkComponents; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index f932a18115d..947dda21c83 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -10,6 +10,7 @@ export { GoogleOneTap, Waitlist, PricingTable, + __experimental_Tasks, } from './uiComponents'; export { diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index efba0fe8130..064a76ab2c8 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -600,3 +600,31 @@ export const PricingTable = withClerk( }, { component: 'PricingTable', renderWhileLoading: true }, ); + +export const __experimental_Tasks = withClerk( + ({ clerk, component, fallback, ...props }: WithClerkProp) => { + const mountingStatus = useWaitForComponentMount(component); + const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; + + const rendererRootProps = { + ...(shouldShowFallback && fallback && { style: { display: 'none' } }), + }; + + return ( + <> + {shouldShowFallback && fallback} + {clerk.loaded && ( + + )} + + ); + }, + { component: '__experimental_Tasks', renderWhileLoading: true }, +); diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 050d12d68a5..dcc340f0864 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -131,6 +131,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountMethodCalls = new Map, MethodCallback>(); private premountWaitlistNodes = new Map(); private premountPricingTableNodes = new Map(); + private premountTasksNodes = new Map(); // A separate Map of `addListener` method calls to handle multiple listeners. private premountAddListenerCalls = new Map< ListenerCallback, @@ -1050,6 +1051,22 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + mountTasks = (node: HTMLDivElement) => { + if (this.clerkjs && this.loaded) { + this.clerkjs.mountTasks(node); + } else { + this.premountTasksNodes.set(node, undefined); + } + }; + + unmountTasks = (node: HTMLDivElement) => { + if (this.clerkjs && this.loaded) { + this.clerkjs.unmountTasks(node); + } else { + this.premountTasksNodes.delete(node); + } + }; + addListener = (listener: ListenerCallback): UnsubscribeCallback => { if (this.clerkjs) { return this.clerkjs.addListener(listener); diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index 292fa51a751..1e6f2261618 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -867,4 +867,8 @@ export type Appearance = T & { * Theme overrides that only apply to the `` component */ checkout?: T; + /** + * Theme overrides that only apply to the `` component + */ + tasks?: T; }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 7ddf374c650..bee6454e71a 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -458,6 +458,20 @@ export interface Clerk { */ unmountPricingTable: (targetNode: HTMLDivElement) => void; + /** + * Mounts tasks component at the target element. + * @param targetNode Target node to mount the Tasks component. + * @param props configuration parameters. + */ + mountTasks: (targetNode: HTMLDivElement) => void; + /** + * Unmount tasks component from the target element. + * If there is no component mounted at the target node, results in a noop. + * + * @param targetNode Target node to unmount the Tasks component from. + */ + unmountTasks: (targetNode: HTMLDivElement) => void; + /** * Register a listener that triggers a callback each time important Clerk resources are changed. * Allows to hook up at different steps in the sign up, sign in processes.