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 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..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 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/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" } ] } 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/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/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index a5c8ca20ebb..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'; @@ -12,10 +11,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 +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/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/TaskChooseOrganization/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index 60b9442bdd5..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 '../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/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..6f802696eca --- /dev/null +++ b/packages/clerk-js/src/ui/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/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..b81ffd5d2f1 --- /dev/null +++ b/packages/clerk-js/src/ui/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/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/withTaskGuard.ts b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts similarity index 51% rename from packages/clerk-js/src/ui/components/SessionTasks/tasks/withTaskGuard.ts rename to packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts index d5702f19760..23f9786a3c5 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/withTaskGuard.ts +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/shared/withTaskGuard.ts @@ -1,19 +1,33 @@ +import type { SessionTask } from '@clerk/shared/types'; import type { ComponentType } from 'react'; import { warnings } from '@/core/warnings'; 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/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index 271773756d3..19229545f0b 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,11 @@ import { UserVerificationContext, WaitlistContext, } from './components'; -import { TaskChooseOrganizationContext } from './components/SessionTasks'; +import { + SessionTasksContext, + TaskChooseOrganizationContext, + TaskResetPasswordContext, +} from './components/SessionTasks'; export function ComponentContextProvider({ componentName, @@ -123,9 +128,21 @@ export function ComponentContextProvider({ - {children} + + {children} + ); + case 'TaskResetPassword': + return ( + + + {children} + + + ); default: throw new Error(`Unknown component context: ${componentName}`); } diff --git a/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts b/packages/clerk-js/src/ui/contexts/components/SessionTasks.ts index e7f15757943..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 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/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 diff --git a/packages/clerk-js/src/ui/elements/contexts/index.tsx b/packages/clerk-js/src/ui/elements/contexts/index.tsx index fb38f0bb4ed..6211f1a0b84 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' diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 3f9fe2a27ef..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, @@ -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']; 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 836e301ee7f..99d4822c1ce 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 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..ae36e3b5258 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 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: '由於安全驗證失敗,註冊未成功。請重新整理頁面再試一次,或聯絡支援以取得協助。', 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..d46434cc2fe 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 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 = { 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;