From 015be7ff7324922a4fa42e2f6df9653bb532a832 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 19 Nov 2025 13:11:46 +0200 Subject: [PATCH 1/9] feat(clerk-js): Initial work for reset password task --- .../tasks/TaskResetPassword/withTaskGuard.ts | 26 ++++++++++++++++ .../components/SignIn/AlternativeMethods.tsx | 8 ++++- .../ui/components/SignIn/SignInFactorOne.tsx | 30 +++++++++++++++---- .../SignIn/SignInFactorOnePasswordCard.tsx | 21 +++++++++---- packages/localizations/src/en-US.ts | 3 ++ packages/shared/src/error.ts | 1 + packages/shared/src/errors/helpers.ts | 9 ++++++ packages/shared/src/types/localization.ts | 3 ++ 8 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts new file mode 100644 index 00000000000..8545c2b1ffc --- /dev/null +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts @@ -0,0 +1,26 @@ +import type { ComponentType } from 'react'; + +import { warnings } from '@/core/warnings'; +import { withRedirect } from '@/ui/common'; +import { useTaskResetPasswordContext } from '@/ui/contexts/components/SessionTasks'; +import type { AvailableComponentProps } from '@/ui/types'; + +export const withTaskGuard =

(Component: ComponentType

) => { + const displayName = Component.displayName || Component.name || 'Component'; + Component.displayName = displayName; + + const HOC = (props: P) => { + const ctx = useTaskResetPasswordContext(); + return withRedirect( + Component, + clerk => !clerk.session?.currentTask, + ({ clerk }) => + !clerk.session ? clerk.buildSignInUrl() : (ctx.redirectUrlComplete ?? clerk.buildAfterSignInUrl()), + warnings.cannotRenderComponentWhenTaskDoesNotExist, + )(props); + }; + + HOC.displayName = `withTaskGuard(${displayName})`; + + return HOC; +}; diff --git a/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx b/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx index 8ca576bdab1..55ff9808bc0 100644 --- a/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx @@ -18,7 +18,7 @@ import { SignInSocialButtons } from './SignInSocialButtons'; import { useResetPasswordFactor } from './useResetPasswordFactor'; import { withHavingTrouble } from './withHavingTrouble'; -type AlternativeMethodsMode = 'forgot' | 'pwned' | 'default'; +export type AlternativeMethodsMode = 'forgot' | 'pwned' | 'default' | 'untrusted-password'; export type AlternativeMethodsProps = { onBackLinkClick: React.MouseEventHandler | undefined; @@ -183,6 +183,8 @@ function determineFlowPart(mode: AlternativeMethodsMode) { return 'forgotPasswordMethods'; case 'pwned': return 'passwordPwnedMethods'; + case 'untrusted-password': + return 'untrustedPasswordMethods'; default: return 'alternativeMethods'; } @@ -194,6 +196,8 @@ function determineTitle(mode: AlternativeMethodsMode): LocalizationKey { return localizationKeys('signIn.forgotPasswordAlternativeMethods.title'); case 'pwned': return localizationKeys('signIn.passwordPwned.title'); + case 'untrusted-password': + return localizationKeys('signIn.passwordUntrusted.title'); default: return localizationKeys('signIn.alternativeMethods.title'); } @@ -204,6 +208,8 @@ function determineIsReset(mode: AlternativeMethodsMode): boolean { case 'forgot': case 'pwned': return true; + case 'untrusted-password': + return false; default: return false; } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx index e318345e719..bf8e8c2e61e 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx @@ -11,6 +11,7 @@ import { useCoreSignIn, useEnvironment } from '../../contexts'; import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies'; import { localizationKeys } from '../../localization'; import { useRouter } from '../../router'; +import type { AlternativeMethodsMode } from './AlternativeMethods'; import { AlternativeMethods } from './AlternativeMethods'; import { hasMultipleEnterpriseConnections } from './shared'; import { SignInFactorOneAlternativePhoneCodeCard } from './SignInFactorOneAlternativePhoneCodeCard'; @@ -41,6 +42,25 @@ const factorKey = (factor: SignInFactor | null | undefined) => { return key; }; +function determineAlternativeMethodsMode( + showForgotPasswordStrategies: boolean, + compromisedPasswordErrorCode: string | null, +): AlternativeMethodsMode { + if (!showForgotPasswordStrategies) { + return 'default'; + } + + if (compromisedPasswordErrorCode === 'form_password_pwned__sign_in') { + return 'pwned'; + } + + if (compromisedPasswordErrorCode === 'form_password_untrusted__sign_in') { + return 'untrusted-password'; + } + + return 'forgot'; +} + function SignInFactorOneInternal(): JSX.Element { const { __internal_setActiveInProgress } = useClerk(); const signIn = useCoreSignIn(); @@ -84,7 +104,7 @@ function SignInFactorOneInternal(): JSX.Element { const [showForgotPasswordStrategies, setShowForgotPasswordStrategies] = React.useState(false); - const [isPasswordPwned, setIsPasswordPwned] = React.useState(false); + const [untrustedPasswordErrorCode, setUntrustedPasswordErrorCode] = React.useState(null); React.useEffect(() => { if (__internal_setActiveInProgress) { @@ -139,11 +159,11 @@ function SignInFactorOneInternal(): JSX.Element { const toggle = showAllStrategies ? toggleAllStrategies : toggleForgotPasswordStrategies; const backHandler = () => { card.setError(undefined); - setIsPasswordPwned(false); + setUntrustedPasswordErrorCode(null); toggle?.(); }; - const mode = showForgotPasswordStrategies ? (isPasswordPwned ? 'pwned' : 'forgot') : 'default'; + const mode = determineAlternativeMethodsMode(showForgotPasswordStrategies, untrustedPasswordErrorCode); return ( { - setIsPasswordPwned(true); + onUntrustedPassword={() => { + setUntrustedPasswordErrorCode('form_password_pwned__sign_in'); toggleForgotPasswordStrategies(); }} /> diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx index f7e05371cee..93c0d910035 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx @@ -1,4 +1,4 @@ -import { isPasswordPwnedError, isUserLockedError } from '@clerk/shared/error'; +import { isPasswordPwnedError, isPasswordUntrustedError, isUserLockedError } from '@clerk/shared/error'; import { useClerk } from '@clerk/shared/react'; import React from 'react'; @@ -21,7 +21,7 @@ import { useResetPasswordFactor } from './useResetPasswordFactor'; type SignInFactorOnePasswordProps = { onForgotPasswordMethodClick: React.MouseEventHandler | undefined; onShowAlternativeMethodsClick: React.MouseEventHandler | undefined; - onPasswordPwned?: () => void; + onUntrustedPassword?: (errorCode: string) => void; }; const usePasswordControl = (props: SignInFactorOnePasswordProps) => { @@ -50,7 +50,7 @@ const usePasswordControl = (props: SignInFactorOnePasswordProps) => { }; export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) => { - const { onShowAlternativeMethodsClick, onPasswordPwned } = props; + const { onShowAlternativeMethodsClick, onUntrustedPassword } = props; const passwordInputRef = React.useRef(null); const card = useCardState(); const { setActive } = useClerk(); @@ -92,9 +92,18 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) return clerk.__internal_navigateWithError('..', err.errors[0]); } - if (isPasswordPwnedError(err) && onPasswordPwned) { - card.setError({ ...err.errors[0], code: 'form_password_pwned__sign_in' }); - onPasswordPwned(); + if (onUntrustedPassword) { + // TODO(vaggelis): those will eventually be unified into a single error code + if (isPasswordPwnedError(err)) { + card.setError({ ...err.errors[0], code: 'form_password_pwned__sign_in' }); + onUntrustedPassword('form_password_pwned__sign_in'); + return; + } + if (isPasswordUntrustedError(err)) { + card.setError({ ...err.errors[0], code: 'form_password_untrusted__sign_in' }); + onUntrustedPassword('form_password_untrusted__sign_in'); + return; + } return; } diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 99d4822c1ce..54ea56b1dab 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -694,6 +694,9 @@ export const enUS: LocalizationResource = { passwordPwned: { title: 'Password compromised', }, + passwordUntrusted: { + title: 'Password untrusted', + }, phoneCode: { formTitle: 'Verification code', resendButton: "Didn't receive a code? Resend", diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 14b32870d1f..2e9b9293866 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -24,6 +24,7 @@ export { isMetamaskError, isNetworkError, isPasswordPwnedError, + isPasswordUntrustedError, isReverificationCancelledError, isUnauthorizedError, isUserLockedError, diff --git a/packages/shared/src/errors/helpers.ts b/packages/shared/src/errors/helpers.ts index 046270fedf9..d7ba9963c71 100644 --- a/packages/shared/src/errors/helpers.ts +++ b/packages/shared/src/errors/helpers.ts @@ -120,6 +120,15 @@ export function isPasswordPwnedError(err: any) { return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'form_password_pwned'; } +/** + * Checks if the provided error is a clerk api response error indicating a password is untrusted. + * + * @internal + */ +export function isPasswordUntrustedError(err: any) { + return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'form_password_untrusted'; +} + /** * Checks if the provided error is an EmailLinkError. * diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index d46434cc2fe..8c9266d1469 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -400,6 +400,9 @@ export type __internal_LocalizationResource = { passwordPwned: { title: LocalizationValue; }; + passwordUntrusted: { + title: LocalizationValue; + }; passkey: { title: LocalizationValue; subtitle: LocalizationValue; From 6ed25335d588c2d73fd1a128cbad049605a0d984 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 19 Nov 2025 14:52:57 +0200 Subject: [PATCH 2/9] fix(clerk-js): Rename variable for clarity in SignInFactorOne component --- .../clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx index bf8e8c2e61e..32265303933 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx @@ -44,17 +44,17 @@ const factorKey = (factor: SignInFactor | null | undefined) => { function determineAlternativeMethodsMode( showForgotPasswordStrategies: boolean, - compromisedPasswordErrorCode: string | null, + untrustedPasswordErrorCode: string | null, ): AlternativeMethodsMode { if (!showForgotPasswordStrategies) { return 'default'; } - if (compromisedPasswordErrorCode === 'form_password_pwned__sign_in') { + if (untrustedPasswordErrorCode === 'form_password_pwned__sign_in') { return 'pwned'; } - if (compromisedPasswordErrorCode === 'form_password_untrusted__sign_in') { + if (untrustedPasswordErrorCode === 'form_password_untrusted__sign_in') { return 'untrusted-password'; } From 47460e75b2b3c4b6651b53ebeb954ed8fbaefe0e Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 15:44:04 +0200 Subject: [PATCH 3/9] revert(clerk-js): Remove the logic for sign-in error on untrusted password --- .../components/SignIn/AlternativeMethods.tsx | 8 +---- .../ui/components/SignIn/SignInFactorOne.tsx | 30 ++++--------------- .../SignIn/SignInFactorOnePasswordCard.tsx | 21 ++++--------- packages/shared/src/error.ts | 1 - packages/shared/src/errors/helpers.ts | 9 ------ 5 files changed, 12 insertions(+), 57 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx b/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx index 55ff9808bc0..8ca576bdab1 100644 --- a/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx @@ -18,7 +18,7 @@ import { SignInSocialButtons } from './SignInSocialButtons'; import { useResetPasswordFactor } from './useResetPasswordFactor'; import { withHavingTrouble } from './withHavingTrouble'; -export type AlternativeMethodsMode = 'forgot' | 'pwned' | 'default' | 'untrusted-password'; +type AlternativeMethodsMode = 'forgot' | 'pwned' | 'default'; export type AlternativeMethodsProps = { onBackLinkClick: React.MouseEventHandler | undefined; @@ -183,8 +183,6 @@ function determineFlowPart(mode: AlternativeMethodsMode) { return 'forgotPasswordMethods'; case 'pwned': return 'passwordPwnedMethods'; - case 'untrusted-password': - return 'untrustedPasswordMethods'; default: return 'alternativeMethods'; } @@ -196,8 +194,6 @@ function determineTitle(mode: AlternativeMethodsMode): LocalizationKey { return localizationKeys('signIn.forgotPasswordAlternativeMethods.title'); case 'pwned': return localizationKeys('signIn.passwordPwned.title'); - case 'untrusted-password': - return localizationKeys('signIn.passwordUntrusted.title'); default: return localizationKeys('signIn.alternativeMethods.title'); } @@ -208,8 +204,6 @@ function determineIsReset(mode: AlternativeMethodsMode): boolean { case 'forgot': case 'pwned': return true; - case 'untrusted-password': - return false; default: return false; } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx index 32265303933..e318345e719 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx @@ -11,7 +11,6 @@ import { useCoreSignIn, useEnvironment } from '../../contexts'; import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies'; import { localizationKeys } from '../../localization'; import { useRouter } from '../../router'; -import type { AlternativeMethodsMode } from './AlternativeMethods'; import { AlternativeMethods } from './AlternativeMethods'; import { hasMultipleEnterpriseConnections } from './shared'; import { SignInFactorOneAlternativePhoneCodeCard } from './SignInFactorOneAlternativePhoneCodeCard'; @@ -42,25 +41,6 @@ const factorKey = (factor: SignInFactor | null | undefined) => { return key; }; -function determineAlternativeMethodsMode( - showForgotPasswordStrategies: boolean, - untrustedPasswordErrorCode: string | null, -): AlternativeMethodsMode { - if (!showForgotPasswordStrategies) { - return 'default'; - } - - if (untrustedPasswordErrorCode === 'form_password_pwned__sign_in') { - return 'pwned'; - } - - if (untrustedPasswordErrorCode === 'form_password_untrusted__sign_in') { - return 'untrusted-password'; - } - - return 'forgot'; -} - function SignInFactorOneInternal(): JSX.Element { const { __internal_setActiveInProgress } = useClerk(); const signIn = useCoreSignIn(); @@ -104,7 +84,7 @@ function SignInFactorOneInternal(): JSX.Element { const [showForgotPasswordStrategies, setShowForgotPasswordStrategies] = React.useState(false); - const [untrustedPasswordErrorCode, setUntrustedPasswordErrorCode] = React.useState(null); + const [isPasswordPwned, setIsPasswordPwned] = React.useState(false); React.useEffect(() => { if (__internal_setActiveInProgress) { @@ -159,11 +139,11 @@ function SignInFactorOneInternal(): JSX.Element { const toggle = showAllStrategies ? toggleAllStrategies : toggleForgotPasswordStrategies; const backHandler = () => { card.setError(undefined); - setUntrustedPasswordErrorCode(null); + setIsPasswordPwned(false); toggle?.(); }; - const mode = determineAlternativeMethodsMode(showForgotPasswordStrategies, untrustedPasswordErrorCode); + const mode = showForgotPasswordStrategies ? (isPasswordPwned ? 'pwned' : 'forgot') : 'default'; return ( { - setUntrustedPasswordErrorCode('form_password_pwned__sign_in'); + onPasswordPwned={() => { + setIsPasswordPwned(true); toggleForgotPasswordStrategies(); }} /> diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx index 93c0d910035..f7e05371cee 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx @@ -1,4 +1,4 @@ -import { isPasswordPwnedError, isPasswordUntrustedError, isUserLockedError } from '@clerk/shared/error'; +import { isPasswordPwnedError, isUserLockedError } from '@clerk/shared/error'; import { useClerk } from '@clerk/shared/react'; import React from 'react'; @@ -21,7 +21,7 @@ import { useResetPasswordFactor } from './useResetPasswordFactor'; type SignInFactorOnePasswordProps = { onForgotPasswordMethodClick: React.MouseEventHandler | undefined; onShowAlternativeMethodsClick: React.MouseEventHandler | undefined; - onUntrustedPassword?: (errorCode: string) => void; + onPasswordPwned?: () => void; }; const usePasswordControl = (props: SignInFactorOnePasswordProps) => { @@ -50,7 +50,7 @@ const usePasswordControl = (props: SignInFactorOnePasswordProps) => { }; export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) => { - const { onShowAlternativeMethodsClick, onUntrustedPassword } = props; + const { onShowAlternativeMethodsClick, onPasswordPwned } = props; const passwordInputRef = React.useRef(null); const card = useCardState(); const { setActive } = useClerk(); @@ -92,18 +92,9 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) return clerk.__internal_navigateWithError('..', err.errors[0]); } - if (onUntrustedPassword) { - // TODO(vaggelis): those will eventually be unified into a single error code - if (isPasswordPwnedError(err)) { - card.setError({ ...err.errors[0], code: 'form_password_pwned__sign_in' }); - onUntrustedPassword('form_password_pwned__sign_in'); - return; - } - if (isPasswordUntrustedError(err)) { - card.setError({ ...err.errors[0], code: 'form_password_untrusted__sign_in' }); - onUntrustedPassword('form_password_untrusted__sign_in'); - return; - } + if (isPasswordPwnedError(err) && onPasswordPwned) { + card.setError({ ...err.errors[0], code: 'form_password_pwned__sign_in' }); + onPasswordPwned(); return; } diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 2e9b9293866..14b32870d1f 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -24,7 +24,6 @@ export { isMetamaskError, isNetworkError, isPasswordPwnedError, - isPasswordUntrustedError, isReverificationCancelledError, isUnauthorizedError, isUserLockedError, diff --git a/packages/shared/src/errors/helpers.ts b/packages/shared/src/errors/helpers.ts index d7ba9963c71..046270fedf9 100644 --- a/packages/shared/src/errors/helpers.ts +++ b/packages/shared/src/errors/helpers.ts @@ -120,15 +120,6 @@ export function isPasswordPwnedError(err: any) { return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'form_password_pwned'; } -/** - * Checks if the provided error is a clerk api response error indicating a password is untrusted. - * - * @internal - */ -export function isPasswordUntrustedError(err: any) { - return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'form_password_untrusted'; -} - /** * Checks if the provided error is an EmailLinkError. * From 538efe12c7d53dc962c568958dc35ae450b9637b Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 19:53:20 +0200 Subject: [PATCH 4/9] refactor(localization): Remove 'passwordUntrusted' key from en-US localization files --- packages/localizations/src/en-US.ts | 3 --- packages/shared/src/types/localization.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 54ea56b1dab..99d4822c1ce 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -694,9 +694,6 @@ export const enUS: LocalizationResource = { passwordPwned: { title: 'Password compromised', }, - passwordUntrusted: { - title: 'Password untrusted', - }, phoneCode: { formTitle: 'Verification code', resendButton: "Didn't receive a code? Resend", diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 8c9266d1469..d46434cc2fe 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -400,9 +400,6 @@ export type __internal_LocalizationResource = { passwordPwned: { title: LocalizationValue; }; - passwordUntrusted: { - title: LocalizationValue; - }; passkey: { title: LocalizationValue; subtitle: LocalizationValue; From 983070c75dbc39cd7aebf1d8450ca3926cb87ce4 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 24 Nov 2025 16:47:40 +0200 Subject: [PATCH 5/9] feat(localization): Add 'taskResetPassword' localization keys --- packages/localizations/src/ja-JP.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index ae34ed69d28..97d5e34d81d 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -881,6 +881,14 @@ export const jaJP: LocalizationResource = { }, title: undefined, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} はすでにこの組織のメンバーです。', captcha_invalid: undefined, From 41f288614af665553e0f9f447ed1ed8f7201c84a Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Tue, 25 Nov 2025 17:36:20 +0200 Subject: [PATCH 6/9] feat(clerk-js,react,nextjs): Export TaskResetPassword component and related functionality --- packages/clerk-js/sandbox/app.ts | 10 +++++++ packages/clerk-js/src/core/clerk.ts | 21 ++++++++++++++ .../clerk-js/src/ui/lazyModules/components.ts | 7 +++++ packages/clerk-js/src/ui/types.ts | 3 +- .../src/client-boundary/uiComponents.tsx | 1 + packages/react/src/components/index.ts | 1 + .../react/src/components/uiComponents.tsx | 29 +++++++++++++++++++ packages/react/src/isomorphicClerk.ts | 19 +++++++++++- packages/shared/src/types/appearance.ts | 4 +++ packages/shared/src/types/clerk.ts | 16 ++++++++++ 10 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 52ae6a4f4d9..1382070c66a 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 da8f4f62bb6..66c90865c04 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -85,6 +85,7 @@ import type { SignUpRedirectOptions, SignUpResource, TaskChooseOrganizationProps, + TaskResetPasswordProps, TasksRedirectOptions, UnsubscribeCallback, UserAvatarProps, @@ -1424,6 +1425,26 @@ export class Clerk implements ClerkInterface { void this.#componentControls.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/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts index cd00ce8ee0b..e5a0d04b73e 100644 --- a/packages/clerk-js/src/ui/lazyModules/components.ts +++ b/packages/clerk-js/src/ui/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/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index e970f9a49ea..6e8dbd0c679 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/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'; diff --git a/packages/nextjs/src/client-boundary/uiComponents.tsx b/packages/nextjs/src/client-boundary/uiComponents.tsx index a38813c8c7c..3838edd423b 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/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 1368145f91e..7ef5424f6d9 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 fc8b450987f..db3dc7b561e 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, @@ -150,7 +151,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, @@ -1218,6 +1219,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/appearance.ts b/packages/shared/src/types/appearance.ts index 62bf94ad661..809aae80217 100644 --- a/packages/shared/src/types/appearance.ts +++ b/packages/shared/src/types/appearance.ts @@ -1125,6 +1125,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/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 84e3b5832b6..bd77a38f91a 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -646,6 +646,22 @@ export interface Clerk { */ unmountTaskChooseOrganization: (targetNode: HTMLDivElement) => void; + /** + * Mounts a TaskResetPassword component at the target element. + * + * @param targetNode - Target node to mount the TaskChooseOrganization 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 TaskChooseOrganization component from. + */ + unmountTaskResetPassword: (targetNode: HTMLDivElement) => void; + /** * @internal * Loads Stripe libraries for commerce functionality From 406ab4dbf8ad1d94d518486845eb42f8899577cb Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 00:24:05 +0200 Subject: [PATCH 7/9] feat(clerk-js,react): Implement TaskResetPassword mounting functionality in IsomorphicClerk --- packages/nextjs/src/index.ts | 1 + packages/react/src/isomorphicClerk.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) 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/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index db3dc7b561e..7ad958d5b86 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -677,6 +677,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. */ @@ -1219,7 +1223,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - mountTaskResetPassword = (node: HTMLDivElement, props?: TaskResetPasswordProps): void => { + __experimental_mountTaskResetPassword = (node: HTMLDivElement, props?: TaskResetPasswordProps): void => { if (this.clerkjs && this.loaded) { this.clerkjs.mountTaskResetPassword(node, props); } else { From 54621f25618de68f18fe03d7398d22029907e316 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 00:31:14 +0200 Subject: [PATCH 8/9] chore(repo): Add changeset chore(repo): Add changeset --- .changeset/modern-coins-camp.md | 8 ++++++++ 1 file changed, 8 insertions(+) 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..2521e992c67 --- /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 +--- + +Export TaskResetPassword components From 13909479ccaccd3564ab00a489bb4c44ea651dc4 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 21:25:56 +0200 Subject: [PATCH 9/9] chore(localization): Remove unused 'taskResetPassword' localization keys and delete withTaskGuard HOC --- .../tasks/TaskResetPassword/withTaskGuard.ts | 26 ------------------- packages/localizations/src/ja-JP.ts | 8 ------ 2 files changed, 34 deletions(-) delete mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts deleted file mode 100644 index 8545c2b1ffc..00000000000 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentType } from 'react'; - -import { warnings } from '@/core/warnings'; -import { withRedirect } from '@/ui/common'; -import { useTaskResetPasswordContext } from '@/ui/contexts/components/SessionTasks'; -import type { AvailableComponentProps } from '@/ui/types'; - -export const withTaskGuard =

(Component: ComponentType

) => { - const displayName = Component.displayName || Component.name || 'Component'; - Component.displayName = displayName; - - const HOC = (props: P) => { - const ctx = useTaskResetPasswordContext(); - return withRedirect( - Component, - clerk => !clerk.session?.currentTask, - ({ clerk }) => - !clerk.session ? clerk.buildSignInUrl() : (ctx.redirectUrlComplete ?? clerk.buildAfterSignInUrl()), - warnings.cannotRenderComponentWhenTaskDoesNotExist, - )(props); - }; - - HOC.displayName = `withTaskGuard(${displayName})`; - - return HOC; -}; diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index 97d5e34d81d..ae34ed69d28 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -881,14 +881,6 @@ export const jaJP: LocalizationResource = { }, title: undefined, }, - taskResetPassword: { - formButtonPrimary: undefined, - signOut: { - actionLink: undefined, - actionText: undefined, - }, - title: undefined, - }, unstable__errors: { already_a_member_in_organization: '{{email}} はすでにこの組織のメンバーです。', captcha_invalid: undefined,