From 569acf122141d1f9089c8797b60980bd3e739505 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Fri, 28 Nov 2025 20:57:20 +0200 Subject: [PATCH 1/3] feat(clerk-js): Introduce reset password session task (#7268) Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> # Conflicts: # packages/shared/src/types/clerk.ts # packages/ui/src/elements/contexts/index.tsx --- .changeset/loose-brooms-occur.md | 7 + .changeset/thick-dancers-battle.md | 5 + 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 + packages/clerk-js/bundlewatch.config.json | 25 ++- packages/clerk-js/src/test/fixture-helpers.ts | 3 +- 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 | 8 + 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 + .../src/internal/clerk-js/sessionTasks.ts | 1 + packages/shared/src/types/clerk.ts | 8 + packages/shared/src/types/localization.ts | 8 + packages/shared/src/types/session.ts | 2 +- .../unstable/page-objects/sessionTask.ts | 20 +- .../ui/src/components/SessionTasks/index.tsx | 22 +- .../ChooseOrganizationScreen.tsx | 10 +- .../CreateOrganizationScreen.tsx | 10 +- .../tasks/TaskChooseOrganization/index.tsx | 4 +- .../__tests__/TaskResetPassword.test.tsx | 126 +++++++++++ .../tasks/TaskResetPassword/index.tsx | 203 ++++++++++++++++++ .../SessionTasks/tasks/shared/index.ts | 1 + .../tasks/{ => shared}/withTaskGuard.ts | 22 +- .../src/contexts/ClerkUIComponentsContext.tsx | 21 +- .../src/contexts/components/SessionTasks.ts | 46 +++- packages/ui/src/contexts/components/SignIn.ts | 6 +- packages/ui/src/contexts/components/SignUp.ts | 8 +- packages/ui/src/elements/contexts/index.tsx | 3 +- packages/ui/src/internal/appearance.ts | 1 + packages/ui/src/types.ts | 11 +- 79 files changed, 1054 insertions(+), 53 deletions(-) create mode 100644 .changeset/loose-brooms-occur.md create mode 100644 .changeset/thick-dancers-battle.md create mode 100644 integration/tests/session-tasks-sign-in-reset-password.test.ts create mode 100644 packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx create mode 100644 packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx create mode 100644 packages/ui/src/components/SessionTasks/tasks/shared/index.ts rename packages/ui/src/components/SessionTasks/tasks/{ => shared}/withTaskGuard.ts (52%) 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 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/integration/presets/envs.ts b/integration/presets/envs.ts index 318bbc4133b..687438157a2 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -164,6 +164,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') @@ -217,6 +224,7 @@ export const envs = { withRestrictedMode, withReverification, withSessionTasks, + withSessionTasksResetPassword, withSignInOrUpEmailLinksFlow, withSignInOrUpFlow, withSignInOrUpwithRestrictedModeFlow, diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index 055a246d924..a14e3c9ef83 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -30,6 +30,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 }, /** @@ -37,7 +38,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 }, @@ -59,14 +60,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 }, @@ -78,7 +79,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..581e53a683d --- /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: 'parallel' }); + + 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 794d4e4b1ef..a96f6051092 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/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 6ed05d9e876..bc940595674 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -13,6 +13,29 @@ { "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" }, { "path": "./dist/query-core-vendors*.js", "maxSize": "11KB" }, { "path": "./dist/zxcvbn-ts-core*.js", "maxSize": "12KB" }, - { "path": "./dist/zxcvbn-common*.js", "maxSize": "226KB" } + { "path": "./dist/zxcvbn-common*.js", "maxSize": "226KB" }, + { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, + { "path": "./dist/impersonationfab*.js", "maxSize": "5KB" }, + { "path": "./dist/organizationprofile*.js", "maxSize": "10KB" }, + { "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" }, + { "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" }, + { "path": "./dist/signin*.js", "maxSize": "18KB" }, + { "path": "./dist/signup*.js", "maxSize": "9.5KB" }, + { "path": "./dist/userbutton*.js", "maxSize": "5KB" }, + { "path": "./dist/userprofile*.js", "maxSize": "16KB" }, + { "path": "./dist/userverification*.js", "maxSize": "5KB" }, + { "path": "./dist/onetap*.js", "maxSize": "1KB" }, + { "path": "./dist/waitlist*.js", "maxSize": "1.5KB" }, + { "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" }, + { "path": "./dist/enableOrganizationsPrompt*.js", "maxSize": "6.5KB" }, + { "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" }, + { "path": "./dist/checkout*.js", "maxSize": "8.82KB" }, + { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, + { "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" }, + { "path": "./dist/up-plans-page*.js", "maxSize": "1.0KB" }, + { "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": "3.0KB" } ] } diff --git a/packages/clerk-js/src/test/fixture-helpers.ts b/packages/clerk-js/src/test/fixture-helpers.ts index 5b188b932d8..86547dae2c0 100644 --- a/packages/clerk-js/src/test/fixture-helpers.ts +++ b/packages/clerk-js/src/test/fixture-helpers.ts @@ -45,6 +45,7 @@ const createUserFixtureHelpers = (baseClient: ClientJSON) => { Partial, 'email_addresses' | 'phone_numbers' | 'external_accounts' | 'organization_memberships' > & { + identifier?: string; email_addresses?: Array>; phone_numbers?: Array>; external_accounts?: Array>; @@ -57,7 +58,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/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index c096adbdf05..0c748dad894 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 94df5653f22..cfc808586fb 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 da96b70e703..68854c51d96 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 f41591d9f3d..e23fdd620e6 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 63aa2061380..72f043c0776 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 3b54b5bde88..c941c2214ee 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 6b9a65efa5f..7c0b99d08d2 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 c17808975f7..f16a737073d 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 ec654ed74ed..aee5ca92183 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 63854c39f14..8256d3bf775 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 d0af15bb207..bbda0fa79c3 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -860,6 +860,14 @@ export const enUS: LocalizationResource = { actionText: 'Signed in as {{identifier}}', }, }, + taskResetPassword: { + formButtonPrimary: 'Reset Password', + signOut: { + actionLink: 'Sign out', + actionText: 'Signed in as {{identifier}}', + }, + title: 'Reset password', + }, unstable__errors: { already_a_member_in_organization: '{{email}} is already a member of the organization.', captcha_invalid: undefined, diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index 4db23bd07cf..582c683516d 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 b03c4588734..1d51a5e8397 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 ef776e6de87..54cfb0e8102 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 6ea039efc54..cb8a218669f 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 c5bcd82ffda..55ce823d65a 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 a3f69e20130..08a4fff3da4 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 55fc512eb9f..c7d96d5d0af 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 091b1834094..46ee40b40cb 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 ddad1720de3..f10e586c598 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 4fb1fda1a16..c4a9056969a 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 149d39064af..72de3c469a3 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 e1b8823df53..56b3fba8f2b 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 33635a33b0b..1e08384e767 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 e364cf2be41..71fd3f220f4 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 525c5eab2b6..d1ece04d110 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -863,6 +863,14 @@ export const jaJP: 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/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index e3cb3b43439..51652ba0316 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 ec210b1dbb5..919f1bd5d8c 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 ef462b0f1da..6a83251942c 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 a9dde1c7ec2..10b5565164e 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 2d475b1acfc..3ad81e887d6 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 1545b0e1524..7ce4fff3dd4 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 5e1bccb8fae..a39c91c1168 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 a7619658f4f..5fa5d7129fb 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 7ee199d0cf3..9d3eef9e152 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: 'Resetar Senha', + signOut: { + actionLink: 'Sair', + actionText: 'Conectado como {{identifier}}', + }, + title: 'Resetar senha', + }, 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 93e9f67877d..4cda68f3624 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 f6c88e591cc..8284ca0a329 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 90a8437e61b..d8d6a010559 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 e0c84b94873..d08d954746c 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 0463e9e9905..952de397572 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 ccb22b290d1..d9b2c4f65a8 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 faf091dab68..d18e325f5e5 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 8ccbd080bc6..40134fa9f1b 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 eb3c78f4c10..99818db1309 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 d2579554085..5a65f006d58 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 98ebb9706af..f95d2b788a5 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 6b668e3a31a..fb594fe9d09 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 a24bc945c99..885f4473c11 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 4af19b91ba5..679b8399d8a 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: '由於安全驗證失敗,註冊未成功。請重新整理頁面再試一次,或聯絡支援以取得協助。', diff --git a/packages/shared/src/internal/clerk-js/sessionTasks.ts b/packages/shared/src/internal/clerk-js/sessionTasks.ts index 2b79fb663ff..eb8a3f3ca99 100644 --- a/packages/shared/src/internal/clerk-js/sessionTasks.ts +++ b/packages/shared/src/internal/clerk-js/sessionTasks.ts @@ -8,6 +8,7 @@ import { buildURL } from './url'; */ export const INTERNAL_SESSION_TASK_ROUTE_BY_KEY: Record = { 'choose-organization': 'choose-organization', + 'reset-password': 'reset-password', } as const; /** diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 419a6015bf3..888fd6b43da 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -2194,6 +2194,14 @@ export type TaskChooseOrganizationProps = { appearance?: ClerkAppearanceTheme; }; +export type TaskResetPasswordProps = { + /** + * Full URL or path to navigate to after successfully resolving all tasks + */ + redirectUrlComplete: string; + appearance?: ClerkAppearanceTheme; +}; + export type CreateOrganizationInvitationParams = { emailAddress: string; role: OrganizationCustomRoleKey; diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 0906135f765..d80f9dbc54f 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1284,6 +1284,14 @@ export type __internal_LocalizationResource = { action__invitationAccept: LocalizationValue; }; }; + taskResetPassword: { + title: LocalizationValue; + signOut: { + actionLink: LocalizationValue; + actionText: LocalizationValue<'identifier'>; + }; + formButtonPrimary: LocalizationValue; + }; }; type WithParamName = T & diff --git a/packages/shared/src/types/session.ts b/packages/shared/src/types/session.ts index 70a512dbddf..b1fb5711026 100644 --- a/packages/shared/src/types/session.ts +++ b/packages/shared/src/types/session.ts @@ -335,7 +335,7 @@ export interface SessionTask { /** * A unique identifier for the task */ - key: 'choose-organization'; + key: 'choose-organization' | 'reset-password'; } export type GetTokenOptions = { 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; diff --git a/packages/ui/src/components/SessionTasks/index.tsx b/packages/ui/src/components/SessionTasks/index.tsx index b5300052d4b..9cca5b9b9c1 100644 --- a/packages/ui/src/components/SessionTasks/index.tsx +++ b/packages/ui/src/components/SessionTasks/index.tsx @@ -1,7 +1,6 @@ import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '@clerk/shared/internal/clerk-js/sessionTasks'; 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'; @@ -12,10 +11,12 @@ import { LoadingCardContainer } from '@/ui/elements/LoadingCard'; 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 +61,13 @@ function SessionTasksRoutes(): JSX.Element { + + + + + @@ -78,6 +86,7 @@ type SessionTasksProps = { export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: SessionTasksProps) => { const clerk = useClerk(); const { navigate } = useRouter(); + const currentTaskContainer = useRef(null); // If there are no pending tasks, navigate away from the tasks flow. @@ -111,17 +120,8 @@ export const SessionTasks = withCardStateProvider(({ redirectUrlComplete }: Sess ); } - const navigateOnSetActive = async ({ session }: { session: SessionResource }) => { - const currentTask = session.currentTask; - if (!currentTask) { - return navigate(redirectUrlComplete); - } - - return navigate(`./${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[currentTask.key]}`); - }; - return ( - + ); diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx index b1a0e7c7a8a..05eec64ad04 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx @@ -17,7 +17,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 { Actions } from '@/ui/elements/Actions'; import { Card } from '@/ui/elements/Card'; @@ -25,7 +25,6 @@ import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { OrganizationPreview } from '@/ui/elements/OrganizationPreview'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; -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: any) { diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx index 75f7ab38878..2cf177d11a4 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx +++ b/packages/ui/src/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: any) { diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index 60b9442bdd5..4d8135592c2 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/ui/src/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'; import { ChooseOrganizationScreen } from './ChooseOrganizationScreen'; import { CreateOrganizationScreen } from './CreateOrganizationScreen'; @@ -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/ui/src/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx new file mode 100644 index 00000000000..6f802696eca --- /dev/null +++ b/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/__tests__/TaskResetPassword.test.tsx @@ -0,0 +1,126 @@ +import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; + +import { bindCreateFixtures } from '@/test/create-fixtures'; +import { render, waitFor } 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.withUser({ + email_addresses: ['test@clerk.com'], + identifier: 'test@clerk.com', + }); + }); + + const { queryByText, queryByRole } = render(, { wrapper }); + + expect(queryByText('New password')).not.toBeInTheDocument(); + expect(queryByText('Confirm password')).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'], + identifier: '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('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' }], + }); + }); + + 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'], + identifier: '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/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx new file mode 100644 index 00000000000..b81ffd5d2f1 --- /dev/null +++ b/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -0,0 +1,203 @@ +import { useClerk, useReverification } from '@clerk/shared/react'; +import type { UserResource } from '@clerk/shared/types'; + +import { useEnvironment, useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; +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'; +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 '../shared'; + +const TaskResetPasswordInternal = () => { + const clerk = useClerk(); + const card = useCardState(); + const { + userSettings: { passwordSettings }, + } = useEnvironment(); + + const { t, locale } = useLocalizations(); + const { redirectUrlComplete } = useTaskResetPasswordContext(); + 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) { + return clerk?.signOut(navigateAfterSignOut); + } + + return clerk?.signOut(navigateAfterMultiSessionSingleSignOutUrl, { sessionId: clerk.session?.id }); + }; + + 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 = () => { + return card.runAsync(async () => { + if (!clerk.user) { + return; + } + + passwordField.clearFeedback(); + confirmField.clearFeedback(); + + try { + await updatePasswordWithReverification(clerk.user, [ + { + newPassword: passwordField.value, + signOutOfOtherSessions: sessionsField.checked, + }, + ]); + + // 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, redirectUrlComplete }); + }, + }); + } catch (e) { + return handleError(e, [passwordField, confirmField], card.setError); + } + }); + }; + + const identifier = clerk.user?.primaryEmailAddress?.emailAddress ?? clerk.user?.username; + + return ( + + + + + + + + {card.error} + + { + void resetPassword(); + }} + onBlur={validateForm} + gap={8} + > + + {/* For password managers */} + + + + + + { + if (e.target.value) { + setConfirmPasswordFeedback(e.target.value); + } + return confirmField.props.onChange(e); + }} + /> + + + + + + + + + + + + + + ({ width: '100%' })} + > + {identifier && ( + + )} + ({ flexShrink: 0 })} + onClick={() => { + void handleSignOut(); + }} + localizationKey={localizationKeys('taskResetPassword.signOut.actionLink')} + /> + + + + + + ); +}; + +export const TaskResetPassword = withCoreSessionSwitchGuard( + withTaskGuard(withCardStateProvider(TaskResetPasswordInternal), 'reset-password'), +); diff --git a/packages/ui/src/components/SessionTasks/tasks/shared/index.ts b/packages/ui/src/components/SessionTasks/tasks/shared/index.ts new file mode 100644 index 00000000000..81fccd43bee --- /dev/null +++ b/packages/ui/src/components/SessionTasks/tasks/shared/index.ts @@ -0,0 +1 @@ +export * from './withTaskGuard'; diff --git a/packages/ui/src/components/SessionTasks/tasks/withTaskGuard.ts b/packages/ui/src/components/SessionTasks/tasks/shared/withTaskGuard.ts similarity index 52% rename from packages/ui/src/components/SessionTasks/tasks/withTaskGuard.ts rename to packages/ui/src/components/SessionTasks/tasks/shared/withTaskGuard.ts index e8489db6f8e..510ff80238e 100644 --- a/packages/ui/src/components/SessionTasks/tasks/withTaskGuard.ts +++ b/packages/ui/src/components/SessionTasks/tasks/shared/withTaskGuard.ts @@ -1,19 +1,33 @@ +import type { SessionTask } from '@clerk/shared/types'; import { warnings } from '@clerk/shared/internal/clerk-js/warnings'; import type { ComponentType } from 'react'; import { withRedirect } from '@/ui/common'; -import { useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; +import { useSessionTasksContext } from '@/ui/contexts/components/SessionTasks'; import type { AvailableComponentProps } from '@/ui/types'; -export const withTaskGuard =

(Component: ComponentType

) => { +/** + * 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; const HOC = (props: P) => { - const ctx = useTaskChooseOrganizationContext(); + const ctx = useSessionTasksContext(); return withRedirect( Component, - clerk => !clerk.session?.currentTask, + clerk => + !clerk.session?.currentTask || + (clerk.session.currentTask.key !== taskKey && !clerk.__internal_setActiveInProgress), ({ clerk }) => !clerk.session ? clerk.buildSignInUrl() : (ctx.redirectUrlComplete ?? clerk.buildAfterSignInUrl()), warnings.cannotRenderComponentWhenTaskDoesNotExist, diff --git a/packages/ui/src/contexts/ClerkUIComponentsContext.tsx b/packages/ui/src/contexts/ClerkUIComponentsContext.tsx index e2670d3314d..a375cb29568 100644 --- a/packages/ui/src/contexts/ClerkUIComponentsContext.tsx +++ b/packages/ui/src/contexts/ClerkUIComponentsContext.tsx @@ -3,6 +3,7 @@ import type { APIKeysProps, PricingTableProps, TaskChooseOrganizationProps, + TaskResetPasswordProps, UserButtonProps, WaitlistProps, } from '@clerk/shared/types'; @@ -27,7 +28,11 @@ import { UserVerificationContext, WaitlistContext, } from './components'; -import { TaskChooseOrganizationContext } from './components/SessionTasks'; +import { + SessionTasksContext, + TaskChooseOrganizationContext, + TaskResetPasswordContext, +} from './components/SessionTasks'; export function ComponentContextProvider({ componentName, @@ -118,9 +123,21 @@ export function ComponentContextProvider({ - {children} + + {children} + ); + case 'TaskResetPassword': + return ( + + + {children} + + + ); default: throw new Error(`Unknown component context: ${componentName}`); } diff --git a/packages/ui/src/contexts/components/SessionTasks.ts b/packages/ui/src/contexts/components/SessionTasks.ts index e7f15757943..1f25c3cdc05 100644 --- a/packages/ui/src/contexts/components/SessionTasks.ts +++ b/packages/ui/src/contexts/components/SessionTasks.ts @@ -1,17 +1,45 @@ +import type { SessionResource } from '@clerk/shared/types'; import { createContext, useContext } from 'react'; -import type { SessionTasksCtx, TaskChooseOrganizationCtx } from '../../types'; +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); @@ -27,3 +55,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/ui/src/contexts/components/SignIn.ts b/packages/ui/src/contexts/components/SignIn.ts index 161a8704c1d..33f8c3eeb15 100644 --- a/packages/ui/src/contexts/components/SignIn.ts +++ b/packages/ui/src/contexts/components/SignIn.ts @@ -36,7 +36,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; @@ -128,7 +128,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/ui/src/contexts/components/SignUp.ts b/packages/ui/src/contexts/components/SignUp.ts index 3c8107bf9c1..202d5bf3274 100644 --- a/packages/ui/src/contexts/components/SignUp.ts +++ b/packages/ui/src/contexts/components/SignUp.ts @@ -35,7 +35,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; @@ -120,7 +120,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 diff --git a/packages/ui/src/elements/contexts/index.tsx b/packages/ui/src/elements/contexts/index.tsx index fb38f0bb4ed..6211f1a0b84 100644 --- a/packages/ui/src/elements/contexts/index.tsx +++ b/packages/ui/src/elements/contexts/index.tsx @@ -102,7 +102,8 @@ export type FlowMetadata = { | 'subscriptionDetails' | 'tasks' | 'taskChooseOrganization' - | 'enableOrganizations'; + | 'enableOrganizations' + | 'taskResetPassword'; part?: | 'start' | 'emailCode' diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 4e55d7afe8a..a0ae91c2c0a 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/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/ui/src/types.ts b/packages/ui/src/types.ts index 3f9fe2a27ef..e970f9a49ea 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -12,7 +12,6 @@ import type { OrganizationProfileProps, OrganizationSwitcherProps, PricingTableProps, - SessionResource, SignInFallbackRedirectUrl, SignInForceRedirectUrl, SignInProps, @@ -20,6 +19,7 @@ import type { SignUpForceRedirectUrl, SignUpProps, TaskChooseOrganizationProps, + TaskResetPasswordProps, UserAvatarProps, UserButtonProps, UserProfileProps, @@ -143,14 +143,16 @@ 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 & { componentName: 'TaskChooseOrganization'; }; +export type TaskResetPasswordCtx = TaskResetPasswordProps & { + componentName: 'TaskResetPassword'; +}; + export type OAuthConsentCtx = __internal_OAuthConsentProps & { componentName: 'OAuthConsent'; }; @@ -182,5 +184,6 @@ export type AvailableComponentCtx = | OAuthConsentCtx | SubscriptionDetailsCtx | PlanDetailsCtx - | TaskChooseOrganizationCtx; + | TaskChooseOrganizationCtx + | TaskResetPasswordCtx; export type AvailableComponentName = AvailableComponentCtx['componentName']; From a3b19ffe7641b2ddcff7a8576a0d67aec440edbd Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Mon, 1 Dec 2025 20:56:05 +0200 Subject: [PATCH 2/3] fix(ui): Fix import paths and type issues --- .../components/SessionTasks/tasks/TaskResetPassword/index.tsx | 2 +- packages/ui/src/contexts/components/SessionTasks.ts | 2 +- packages/ui/src/contexts/components/SignUp.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx index b81ffd5d2f1..91e44e85239 100644 --- a/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx +++ b/packages/ui/src/components/SessionTasks/tasks/TaskResetPassword/index.tsx @@ -97,7 +97,7 @@ const TaskResetPasswordInternal = () => { await navigateOnSetActive?.({ session, redirectUrlComplete }); }, }); - } catch (e) { + } catch (e: any) { return handleError(e, [passwordField, confirmField], card.setError); } }); diff --git a/packages/ui/src/contexts/components/SessionTasks.ts b/packages/ui/src/contexts/components/SessionTasks.ts index 1f25c3cdc05..9fc69910fa2 100644 --- a/packages/ui/src/contexts/components/SessionTasks.ts +++ b/packages/ui/src/contexts/components/SessionTasks.ts @@ -1,7 +1,7 @@ +import { getTaskEndpoint } from '@clerk/shared/internal/clerk-js/sessionTasks'; 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'; diff --git a/packages/ui/src/contexts/components/SignUp.ts b/packages/ui/src/contexts/components/SignUp.ts index 202d5bf3274..a614dc30941 100644 --- a/packages/ui/src/contexts/components/SignUp.ts +++ b/packages/ui/src/contexts/components/SignUp.ts @@ -1,6 +1,6 @@ import { SIGN_UP_INITIAL_VALUE_KEYS } from '@clerk/shared/internal/clerk-js/constants'; import { RedirectUrls } from '@clerk/shared/internal/clerk-js/redirectUrls'; -import { getTaskEndpoint, INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '@clerk/shared/internal/clerk-js/sessionTasks'; +import { getTaskEndpoint } from '@clerk/shared/internal/clerk-js/sessionTasks'; import { buildURL } from '@clerk/shared/internal/clerk-js/url'; import { useClerk } from '@clerk/shared/react'; import type { SessionResource } from '@clerk/shared/types'; From e2d734ac7416194214b0e207ede60456e79de568 Mon Sep 17 00:00:00 2001 From: Vaggelis Yfantis Date: Tue, 2 Dec 2025 09:06:00 +0200 Subject: [PATCH 3/3] chore: Remove changesets --- .changeset/loose-brooms-occur.md | 7 ------- .changeset/thick-dancers-battle.md | 5 ----- 2 files changed, 12 deletions(-) delete mode 100644 .changeset/loose-brooms-occur.md delete mode 100644 .changeset/thick-dancers-battle.md diff --git a/.changeset/loose-brooms-occur.md b/.changeset/loose-brooms-occur.md deleted file mode 100644 index 7dccf85413a..00000000000 --- a/.changeset/loose-brooms-occur.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@clerk/localizations': minor -'@clerk/clerk-js': minor -'@clerk/shared': minor ---- - -Introduce `reset-password` session task diff --git a/.changeset/thick-dancers-battle.md b/.changeset/thick-dancers-battle.md deleted file mode 100644 index f12f01fd0fa..00000000000 --- a/.changeset/thick-dancers-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@clerk/backend': minor ---- - -Introducing `users.__experimental_passwordUntrusted` action