From 6ee96e8e637155a2cc9da0766dde67feee4de95d Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 5 Dec 2025 13:19:03 +0200 Subject: [PATCH 1/2] feat(clerk-js,clerk-react,nextjs): Export TaskResetPassword component (#7314) --- .changeset/modern-coins-camp.md | 8 +++++ packages/clerk-js/sandbox/app.ts | 10 +++++++ packages/clerk-js/src/core/clerk.ts | 21 ++++++++++++++ .../src/client-boundary/uiComponents.tsx | 1 + packages/nextjs/src/index.ts | 1 + .../__snapshots__/exports.test.ts.snap | 1 + packages/react/src/components/index.ts | 1 + .../react/src/components/uiComponents.tsx | 29 +++++++++++++++++++ packages/react/src/isomorphicClerk.ts | 23 ++++++++++++++- packages/shared/src/types/clerk.ts | 16 ++++++++++ .../__snapshots__/exports.test.ts.snap | 1 + packages/ui/src/internal/appearance.ts | 4 +++ packages/ui/src/lazyModules/components.ts | 7 +++++ packages/ui/src/types.ts | 3 +- 14 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 .changeset/modern-coins-camp.md diff --git a/.changeset/modern-coins-camp.md b/.changeset/modern-coins-camp.md new file mode 100644 index 00000000000..e66dfbc9a06 --- /dev/null +++ b/.changeset/modern-coins-camp.md @@ -0,0 +1,8 @@ +--- +"@clerk/clerk-js": minor +"@clerk/nextjs": minor +"@clerk/clerk-react": minor +"@clerk/shared": minor +--- + +Introduce new `` session task component diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index ef0d9201ea9..97cd8722af9 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -37,6 +37,7 @@ const AVAILABLE_COMPONENTS = [ 'apiKeys', 'oauthConsent', 'taskChooseOrganization', + 'taskResetPassword', ] as const; const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox'; @@ -99,6 +100,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component apiKeys: buildComponentControls('apiKeys'), oauthConsent: buildComponentControls('oauthConsent'), taskChooseOrganization: buildComponentControls('taskChooseOrganization'), + taskResetPassword: buildComponentControls('taskResetPassword'), }; declare global { @@ -352,6 +354,14 @@ void (async () => { }, ); }, + '/task-reset-password': () => { + Clerk.mountTaskResetPassword( + app, + componentControls.taskResetPassword.getProps() ?? { + redirectUrlComplete: '/user-profile', + }, + ); + }, '/open-sign-in': () => { mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {}); }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 6d03cc322a6..3b12a1f993a 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -108,6 +108,7 @@ import type { SignUpRedirectOptions, SignUpResource, TaskChooseOrganizationProps, + TaskResetPasswordProps, TasksRedirectOptions, UnsubscribeCallback, UserAvatarProps, @@ -1391,6 +1392,26 @@ export class Clerk implements ClerkInterface { void this.#clerkUi?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; + public mountTaskResetPassword = (node: HTMLDivElement, props?: TaskResetPasswordProps) => { + this.assertComponentsReady(this.#componentControls); + + void this.#componentControls.ensureMounted({ preloadHint: 'TaskResetPassword' }).then(controls => + controls.mountComponent({ + name: 'TaskResetPassword', + appearanceKey: 'taskResetPassword', + node, + props, + }), + ); + + this.telemetry?.record(eventPrebuiltComponentMounted('TaskResetPassword', props)); + }; + + public unmountTaskResetPassword = (node: HTMLDivElement) => { + 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/nextjs/src/client-boundary/uiComponents.tsx b/packages/nextjs/src/client-boundary/uiComponents.tsx index 84407b3e811..6d032dc45c5 100644 --- a/packages/nextjs/src/client-boundary/uiComponents.tsx +++ b/packages/nextjs/src/client-boundary/uiComponents.tsx @@ -23,6 +23,7 @@ export { SignOutButton, SignUpButton, TaskChooseOrganization, + TaskResetPassword, UserAvatar, UserButton, Waitlist, diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 2e29bcd7568..b9c24e9b7ce 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -35,6 +35,7 @@ export { SignUp, SignUpButton, TaskChooseOrganization, + TaskResetPassword, UserAvatar, UserButton, UserProfile, diff --git a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap index 8b268b93a36..54b196e9899 100644 --- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap @@ -45,6 +45,7 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "SignedIn", "SignedOut", "TaskChooseOrganization", + "TaskResetPassword", "UserAvatar", "UserButton", "UserProfile", diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index cbf9b77aba1..dfbcedcfa93 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -9,6 +9,7 @@ export { SignIn, SignUp, TaskChooseOrganization, + TaskResetPassword, UserAvatar, UserButton, UserProfile, diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index 6233b985516..f49c7618731 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -9,6 +9,7 @@ import type { SignInProps, SignUpProps, TaskChooseOrganizationProps, + TaskResetPasswordProps, UserAvatarProps, UserButtonProps, UserProfileProps, @@ -696,3 +697,31 @@ export const TaskChooseOrganization = withClerk( }, { component: 'TaskChooseOrganization', renderWhileLoading: true }, ); + +export const TaskResetPassword = 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: 'TaskResetPassword', renderWhileLoading: true }, +); diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 9e94ca83f57..7328457558d 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -48,6 +48,7 @@ import type { SignUpResource, State, TaskChooseOrganizationProps, + TaskResetPasswordProps, TasksRedirectOptions, UnsubscribeCallback, UserAvatarProps, @@ -152,7 +153,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountAPIKeysNodes = new Map(); private premountOAuthConsentNodes = new Map(); private premountTaskChooseOrganizationNodes = new Map(); - + private premountTaskResetPasswordNodes = new Map(); // A separate Map of `addListener` method calls to handle multiple listeners. private premountAddListenerCalls = new Map< ListenerCallback, @@ -689,6 +690,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.mountTaskChooseOrganization(node, props); }); + this.premountTaskResetPasswordNodes.forEach((props, node) => { + clerkjs.mountTaskResetPassword(node, props); + }); + /** * Only update status in case `clerk.status` is missing. In any other case, `clerk-js` should be the orchestrator. */ @@ -1233,6 +1238,22 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + mountTaskResetPassword = (node: HTMLDivElement, props?: TaskResetPasswordProps): void => { + if (this.clerkjs && this.loaded) { + this.clerkjs.mountTaskResetPassword(node, props); + } else { + this.premountTaskResetPasswordNodes.set(node, props); + } + }; + + unmountTaskResetPassword = (node: HTMLDivElement): void => { + if (this.clerkjs && this.loaded) { + this.clerkjs.unmountTaskResetPassword(node); + } else { + this.premountTaskResetPasswordNodes.delete(node); + } + }; + addListener = (listener: ListenerCallback): UnsubscribeCallback => { if (this.clerkjs) { return this.clerkjs.addListener(listener); diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 582978461f3..f7ae200b913 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -656,6 +656,22 @@ export interface Clerk { */ unmountTaskChooseOrganization: (targetNode: HTMLDivElement) => void; + /** + * Mounts a TaskResetPassword component at the target element. + * + * @param targetNode - Target node to mount the TaskResetPassword component. + * @param props - configuration parameters. + */ + mountTaskResetPassword: (targetNode: HTMLDivElement, props?: TaskResetPasswordProps) => void; + + /** + * Unmount a TaskResetPassword 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 TaskResetPassword component from. + */ + unmountTaskResetPassword: (targetNode: HTMLDivElement) => void; + /** * @internal * Loads Stripe libraries for commerce functionality diff --git a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap index c8a1f96ceba..3e1c592195b 100644 --- a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap @@ -50,6 +50,7 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "SignedIn", "SignedOut", "TaskChooseOrganization", + "TaskResetPassword", "UserAvatar", "UserButton", "UserProfile", diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 092e14d5c4e..d1aba188a80 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -1065,6 +1065,10 @@ export type Appearance = T & * Theme overrides that only apply to the `` component */ taskChooseOrganization?: T; + /** + * Theme overrides that only apply to the `` component + */ + taskResetPassword?: T; /** * Theme overrides that only apply to the `` component */ diff --git a/packages/ui/src/lazyModules/components.ts b/packages/ui/src/lazyModules/components.ts index cd00ce8ee0b..e5a0d04b73e 100644 --- a/packages/ui/src/lazyModules/components.ts +++ b/packages/ui/src/lazyModules/components.ts @@ -23,6 +23,8 @@ const componentImportPaths = { SessionTasks: () => import(/* webpackChunkName: "sessionTasks" */ '../components/SessionTasks'), TaskChooseOrganization: () => import(/* webpackChunkName: "taskChooseOrganization" */ '../components/SessionTasks/tasks/TaskChooseOrganization'), + TaskResetPassword: () => + import(/* webpackChunkName: "taskResetPassword" */ '../components/SessionTasks/tasks/TaskResetPassword'), PlanDetails: () => import(/* webpackChunkName: "planDetails" */ '../components/Plans/PlanDetails'), SubscriptionDetails: () => import(/* webpackChunkName: "subscriptionDetails" */ '../components/SubscriptionDetails'), APIKeys: () => import(/* webpackChunkName: "apiKeys" */ '../components/APIKeys/APIKeys'), @@ -123,6 +125,10 @@ export const TaskChooseOrganization = lazy(() => componentImportPaths.TaskChooseOrganization().then(module => ({ default: module.TaskChooseOrganization })), ); +export const TaskResetPassword = lazy(() => + componentImportPaths.TaskResetPassword().then(module => ({ default: module.TaskResetPassword })), +); + export const PlanDetails = lazy(() => componentImportPaths.PlanDetails().then(module => ({ default: module.PlanDetails })), ); @@ -172,6 +178,7 @@ export const ClerkComponents = { OAuthConsent, SubscriptionDetails, TaskChooseOrganization, + TaskResetPassword, }; export type ClerkComponentName = keyof typeof ClerkComponents; diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index e970f9a49ea..6e8dbd0c679 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -59,7 +59,8 @@ export type AvailableComponentProps = | __internal_SubscriptionDetailsProps | __internal_PlanDetailsProps | APIKeysProps - | TaskChooseOrganizationProps; + | TaskChooseOrganizationProps + | TaskResetPasswordProps; type ComponentMode = 'modal' | 'mounted'; type SignInMode = 'modal' | 'redirect'; From 3c6cf12747c3b08b30c656b2c978fc008a578840 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 5 Dec 2025 16:44:56 +0200 Subject: [PATCH 2/2] chore: Remove changeset --- .changeset/modern-coins-camp.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .changeset/modern-coins-camp.md diff --git a/.changeset/modern-coins-camp.md b/.changeset/modern-coins-camp.md deleted file mode 100644 index e66dfbc9a06..00000000000 --- a/.changeset/modern-coins-camp.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@clerk/clerk-js": minor -"@clerk/nextjs": minor -"@clerk/clerk-react": minor -"@clerk/shared": minor ---- - -Introduce new `` session task component