From 48db5ac4ef58243d2e830a274009fbcfb876358e Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 19 Nov 2025 13:11:46 +0200 Subject: [PATCH 01/39] feat(clerk-js): Initial work for reset password task --- packages/clerk-js/src/core/sessionTasks.ts | 1 + .../src/ui/components/SessionTasks/index.tsx | 9 + .../tasks/TaskChooseOrganization/index.tsx | 2 +- .../withTaskGuard.ts | 0 .../__tests__/TaskResetPassword.test.tsx | 26 +++ .../tasks/TaskResetPassword/index.tsx | 209 ++++++++++++++++++ .../tasks/TaskResetPassword/withTaskGuard.ts | 26 +++ .../components/SignIn/AlternativeMethods.tsx | 8 +- .../ui/components/SignIn/SignInFactorOne.tsx | 30 ++- .../SignIn/SignInFactorOnePasswordCard.tsx | 21 +- .../ui/contexts/components/SessionTasks.ts | 14 +- .../src/ui/elements/contexts/index.tsx | 4 +- packages/clerk-js/src/ui/types.ts | 5 + packages/localizations/src/en-US.ts | 3 + packages/shared/src/error.ts | 1 + packages/shared/src/errors/helpers.ts | 9 + packages/shared/src/types/appearance.ts | 1 + packages/shared/src/types/clerk.ts | 9 + packages/shared/src/types/localization.ts | 3 + packages/shared/src/types/session.ts | 3 +- 20 files changed, 368 insertions(+), 16 deletions(-) rename packages/clerk-js/src/ui/components/SessionTasks/tasks/{ => TaskChooseOrganization}/withTaskGuard.ts (100%) create mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx create mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx create mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts index 7713ee53eb3..7c775022840 100644 --- a/packages/clerk-js/src/core/sessionTasks.ts +++ b/packages/clerk-js/src/core/sessionTasks.ts @@ -8,6 +8,7 @@ import { buildURL, forwardClerkQueryParams } from '../utils'; */ export const INTERNAL_SESSION_TASK_ROUTE_BY_KEY: Record = { 'choose-organization': 'choose-organization', + 'reset-password': 'reset-password', } as const; /** diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index a5c8ca20ebb..1ab3184b079 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -12,10 +12,12 @@ import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; import { SessionTasksContext, TaskChooseOrganizationContext, + TaskResetPasswordContext, useSessionTasksContext, } from '../../contexts/components/SessionTasks'; import { Route, Switch, useRouter } from '../../router'; import { TaskChooseOrganization } from './tasks/TaskChooseOrganization'; +import { TaskResetPassword } from './tasks/TaskResetPassword'; const SessionTasksStart = () => { const clerk = useClerk(); @@ -60,6 +62,13 @@ function SessionTasksRoutes(): JSX.Element { + + + + + diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index 60b9442bdd5..592427297bc 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -8,7 +8,7 @@ import { withCardStateProvider } from '@/ui/elements/contexts'; import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; -import { withTaskGuard } from '../withTaskGuard'; +import { withTaskGuard } from './withTaskGuard'; import { ChooseOrganizationScreen } from './ChooseOrganizationScreen'; import { CreateOrganizationScreen } from './CreateOrganizationScreen'; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/withTaskGuard.ts similarity index 100% rename from packages/clerk-js/src/ui/components/SessionTasks/tasks/withTaskGuard.ts rename to packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/withTaskGuard.ts diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx new file mode 100644 index 00000000000..93352c33d19 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { bindCreateFixtures } from '@/test/create-fixtures'; +import { render } from '@/test/utils'; + +import { TaskResetPassword } from '..'; + +const { createFixtures } = bindCreateFixtures('TaskResetPassword'); + +describe('TaskResetPassword', () => { + it('does not render component without existing session task', async () => { + const { wrapper } = await createFixtures(f => { + f.withOrganizations(); + f.withForceOrganizationSelection(); + f.withUser({ + email_addresses: ['test@clerk.com'], + }); + }); + + const { queryByText, queryByRole } = render(, { wrapper }); + + expect(queryByText('New password')).not.toBeInTheDocument(); + expect(queryByText('Confirm password')).not.toBeInTheDocument(); + expect(queryByRole('button', { name: /sign out/i })).not.toBeInTheDocument(); + }); +}); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx new file mode 100644 index 00000000000..86133a33328 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -0,0 +1,209 @@ +import { useClerk, useReverification, useSession } from '@clerk/shared/react'; +import type { UserResource } from '@clerk/shared/types'; + +import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; +import { Col, descriptors, Flow, localizationKeys, useLocalizations } from '@/ui/customizables'; +import { Card } from '@/ui/elements/Card'; +import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; +import { Form } from '@/ui/elements/Form'; +import { Header } from '@/ui/elements/Header'; +import { useConfirmPassword } from '@/ui/hooks'; +import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; +import { handleError } from '@/ui/utils/errorHandler'; +import { createPasswordError } from '@/ui/utils/passwordUtils'; +import { useFormControl } from '@/ui/utils/useFormControl'; + +import { withTaskGuard } from './withTaskGuard'; + +const TaskResetPasswordInternal = () => { + const { signOut, user } = useClerk(); + const { session } = useSession(); + const card = useCardState(); + const { + userSettings: { passwordSettings }, + authConfig: { reverification }, + } = useEnvironment(); + + const { t, locale } = useLocalizations(); + const { otherSessions } = useMultipleSessions({ user }); + const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); + const updatePasswordWithReverification = useReverification( + (user: UserResource, opts: Parameters) => user.updatePassword(...opts), + ); + + const currentPasswordRequired = user && user.passwordEnabled && !reverification; + + const handleSignOut = () => { + if (otherSessions.length === 0) { + return signOut(navigateAfterSignOut); + } + + return signOut(navigateAfterMultiSessionSingleSignOutUrl, { sessionId: session?.id }); + }; + + // TODO: remove this field + const currentPasswordField = useFormControl('currentPassword', '', { + type: 'password', + label: localizationKeys('formFieldLabel__currentPassword'), + isRequired: true, + }); + + const passwordField = useFormControl('newPassword', '', { + type: 'password', + label: localizationKeys('formFieldLabel__newPassword'), + isRequired: true, + validatePassword: true, + buildErrorMessage: errors => createPasswordError(errors, { t, locale, passwordSettings }), + }); + + const confirmField = useFormControl('confirmPassword', '', { + type: 'password', + label: localizationKeys('formFieldLabel__confirmPassword'), + isRequired: true, + }); + + const sessionsField = useFormControl('signOutOfOtherSessions', '', { + type: 'checkbox', + label: localizationKeys('formFieldLabel__signOutOfOtherSessions'), + defaultChecked: true, + }); + + const { setConfirmPasswordFeedback, isPasswordMatch } = useConfirmPassword({ + passwordField, + confirmPasswordField: confirmField, + }); + + const canSubmit = isPasswordMatch; + + const validateForm = () => { + if (passwordField.value) { + setConfirmPasswordFeedback(confirmField.value); + } + }; + + const resetPassword = async () => { + if (!user) { + return; + } + passwordField.clearFeedback(); + confirmField.clearFeedback(); + try { + await updatePasswordWithReverification(user, [ + { + currentPassword: currentPasswordRequired ? currentPasswordField.value : undefined, + newPassword: passwordField.value, + signOutOfOtherSessions: sessionsField.checked, + }, + ]); + } catch (e) { + return handleError(e, [currentPasswordField, passwordField, confirmField], card.setError); + } + }; + + const identifier = user?.primaryEmailAddress?.emailAddress ?? user?.username; + + return ( + + + + + + + + {card.error} + + { + void resetPassword(); + }} + onBlur={validateForm} + gap={8} + > + + {/* For password managers */} + + {currentPasswordRequired && ( + + + + )} + + + + + { + if (e.target.value) { + setConfirmPasswordFeedback(e.target.value); + } + return confirmField.props.onChange(e); + }} + /> + + + + + + + + + + + + + + ({ width: '100%' })} + > + {identifier && ( + + )} + ({ flexShrink: 0 })} + onClick={() => { + void handleSignOut(); + }} + localizationKey={localizationKeys('taskChooseOrganization.signOut.actionLink')} + /> + + + + + + ); +}; + +export const TaskResetPassword = withCoreSessionSwitchGuard( + withTaskGuard(withCardStateProvider(TaskResetPasswordInternal)), +); 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/clerk-js/src/ui/contexts/components/SessionTasks.ts b/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts index e7f15757943..0c9f1abb4a0 100644 --- a/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts +++ b/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts @@ -1,6 +1,6 @@ import { createContext, useContext } from 'react'; -import type { SessionTasksCtx, TaskChooseOrganizationCtx } from '../../types'; +import type { SessionTasksCtx, TaskChooseOrganizationCtx, TaskResetPasswordCtx } from '../../types'; export const SessionTasksContext = createContext(null); @@ -27,3 +27,15 @@ export const useTaskChooseOrganizationContext = (): TaskChooseOrganizationCtx => return context; }; + +export const TaskResetPasswordContext = createContext(null); + +export const useTaskResetPasswordContext = (): TaskResetPasswordCtx => { + const context = useContext(TaskResetPasswordContext); + + if (context === null) { + throw new Error('Clerk: useTaskResetPasswordContext called outside of the mounted TaskResetPassword component.'); + } + + return context; +}; diff --git a/packages/clerk-js/src/ui/elements/contexts/index.tsx b/packages/clerk-js/src/ui/elements/contexts/index.tsx index fb38f0bb4ed..5c4bb99a9e2 100644 --- a/packages/clerk-js/src/ui/elements/contexts/index.tsx +++ b/packages/clerk-js/src/ui/elements/contexts/index.tsx @@ -102,7 +102,8 @@ export type FlowMetadata = { | 'subscriptionDetails' | 'tasks' | 'taskChooseOrganization' - | 'enableOrganizations'; + | 'enableOrganizations' + | 'taskResetPassword'; part?: | 'start' | 'emailCode' @@ -119,6 +120,7 @@ export type FlowMetadata = { | 'alternativeMethods' | 'forgotPasswordMethods' | 'passwordPwnedMethods' + | 'untrustedPasswordMethods' | 'havingTrouble' | 'ssoCallback' | 'popupCallback' diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 3f9fe2a27ef..f7acc6336de 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -20,6 +20,7 @@ import type { SignUpForceRedirectUrl, SignUpProps, TaskChooseOrganizationProps, + TaskResetPasswordProps, UserAvatarProps, UserButtonProps, UserProfileProps, @@ -151,6 +152,10 @@ export type TaskChooseOrganizationCtx = TaskChooseOrganizationProps & { componentName: 'TaskChooseOrganization'; }; +export type TaskResetPasswordCtx = TaskResetPasswordProps & { + componentName: 'TaskResetPassword'; +}; + export type OAuthConsentCtx = __internal_OAuthConsentProps & { componentName: 'OAuthConsent'; }; diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 836e301ee7f..cc437d6d076 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/appearance.ts b/packages/shared/src/types/appearance.ts index 71ba756dbcd..62bf94ad661 100644 --- a/packages/shared/src/types/appearance.ts +++ b/packages/shared/src/types/appearance.ts @@ -1044,6 +1044,7 @@ export type SubscriptionDetailsTheme = Theme; export type APIKeysTheme = Theme; export type OAuthConsentTheme = Theme; export type TaskChooseOrganizationTheme = Theme; +export type TaskResetPasswordTheme = Theme; type GlobalAppearanceOptions = { /** diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 1100d1543e5..84e3b5832b6 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -14,6 +14,7 @@ import type { SignUpTheme, SubscriptionDetailsTheme, TaskChooseOrganizationTheme, + TaskResetPasswordTheme, UserAvatarTheme, UserButtonTheme, UserProfileTheme, @@ -2243,6 +2244,14 @@ export type TaskChooseOrganizationProps = { appearance?: TaskChooseOrganizationTheme; }; +export type TaskResetPasswordProps = { + /** + * Full URL or path to navigate to after successfully resolving all tasks + */ + redirectUrlComplete: string; + appearance?: TaskResetPasswordTheme; +}; + export type CreateOrganizationInvitationParams = { emailAddress: string; role: OrganizationCustomRoleKey; diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 74ec059905a..9fb8948ba31 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; diff --git a/packages/shared/src/types/session.ts b/packages/shared/src/types/session.ts index 11629a838d4..b1fb5711026 100644 --- a/packages/shared/src/types/session.ts +++ b/packages/shared/src/types/session.ts @@ -35,6 +35,7 @@ import type { Autocomplete } from './utils'; export type PendingSessionOptions = { /** * A boolean that indicates whether pending sessions are considered as signed out or not. + * * @default true */ treatPendingAsSignedOut?: boolean; @@ -334,7 +335,7 @@ export interface SessionTask { /** * A unique identifier for the task */ - key: 'choose-organization'; + key: 'choose-organization' | 'reset-password'; } export type GetTokenOptions = { From 23c35e1a54b171e61d71b46e6242cab09e1b5818 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 19 Nov 2025 14:52:57 +0200 Subject: [PATCH 02/39] 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 2cf92e9842d1f4cca326a32a4d50c8f8bc2c3d1d Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 15:44:04 +0200 Subject: [PATCH 03/39] 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 f61d44420e36c9cedebfab4d4cbe81a79d59935e Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 15:44:57 +0200 Subject: [PATCH 04/39] refactor(clerk-js): Remove 'untrustedPasswordMethods' from FlowMetadata type --- packages/clerk-js/src/ui/elements/contexts/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/src/ui/elements/contexts/index.tsx b/packages/clerk-js/src/ui/elements/contexts/index.tsx index 5c4bb99a9e2..6211f1a0b84 100644 --- a/packages/clerk-js/src/ui/elements/contexts/index.tsx +++ b/packages/clerk-js/src/ui/elements/contexts/index.tsx @@ -120,7 +120,6 @@ export type FlowMetadata = { | 'alternativeMethods' | 'forgotPasswordMethods' | 'passwordPwnedMethods' - | 'untrustedPasswordMethods' | 'havingTrouble' | 'ssoCallback' | 'popupCallback' From f771b6abb7932800b448dd8ca03b8e77e5cf6e98 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 17:52:51 +0200 Subject: [PATCH 05/39] chore(repo): Add changeset --- .changeset/loose-brooms-occur.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/loose-brooms-occur.md diff --git a/.changeset/loose-brooms-occur.md b/.changeset/loose-brooms-occur.md new file mode 100644 index 00000000000..7dccf85413a --- /dev/null +++ b/.changeset/loose-brooms-occur.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': minor +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Introduce `reset-password` session task From b032b92f33496f83a61c0ba56a0a98eec0c15bc0 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 18:09:50 +0200 Subject: [PATCH 06/39] feat(clerk-js): Implement TaskResetPassword component and update context and update tests --- .../__tests__/TaskResetPassword.test.tsx | 83 ++++++++++++++++++- .../ui/contexts/ClerkUIComponentsContext.tsx | 11 ++- packages/clerk-js/src/ui/types.ts | 3 +- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx index 93352c33d19..0b0794bea49 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx @@ -1,3 +1,4 @@ +import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; @@ -10,8 +11,6 @@ const { createFixtures } = bindCreateFixtures('TaskResetPassword'); describe('TaskResetPassword', () => { it('does not render component without existing session task', async () => { const { wrapper } = await createFixtures(f => { - f.withOrganizations(); - f.withForceOrganizationSelection(); f.withUser({ email_addresses: ['test@clerk.com'], }); @@ -21,6 +20,84 @@ describe('TaskResetPassword', () => { expect(queryByText('New password')).not.toBeInTheDocument(); expect(queryByText('Confirm password')).not.toBeInTheDocument(); - expect(queryByRole('button', { name: /sign out/i })).not.toBeInTheDocument(); + expect(queryByText('Sign out of all other devices')).not.toBeInTheDocument(); + expect(queryByRole('link', { name: /sign out/i })).not.toBeInTheDocument(); + }); + + it('renders component when session task exists', async () => { + const { wrapper } = await createFixtures(f => { + f.withUser({ + email_addresses: ['test@clerk.com'], + tasks: [{ key: 'reset-password' }], + }); + }); + + const { queryByText, queryByRole } = render(, { wrapper }); + + expect(queryByText('New password')).toBeInTheDocument(); + expect(queryByText('Confirm password')).toBeInTheDocument(); + expect(queryByText('Sign out of all other devices')).toBeInTheDocument(); + expect(queryByRole('link', { name: /sign out/i })).toBeInTheDocument(); + }); + + it('renders the task components in the order of the tasks', async () => { + const { wrapper } = await createFixtures(f => { + f.withOrganizations(); + f.withForceOrganizationSelection(); + f.withUser({ + email_addresses: ['test@clerk.com'], + tasks: [{ key: 'reset-password' }, { key: 'choose-organization' }], + }); + }); + + const { queryByText, queryByRole } = render(, { wrapper }); + + expect(queryByText('New password')).toBeInTheDocument(); + expect(queryByText('Confirm password')).toBeInTheDocument(); + expect(queryByText('Sign out of all other devices')).toBeInTheDocument(); + expect(queryByRole('link', { name: /sign out/i })).toBeInTheDocument(); + }); + + it('displays user identifier in sign out section', async () => { + const { wrapper } = await createFixtures(f => { + f.withUser({ + email_addresses: ['user@test.com'], + tasks: [{ key: 'reset-password' }], + }); + }); + + const { findByText } = render(, { wrapper }); + + expect(await findByText(/user@test\.com/)).toBeInTheDocument(); + expect(await findByText('Sign out')).toBeInTheDocument(); + }); + + it('handles sign out correctly', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ + email_addresses: ['test@clerk.com'], + tasks: [{ key: 'reset-password' }], + }); + }); + + const { findByRole } = render(, { wrapper }); + const signOutButton = await findByRole('link', { name: /sign out/i }); + + await userEvent.click(signOutButton); + + expect(fixtures.clerk.signOut).toHaveBeenCalled(); + }); + + it('renders with username when email is not available', async () => { + const { wrapper } = await createFixtures(f => { + f.withUser({ + username: 'testuser', + tasks: [{ key: 'reset-password' }], + }); + }); + + const { findByText } = render(, { wrapper }); + + expect(await findByText(/testuser/)).toBeInTheDocument(); }); }); diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 271773756d3..2c6e0cf0d7e 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -3,6 +3,7 @@ import type { APIKeysProps, PricingTableProps, TaskChooseOrganizationProps, + TaskResetPasswordProps, UserButtonProps, WaitlistProps, } from '@clerk/shared/types'; @@ -27,7 +28,7 @@ import { UserVerificationContext, WaitlistContext, } from './components'; -import { TaskChooseOrganizationContext } from './components/SessionTasks'; +import { TaskChooseOrganizationContext, TaskResetPasswordContext } from './components/SessionTasks'; export function ComponentContextProvider({ componentName, @@ -126,6 +127,14 @@ export function ComponentContextProvider({ {children} ); + case 'TaskResetPassword': + return ( + + {children} + + ); default: throw new Error(`Unknown component context: ${componentName}`); } diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index f7acc6336de..8da54303daa 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -187,5 +187,6 @@ export type AvailableComponentCtx = | OAuthConsentCtx | SubscriptionDetailsCtx | PlanDetailsCtx - | TaskChooseOrganizationCtx; + | TaskChooseOrganizationCtx + | TaskResetPasswordCtx; export type AvailableComponentName = AvailableComponentCtx['componentName']; From 580026cbf2ff30738c51f3c6cd69251bbfe7b416 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 19:31:34 +0200 Subject: [PATCH 07/39] refactor(clerk-js): Simplify TaskResetPassword component by removing current password requirement and related fields --- .../tasks/TaskResetPassword/index.tsx | 23 +------------------ packages/localizations/src/en-US.ts | 7 ++++++ packages/shared/src/types/localization.ts | 7 ++++++ 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index 86133a33328..d3e3a73d3d0 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -21,7 +21,6 @@ const TaskResetPasswordInternal = () => { const card = useCardState(); const { userSettings: { passwordSettings }, - authConfig: { reverification }, } = useEnvironment(); const { t, locale } = useLocalizations(); @@ -31,8 +30,6 @@ const TaskResetPasswordInternal = () => { (user: UserResource, opts: Parameters) => user.updatePassword(...opts), ); - const currentPasswordRequired = user && user.passwordEnabled && !reverification; - const handleSignOut = () => { if (otherSessions.length === 0) { return signOut(navigateAfterSignOut); @@ -41,13 +38,6 @@ const TaskResetPasswordInternal = () => { return signOut(navigateAfterMultiSessionSingleSignOutUrl, { sessionId: session?.id }); }; - // TODO: remove this field - const currentPasswordField = useFormControl('currentPassword', '', { - type: 'password', - label: localizationKeys('formFieldLabel__currentPassword'), - isRequired: true, - }); - const passwordField = useFormControl('newPassword', '', { type: 'password', label: localizationKeys('formFieldLabel__newPassword'), @@ -90,13 +80,12 @@ const TaskResetPasswordInternal = () => { try { await updatePasswordWithReverification(user, [ { - currentPassword: currentPasswordRequired ? currentPasswordField.value : undefined, newPassword: passwordField.value, signOutOfOtherSessions: sessionsField.checked, }, ]); } catch (e) { - return handleError(e, [currentPasswordField, passwordField, confirmField], card.setError); + return handleError(e, [passwordField, confirmField], card.setError); } }; @@ -132,16 +121,6 @@ const TaskResetPasswordInternal = () => { value={session?.publicUserData.identifier || ''} style={{ display: 'none' }} /> - {currentPasswordRequired && ( - - - - )} ; + }; + }; }; type WithParamName = T & From ad662107e6d0ed0e5432be5d3745e3ba30dedbdd Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 19:42:05 +0200 Subject: [PATCH 08/39] fix(clerk-js): Update localization keys in TaskResetPassword component for consistency --- .../SessionTasks/tasks/TaskResetPassword/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index d3e3a73d3d0..5312dae74b8 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -97,7 +97,7 @@ const TaskResetPasswordInternal = () => { - + {card.error} { {identifier && ( @@ -173,7 +173,7 @@ const TaskResetPasswordInternal = () => { onClick={() => { void handleSignOut(); }} - localizationKey={localizationKeys('taskChooseOrganization.signOut.actionLink')} + localizationKey={localizationKeys('taskResetPassword.signOut.actionLink')} /> From e482de0b814121b1bd74acd015fef96596abd0f2 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Thu, 20 Nov 2025 19:53:20 +0200 Subject: [PATCH 09/39] 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 d47f7f5ae7b..543d3534f88 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 585ca37c5e7..a2725d8e132 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 d9a5d0726951fe16b162a762a4f65979a8d72800 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 24 Nov 2025 11:28:20 +0200 Subject: [PATCH 10/39] fix(clerk-js): Increase maxSize for sessionTasks.js in bundlewatch configuration --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index f1da7a34772..3425146261d 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -31,6 +31,6 @@ { "path": "./dist/op-plans-page*.js", "maxSize": "1.0KB" }, { "path": "./dist/statement-page*.js", "maxSize": "1.0KB" }, { "path": "./dist/payment-attempt-page*.js", "maxSize": "3.0KB" }, - { "path": "./dist/sessionTasks*.js", "maxSize": "1.5KB" } + { "path": "./dist/sessionTasks*.js", "maxSize": "3.0KB" } ] } From 0dd1b54e01f2c6a41674aa987d21063520fbb9b3 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 24 Nov 2025 12:03:29 +0200 Subject: [PATCH 11/39] refactor(clerk-js): Reorder import statements in TaskChooseOrganization component for clarity --- .../SessionTasks/tasks/TaskChooseOrganization/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index 592427297bc..d44d33f19ca 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -8,9 +8,9 @@ import { withCardStateProvider } from '@/ui/elements/contexts'; import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; -import { withTaskGuard } from './withTaskGuard'; import { ChooseOrganizationScreen } from './ChooseOrganizationScreen'; import { CreateOrganizationScreen } from './CreateOrganizationScreen'; +import { withTaskGuard } from './withTaskGuard'; const TaskChooseOrganizationInternal = () => { const { signOut } = useClerk(); From 2d03298967d8bd619bea5572465e3a992bd500dd Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 24 Nov 2025 16:15:41 +0200 Subject: [PATCH 12/39] feat(clerk-js): Enhance tests and add localization key for the form button --- packages/clerk-js/src/test/fixture-helpers.ts | 3 +- .../__tests__/TaskResetPassword.test.tsx | 46 ++++++++++++++++++- .../tasks/TaskResetPassword/index.tsx | 2 +- packages/localizations/src/en-US.ts | 1 + packages/shared/src/types/localization.ts | 1 + 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/test/fixture-helpers.ts b/packages/clerk-js/src/test/fixture-helpers.ts index ad32d5c9d2d..1a3655d754c 100644 --- a/packages/clerk-js/src/test/fixture-helpers.ts +++ b/packages/clerk-js/src/test/fixture-helpers.ts @@ -46,6 +46,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => { Partial, 'email_addresses' | 'phone_numbers' | 'external_accounts' | 'saml_accounts' | 'organization_memberships' > & { + identifier?: string; email_addresses?: Array>; phone_numbers?: Array>; external_accounts?: Array>; @@ -59,7 +60,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => { first_name: 'FirstName', last_name: 'LastName', image_url: '', - identifier: 'email@test.com', + identifier: params.identifier || 'email@test.com', user_id: '', ...params, } as PublicUserDataJSON; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx index 0b0794bea49..c6782990157 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx @@ -2,7 +2,7 @@ import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; -import { render } from '@/test/utils'; +import { render, waitFor } from '@/test/utils'; import { TaskResetPassword } from '..'; @@ -13,6 +13,7 @@ describe('TaskResetPassword', () => { const { wrapper } = await createFixtures(f => { f.withUser({ email_addresses: ['test@clerk.com'], + identifier: 'test@clerk.com', }); }); @@ -28,6 +29,7 @@ describe('TaskResetPassword', () => { const { wrapper } = await createFixtures(f => { f.withUser({ email_addresses: ['test@clerk.com'], + identifier: 'test@clerk.com', tasks: [{ key: 'reset-password' }], }); }); @@ -40,12 +42,13 @@ describe('TaskResetPassword', () => { expect(queryByRole('link', { name: /sign out/i })).toBeInTheDocument(); }); - it('renders the task components in the order of the tasks', async () => { + it('renders the task components in the exact order of the tasks returned by the API', async () => { const { wrapper } = await createFixtures(f => { f.withOrganizations(); f.withForceOrganizationSelection(); f.withUser({ email_addresses: ['test@clerk.com'], + identifier: 'test@clerk.com', tasks: [{ key: 'reset-password' }, { key: 'choose-organization' }], }); }); @@ -58,10 +61,48 @@ describe('TaskResetPassword', () => { expect(queryByRole('link', { name: /sign out/i })).toBeInTheDocument(); }); + it('tries to reset the password and calls the appropriate function', async () => { + const { wrapper, fixtures } = await createFixtures(f => + f.withUser({ + email_addresses: ['test@clerk.com'], + identifier: 'test@clerk.com', + tasks: [{ key: 'reset-password' }], + }), + ); + + fixtures.clerk.user?.updatePassword.mockResolvedValue({}); + const { getByRole, userEvent, getByLabelText } = render(, { wrapper }); + await waitFor(() => getByRole('heading', { name: /Reset password/i })); + + await userEvent.type(getByLabelText(/new password/i), 'testtest'); + await userEvent.type(getByLabelText(/confirm password/i), 'testtest'); + await userEvent.click(getByRole('button', { name: /reset password$/i })); + expect(fixtures.clerk.user?.updatePassword).toHaveBeenCalledWith({ + newPassword: 'testtest', + signOutOfOtherSessions: true, + }); + }); + + it('renders a hidden identifier field', async () => { + const { wrapper } = await createFixtures(f => { + f.withUser({ + email_addresses: ['test@clerk.com'], + identifier: 'test@clerk.com', + tasks: [{ key: 'reset-password' }], + }); + }); + const { getByRole, getByTestId } = render(, { wrapper }); + await waitFor(() => getByRole('heading', { name: /Reset password/i })); + + const identifierField = getByTestId('hidden-identifier'); + expect(identifierField).toHaveValue('test@clerk.com'); + }); + it('displays user identifier in sign out section', async () => { const { wrapper } = await createFixtures(f => { f.withUser({ email_addresses: ['user@test.com'], + identifier: 'user@test.com', tasks: [{ key: 'reset-password' }], }); }); @@ -76,6 +117,7 @@ describe('TaskResetPassword', () => { const { wrapper, fixtures } = await createFixtures(f => { f.withUser({ email_addresses: ['test@clerk.com'], + identifier: 'test@clerk.com', tasks: [{ key: 'reset-password' }], }); }); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index 5312dae74b8..8ac055e4f6e 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -146,7 +146,7 @@ const TaskResetPasswordInternal = () => { diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 543d3534f88..0ada627eae3 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -866,6 +866,7 @@ export const enUS: LocalizationResource = { actionLink: 'Sign out', actionText: 'Signed in as {{identifier}}', }, + formButtonPrimary: 'Reset Password', }, unstable__errors: { already_a_member_in_organization: '{{email}} is already a member of the organization.', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index a2725d8e132..d46434cc2fe 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1290,6 +1290,7 @@ export type __internal_LocalizationResource = { actionLink: LocalizationValue; actionText: LocalizationValue<'identifier'>; }; + formButtonPrimary: LocalizationValue; }; }; From ddf655df33a81bc8379f5176ecddd862e8b6323d Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 24 Nov 2025 16:47:40 +0200 Subject: [PATCH 13/39] feat(localization): Add 'taskResetPassword' localization keys --- packages/localizations/src/ar-SA.ts | 8 ++++++++ packages/localizations/src/be-BY.ts | 8 ++++++++ packages/localizations/src/bg-BG.ts | 8 ++++++++ packages/localizations/src/bn-IN.ts | 8 ++++++++ packages/localizations/src/ca-ES.ts | 8 ++++++++ packages/localizations/src/cs-CZ.ts | 8 ++++++++ packages/localizations/src/da-DK.ts | 8 ++++++++ packages/localizations/src/de-DE.ts | 8 ++++++++ packages/localizations/src/el-GR.ts | 8 ++++++++ packages/localizations/src/en-GB.ts | 8 ++++++++ packages/localizations/src/en-US.ts | 4 ++-- packages/localizations/src/es-CR.ts | 8 ++++++++ packages/localizations/src/es-ES.ts | 8 ++++++++ packages/localizations/src/es-MX.ts | 8 ++++++++ packages/localizations/src/es-UY.ts | 8 ++++++++ packages/localizations/src/fa-IR.ts | 8 ++++++++ packages/localizations/src/fi-FI.ts | 8 ++++++++ packages/localizations/src/fr-FR.ts | 8 ++++++++ packages/localizations/src/he-IL.ts | 8 ++++++++ packages/localizations/src/hi-IN.ts | 8 ++++++++ packages/localizations/src/hr-HR.ts | 8 ++++++++ packages/localizations/src/hu-HU.ts | 8 ++++++++ packages/localizations/src/id-ID.ts | 8 ++++++++ packages/localizations/src/is-IS.ts | 8 ++++++++ packages/localizations/src/it-IT.ts | 8 ++++++++ packages/localizations/src/ja-JP.ts | 8 ++++++++ packages/localizations/src/kk-KZ.ts | 8 ++++++++ packages/localizations/src/ko-KR.ts | 8 ++++++++ packages/localizations/src/mn-MN.ts | 8 ++++++++ packages/localizations/src/ms-MY.ts | 8 ++++++++ packages/localizations/src/nb-NO.ts | 8 ++++++++ packages/localizations/src/nl-BE.ts | 8 ++++++++ packages/localizations/src/nl-NL.ts | 8 ++++++++ packages/localizations/src/pl-PL.ts | 8 ++++++++ packages/localizations/src/pt-BR.ts | 8 ++++++++ packages/localizations/src/pt-PT.ts | 8 ++++++++ packages/localizations/src/ro-RO.ts | 8 ++++++++ packages/localizations/src/ru-RU.ts | 8 ++++++++ packages/localizations/src/sk-SK.ts | 8 ++++++++ packages/localizations/src/sr-RS.ts | 8 ++++++++ packages/localizations/src/sv-SE.ts | 8 ++++++++ packages/localizations/src/ta-IN.ts | 8 ++++++++ packages/localizations/src/te-IN.ts | 8 ++++++++ packages/localizations/src/th-TH.ts | 8 ++++++++ packages/localizations/src/tr-TR.ts | 8 ++++++++ packages/localizations/src/uk-UA.ts | 8 ++++++++ packages/localizations/src/vi-VN.ts | 8 ++++++++ packages/localizations/src/zh-CN.ts | 8 ++++++++ packages/localizations/src/zh-TW.ts | 8 ++++++++ 49 files changed, 386 insertions(+), 2 deletions(-) diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index fa72118dae6..0a6eb8fc297 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -858,6 +858,14 @@ export const arSA: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index 675e326cc28..5edf1b0be0e 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -866,6 +866,14 @@ export const beBY: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Вы ўжо з’яўляецеся членам гэтай арганізацыі.', captcha_invalid: diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 29dd641e9e0..fd25218bd00 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -862,6 +862,14 @@ export const bgBG: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Вие вече сте член на тази организация.', captcha_invalid: undefined, diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index 8d7a6a2740f..db7f5bf7119 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -866,6 +866,14 @@ export const bnIN: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ইতিমধ্যে সংগঠনের একজন সদস্য।', captcha_invalid: diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index 70e29a7cdf7..4c13f615ded 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -861,6 +861,14 @@ export const caES: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index 03ad31dac03..8386a17118a 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -872,6 +872,14 @@ export const csCZ: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} je již členem organizace.', captcha_invalid: diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index 6229900c91a..55ab3213d53 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -859,6 +859,14 @@ export const daDK: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 434fcd61f7e..d27ff2c5c4f 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -876,6 +876,14 @@ export const deDE: LocalizationResource = { actionText: 'Angemeldet als {{identifier}}', }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Sie sind bereits Mitglied in dieser Organisation.', captcha_invalid: diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 953f35041f7..ef6df79a222 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -863,6 +863,14 @@ export const elGR: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index 1d3dba11a57..1192dfecc62 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -863,6 +863,14 @@ export const enGB: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} is already a member of the organisation.', captcha_invalid: diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 0ada627eae3..99d4822c1ce 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -861,12 +861,12 @@ export const enUS: LocalizationResource = { }, }, taskResetPassword: { - title: 'Reset password', + formButtonPrimary: 'Reset Password', signOut: { actionLink: 'Sign out', actionText: 'Signed in as {{identifier}}', }, - formButtonPrimary: 'Reset Password', + title: 'Reset password', }, unstable__errors: { already_a_member_in_organization: '{{email}} is already a member of the organization.', diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index 06f54a7bfa2..1c8d4a6293a 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -868,6 +868,14 @@ export const esCR: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ya es miembro de la organización.', captcha_invalid: diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 1a9beded1ef..8e414f8962d 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -862,6 +862,14 @@ export const esES: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ya es miembro de la organización.', captcha_invalid: diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index 2a0da129db8..9a6b8749924 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -869,6 +869,14 @@ export const esMX: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ya es miembro de la organización.', captcha_invalid: diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 6817394cb11..0e35a91ff85 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -868,6 +868,14 @@ export const esUY: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ya es miembro de la organización.', captcha_invalid: diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index d4f1b5bfe0a..3d96cb2db32 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -872,6 +872,14 @@ export const faIR: LocalizationResource = { actionText: 'می‌خواهید خارج شوید؟', }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} از قبل عضو سازمان است.', captcha_invalid: 'کپچا نامعتبر است. لطفاً دوباره امتحان کنید.', diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 577e472b83d..cbedb7e1ba5 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -862,6 +862,14 @@ export const fiFI: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index 93effa9d2c3..ce67548eab6 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -877,6 +877,14 @@ export const frFR: LocalizationResource = { actionText: 'Connecté en tant que {{identifier}}', }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Vous êtes déjà membre de cette organisation.', captcha_invalid: diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 5281794cf0b..ebb1f905732 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -852,6 +852,14 @@ export const heIL: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} כבר חבר בארגון', captcha_invalid: 'ההרשמה נכשלה עקב כשל באימות האבטחה. אנא רענן את הדף ונסה שוב, או פנה לתמיכה לעזרה נוספת.', diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index 05b3e13d4fb..91b3bfb24b7 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -866,6 +866,14 @@ export const hiIN: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} पहले से ही संगठन का सदस्य है।', captcha_invalid: diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index bed1501625e..7a2410446b4 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -863,6 +863,14 @@ export const hrHR: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} je već član organizacije.', captcha_invalid: diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index 68d6be99c7d..3b4ad30f8dc 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -860,6 +860,14 @@ export const huHU: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index 1245b2c2063..7328c119fce 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -867,6 +867,14 @@ export const idID: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} sudah menjadi anggota organisasi.', captcha_invalid: diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index 529c9053887..6473e293d9f 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -863,6 +863,14 @@ export const isIS: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index dea5577fde3..a03c0d84b4a 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -869,6 +869,14 @@ export const itIT: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Sei già un membro di questa organizzazione.', captcha_invalid: diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index e76a087a5a2..ae34ed69d28 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -873,6 +873,14 @@ export const jaJP: LocalizationResource = { actionText: '{{identifier}} としてサインイン中', }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} はすでにこの組織のメンバーです。', captcha_invalid: undefined, diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index 8900b3a0cf4..6459c076f80 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -853,6 +853,14 @@ export const kkKZ: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ұйымға қазірдің өзінде қосылған.', captcha_invalid: 'Қауіпсіздік тексерілуі сәтсіз аяқталды. Браузерді өзгерту немесе кеңейтулерді өшіруге тырысыңыз.', diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 1020332e8d6..12a8d878ae5 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -854,6 +854,14 @@ export const koKR: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index 2dcdd758b2a..7ca05237166 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -861,6 +861,14 @@ export const mnMN: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index 1a3c45bfea9..c5384fe4599 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -869,6 +869,14 @@ export const msMY: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} sudah menjadi ahli organisasi.', captcha_invalid: diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index cd6463cc674..1bf6fea355c 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -860,6 +860,14 @@ export const nbNO: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index b40cfee5fff..8cb1fc8774f 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -861,6 +861,14 @@ export const nlBE: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Je bent al lid van de organisatie.', captcha_invalid: diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index b65d84f76b2..519917a2083 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -861,6 +861,14 @@ export const nlNL: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Je bent al lid van de organisatie.', captcha_invalid: diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index b830e7b6db0..4170b27d3fc 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -866,6 +866,14 @@ export const plPL: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} jest już członkiem organizacji.', captcha_invalid: diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index ba05dc94645..1ac60610f59 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -873,6 +873,14 @@ export const ptBR: LocalizationResource = { actionText: 'Conectado como {{identifier}}', }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} já é membro da organização.', captcha_invalid: diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 8b2b6c25d02..c05a1ee72cb 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -859,6 +859,14 @@ export const ptPT: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Já é membro nesta organização.', captcha_invalid: diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index 795bc61d07c..1d392bfd07e 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -874,6 +874,14 @@ export const roRO: LocalizationResource = { actionText: 'Autentificat ca {{identifier}}', }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} este deja membru al organizației.', captcha_invalid: undefined, diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index 6ecdbdf2bf3..4db50c8da5f 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -873,6 +873,14 @@ export const ruRU: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} уже является членом организации.', captcha_invalid: diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index 29427043dde..e2029436627 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -866,6 +866,14 @@ export const skSK: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index 7ee191e4ff4..7329de1f3a1 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -859,6 +859,14 @@ export const srRS: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 739619779fc..2867531242b 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -864,6 +864,14 @@ export const svSE: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} är redan medlem i organisationen.', captcha_invalid: diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index c61c6bfc3d9..8aca964b379 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -868,6 +868,14 @@ export const taIN: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ஏற்கனவே நிறுவனத்தின் உறுப்பினராக உள்ளார்.', captcha_invalid: diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index 7e074efa736..169be047ce3 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -868,6 +868,14 @@ export const teIN: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} ఇప్పటికే సంస్థ సభ్యుడు.', captcha_invalid: diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 7f4d31f6e58..2bb86668ef0 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -862,6 +862,14 @@ export const thTH: LocalizationResource = { actionText: 'เข้าสู่ระบบในนาม {{identifier}}', }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} เป็นสมาชิกขององค์กรอยู่แล้ว', captcha_invalid: undefined, diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index d8f62096430..6536b1f2fd0 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -862,6 +862,14 @@ export const trTR: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: 'Bu organizasyonda zaten üyesiniz.', captcha_invalid: diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index dd766198791..7dfcd029b96 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -858,6 +858,14 @@ export const ukUA: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index 6f149fd0038..d51877d4427 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -869,6 +869,14 @@ export const viVN: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: '{{email}} đã là thành viên của tổ chức.', captcha_invalid: undefined, diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 7c8cf81cbee..9188f86c81a 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -848,6 +848,14 @@ export const zhCN: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: '由于安全验证失败,注册未成功。请刷新页面重试或联系支持获取更多帮助。', diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index d35b9829070..fc9494e24e7 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -849,6 +849,14 @@ export const zhTW: LocalizationResource = { actionText: undefined, }, }, + taskResetPassword: { + formButtonPrimary: undefined, + signOut: { + actionLink: undefined, + actionText: undefined, + }, + title: undefined, + }, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: '由於安全驗證失敗,註冊未成功。請重新整理頁面再試一次,或聯絡支援以取得協助。', From 98952e097fd36450195e94d0ef5cf7940de0af88 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 24 Nov 2025 20:23:09 +0200 Subject: [PATCH 14/39] fix(clerk-js): When we are on a url for a task that not exists anymore redirec to the next one --- .../src/ui/components/SessionTasks/index.tsx | 15 +++++++++++++-- .../tasks/TaskResetPassword/index.tsx | 12 +++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 1ab3184b079..170df9d0b32 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -86,7 +86,7 @@ type SessionTasksProps = { */ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: SessionTasksProps) => { const clerk = useClerk(); - const { navigate } = useRouter(); + const { navigate, matches } = useRouter(); const currentTaskContainer = useRef(null); // If there are no pending tasks, navigate away from the tasks flow. @@ -102,8 +102,19 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess return; } + // If the current path does does not match any of the tasks, navigate away to the initial path, + // this handles cases where a user uses browser back navigation to the tasks URL. + if ( + task && + clerk.session?.tasks && + clerk.session.tasks.length > 0 && + !clerk.session.tasks.find(t => matches(INTERNAL_SESSION_TASK_ROUTE_BY_KEY[t.key])) + ) { + void navigate(`./${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[task.key]}`); + } + clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); - }, [clerk, navigate, redirectUrlComplete]); + }, [clerk, matches, navigate, redirectUrlComplete]); if (!clerk.session?.currentTask) { return ( diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index 8ac055e4f6e..ee1d33a70fb 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -1,7 +1,9 @@ -import { useClerk, useReverification, useSession } from '@clerk/shared/react'; +import { useClerk, useReverification } from '@clerk/shared/react'; import type { UserResource } from '@clerk/shared/types'; +import { useCallback } from 'react'; import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; +import { useSessionTasksContext } from '@/ui/contexts/components/SessionTasks'; import { Col, descriptors, Flow, localizationKeys, useLocalizations } from '@/ui/customizables'; import { Card } from '@/ui/elements/Card'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; @@ -16,8 +18,8 @@ import { useFormControl } from '@/ui/utils/useFormControl'; import { withTaskGuard } from './withTaskGuard'; const TaskResetPasswordInternal = () => { - const { signOut, user } = useClerk(); - const { session } = useSession(); + const { signOut, user, session } = useClerk(); + const { redirectUrlComplete, navigateOnSetActive } = useSessionTasksContext(); const card = useCardState(); const { userSettings: { passwordSettings }, @@ -71,7 +73,7 @@ const TaskResetPasswordInternal = () => { } }; - const resetPassword = async () => { + const resetPassword = useCallback(async () => { if (!user) { return; } @@ -87,7 +89,7 @@ const TaskResetPasswordInternal = () => { } catch (e) { return handleError(e, [passwordField, confirmField], card.setError); } - }; + }, [user, passwordField, confirmField, updatePasswordWithReverification, sessionsField.checked, card]); const identifier = user?.primaryEmailAddress?.emailAddress ?? user?.username; From 3c1b5f130cef185990a463723693ae41dcc046f9 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 24 Nov 2025 20:54:03 +0200 Subject: [PATCH 15/39] chore(clerk-js): Remove unused context import from TaskResetPassword component --- .../components/SessionTasks/tasks/TaskResetPassword/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index ee1d33a70fb..eb6455c21a8 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -3,7 +3,6 @@ import type { UserResource } from '@clerk/shared/types'; import { useCallback } from 'react'; import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; -import { useSessionTasksContext } from '@/ui/contexts/components/SessionTasks'; import { Col, descriptors, Flow, localizationKeys, useLocalizations } from '@/ui/customizables'; import { Card } from '@/ui/elements/Card'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; @@ -19,7 +18,6 @@ import { withTaskGuard } from './withTaskGuard'; const TaskResetPasswordInternal = () => { const { signOut, user, session } = useClerk(); - const { redirectUrlComplete, navigateOnSetActive } = useSessionTasksContext(); const card = useCardState(); const { userSettings: { passwordSettings }, From 036c93a4548804f526af5efb6e477b4eba2d3729 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Tue, 25 Nov 2025 13:48:23 +0200 Subject: [PATCH 16/39] fix(clerk-js,localization): Remove redundant test for task order and update Portuguese localization for reset password --- .../__tests__/TaskResetPassword.test.tsx | 19 ------------------- packages/localizations/src/pt-BR.ts | 8 ++++---- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx index c6782990157..6f802696eca 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx @@ -42,25 +42,6 @@ describe('TaskResetPassword', () => { expect(queryByRole('link', { name: /sign out/i })).toBeInTheDocument(); }); - it('renders the task components in the exact order of the tasks returned by the API', async () => { - const { wrapper } = await createFixtures(f => { - f.withOrganizations(); - f.withForceOrganizationSelection(); - f.withUser({ - email_addresses: ['test@clerk.com'], - identifier: 'test@clerk.com', - tasks: [{ key: 'reset-password' }, { key: 'choose-organization' }], - }); - }); - - const { queryByText, queryByRole } = render(, { wrapper }); - - expect(queryByText('New password')).toBeInTheDocument(); - expect(queryByText('Confirm password')).toBeInTheDocument(); - expect(queryByText('Sign out of all other devices')).toBeInTheDocument(); - expect(queryByRole('link', { name: /sign out/i })).toBeInTheDocument(); - }); - it('tries to reset the password and calls the appropriate function', async () => { const { wrapper, fixtures } = await createFixtures(f => f.withUser({ diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index 1ac60610f59..ae36e3b5258 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -874,12 +874,12 @@ export const ptBR: LocalizationResource = { }, }, taskResetPassword: { - formButtonPrimary: undefined, + formButtonPrimary: 'Resetar Senha', signOut: { - actionLink: undefined, - actionText: undefined, + actionLink: 'Sair', + actionText: 'Conectado como {{identifier}}', }, - title: undefined, + title: 'Resetar senha', }, unstable__errors: { already_a_member_in_organization: '{{email}} já é membro da organização.', From 84bc6aa6e02bf8ff278b05b5cb689b9b76a0ae32 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 12:49:10 +0200 Subject: [PATCH 17/39] fix(clerk-js): Simplify navigation logic in session task components and enhance task handling in ChooseOrganization and ResetPassword screens --- .../src/ui/components/SessionTasks/index.tsx | 11 ----- .../ChooseOrganizationScreen.tsx | 13 +++++- .../CreateOrganizationScreen.tsx | 15 +++++-- .../tasks/TaskResetPassword/index.tsx | 41 +++++++++++++++---- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 170df9d0b32..d9ca110436b 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -102,17 +102,6 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess return; } - // If the current path does does not match any of the tasks, navigate away to the initial path, - // this handles cases where a user uses browser back navigation to the tasks URL. - if ( - task && - clerk.session?.tasks && - clerk.session.tasks.length > 0 && - !clerk.session.tasks.find(t => matches(INTERNAL_SESSION_TASK_ROUTE_BY_KEY[t.key])) - ) { - void navigate(`./${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[task.key]}`); - } - clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); }, [clerk, matches, navigate, redirectUrlComplete]); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx index 94340b62856..4fabd78d57f 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx @@ -107,6 +107,7 @@ export const ChooseOrganizationScreen = (props: ChooseOrganizationScreenProps) = const MembershipPreview = (props: { organization: OrganizationResource }) => { const { user } = useUser(); const card = useCardState(); + const clerk = useClerk(); const { navigate } = useRouter(); const { redirectUrlComplete } = useTaskChooseOrganizationContext(); const { isLoaded, setActive } = useOrganizationList(); @@ -121,8 +122,16 @@ const MembershipPreview = (props: { organization: OrganizationResource }) => { try { await setActive({ organization, - navigate: async () => { - // TODO(after-auth) ORGS-779 - Handle next tasks + navigate: async ({ session }) => { + const task = session.currentTask; + if (task && task.key !== 'choose-organization') { + await navigate( + clerk.buildTasksUrl({ + redirectUrl: redirectUrlComplete, + }), + ); + return; + } await navigate(redirectUrlComplete); }, }); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 130f057a75c..26219370537 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -1,4 +1,4 @@ -import { useOrganizationList } from '@clerk/shared/react'; +import { useClerk, useOrganizationList } from '@clerk/shared/react'; import type { CreateOrganizationParams } from '@clerk/shared/types'; import { useEnvironment } from '@/ui/contexts'; @@ -22,6 +22,7 @@ type CreateOrganizationScreenProps = { export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) => { const card = useCardState(); + const clerk = useClerk(); const { navigate } = useRouter(); const { redirectUrlComplete } = useTaskChooseOrganizationContext(); const { createOrganization, isLoaded, setActive } = useOrganizationList({ @@ -60,8 +61,16 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = await setActive({ organization, - navigate: async () => { - // TODO(after-auth) ORGS-779 - Handle next tasks + navigate: async ({ session }) => { + const task = session.currentTask; + if (task && task.key !== 'choose-organization') { + await navigate( + clerk.buildTasksUrl({ + redirectUrl: redirectUrlComplete, + }), + ); + return; + } await navigate(redirectUrlComplete); }, }); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index eb6455c21a8..ef64bcfc51f 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -3,6 +3,7 @@ import type { UserResource } from '@clerk/shared/types'; import { useCallback } from 'react'; import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; +import { useTaskResetPasswordContext } from '@/ui/contexts/components/SessionTasks'; import { Col, descriptors, Flow, localizationKeys, useLocalizations } from '@/ui/customizables'; import { Card } from '@/ui/elements/Card'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; @@ -17,14 +18,15 @@ import { useFormControl } from '@/ui/utils/useFormControl'; import { withTaskGuard } from './withTaskGuard'; const TaskResetPasswordInternal = () => { - const { signOut, user, session } = useClerk(); + const clerk = useClerk(); const card = useCardState(); const { userSettings: { passwordSettings }, } = useEnvironment(); const { t, locale } = useLocalizations(); - const { otherSessions } = useMultipleSessions({ user }); + const { redirectUrlComplete } = useTaskResetPasswordContext(); + const { otherSessions } = useMultipleSessions({ user: clerk.user }); const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); const updatePasswordWithReverification = useReverification( (user: UserResource, opts: Parameters) => user.updatePassword(...opts), @@ -32,10 +34,10 @@ const TaskResetPasswordInternal = () => { const handleSignOut = () => { if (otherSessions.length === 0) { - return signOut(navigateAfterSignOut); + return clerk?.signOut(navigateAfterSignOut); } - return signOut(navigateAfterMultiSessionSingleSignOutUrl, { sessionId: session?.id }); + return clerk?.signOut(navigateAfterMultiSessionSingleSignOutUrl, { sessionId: clerk.session?.id }); }; const passwordField = useFormControl('newPassword', '', { @@ -72,24 +74,45 @@ const TaskResetPasswordInternal = () => { }; const resetPassword = useCallback(async () => { - if (!user) { + if (!clerk.user) { return; } passwordField.clearFeedback(); confirmField.clearFeedback(); try { - await updatePasswordWithReverification(user, [ + await updatePasswordWithReverification(clerk.user, [ { newPassword: passwordField.value, signOutOfOtherSessions: sessionsField.checked, }, ]); + + // Handle the next task if it exists or redirect to the complete url + const task = clerk.session?.currentTask; + if (task && task.key !== 'reset-password') { + await clerk?.navigate( + clerk.buildTasksUrl({ + redirectUrl: redirectUrlComplete, + }), + ); + return; + } + + await clerk?.navigate(redirectUrlComplete); } catch (e) { return handleError(e, [passwordField, confirmField], card.setError); } - }, [user, passwordField, confirmField, updatePasswordWithReverification, sessionsField.checked, card]); + }, [ + clerk, + passwordField, + confirmField, + updatePasswordWithReverification, + sessionsField.checked, + redirectUrlComplete, + card.setError, + ]); - const identifier = user?.primaryEmailAddress?.emailAddress ?? user?.username; + const identifier = clerk.user?.primaryEmailAddress?.emailAddress ?? clerk.user?.username; return ( @@ -118,7 +141,7 @@ const TaskResetPasswordInternal = () => { data-testid='hidden-identifier' id='identifier-field' name='identifier' - value={session?.publicUserData.identifier || ''} + value={clerk.user?.primaryEmailAddress?.emailAddress || clerk.user?.username || ''} style={{ display: 'none' }} /> From 05a57ccdf4f154052aa75a15b8659353d39ae5a2 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 13:39:09 +0200 Subject: [PATCH 18/39] fix(clerk-js): Update buildTasksUrl method to accept optional redirectUrl parameter --- packages/shared/src/types/clerk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 84e3b5832b6..bf7e10288b3 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -744,7 +744,7 @@ export interface Clerk { /** * Returns the configured url where tasks are mounted. */ - buildTasksUrl(): string; + buildTasksUrl({ redirectUrl }?: { redirectUrl?: string }): string; /** * Returns the configured afterSignInUrl of the instance. From e6fc673dc20aa728a97c1c4be2a117dfb4092c9e Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 13:49:01 +0200 Subject: [PATCH 19/39] fix(clerk-js): Update buildTasksUrl method to accept optional TasksRedirectOptions parameter --- packages/react/src/isomorphicClerk.ts | 4 ++-- packages/shared/src/types/clerk.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index fc8b450987f..f50686702fe 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -404,8 +404,8 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - buildTasksUrl = (): string | void => { - const callback = () => this.clerkjs?.buildTasksUrl() || ''; + buildTasksUrl = (opts?: TasksRedirectOptions): string | void => { + const callback = () => this.clerkjs?.buildTasksUrl(opts) || ''; if (this.clerkjs && this.loaded) { return callback(); } else { diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index bf7e10288b3..6f49b6289ea 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -744,7 +744,7 @@ export interface Clerk { /** * Returns the configured url where tasks are mounted. */ - buildTasksUrl({ redirectUrl }?: { redirectUrl?: string }): string; + buildTasksUrl(opts?: TasksRedirectOptions): string; /** * Returns the configured afterSignInUrl of the instance. From e4febd7531a281b42993051fca7ee1bd6be6669f Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 19:05:53 +0200 Subject: [PATCH 20/39] feat(clerk-js,backend): Implement reset password session task and related test cases --- integration/presets/envs.ts | 8 ++ integration/presets/longRunningApps.ts | 11 ++- integration/testUtils/organizationsService.ts | 9 ++ integration/testUtils/usersService.ts | 4 + ...ssion-tasks-sign-in-reset-password.test.ts | 99 +++++++++++++++++++ packages/backend/src/api/endpoints/UserApi.ts | 11 +++ .../tasks/TaskResetPassword/index.tsx | 19 ++-- .../tasks/TaskResetPassword/withTaskGuard.ts | 2 +- .../unstable/page-objects/sessionTask.ts | 20 +++- 9 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 integration/tests/session-tasks-sign-in-reset-password.test.ts diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index ba0e6e128b1..d9a617739b3 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -151,6 +151,13 @@ const withSessionTasks = base .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks').pk) .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); +const withSessionTasksResetPassword = base + .clone() + .setId('withSessionTasksResetPassword') + .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks-reset-password').sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks-reset-password').pk); + const withBillingJwtV2 = base .clone() .setId('withBillingJwtV2') @@ -203,6 +210,7 @@ export const envs = { withRestrictedMode, withReverification, withSessionTasks, + withSessionTasksResetPassword, withSignInOrUpEmailLinksFlow, withSignInOrUpFlow, withSignInOrUpwithRestrictedModeFlow, diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index ebca49ac50a..a5acc533fc6 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -31,6 +31,7 @@ export const createLongRunningApps = () => { { id: 'next.appRouter.withSignInOrUpFlow', config: next.appRouter, env: envs.withSignInOrUpFlow }, { id: 'next.appRouter.withSignInOrUpEmailLinksFlow', config: next.appRouter, env: envs.withSignInOrUpEmailLinksFlow }, { id: 'next.appRouter.withSessionTasks', config: next.appRouter, env: envs.withSessionTasks }, + { id: 'next.appRouter.withSessionTasksResetPassword', config: next.appRouter, env: envs.withSessionTasksResetPassword }, { id: 'next.appRouter.withLegalConsent', config: next.appRouter, env: envs.withLegalConsent }, /** @@ -38,7 +39,7 @@ export const createLongRunningApps = () => { */ { id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart }, - /** + /** * Billing apps */ { id: 'withBillingJwtV2.next.appRouter', config: next.appRouter, env: envs.withBillingJwtV2 }, @@ -60,14 +61,14 @@ export const createLongRunningApps = () => { { id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks }, { id: 'vue.vite', config: vue.vite, env: envs.withCustomRoles }, - /** + /** * Tanstack apps - basic flows */ { id: 'tanstack.react-start', config: tanstack.reactStart, env: envs.withEmailCodes }, - + /** * Various apps - basic flows - */ + */ { id: 'withBilling.astro.node', config: astro.node, env: envs.withBilling }, { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles }, { id: 'astro.static.withCustomRoles', config: astro.static, env: envs.withCustomRoles }, @@ -80,7 +81,7 @@ export const createLongRunningApps = () => { const apps = configs.map(longRunningApplication); - return { + return { getByPattern: (patterns: Array) => { const res = new Set(patterns.map(pattern => apps.filter(app => idMatchesPattern(app.id, pattern))).flat()); if (!res.size) { diff --git a/integration/testUtils/organizationsService.ts b/integration/testUtils/organizationsService.ts index cf1f7f29001..9b771248b23 100644 --- a/integration/testUtils/organizationsService.ts +++ b/integration/testUtils/organizationsService.ts @@ -6,6 +6,7 @@ export type FakeOrganization = Pick; export type OrganizationService = { deleteAll: () => Promise; createFakeOrganization: () => FakeOrganization; + createBapiOrganization: (fakeOrganization: FakeOrganization & { createdBy: string }) => Promise; }; export const createOrganizationsService = (clerkClient: ClerkClient) => { @@ -19,6 +20,14 @@ export const createOrganizationsService = (clerkClient: ClerkClient) => { const bulkDeletionPromises = organizations.data.map(({ id }) => clerkClient.organizations.deleteOrganization(id)); await Promise.all(bulkDeletionPromises); }, + createBapiOrganization: async (fakeOrganization: FakeOrganization & { createdBy: string }) => { + const organization = await clerkClient.organizations.createOrganization({ + name: fakeOrganization.name, + slug: fakeOrganization.slug, + createdBy: fakeOrganization.createdBy, + }); + return organization; + }, }; return self; diff --git a/integration/testUtils/usersService.ts b/integration/testUtils/usersService.ts index 4daa90853c7..3b88e971db0 100644 --- a/integration/testUtils/usersService.ts +++ b/integration/testUtils/usersService.ts @@ -76,6 +76,7 @@ export type UserService = { createFakeOrganization: (userId: string) => Promise; getUser: (opts: { id?: string; email?: string }) => Promise; createFakeAPIKey: (userId: string) => Promise; + passwordUntrusted: (userId: string) => Promise; }; /** @@ -210,6 +211,9 @@ export const createUserService = (clerkClient: ClerkClient) => { revoke: () => clerkClient.apiKeys.revoke({ apiKeyId: apiKey.id, revocationReason: 'For testing purposes' }), } satisfies FakeAPIKey; }, + passwordUntrusted: async (userId: string) => { + await clerkClient.users.__experimental_passwordUntrusted(userId); + }, }; return self; diff --git a/integration/tests/session-tasks-sign-in-reset-password.test.ts b/integration/tests/session-tasks-sign-in-reset-password.test.ts new file mode 100644 index 00000000000..98a4888d674 --- /dev/null +++ b/integration/tests/session-tasks-sign-in-reset-password.test.ts @@ -0,0 +1,99 @@ +import { test } from '@playwright/test'; + +import { hash } from '../models/helpers'; +import { appConfigs } from '../presets'; +import { createTestUtils, testAgainstRunningApps } from '../testUtils'; + +testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasksResetPassword] })( + 'session tasks after sign-in reset password flow @nextjs', + ({ app }) => { + test.describe.configure({ mode: 'serial' }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test('resolve both reset password and organization selection tasks after sign-in', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const user = u.services.users.createFakeUser(); + const createdUser = await u.services.users.createBapiUser(user); + + await u.services.users.passwordUntrusted(createdUser.id); + + // Performs sign-in + await u.po.signIn.goTo(); + await u.po.signIn.setIdentifier(user.email); + await u.po.signIn.continue(); + await u.po.signIn.setPassword(user.password); + await u.po.signIn.continue(); + + await u.page.getByRole('textbox', { name: 'code' }).click(); + await u.page.keyboard.type('424242', { delay: 100 }); + + // Redirects back to tasks when accessing protected route by `auth.protect` + await u.page.goToRelative('/page-protected'); + + const newPassword = `${hash()}_testtest`; + await u.po.sessionTask.resolveResetPasswordTask({ + newPassword: newPassword, + confirmPassword: newPassword, + }); + + await u.po.sessionTask.resolveForceOrganizationSelectionTask({ + name: 'Test Organization', + }); + + // Navigates to after sign-in + await u.page.waitForAppUrl('/page-protected'); + + await u.page.signOut(); + await u.page.context().clearCookies(); + + await user.deleteIfExists(); + await u.services.organizations.deleteAll(); + }); + + test('sign-in with email and resolve the reset password task', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + const user = u.services.users.createFakeUser(); + const createdUser = await u.services.users.createBapiUser(user); + + await u.services.users.passwordUntrusted(createdUser.id); + const fakeOrganization = u.services.organizations.createFakeOrganization(); + await u.services.organizations.createBapiOrganization({ + ...fakeOrganization, + createdBy: createdUser.id, + }); + + // Performs sign-in + await u.po.signIn.goTo(); + await u.po.signIn.setIdentifier(user.email); + await u.po.signIn.continue(); + await u.po.signIn.setPassword(user.password); + await u.po.signIn.continue(); + + await u.page.getByRole('textbox', { name: 'code' }).fill('424242'); + + await u.po.expect.toBeSignedIn(); + + // Redirects back to tasks when accessing protected route by `auth.protect` + await u.page.goToRelative('/page-protected'); + + const newPassword = `${hash()}_testtest`; + await u.po.sessionTask.resolveResetPasswordTask({ + newPassword: newPassword, + confirmPassword: newPassword, + }); + + // Navigates to after sign-in + await u.page.waitForAppUrl('/page-protected'); + + await u.page.signOut(); + await u.page.context().clearCookies(); + + await user.deleteIfExists(); + await u.services.organizations.deleteAll(); + }); + }, +); diff --git a/packages/backend/src/api/endpoints/UserApi.ts b/packages/backend/src/api/endpoints/UserApi.ts index 06c8ae8e974..84fe4b720c3 100644 --- a/packages/backend/src/api/endpoints/UserApi.ts +++ b/packages/backend/src/api/endpoints/UserApi.ts @@ -447,4 +447,15 @@ export class UserAPI extends AbstractAPI { path: joinPaths(basePath, userId, 'totp'), }); } + + public async __experimental_passwordUntrusted(userId: string) { + this.requireId(userId); + return this.request({ + method: 'POST', + path: joinPaths(basePath, userId, 'password_untrusted'), + bodyParams: { + revokeAllSessions: false, + }, + }); + } } diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index ef64bcfc51f..ac4f37179a7 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -1,6 +1,5 @@ import { useClerk, useReverification } from '@clerk/shared/react'; import type { UserResource } from '@clerk/shared/types'; -import { useCallback } from 'react'; import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; import { useTaskResetPasswordContext } from '@/ui/contexts/components/SessionTasks'; @@ -11,6 +10,7 @@ import { Form } from '@/ui/elements/Form'; import { Header } from '@/ui/elements/Header'; import { useConfirmPassword } from '@/ui/hooks'; import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; +import { useRouter } from '@/ui/router'; import { handleError } from '@/ui/utils/errorHandler'; import { createPasswordError } from '@/ui/utils/passwordUtils'; import { useFormControl } from '@/ui/utils/useFormControl'; @@ -25,6 +25,7 @@ const TaskResetPasswordInternal = () => { } = useEnvironment(); const { t, locale } = useLocalizations(); + const { navigate } = useRouter(); const { redirectUrlComplete } = useTaskResetPasswordContext(); const { otherSessions } = useMultipleSessions({ user: clerk.user }); const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); @@ -73,7 +74,7 @@ const TaskResetPasswordInternal = () => { } }; - const resetPassword = useCallback(async () => { + const resetPassword = async () => { if (!clerk.user) { return; } @@ -90,7 +91,7 @@ const TaskResetPasswordInternal = () => { // Handle the next task if it exists or redirect to the complete url const task = clerk.session?.currentTask; if (task && task.key !== 'reset-password') { - await clerk?.navigate( + await navigate( clerk.buildTasksUrl({ redirectUrl: redirectUrlComplete, }), @@ -98,19 +99,11 @@ const TaskResetPasswordInternal = () => { return; } - await clerk?.navigate(redirectUrlComplete); + await navigate(redirectUrlComplete); } catch (e) { return handleError(e, [passwordField, confirmField], card.setError); } - }, [ - clerk, - passwordField, - confirmField, - updatePasswordWithReverification, - sessionsField.checked, - redirectUrlComplete, - card.setError, - ]); + }; const identifier = clerk.user?.primaryEmailAddress?.emailAddress ?? clerk.user?.username; 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 index 8545c2b1ffc..b145ef6c292 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts @@ -13,7 +13,7 @@ export const withTaskGuard =

(Component: Comp const ctx = useTaskResetPasswordContext(); return withRedirect( Component, - clerk => !clerk.session?.currentTask, + clerk => !clerk.session?.currentTask || clerk.session.currentTask.key !== 'reset-password', ({ clerk }) => !clerk.session ? clerk.buildSignInUrl() : (ctx.redirectUrlComplete ?? clerk.buildAfterSignInUrl()), warnings.cannotRenderComponentWhenTaskDoesNotExist, diff --git a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts index 9ffd112da88..3a38d34e062 100644 --- a/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts +++ b/packages/testing/src/playwright/unstable/page-objects/sessionTask.ts @@ -8,16 +8,32 @@ export const createSessionTaskComponentPageObject = (testArgs: { page: EnhancedP const self = { ...common(testArgs), - resolveForceOrganizationSelectionTask: async (fakeOrganization: { name: string; slug: string }) => { + resolveForceOrganizationSelectionTask: async (fakeOrganization: { name: string; slug?: string }) => { const createOrganizationButton = page.getByRole('button', { name: /continue/i }); await expect(createOrganizationButton).toBeVisible(); await page.locator('input[name=name]').fill(fakeOrganization.name); - await page.locator('input[name=slug]').fill(fakeOrganization.slug); + if (fakeOrganization.slug) { + await page.locator('input[name=slug]').fill(fakeOrganization.slug); + } await createOrganizationButton.click(); }, + resolveResetPasswordTask: async ({ + newPassword, + confirmPassword, + }: { + newPassword: string; + confirmPassword: string; + }) => { + await page.locator('input[name=newPassword]').fill(newPassword); + await page.locator('input[name=confirmPassword]').fill(confirmPassword); + + const resetPasswordButton = page.getByRole('button', { name: /reset password/i }); + await expect(resetPasswordButton).toBeVisible(); + await resetPasswordButton.click(); + }, }; return self; From 0f33ec8b6de7f34021bf1eb70514a7df27984f9f Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 19:24:31 +0200 Subject: [PATCH 21/39] tests(e2e): Update tests --- integration/tests/session-tasks-sign-in-reset-password.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/tests/session-tasks-sign-in-reset-password.test.ts b/integration/tests/session-tasks-sign-in-reset-password.test.ts index 98a4888d674..581e53a683d 100644 --- a/integration/tests/session-tasks-sign-in-reset-password.test.ts +++ b/integration/tests/session-tasks-sign-in-reset-password.test.ts @@ -7,7 +7,7 @@ import { createTestUtils, testAgainstRunningApps } from '../testUtils'; testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasksResetPassword] })( 'session tasks after sign-in reset password flow @nextjs', ({ app }) => { - test.describe.configure({ mode: 'serial' }); + test.describe.configure({ mode: 'parallel' }); test.afterAll(async () => { await app.teardown(); From baa5c2b471a4e80ec85ad5c7f0aaf225e7584d56 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 20:04:24 +0200 Subject: [PATCH 22/39] fix(clerk-js): Revert navigation changes from TaskChooseOrganization --- .changeset/thick-dancers-battle.md | 5 ++ .changeset/yummy-geese-work.md | 5 ++ .../ChooseOrganizationScreen.tsx | 12 +---- .../CreateOrganizationScreen.tsx | 15 ++---- .../tasks/TaskResetPassword/index.tsx | 53 ++++++++++--------- 5 files changed, 43 insertions(+), 47 deletions(-) create mode 100644 .changeset/thick-dancers-battle.md create mode 100644 .changeset/yummy-geese-work.md diff --git a/.changeset/thick-dancers-battle.md b/.changeset/thick-dancers-battle.md new file mode 100644 index 00000000000..f12f01fd0fa --- /dev/null +++ b/.changeset/thick-dancers-battle.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +Introducing `users.__experimental_passwordUntrusted` action diff --git a/.changeset/yummy-geese-work.md b/.changeset/yummy-geese-work.md new file mode 100644 index 00000000000..7388c6df720 --- /dev/null +++ b/.changeset/yummy-geese-work.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-react': minor +--- + +Expose `buildTasksUrl` parameters diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx index 4fabd78d57f..b9eb382a04c 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx @@ -122,16 +122,8 @@ const MembershipPreview = (props: { organization: OrganizationResource }) => { try { await setActive({ organization, - navigate: async ({ session }) => { - const task = session.currentTask; - if (task && task.key !== 'choose-organization') { - await navigate( - clerk.buildTasksUrl({ - redirectUrl: redirectUrlComplete, - }), - ); - return; - } + navigate: async () => { + // TODO(after-auth) ORGS-779 - Handle next tasks await navigate(redirectUrlComplete); }, }); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 26219370537..130f057a75c 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -1,4 +1,4 @@ -import { useClerk, useOrganizationList } from '@clerk/shared/react'; +import { useOrganizationList } from '@clerk/shared/react'; import type { CreateOrganizationParams } from '@clerk/shared/types'; import { useEnvironment } from '@/ui/contexts'; @@ -22,7 +22,6 @@ type CreateOrganizationScreenProps = { export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) => { const card = useCardState(); - const clerk = useClerk(); const { navigate } = useRouter(); const { redirectUrlComplete } = useTaskChooseOrganizationContext(); const { createOrganization, isLoaded, setActive } = useOrganizationList({ @@ -61,16 +60,8 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = await setActive({ organization, - navigate: async ({ session }) => { - const task = session.currentTask; - if (task && task.key !== 'choose-organization') { - await navigate( - clerk.buildTasksUrl({ - redirectUrl: redirectUrlComplete, - }), - ); - return; - } + navigate: async () => { + // TODO(after-auth) ORGS-779 - Handle next tasks await navigate(redirectUrlComplete); }, }); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index ac4f37179a7..71143b448d0 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -75,34 +75,37 @@ const TaskResetPasswordInternal = () => { }; const resetPassword = async () => { - if (!clerk.user) { - return; - } - passwordField.clearFeedback(); - confirmField.clearFeedback(); - try { - await updatePasswordWithReverification(clerk.user, [ - { - newPassword: passwordField.value, - signOutOfOtherSessions: sessionsField.checked, - }, - ]); - - // Handle the next task if it exists or redirect to the complete url - const task = clerk.session?.currentTask; - if (task && task.key !== 'reset-password') { - await navigate( - clerk.buildTasksUrl({ - redirectUrl: redirectUrlComplete, - }), - ); + await card.runAsync(async () => { + if (!clerk.user) { return; } - await navigate(redirectUrlComplete); - } catch (e) { - return handleError(e, [passwordField, confirmField], card.setError); - } + passwordField.clearFeedback(); + confirmField.clearFeedback(); + try { + await updatePasswordWithReverification(clerk.user, [ + { + newPassword: passwordField.value, + signOutOfOtherSessions: sessionsField.checked, + }, + ]); + + // Handle the next task if it exists or redirect to the complete url + const task = clerk.session?.currentTask; + if (task && task.key !== 'reset-password') { + await navigate( + clerk.buildTasksUrl({ + redirectUrl: redirectUrlComplete, + }), + ); + return; + } + + await navigate(redirectUrlComplete); + } catch (e) { + return handleError(e, [passwordField, confirmField], card.setError); + } + }); }; const identifier = clerk.user?.primaryEmailAddress?.emailAddress ?? clerk.user?.username; From bea6e4bb09c44a2151941f2e22fff1b6242e3f33 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Wed, 26 Nov 2025 20:09:01 +0200 Subject: [PATCH 23/39] chore(clerk-js): Remove unused usage --- .../tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx index b9eb382a04c..94340b62856 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx @@ -107,7 +107,6 @@ export const ChooseOrganizationScreen = (props: ChooseOrganizationScreenProps) = const MembershipPreview = (props: { organization: OrganizationResource }) => { const { user } = useUser(); const card = useCardState(); - const clerk = useClerk(); const { navigate } = useRouter(); const { redirectUrlComplete } = useTaskChooseOrganizationContext(); const { isLoaded, setActive } = useOrganizationList(); From 205ec3b2d90a0f243a414fd9bb05092ac0116b5d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:12:45 -0300 Subject: [PATCH 24/39] Fix navigation for tasks within virtual router --- packages/clerk-js/src/ui/contexts/components/SignIn.ts | 6 ++++-- packages/clerk-js/src/ui/contexts/components/SignUp.ts | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index ef07c2f095f..829b073f71a 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -37,7 +37,7 @@ export const SignInContext = createContext(null); export const useSignInContext = (): SignInContextType => { const context = useContext(SignInContext); - const { navigate, basePath } = useRouter(); + const { navigate, basePath, startPath } = useRouter(); const { displayConfig, userSettings } = useEnvironment(); const { queryParams, queryString } = useRouter(); const signUpMode = userSettings.signUp.mode; @@ -129,7 +129,9 @@ export const useSignInContext = (): SignInContextType => { const taskEndpoint = getTaskEndpoint(currentTask); const taskNavigationPath = isCombinedFlow ? '/create' + taskEndpoint : taskEndpoint; - return navigate(`/${basePath + taskNavigationPath}`); + // Base path is required for virtual routing with start path + // eg: to navigate from /sign-in/factor-one to /sign-in/tasks/choose-organization + return navigate(`/${basePath + startPath + taskNavigationPath}`); }; const taskUrl = clerk.session?.currentTask diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index 783b610918b..7e150b233b8 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -3,7 +3,7 @@ import type { SessionResource } from '@clerk/shared/types'; import { isAbsoluteUrl } from '@clerk/shared/url'; import { createContext, useContext, useMemo } from 'react'; -import { getTaskEndpoint, INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '@/core/sessionTasks'; +import { getTaskEndpoint } from '@/core/sessionTasks'; import { SIGN_UP_INITIAL_VALUE_KEYS } from '../../../core/constants'; import { buildURL } from '../../../utils'; @@ -36,7 +36,7 @@ export const SignUpContext = createContext(null); export const useSignUpContext = (): SignUpContextType => { const context = useContext(SignUpContext); - const { navigate, basePath } = useRouter(); + const { navigate, basePath, startPath } = useRouter(); const { displayConfig, userSettings } = useEnvironment(); const { queryParams, queryString } = useRouter(); const signUpMode = userSettings.signUp.mode; @@ -121,7 +121,11 @@ export const useSignUpContext = (): SignUpContextType => { return navigate(redirectUrl); } - return navigate(`/${basePath}/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[currentTask.key]}`); + const taskEndpoint = getTaskEndpoint(currentTask); + + // Base path is required for virtual routing with start path + // eg: to navigate from /sign-in/factor-one to /sign-in/tasks/choose-organization + return navigate(`/${basePath + startPath + taskEndpoint}`); }; const taskUrl = clerk.session?.currentTask From 2d6b3e2bb3691b143a93c22bd9ec6f9a788a0907 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:49:59 -0300 Subject: [PATCH 25/39] Fix navigation to n+1 task within modal --- .../src/ui/components/SessionTasks/index.tsx | 13 ++++--- .../tasks/TaskChooseOrganization/index.tsx | 4 +-- .../TaskChooseOrganization/withTaskGuard.ts | 26 -------------- .../tasks/TaskResetPassword/index.tsx | 35 ++++++++----------- .../tasks/TaskResetPassword/withTaskGuard.ts | 26 -------------- 5 files changed, 26 insertions(+), 78 deletions(-) delete mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/withTaskGuard.ts delete mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/withTaskGuard.ts diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index d9ca110436b..52cb794ec12 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -8,7 +8,7 @@ import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { LoadingCardContainer } from '@/ui/elements/LoadingCard'; -import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; +import { getTaskEndpoint, INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; import { SessionTasksContext, TaskChooseOrganizationContext, @@ -86,7 +86,8 @@ type SessionTasksProps = { */ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: SessionTasksProps) => { const clerk = useClerk(); - const { navigate, matches } = useRouter(); + const { navigate, basePath, startPath } = useRouter(); + const currentTaskContainer = useRef(null); // If there are no pending tasks, navigate away from the tasks flow. @@ -103,7 +104,7 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess } clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key })); - }, [clerk, matches, navigate, redirectUrlComplete]); + }, [clerk, navigate, redirectUrlComplete]); if (!clerk.session?.currentTask) { return ( @@ -126,7 +127,11 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess return navigate(redirectUrlComplete); } - return navigate(`./${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[currentTask.key]}`); + const taskEndpoint = getTaskEndpoint(currentTask); + + // Base path is required for virtual routing with start path + // eg: to navigate from /sign-in/factor-one to /sign-in/tasks/choose-organization + return navigate(`/${basePath + startPath + taskEndpoint}`); }; return ( diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index d44d33f19ca..c468cf67bcf 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -8,9 +8,9 @@ import { withCardStateProvider } from '@/ui/elements/contexts'; import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; +import { withTaskGuard } from '../withTaskGuard'; import { ChooseOrganizationScreen } from './ChooseOrganizationScreen'; import { CreateOrganizationScreen } from './CreateOrganizationScreen'; -import { withTaskGuard } from './withTaskGuard'; const TaskChooseOrganizationInternal = () => { const { signOut } = useClerk(); @@ -105,5 +105,5 @@ const TaskChooseOrganizationFlows = withCardStateProvider((props: TaskChooseOrga }); export const TaskChooseOrganization = withCoreSessionSwitchGuard( - withTaskGuard(withCardStateProvider(TaskChooseOrganizationInternal)), + withTaskGuard(withCardStateProvider(TaskChooseOrganizationInternal), 'choose-organization'), ); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/withTaskGuard.ts deleted file mode 100644 index d5702f19760..00000000000 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/withTaskGuard.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentType } from 'react'; - -import { warnings } from '@/core/warnings'; -import { withRedirect } from '@/ui/common'; -import { useTaskChooseOrganizationContext } 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 = useTaskChooseOrganizationContext(); - 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/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index 71143b448d0..fa5944dd562 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -2,7 +2,7 @@ import { useClerk, useReverification } from '@clerk/shared/react'; import type { UserResource } from '@clerk/shared/types'; import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; -import { useTaskResetPasswordContext } from '@/ui/contexts/components/SessionTasks'; +import { useSessionTasksContext, useTaskResetPasswordContext } from '@/ui/contexts/components/SessionTasks'; import { Col, descriptors, Flow, localizationKeys, useLocalizations } from '@/ui/customizables'; import { Card } from '@/ui/elements/Card'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; @@ -10,12 +10,10 @@ import { Form } from '@/ui/elements/Form'; import { Header } from '@/ui/elements/Header'; import { useConfirmPassword } from '@/ui/hooks'; import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; -import { useRouter } from '@/ui/router'; import { handleError } from '@/ui/utils/errorHandler'; import { createPasswordError } from '@/ui/utils/passwordUtils'; import { useFormControl } from '@/ui/utils/useFormControl'; - -import { withTaskGuard } from './withTaskGuard'; +import { withTaskGuard } from '../withTaskGuard'; const TaskResetPasswordInternal = () => { const clerk = useClerk(); @@ -25,8 +23,8 @@ const TaskResetPasswordInternal = () => { } = useEnvironment(); const { t, locale } = useLocalizations(); - const { navigate } = useRouter(); const { redirectUrlComplete } = useTaskResetPasswordContext(); + const { navigateOnSetActive } = useSessionTasksContext(); const { otherSessions } = useMultipleSessions({ user: clerk.user }); const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); const updatePasswordWithReverification = useReverification( @@ -74,14 +72,15 @@ const TaskResetPasswordInternal = () => { } }; - const resetPassword = async () => { - await card.runAsync(async () => { + const resetPassword = () => { + return card.runAsync(async () => { if (!clerk.user) { return; } passwordField.clearFeedback(); confirmField.clearFeedback(); + try { await updatePasswordWithReverification(clerk.user, [ { @@ -90,18 +89,13 @@ const TaskResetPasswordInternal = () => { }, ]); - // Handle the next task if it exists or redirect to the complete url - const task = clerk.session?.currentTask; - if (task && task.key !== 'reset-password') { - await navigate( - clerk.buildTasksUrl({ - redirectUrl: redirectUrlComplete, - }), - ); - return; - } - - await navigate(redirectUrlComplete); + // Update session to have the latest list of tasks (eg: if reset-password gets resolved) + await clerk.setActive({ + session: clerk.session, + navigate: async ({ session }) => { + await navigateOnSetActive({ session, redirectUrl: redirectUrlComplete }); + }, + }); } catch (e) { return handleError(e, [passwordField, confirmField], card.setError); } @@ -164,6 +158,7 @@ const TaskResetPasswordInternal = () => { @@ -203,5 +198,5 @@ const TaskResetPasswordInternal = () => { }; export const TaskResetPassword = withCoreSessionSwitchGuard( - withTaskGuard(withCardStateProvider(TaskResetPasswordInternal)), + withTaskGuard(withCardStateProvider(TaskResetPasswordInternal), 'reset-password'), ); 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 b145ef6c292..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.session.currentTask.key !== 'reset-password', - ({ clerk }) => - !clerk.session ? clerk.buildSignInUrl() : (ctx.redirectUrlComplete ?? clerk.buildAfterSignInUrl()), - warnings.cannotRenderComponentWhenTaskDoesNotExist, - )(props); - }; - - HOC.displayName = `withTaskGuard(${displayName})`; - - return HOC; -}; From a8b560556ee1c424bd2bc6160583e0d305178db3 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:00:28 -0300 Subject: [PATCH 26/39] Move `withTaskGuard` to shared folder --- .../SessionTasks/tasks/TaskChooseOrganization/index.tsx | 2 +- .../components/SessionTasks/tasks/TaskResetPassword/index.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index c468cf67bcf..e99f56aac6c 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -8,7 +8,7 @@ import { withCardStateProvider } from '@/ui/elements/contexts'; import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; -import { withTaskGuard } from '../withTaskGuard'; +import { withTaskGuard } from '../shared/withTaskGuard'; import { ChooseOrganizationScreen } from './ChooseOrganizationScreen'; import { CreateOrganizationScreen } from './CreateOrganizationScreen'; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index fa5944dd562..e445001beb8 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -13,7 +13,8 @@ import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; import { handleError } from '@/ui/utils/errorHandler'; import { createPasswordError } from '@/ui/utils/passwordUtils'; import { useFormControl } from '@/ui/utils/useFormControl'; -import { withTaskGuard } from '../withTaskGuard'; + +import { withTaskGuard } from '../shared/withTaskGuard'; const TaskResetPasswordInternal = () => { const clerk = useClerk(); From e515c50c8df42cb205ba039b8a2e1a9b30fdfa9f Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:08:30 -0300 Subject: [PATCH 27/39] Fix `withTaskGuard` import path --- .../tasks/TaskChooseOrganization/index.tsx | 2 +- .../tasks/TaskResetPassword/index.tsx | 2 +- .../SessionTasks/tasks/shared/index.ts | 1 + .../tasks/shared/withTaskGuard.ts | 30 +++++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/index.ts create mode 100644 packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index e99f56aac6c..4d8135592c2 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -8,7 +8,7 @@ import { withCardStateProvider } from '@/ui/elements/contexts'; import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; -import { withTaskGuard } from '../shared/withTaskGuard'; +import { withTaskGuard } from '../shared'; import { ChooseOrganizationScreen } from './ChooseOrganizationScreen'; import { CreateOrganizationScreen } from './CreateOrganizationScreen'; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index e445001beb8..85d1b8c19aa 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -14,7 +14,7 @@ import { handleError } from '@/ui/utils/errorHandler'; import { createPasswordError } from '@/ui/utils/passwordUtils'; import { useFormControl } from '@/ui/utils/useFormControl'; -import { withTaskGuard } from '../shared/withTaskGuard'; +import { withTaskGuard } from '../shared'; const TaskResetPasswordInternal = () => { const clerk = useClerk(); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/index.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/index.ts new file mode 100644 index 00000000000..c78a073ba17 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/index.ts @@ -0,0 +1 @@ +export { withTaskGuard } from './withTaskGuard'; diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts new file mode 100644 index 00000000000..34deb361b45 --- /dev/null +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts @@ -0,0 +1,30 @@ +import type { SessionTask } from '@clerk/shared/types'; +import type { ComponentType } from 'react'; + +import { warnings } from '@/core/warnings'; +import { withRedirect } from '@/ui/common'; +import { useSessionTasksContext } from '@/ui/contexts/components/SessionTasks'; +import type { AvailableComponentProps } from '@/ui/types'; + +export const withTaskGuard =

( + Component: ComponentType

, + taskKey: SessionTask['key'], +) => { + const displayName = Component.displayName || Component.name || 'Component'; + Component.displayName = displayName; + + const HOC = (props: P) => { + const ctx = useSessionTasksContext(); + return withRedirect( + Component, + clerk => !clerk.session?.currentTask || clerk.session.currentTask.key !== taskKey, + ({ clerk }) => + !clerk.session ? clerk.buildSignInUrl() : (ctx.redirectUrlComplete ?? clerk.buildAfterSignInUrl()), + warnings.cannotRenderComponentWhenTaskDoesNotExist, + )(props); + }; + + HOC.displayName = `withTaskGuard(${displayName})`; + + return HOC; +}; From d971e71434d70c5cb44c1bb57081dfb2605f7dc5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:13:11 -0300 Subject: [PATCH 28/39] Render session task context for separate components --- .../src/ui/components/SessionTasks/index.tsx | 20 ++---------- .../ChooseOrganizationScreen.tsx | 10 +++--- .../CreateOrganizationScreen.tsx | 10 +++--- .../tasks/TaskResetPassword/index.tsx | 4 +-- .../tasks/shared/withTaskGuard.ts | 10 +++++- .../ui/contexts/ClerkUIComponentsContext.tsx | 14 ++++++-- .../ui/contexts/components/SessionTasks.ts | 32 +++++++++++++++++-- packages/clerk-js/src/ui/types.ts | 3 -- 8 files changed, 63 insertions(+), 40 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 52cb794ec12..dc728a60c26 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 { SessionResource } from '@clerk/shared/types'; import { useEffect, useRef } from 'react'; import { Flow } from '@/ui/customizables'; @@ -8,7 +7,7 @@ import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { LoadingCardContainer } from '@/ui/elements/LoadingCard'; -import { getTaskEndpoint, INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; +import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; import { SessionTasksContext, TaskChooseOrganizationContext, @@ -86,7 +85,7 @@ type SessionTasksProps = { */ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: SessionTasksProps) => { const clerk = useClerk(); - const { navigate, basePath, startPath } = useRouter(); + const { navigate } = useRouter(); const currentTaskContainer = useRef(null); @@ -121,21 +120,8 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess ); } - const navigateOnSetActive = async ({ session }: { session: SessionResource }) => { - const currentTask = session.currentTask; - if (!currentTask) { - return navigate(redirectUrlComplete); - } - - const taskEndpoint = getTaskEndpoint(currentTask); - - // Base path is required for virtual routing with start path - // eg: to navigate from /sign-in/factor-one to /sign-in/tasks/choose-organization - return navigate(`/${basePath + startPath + taskEndpoint}`); - }; - return ( - + ); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx index 94340b62856..6942ae08806 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx @@ -16,7 +16,7 @@ import { sharedMainIdentifierSx, } from '@/ui/common/organizations/OrganizationPreview'; import { organizationListParams, populateCacheUpdateItem } from '@/ui/components/OrganizationSwitcher/utils'; -import { useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; +import { useSessionTasksContext, useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; import { Col, descriptors, localizationKeys, Text, useLocalizations } from '@/ui/customizables'; import { Action, Actions } from '@/ui/elements/Actions'; import { Card } from '@/ui/elements/Card'; @@ -25,7 +25,6 @@ import { Header } from '@/ui/elements/Header'; import { OrganizationPreview } from '@/ui/elements/OrganizationPreview'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; import { Add } from '@/ui/icons'; -import { useRouter } from '@/ui/router'; import { handleError } from '@/ui/utils/errorHandler'; type ChooseOrganizationScreenProps = { @@ -107,7 +106,7 @@ export const ChooseOrganizationScreen = (props: ChooseOrganizationScreenProps) = const MembershipPreview = (props: { organization: OrganizationResource }) => { const { user } = useUser(); const card = useCardState(); - const { navigate } = useRouter(); + const { navigateOnSetActive } = useSessionTasksContext(); const { redirectUrlComplete } = useTaskChooseOrganizationContext(); const { isLoaded, setActive } = useOrganizationList(); const { t } = useLocalizations(); @@ -121,9 +120,8 @@ const MembershipPreview = (props: { organization: OrganizationResource }) => { try { await setActive({ organization, - navigate: async () => { - // TODO(after-auth) ORGS-779 - Handle next tasks - await navigate(redirectUrlComplete); + navigate: async ({ session }) => { + await navigateOnSetActive?.({ session, redirectUrlComplete }); }, }); } catch (err) { diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 130f057a75c..97b9241e5f6 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx @@ -2,14 +2,13 @@ import { useOrganizationList } from '@clerk/shared/react'; import type { CreateOrganizationParams } from '@clerk/shared/types'; import { useEnvironment } from '@/ui/contexts'; -import { useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; +import { useSessionTasksContext, useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; import { localizationKeys } from '@/ui/customizables'; import { useCardState } from '@/ui/elements/contexts'; import { Form } from '@/ui/elements/Form'; import { FormButtonContainer } from '@/ui/elements/FormButtons'; import { FormContainer } from '@/ui/elements/FormContainer'; import { Header } from '@/ui/elements/Header'; -import { useRouter } from '@/ui/router'; import { createSlug } from '@/ui/utils/createSlug'; import { handleError } from '@/ui/utils/errorHandler'; import { useFormControl } from '@/ui/utils/useFormControl'; @@ -22,7 +21,7 @@ type CreateOrganizationScreenProps = { export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) => { const card = useCardState(); - const { navigate } = useRouter(); + const { navigateOnSetActive } = useSessionTasksContext(); const { redirectUrlComplete } = useTaskChooseOrganizationContext(); const { createOrganization, isLoaded, setActive } = useOrganizationList({ userMemberships: organizationListParams.userMemberships, @@ -60,9 +59,8 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) = await setActive({ organization, - navigate: async () => { - // TODO(after-auth) ORGS-779 - Handle next tasks - await navigate(redirectUrlComplete); + navigate: async ({ session }) => { + await navigateOnSetActive?.({ session, redirectUrlComplete }); }, }); } catch (err) { diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx index 85d1b8c19aa..b81ffd5d2f1 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -25,12 +25,12 @@ const TaskResetPasswordInternal = () => { const { t, locale } = useLocalizations(); const { redirectUrlComplete } = useTaskResetPasswordContext(); - const { navigateOnSetActive } = useSessionTasksContext(); const { otherSessions } = useMultipleSessions({ user: clerk.user }); const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); const updatePasswordWithReverification = useReverification( (user: UserResource, opts: Parameters) => user.updatePassword(...opts), ); + const { navigateOnSetActive } = useSessionTasksContext(); const handleSignOut = () => { if (otherSessions.length === 0) { @@ -94,7 +94,7 @@ const TaskResetPasswordInternal = () => { await clerk.setActive({ session: clerk.session, navigate: async ({ session }) => { - await navigateOnSetActive({ session, redirectUrl: redirectUrlComplete }); + await navigateOnSetActive?.({ session, redirectUrlComplete }); }, }); } catch (e) { diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts index 34deb361b45..c53d60b3bba 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts @@ -6,10 +6,18 @@ import { withRedirect } from '@/ui/common'; import { useSessionTasksContext } from '@/ui/contexts/components/SessionTasks'; import type { AvailableComponentProps } from '@/ui/types'; +/** + * Triggers a redirect if current task is not the given task key. + * + * If there's a current session, it will redirect to the `redirectUrlComplete` prop. + * If there's no current session, it will redirect to the sign in URL. + * + * @internal + */ export const withTaskGuard =

( Component: ComponentType

, taskKey: SessionTask['key'], -) => { +): ((props: P) => null | JSX.Element) => { const displayName = Component.displayName || Component.name || 'Component'; Component.displayName = displayName; diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 2c6e0cf0d7e..19229545f0b 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -28,7 +28,11 @@ import { UserVerificationContext, WaitlistContext, } from './components'; -import { TaskChooseOrganizationContext, TaskResetPasswordContext } from './components/SessionTasks'; +import { + SessionTasksContext, + TaskChooseOrganizationContext, + TaskResetPasswordContext, +} from './components/SessionTasks'; export function ComponentContextProvider({ componentName, @@ -124,7 +128,9 @@ export function ComponentContextProvider({ - {children} + + {children} + ); case 'TaskResetPassword': @@ -132,7 +138,9 @@ export function ComponentContextProvider({ - {children} + + {children} + ); default: diff --git a/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts b/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts index 0c9f1abb4a0..1f25c3cdc05 100644 --- a/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts +++ b/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts @@ -1,17 +1,45 @@ +import type { SessionResource } from '@clerk/shared/types'; import { createContext, useContext } from 'react'; +import { getTaskEndpoint } from '@/core/sessionTasks'; +import { useRouter } from '@/ui/router'; + import type { SessionTasksCtx, TaskChooseOrganizationCtx, TaskResetPasswordCtx } from '../../types'; export const SessionTasksContext = createContext(null); -export const useSessionTasksContext = (): SessionTasksCtx => { +type SessionTasksContextType = SessionTasksCtx & { + navigateOnSetActive: (opts: { session: SessionResource; redirectUrlComplete: string }) => Promise; +}; + +export const useSessionTasksContext = (): SessionTasksContextType => { const context = useContext(SessionTasksContext); + const { navigate, basePath, startPath } = useRouter(); if (context === null) { throw new Error('Clerk: useSessionTasksContext called outside of the mounted SessionTasks component.'); } - return context; + const navigateOnSetActive = async ({ + session, + redirectUrlComplete, + }: { + session: SessionResource; + redirectUrlComplete: string; + }) => { + const currentTask = session.currentTask; + if (!currentTask) { + return navigate(redirectUrlComplete); + } + + const taskEndpoint = getTaskEndpoint(currentTask); + + // Base path is required for virtual routing with start path + // eg: to navigate from /sign-in/factor-one to /sign-in/tasks/choose-organization + return navigate(`/${basePath + startPath + taskEndpoint}`); + }; + + return { ...context, navigateOnSetActive }; }; export const TaskChooseOrganizationContext = createContext(null); diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 8da54303daa..e970f9a49ea 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -12,7 +12,6 @@ import type { OrganizationProfileProps, OrganizationSwitcherProps, PricingTableProps, - SessionResource, SignInFallbackRedirectUrl, SignInForceRedirectUrl, SignInProps, @@ -144,8 +143,6 @@ export type CheckoutCtx = __internal_CheckoutProps & { export type SessionTasksCtx = { redirectUrlComplete: string; - currentTaskContainer?: React.RefObject | null; - navigateOnSetActive: (opts: { session: SessionResource; redirectUrl: string }) => Promise; }; export type TaskChooseOrganizationCtx = TaskChooseOrganizationProps & { From 7e3dcf87b48e92fe8a3b8cacc3becc26de39dcb0 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 01:38:25 +0200 Subject: [PATCH 29/39] fix(clerk-js): Update token handling in Session class to ensure correct token is emitted on updates --- packages/clerk-js/src/core/resources/Session.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index ee5df2c43a1..6aba92a59e3 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -377,11 +377,17 @@ export class Session extends BaseResource implements SessionResource { 'session', ); const cachedToken = await cachedEntry.tokenResolver; + const isCacheTokenDifferent = + this.lastActiveToken && this.lastActiveToken.getRawString() !== cachedToken.getRawString(); + + const token = isCacheTokenDifferent ? this.lastActiveToken : cachedToken; + if (shouldDispatchTokenUpdate) { - eventBus.emit(events.TokenUpdate, { token: cachedToken }); + eventBus.emit(events.TokenUpdate, { token }); } + // Return null when raw string is empty to indicate that there it's signed-out - return cachedToken.getRawString() || null; + return token?.getRawString() || cachedToken.getRawString() || null; } debugLogger.info( From ad7815c24b62c5dc03eac9118836bdfd06b67752 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 11:56:11 +0200 Subject: [PATCH 30/39] fix(clerk-js): Enhance token resolve to ensure accurate token retrieval and caching --- .../clerk-js/src/core/resources/Session.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 6aba92a59e3..9fbb1a3afd2 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -377,17 +377,29 @@ export class Session extends BaseResource implements SessionResource { 'session', ); const cachedToken = await cachedEntry.tokenResolver; - const isCacheTokenDifferent = - this.lastActiveToken && this.lastActiveToken.getRawString() !== cachedToken.getRawString(); - - const token = isCacheTokenDifferent ? this.lastActiveToken : cachedToken; + const cachedTokenIat = cachedToken.jwt?.claims?.iat || 0; + const lastActiveAtTokenIat = this.lastActiveToken?.jwt?.claims?.iat || 0; + const isLastActiveTokenTheSameAsCached = this.lastActiveToken?.id === cachedToken.id; + const isLastActiveIssuedAfterCached = + this.lastActiveToken && isLastActiveTokenTheSameAsCached && lastActiveAtTokenIat >= cachedTokenIat; + + // If the last active token is the same as the cached token and is issued after the cached token, update the cache with the last active token + if (isLastActiveIssuedAfterCached && this.lastActiveToken) { + SessionTokenCache.set({ tokenId, tokenResolver: new Promise(() => this.lastActiveToken) }); + } if (shouldDispatchTokenUpdate) { - eventBus.emit(events.TokenUpdate, { token }); + eventBus.emit(events.TokenUpdate, { + token: isLastActiveIssuedAfterCached ? this.lastActiveToken : cachedToken, + }); + } + + if (isLastActiveIssuedAfterCached && this.lastActiveToken) { + return this.lastActiveToken.getRawString() || null; } // Return null when raw string is empty to indicate that there it's signed-out - return token?.getRawString() || cachedToken.getRawString() || null; + return cachedToken.getRawString() || null; } debugLogger.info( From ad95b14334d54197f7249a71b766a4553dedf4ec Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 12:01:01 +0200 Subject: [PATCH 31/39] fix(clerk-js): Update token caching to use Promise.resolve for last active token --- packages/clerk-js/src/core/resources/Session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 9fbb1a3afd2..bea6beacfd4 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -385,7 +385,7 @@ export class Session extends BaseResource implements SessionResource { // If the last active token is the same as the cached token and is issued after the cached token, update the cache with the last active token if (isLastActiveIssuedAfterCached && this.lastActiveToken) { - SessionTokenCache.set({ tokenId, tokenResolver: new Promise(() => this.lastActiveToken) }); + SessionTokenCache.set({ tokenId, tokenResolver: Promise.resolve(this.lastActiveToken) }); } if (shouldDispatchTokenUpdate) { From 3e97845fc09a887278c1c3cb091f8e53d9bd4ce5 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 12:40:22 +0200 Subject: [PATCH 32/39] wip --- .../clerk-js/src/core/resources/Session.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index bea6beacfd4..64901078fa5 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -377,26 +377,26 @@ export class Session extends BaseResource implements SessionResource { 'session', ); const cachedToken = await cachedEntry.tokenResolver; - const cachedTokenIat = cachedToken.jwt?.claims?.iat || 0; - const lastActiveAtTokenIat = this.lastActiveToken?.jwt?.claims?.iat || 0; - const isLastActiveTokenTheSameAsCached = this.lastActiveToken?.id === cachedToken.id; - const isLastActiveIssuedAfterCached = - this.lastActiveToken && isLastActiveTokenTheSameAsCached && lastActiveAtTokenIat >= cachedTokenIat; - - // If the last active token is the same as the cached token and is issued after the cached token, update the cache with the last active token - if (isLastActiveIssuedAfterCached && this.lastActiveToken) { - SessionTokenCache.set({ tokenId, tokenResolver: Promise.resolve(this.lastActiveToken) }); - } + // const cachedTokenIat = cachedToken.jwt?.claims?.iat || 0; + // const lastActiveAtTokenIat = this.lastActiveToken?.jwt?.claims?.iat || 0; + // const isLastActiveTokenTheSameAsCached = this.lastActiveToken?.id === cachedToken.id; + // const isLastActiveIssuedAfterCached = + // this.lastActiveToken && isLastActiveTokenTheSameAsCached && lastActiveAtTokenIat >= cachedTokenIat; + + // // If the last active token is the same as the cached token and is issued after the cached token, update the cache with the last active token + // if (isLastActiveIssuedAfterCached && this.lastActiveToken) { + // SessionTokenCache.set({ tokenId, tokenResolver: Promise.resolve(this.lastActiveToken) }); + // } if (shouldDispatchTokenUpdate) { eventBus.emit(events.TokenUpdate, { - token: isLastActiveIssuedAfterCached ? this.lastActiveToken : cachedToken, + token: cachedToken, }); } - if (isLastActiveIssuedAfterCached && this.lastActiveToken) { - return this.lastActiveToken.getRawString() || null; - } + // if (isLastActiveIssuedAfterCached && this.lastActiveToken) { + // return this.lastActiveToken.getRawString() || null; + // } // Return null when raw string is empty to indicate that there it's signed-out return cachedToken.getRawString() || null; From 579ca04abaca4788f21ab8a9483f27beb000f647 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 14:01:32 +0200 Subject: [PATCH 33/39] fix(clerk-js): Revert changes --- packages/clerk-js/src/core/resources/Session.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 64901078fa5..81ab8d7cbf5 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -377,16 +377,6 @@ export class Session extends BaseResource implements SessionResource { 'session', ); const cachedToken = await cachedEntry.tokenResolver; - // const cachedTokenIat = cachedToken.jwt?.claims?.iat || 0; - // const lastActiveAtTokenIat = this.lastActiveToken?.jwt?.claims?.iat || 0; - // const isLastActiveTokenTheSameAsCached = this.lastActiveToken?.id === cachedToken.id; - // const isLastActiveIssuedAfterCached = - // this.lastActiveToken && isLastActiveTokenTheSameAsCached && lastActiveAtTokenIat >= cachedTokenIat; - - // // If the last active token is the same as the cached token and is issued after the cached token, update the cache with the last active token - // if (isLastActiveIssuedAfterCached && this.lastActiveToken) { - // SessionTokenCache.set({ tokenId, tokenResolver: Promise.resolve(this.lastActiveToken) }); - // } if (shouldDispatchTokenUpdate) { eventBus.emit(events.TokenUpdate, { @@ -394,10 +384,6 @@ export class Session extends BaseResource implements SessionResource { }); } - // if (isLastActiveIssuedAfterCached && this.lastActiveToken) { - // return this.lastActiveToken.getRawString() || null; - // } - // Return null when raw string is empty to indicate that there it's signed-out return cachedToken.getRawString() || null; } From 6a2ba9ab44275192cfcf589417a2d6198be20daa Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 15:53:35 +0200 Subject: [PATCH 34/39] revert changes --- packages/clerk-js/src/core/resources/Session.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 81ab8d7cbf5..a2abb77e19a 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -377,11 +377,8 @@ export class Session extends BaseResource implements SessionResource { 'session', ); const cachedToken = await cachedEntry.tokenResolver; - if (shouldDispatchTokenUpdate) { - eventBus.emit(events.TokenUpdate, { - token: cachedToken, - }); + eventBus.emit(events.TokenUpdate, { token: cachedToken }); } // Return null when raw string is empty to indicate that there it's signed-out From e7cc9d98ea0d90ed72a0469c60e244199508f8e7 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 15:54:23 +0200 Subject: [PATCH 35/39] revert changes --- packages/clerk-js/src/core/resources/Session.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index a2abb77e19a..ee5df2c43a1 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -380,7 +380,6 @@ export class Session extends BaseResource implements SessionResource { if (shouldDispatchTokenUpdate) { eventBus.emit(events.TokenUpdate, { token: cachedToken }); } - // Return null when raw string is empty to indicate that there it's signed-out return cachedToken.getRawString() || null; } From f4d8faae7ecb419d19d6e81d62ec0e1c9164700b Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 16:40:41 +0200 Subject: [PATCH 36/39] fix: Revert buildTasksUrl changes --- .changeset/yummy-geese-work.md | 5 ----- packages/react/src/isomorphicClerk.ts | 4 ++-- packages/shared/src/types/clerk.ts | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 .changeset/yummy-geese-work.md diff --git a/.changeset/yummy-geese-work.md b/.changeset/yummy-geese-work.md deleted file mode 100644 index 7388c6df720..00000000000 --- a/.changeset/yummy-geese-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@clerk/clerk-react': minor ---- - -Expose `buildTasksUrl` parameters diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index f50686702fe..fc8b450987f 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -404,8 +404,8 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - buildTasksUrl = (opts?: TasksRedirectOptions): string | void => { - const callback = () => this.clerkjs?.buildTasksUrl(opts) || ''; + buildTasksUrl = (): string | void => { + const callback = () => this.clerkjs?.buildTasksUrl() || ''; if (this.clerkjs && this.loaded) { return callback(); } else { diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 6f49b6289ea..84e3b5832b6 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -744,7 +744,7 @@ export interface Clerk { /** * Returns the configured url where tasks are mounted. */ - buildTasksUrl(opts?: TasksRedirectOptions): string; + buildTasksUrl(): string; /** * Returns the configured afterSignInUrl of the instance. From 1cf449636bba04720951dca2f0e5f7eac4a56e5d Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 18:11:58 +0200 Subject: [PATCH 37/39] fix(clerk-js): Prevent components unmounting when current task changes --- .../ui/components/SessionTasks/tasks/shared/withTaskGuard.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts index c53d60b3bba..23f9786a3c5 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts @@ -25,7 +25,9 @@ export const withTaskGuard =

( const ctx = useSessionTasksContext(); return withRedirect( Component, - clerk => !clerk.session?.currentTask || clerk.session.currentTask.key !== taskKey, + clerk => + !clerk.session?.currentTask || + (clerk.session.currentTask.key !== taskKey && !clerk.__internal_setActiveInProgress), ({ clerk }) => !clerk.session ? clerk.buildSignInUrl() : (ctx.redirectUrlComplete ?? clerk.buildAfterSignInUrl()), warnings.cannotRenderComponentWhenTaskDoesNotExist, From 92f503b128d65c67c79bb798289848df63b21c69 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 18:22:05 +0200 Subject: [PATCH 38/39] fix(clerk-js): Prevent double navigation when resolving the last task --- packages/clerk-js/src/ui/components/SessionTasks/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index dc728a60c26..ef3809d35a6 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -97,7 +97,8 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess // Tasks can only exist on pending sessions, but we check both conditions // here to be defensive and ensure proper redirection const task = clerk.session?.currentTask; - if (!task || clerk.session?.status === 'active') { + if (!task || clerk.session?.status === 'active' || clerk.__internal_setActiveInProgress) { + console.log('navigating to redirectUrlComplete', redirectUrlComplete); void navigate(redirectUrlComplete); return; } From f871d5a2a80d352f034b493dd43fdfbf5e6ae966 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 18:47:01 +0200 Subject: [PATCH 39/39] fix(clerk-js): Remove uneeded changes --- packages/clerk-js/src/ui/components/SessionTasks/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index ef3809d35a6..dc728a60c26 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -97,8 +97,7 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess // Tasks can only exist on pending sessions, but we check both conditions // here to be defensive and ensure proper redirection const task = clerk.session?.currentTask; - if (!task || clerk.session?.status === 'active' || clerk.__internal_setActiveInProgress) { - console.log('navigating to redirectUrlComplete', redirectUrlComplete); + if (!task || clerk.session?.status === 'active') { void navigate(redirectUrlComplete); return; }