@@ -29,13 +29,13 @@ const meta: Meta
= {
],
parameters: {
codeSection: {
- docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmailEffect, always wrap it with ModalContent to maintain consistent styling.',
+ docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmail, always wrap it with ModalContent to maintain consistent styling.',
},
},
};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const ErrorState: Story = {
args: {
@@ -43,7 +43,7 @@ export const ErrorState: Story = {
},
};
-export const IntegratedExample: StoryObj = {
+export const IntegratedExample: StoryObj = {
render: () => (
= {
}
+ element={}
/>
diff --git a/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmail.test.tsx
similarity index 92%
rename from packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx
rename to packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmail.test.tsx
index 677982459a..e4c2b2ed0a 100644
--- a/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx
+++ b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmail.test.tsx
@@ -7,7 +7,7 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { AppPath } from 'twenty-shared/types';
import { ThemeProvider } from 'twenty-ui/theme-constants';
-import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect';
+import { VerifyEmail } from '@/auth/components/VerifyEmail';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import {
jotaiStore,
@@ -60,7 +60,7 @@ jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({
}),
}));
-// Rendered by VerifyEmailEffect in the error state; isolate it from Apollo.
+// Rendered by VerifyEmail in the error state; isolate it from Apollo.
jest.mock(
'@/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken',
() => ({
@@ -76,20 +76,20 @@ dynamicActivate(SOURCE_LOCALE);
const VERIFY_EMAIL_URL =
'/verify-email?email=user%40example.com&emailVerificationToken=valid-token';
-const renderEffect = (initialEntry: string) =>
+const renderVerifyEmail = (initialEntry: string) =>
render(
-
+
,
);
-describe('VerifyEmailEffect', () => {
+describe('VerifyEmail', () => {
beforeEach(() => {
jest.clearAllMocks();
resetJotaiStore();
@@ -106,7 +106,7 @@ describe('VerifyEmailEffect', () => {
it('navigates to the SignInUp page after a successful workspace-agnostic verification on the central domain', async () => {
verifyEmailAndGetWorkspaceAgnosticTokenMock.mockResolvedValue(undefined);
- renderEffect(VERIFY_EMAIL_URL);
+ renderVerifyEmail(VERIFY_EMAIL_URL);
await waitFor(() => {
expect(verifyEmailAndGetWorkspaceAgnosticTokenMock).toHaveBeenCalledWith(
@@ -128,7 +128,7 @@ describe('VerifyEmailEffect', () => {
new Error('verification failed'),
);
- renderEffect(VERIFY_EMAIL_URL);
+ renderVerifyEmail(VERIFY_EMAIL_URL);
await waitFor(() => {
expect(enqueueErrorSnackBarMock).toHaveBeenCalled();
@@ -143,7 +143,7 @@ describe('VerifyEmailEffect', () => {
workspaceUrls: { subdomainUrl: 'https://foo.twenty.com/' },
});
- renderEffect(VERIFY_EMAIL_URL);
+ renderVerifyEmail(VERIFY_EMAIL_URL);
await waitFor(() => {
expect(verifyEmailAndGetLoginTokenMock).toHaveBeenCalledWith(
diff --git a/packages/twenty-front/src/modules/auth/components/__tests__/VerifyLoginTokenEffect.test.tsx b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyLoginTokenEffect.test.tsx
new file mode 100644
index 0000000000..723c78f69a
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyLoginTokenEffect.test.tsx
@@ -0,0 +1,72 @@
+import { act, render, waitFor } from '@testing-library/react';
+import { Provider as JotaiProvider } from 'jotai';
+import { StrictMode } from 'react';
+import { MemoryRouter } from 'react-router-dom';
+
+import { VerifyLoginTokenEffect } from '@/auth/components/VerifyLoginTokenEffect';
+import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
+import {
+ jotaiStore,
+ resetJotaiStore,
+} from '@/ui/utilities/state/jotai/jotaiStore';
+
+const verifyLoginTokenMock = jest.fn();
+const navigateMock = jest.fn();
+
+jest.mock('@/auth/hooks/useVerifyLogin', () => ({
+ useVerifyLogin: () => ({ verifyLoginToken: verifyLoginTokenMock }),
+}));
+
+jest.mock('~/hooks/useNavigateApp', () => ({
+ useNavigateApp: () => navigateMock,
+}));
+
+jest.mock('@/auth/hooks/useHasAccessTokenPair', () => ({
+ useHasAccessTokenPair: () => false,
+}));
+
+const setClientConfigSaved = (isSaved: boolean) => {
+ jotaiStore.set(clientConfigApiStatusState.atom, {
+ isLoadedOnce: true,
+ isLoading: false,
+ isErrored: false,
+ isSaved,
+ });
+};
+
+const renderEffect = (initialEntry: string) =>
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+describe('VerifyLoginTokenEffect', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ resetJotaiStore();
+ setClientConfigSaved(true);
+ });
+
+ it('verifies the login token at most once even when the gating config re-triggers the effect', async () => {
+ renderEffect('/verify?loginToken=login-token');
+
+ await waitFor(() => {
+ expect(verifyLoginTokenMock).toHaveBeenCalledWith('login-token');
+ });
+ expect(verifyLoginTokenMock).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ setClientConfigSaved(false);
+ });
+ await act(async () => {
+ setClientConfigSaved(true);
+ });
+
+ expect(verifyLoginTokenMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/twenty-front/src/modules/auth/constants/AuthAndOnboardingPaths.ts b/packages/twenty-front/src/modules/auth/constants/AuthAndOnboardingPaths.ts
new file mode 100644
index 0000000000..4602abbba1
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/constants/AuthAndOnboardingPaths.ts
@@ -0,0 +1,9 @@
+import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths';
+import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths';
+import { AppPath } from 'twenty-shared/types';
+
+export const AUTH_AND_ONBOARDING_PATHS = [
+ ...ONGOING_USER_CREATION_PATHS,
+ ...ONBOARDING_PATHS,
+ AppPath.ResetPassword,
+];
diff --git a/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts b/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts
deleted file mode 100644
index a0eac56aa2..0000000000
--- a/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { type ModalOverlay, type ModalSize } from 'twenty-ui/surfaces';
-import { AppPath } from 'twenty-shared/types';
-
-type AuthModalConfigType = {
- size: ModalSize;
- overlay: ModalOverlay;
- showScrollWrapper: boolean;
-};
-
-export const AUTH_MODAL_CONFIG: {
- default: AuthModalConfigType;
- [key: string]: AuthModalConfigType;
-} = {
- default: {
- size: 'medium',
- overlay: 'dark',
- showScrollWrapper: true,
- },
- [AppPath.BookCall]: {
- size: 'extraLarge',
- overlay: 'transparent',
- showScrollWrapper: false,
- },
-};
diff --git a/packages/twenty-front/src/modules/auth/constants/AuthModalId.ts b/packages/twenty-front/src/modules/auth/constants/AuthModalId.ts
deleted file mode 100644
index 934f56df24..0000000000
--- a/packages/twenty-front/src/modules/auth/constants/AuthModalId.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const AUTH_MODAL_ID = 'auth-modal';
diff --git a/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts b/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts
index d7bcf74bf1..7c84b997b9 100644
--- a/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts
+++ b/packages/twenty-front/src/modules/auth/constants/OnboardingPaths.ts
@@ -2,16 +2,11 @@ import { AppPath } from 'twenty-shared/types';
export const ONBOARDING_PATHS = [
AppPath.WorkspaceActivation,
- AppPath.WorkspaceActivationV2,
AppPath.CreateProfile,
- AppPath.CreateProfileV2,
AppPath.SyncEmails,
- AppPath.SyncEmailsV2,
- AppPath.InstallAppsV2,
+ AppPath.InstallApps,
AppPath.InviteTeam,
- AppPath.InviteTeamV2,
AppPath.PlanRequired,
- AppPath.PlanRequiredV2,
AppPath.PlanRequiredSuccess,
AppPath.BookCallDecision,
AppPath.BookCall,
diff --git a/packages/twenty-front/src/modules/auth/constants/OnboardingTransitionPaths.ts b/packages/twenty-front/src/modules/auth/constants/OnboardingTransitionPaths.ts
new file mode 100644
index 0000000000..5c399c74e1
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/constants/OnboardingTransitionPaths.ts
@@ -0,0 +1,15 @@
+import { AppPath } from 'twenty-shared/types';
+
+export const ONBOARDING_TRANSITION_PATHS = [
+ AppPath.SignInUp,
+ AppPath.Invite,
+ AppPath.Verify,
+ AppPath.VerifyEmail,
+ AppPath.WorkspaceActivation,
+ AppPath.CreateProfile,
+ AppPath.SyncEmails,
+ AppPath.InstallApps,
+ AppPath.InviteTeam,
+ AppPath.PlanRequired,
+ AppPath.PlanRequiredSuccess,
+];
diff --git a/packages/twenty-front/src/modules/auth/constants/OnboardingV2Paths.ts b/packages/twenty-front/src/modules/auth/constants/OnboardingV2Paths.ts
deleted file mode 100644
index bb858f0cb8..0000000000
--- a/packages/twenty-front/src/modules/auth/constants/OnboardingV2Paths.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { AppPath } from 'twenty-shared/types';
-
-export const ONBOARDING_V2_PATHS = [
- AppPath.SignInUpV2,
- AppPath.VerifyV2,
- AppPath.WorkspaceActivationV2,
- AppPath.CreateProfileV2,
- AppPath.SyncEmailsV2,
- AppPath.InstallAppsV2,
- AppPath.InviteTeamV2,
- AppPath.PlanRequiredV2,
-];
diff --git a/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts b/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts
index da2d31434f..db499b4c50 100644
--- a/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts
+++ b/packages/twenty-front/src/modules/auth/constants/OngoingUserCreationPaths.ts
@@ -3,8 +3,6 @@ import { AppPath } from 'twenty-shared/types';
export const ONGOING_USER_CREATION_PATHS = [
AppPath.Invite,
AppPath.SignInUp,
- AppPath.SignInUpV2,
AppPath.VerifyEmail,
AppPath.Verify,
- AppPath.VerifyV2,
];
diff --git a/packages/twenty-front/src/modules/auth/hooks/useIsOnAuthOrOnboardingPage.ts b/packages/twenty-front/src/modules/auth/hooks/useIsOnAuthOrOnboardingPage.ts
new file mode 100644
index 0000000000..f798598fe4
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/hooks/useIsOnAuthOrOnboardingPage.ts
@@ -0,0 +1,11 @@
+import { AUTH_AND_ONBOARDING_PATHS } from '@/auth/constants/AuthAndOnboardingPaths';
+import { useLocation } from 'react-router-dom';
+import { isMatchingLocation } from '~/utils/isMatchingLocation';
+
+export const useIsOnAuthOrOnboardingPage = () => {
+ const location = useLocation();
+
+ return AUTH_AND_ONBOARDING_PATHS.some((appPath) =>
+ isMatchingLocation(location, appPath),
+ );
+};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx
index 9e843aca5f..94bd5d1677 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx
@@ -3,13 +3,15 @@ import { Trans } from '@lingui/react/macro';
import { useWorkspaceBypass } from '@/auth/sign-in-up/hooks/useWorkspaceBypass';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
+import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCopyContainer = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
- max-width: 280px;
+ line-height: 1.4;
+ max-width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
text-align: center;
& > a {
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
index 38881e7d96..abeb172ffe 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx
@@ -8,6 +8,8 @@ import { FormProvider } from 'react-hook-form';
import { ClickToActionLink, UndecoratedLink } from 'twenty-ui/navigation';
import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
+import { OnboardingStepAnimatedItem } from '@/onboarding/components/OnboardingStepAnimatedItem';
+import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/internal/SignInUpWithCredentials';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/internal/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/internal/SignInUpWithMicrosoft';
@@ -36,6 +38,11 @@ import {
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
+const StyledContentContainer = styled(StyledOnboardingContentContainer)`
+ max-width: 100%;
+ width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
+`;
+
const StyledWorkspaceContainer = styled.div`
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.light};
@@ -156,70 +163,82 @@ export const SignInUpGlobalScopeForm = () => {
);
};
+ const availableWorkspacesList = [
+ ...availableWorkspaces.availableWorkspacesForSignIn,
+ ...availableWorkspaces.availableWorkspacesForSignUp,
+ ];
+
return (
<>
{signInUpStep === SignInUpStep.WorkspaceSelection && (
-
+
- {[
- ...availableWorkspaces.availableWorkspacesForSignIn,
- ...availableWorkspaces.availableWorkspacesForSignUp,
- ].map((availableWorkspace) => (
- (
+
-
+
+
+
+
+
+
+ {availableWorkspace.displayName ||
+ availableWorkspace.id}
+
+
+ {
+ new URL(
+ getWorkspaceUrl(availableWorkspace.workspaceUrls),
+ ).hostname
+ }
+
+
+
+
+
+
+
+
+
+ ))}
+ {!isDDLLocked && (
+
+
+ setSignInUpStep(SignInUpStep.WorkspaceCreation)
+ }
+ >
-
+
+
+
-
- {availableWorkspace.displayName ||
- availableWorkspace.id}
-
-
- {
- new URL(
- getWorkspaceUrl(availableWorkspace.workspaceUrls),
- ).hostname
- }
-
+ {t`Create a workspace`}
-
- ))}
- {!isDDLLocked && (
- setSignInUpStep(SignInUpStep.WorkspaceCreation)}
- >
-
-
-
-
-
- {t`Create a workspace`}
-
-
-
-
-
-
+
)}
-
+
)}
{signInUpStep !== SignInUpStep.WorkspaceSelection && (
-
+
{authProviders.google && (
{
/>
)}
{(authProviders.google || authProviders.microsoft) && (
-
+
)}
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
@@ -248,7 +269,7 @@ export const SignInUpGlobalScopeForm = () => {
)}
-
+
)}
>
);
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpV2StandardContent.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpStandardContent.tsx
similarity index 68%
rename from packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpV2StandardContent.tsx
rename to packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpStandardContent.tsx
index 40427385b8..f7b6bb9243 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpV2StandardContent.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpStandardContent.tsx
@@ -3,13 +3,28 @@ import { Title } from '@/auth/components/Title';
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { WorkspaceSelectionFooter } from '@/auth/sign-in-up/components/WorkspaceSelectionFooter';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
+import { styled } from '@linaria/react';
import { type JSX } from 'react';
import { AppPath } from 'twenty-shared/types';
import { AnimatedEaseIn } from 'twenty-ui/layout';
import { ModalContent } from 'twenty-ui/surfaces';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type PublicWorkspaceData } from '~/generated-metadata/graphql';
-type SignInUpV2StandardContentProps = {
+const StyledTitleContainer = styled.div`
+ line-height: 1.2;
+ margin-top: ${themeCssVariables.spacing[10]};
+`;
+
+const StyledFormContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: ${themeCssVariables.spacing[6]};
+ margin-top: ${themeCssVariables.spacing[6]};
+`;
+
+type SignInUpStandardContentProps = {
workspacePublicData: PublicWorkspaceData | null;
signInUpForm: JSX.Element | null;
signInUpStep: SignInUpStep;
@@ -17,13 +32,13 @@ type SignInUpV2StandardContentProps = {
onClickOnLogo: () => void;
};
-export const SignInUpV2StandardContent = ({
+export const SignInUpStandardContent = ({
workspacePublicData,
signInUpForm,
signInUpStep,
title,
onClickOnLogo,
-}: SignInUpV2StandardContentProps) => {
+}: SignInUpStandardContentProps) => {
return (
@@ -31,11 +46,13 @@ export const SignInUpV2StandardContent = ({
secondaryLogo={workspacePublicData?.logo}
placeholder={workspacePublicData?.displayName}
onClick={onClickOnLogo}
- to={AppPath.SignInUpV2}
+ to={AppPath.SignInUp}
/>
- {title}
- {signInUpForm}
+
+ {title}
+
+ {signInUpForm}
{signInUpStep === SignInUpStep.WorkspaceSelection && (
)}
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceActivationV2.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceActivationV2.tsx
deleted file mode 100644
index 7146279550..0000000000
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceActivationV2.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { SubTitle } from '@/auth/components/SubTitle';
-import { WORKSPACE_ACTIVATION_MESSAGES } from '@/auth/sign-in-up/constants/WorkspaceActivationMessages';
-import { OnboardingPulsingLogo } from '@/onboarding/components/OnboardingPulsingLogo';
-import { styled } from '@linaria/react';
-import { useLingui } from '@lingui/react/macro';
-import { motion, useReducedMotion } from 'framer-motion';
-import { useContext } from 'react';
-import { ThemeContext } from 'twenty-ui/theme-constants';
-
-const STEP_OPACITIES = [1, 0.4, 0.12];
-const VISIBLE_STEP_COUNT = STEP_OPACITIES.length;
-const STEP_HEIGHT_IN_PX = 28;
-const STEPS_CONTAINER_HEIGHT_IN_PX = STEP_HEIGHT_IN_PX * VISIBLE_STEP_COUNT;
-
-const StyledStepsContainer = styled.div`
- height: ${STEPS_CONTAINER_HEIGHT_IN_PX}px;
- position: relative;
- width: 100%;
-`;
-
-const StyledStepBase = styled.div`
- left: 0;
- position: absolute;
- right: 0;
- top: 0;
-`;
-
-const StyledStep = motion.create(StyledStepBase);
-
-type SignInUpWorkspaceActivationV2Props = {
- messageIndex: number;
-};
-
-export const SignInUpWorkspaceActivationV2 = ({
- messageIndex,
-}: SignInUpWorkspaceActivationV2Props) => {
- const { i18n } = useLingui();
- const { theme } = useContext(ThemeContext);
- const shouldReduceMotion = useReducedMotion();
-
- const messages = WORKSPACE_ACTIVATION_MESSAGES.map((message) =>
- i18n._(message),
- );
-
- return (
- <>
-
-
- {messages.map((message, index) => {
- const stepOffset = index - messageIndex;
- const isVisible = stepOffset >= 0 && stepOffset < VISIBLE_STEP_COUNT;
-
- return (
-
- {message}
-
- );
- })}
-
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceCreationLoader.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceCreationLoader.tsx
deleted file mode 100644
index 9fa4928120..0000000000
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpWorkspaceCreationLoader.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { SignInUpWorkspaceActivationV2 } from '@/auth/sign-in-up/components/SignInUpWorkspaceActivationV2';
-import { SignInUpWorkspaceActivationV2Effect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect';
-import { styled } from '@linaria/react';
-import { useState } from 'react';
-
-const StyledContainer = styled.div`
- align-items: center;
- display: flex;
- flex: 1;
- flex-direction: column;
- justify-content: center;
- min-height: 100%;
- width: 100%;
-`;
-
-export const SignInUpWorkspaceCreationLoader = () => {
- const [messageIndex, setMessageIndex] = useState(0);
-
- return (
-
-
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/SignInUpWorkspaceActivationV2.stories.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/SignInUpWorkspaceActivationV2.stories.tsx
deleted file mode 100644
index e3801b9854..0000000000
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/SignInUpWorkspaceActivationV2.stories.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { type Meta, type StoryObj } from '@storybook/react-vite';
-
-import { SignInUpWorkspaceActivationV2 } from '@/auth/sign-in-up/components/SignInUpWorkspaceActivationV2';
-import { SignInUpWorkspaceActivationV2Effect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect';
-import { useState } from 'react';
-import { ModalContent } from 'twenty-ui/surfaces';
-import { ComponentDecorator } from 'twenty-ui/testing';
-
-const RenderWithModalContent = () => {
- const [messageIndex, setMessageIndex] = useState(0);
-
- return (
-
-
-
-
- );
-};
-
-const meta: Meta = {
- title: 'Modules/Auth/SignInUpWorkspaceActivationV2',
- component: SignInUpWorkspaceActivationV2,
- decorators: [ComponentDecorator],
- parameters: {
- codeSection: {
- docs: 'This component should always be wrapped with ModalContent in the app.\n\nCorrect usage:\n```tsx\n\n \n\n```\n',
- },
- },
- render: RenderWithModalContent,
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx
index 112478d26a..efb02e7747 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/LastUsedPill.tsx
@@ -5,15 +5,16 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledPillContainer = styled.span`
position: absolute;
- right: calc(-1 * ${themeCssVariables.spacing[5]});
- top: calc(-1 * ${themeCssVariables.spacing[2]});
+ right: -14px;
+ top: -10px;
> span {
- background: ${themeCssVariables.color.blue3};
- border: 1px solid ${themeCssVariables.color.blue5};
- border-radius: ${themeCssVariables.border.radius.pill};
- color: ${themeCssVariables.color.blue};
+ background: ${themeCssVariables.accent.accent3};
+ border: 1px solid ${themeCssVariables.accent.accent5};
+ border-radius: ${themeCssVariables.border.radius.md};
+ color: ${themeCssVariables.accent.accent9};
font-weight: ${themeCssVariables.font.weight.semiBold};
+ height: ${themeCssVariables.spacing[5]};
}
`;
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx
index 7449b31a46..4ab2e519e4 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification.tsx
@@ -17,13 +17,11 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { OTPInput, type SlotProps } from 'input-otp';
import { useState } from 'react';
import { Controller } from 'react-hook-form';
-import { useLocation } from 'react-router-dom';
import { AppPath } from 'twenty-shared/types';
import { MainButton } from 'twenty-ui/input';
import { ClickToActionLink } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useNavigateApp } from '~/hooks/useNavigateApp';
-import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
@@ -183,7 +181,6 @@ export const SignInUpTOTPVerification = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const navigate = useNavigateApp();
- const location = useLocation();
const { readCaptchaToken } = useReadCaptchaToken();
const { isCaptchaReady } = useCaptcha();
const loginToken = useAtomStateValue(loginTokenState);
@@ -206,11 +203,7 @@ export const SignInUpTOTPVerification = () => {
const captchaToken = readCaptchaToken();
if (!loginToken) {
- return navigate(
- isMatchingLocation(location, AppPath.SignInUpV2)
- ? AppPath.SignInUpV2
- : AppPath.SignInUp,
- );
+ return navigate(AppPath.SignInUp);
}
await getAuthTokensFromOTP(values.otp, loginToken, captchaToken);
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx
index 518fdd919b..ab149988d6 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm.tsx
@@ -1,51 +1,137 @@
-import { SubTitle } from '@/auth/components/SubTitle';
-import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
+import { OnboardingAnimatedReveal } from '@/onboarding/components/OnboardingAnimatedReveal';
+import { OnboardingStepAnimatedItem } from '@/onboarding/components/OnboardingStepAnimatedItem';
+import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { useWorkspaceSubdomainField } from '@/auth/sign-in-up/hooks/useWorkspaceSubdomainField';
-import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State';
+import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
-import { ImageInput } from '@/ui/input/components/ImageInput';
-import { InputHint } from '@/ui/input/components/InputHint';
-import { InputLabel } from '@/ui/input/components/InputLabel';
import { TextInput } from '@/ui/input/components/TextInput';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-import { useLingui } from '@lingui/react/macro';
import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
-import { Loader } from 'twenty-ui/feedback';
-import { MainButton } from 'twenty-ui/input';
-import { ClickToActionLink } from 'twenty-ui/navigation';
+import { Avatar } from 'twenty-ui/data-display';
+import { IconTrash, IconUpload } from 'twenty-ui/icon';
+import { Button, LightIconButton, MainButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-const StyledSection = styled.div`
- margin-top: ${themeCssVariables.spacing[4]};
- width: 100%;
-`;
-
-const StyledButtonContainer = styled.div`
- margin-top: ${themeCssVariables.spacing[6]};
- width: 100%;
-`;
-
-const StyledAvailableHint = styled.div`
- color: ${themeCssVariables.color.green};
- font-size: ${themeCssVariables.font.size.xs};
- font-weight: ${themeCssVariables.font.weight.regular};
- margin-top: ${themeCssVariables.spacing[0.5]};
-`;
-
-const StyledUnavailableHint = styled.div`
- color: ${themeCssVariables.color.red};
+const StyledContentContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[14]};
+ max-width: 100%;
+ width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
+`;
+
+const StyledHeading = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[4]};
+`;
+
+const StyledTitle = styled.div`
+ color: ${themeCssVariables.font.color.primary};
+ font-size: ${themeCssVariables.font.size.xl};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+ line-height: 1.2;
+`;
+
+const StyledSubtitle = styled.div`
+ color: ${themeCssVariables.font.color.secondary};
+ font-size: ${themeCssVariables.font.size.md};
+ line-height: 1.4;
+`;
+
+const StyledFormSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[8]};
+ padding-bottom: ${themeCssVariables.spacing[4]};
+ width: 100%;
+`;
+
+const StyledLogoRow = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+const StyledLogoAvatar = styled(Avatar)`
+ height: ${themeCssVariables.spacing[8]};
+ width: ${themeCssVariables.spacing[8]};
+`;
+
+const StyledLogoButtons = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing['0.5']};
+`;
+
+const StyledHiddenFileInput = styled.input`
+ display: none;
+`;
+
+const StyledSubdomainSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[2]};
+ width: 100%;
+`;
+
+const StyledAlternativesBox = styled.div`
+ background-color: ${themeCssVariables.background.transparent.lighter};
+ border: 1px solid ${themeCssVariables.border.color.medium};
+ border-radius: ${themeCssVariables.border.radius.md};
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[2]};
+ padding: ${themeCssVariables.spacing[3]};
+`;
+
+const StyledAlternativesLabel = styled.span`
+ color: ${themeCssVariables.font.color.secondary};
+ font-size: ${themeCssVariables.font.size.sm};
+ font-weight: ${themeCssVariables.font.weight.medium};
+ padding-bottom: ${themeCssVariables.spacing[1]};
+`;
+
+const StyledAlternativeRows = styled.div`
display: flex;
flex-direction: column;
- font-size: ${themeCssVariables.font.size.xs};
gap: ${themeCssVariables.spacing[1]};
- margin-top: ${themeCssVariables.spacing[0.5]};
+`;
+
+const StyledAlternativeRow = styled.button`
+ align-items: center;
+ background: transparent;
+ border: none;
+ color: ${themeCssVariables.color.green};
+ cursor: pointer;
+ display: flex;
+ font-size: ${themeCssVariables.font.size.xs};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+ gap: ${themeCssVariables.spacing[1]};
+ padding: 2px 0;
+ text-align: left;
+`;
+
+const StyledAvailabilityDotBox = styled.div`
+ display: flex;
+ padding: ${themeCssVariables.spacing[1]};
+`;
+
+const StyledAvailabilityDot = styled.div`
+ background-color: ${themeCssVariables.color.green};
+ border-radius: 50%;
+ box-shadow: 0 0 0 3px ${themeCssVariables.color.green5};
+ flex-shrink: 0;
+ height: 6px;
+ width: 6px;
`;
export const SignInUpWorkspaceCreationForm = () => {
@@ -56,33 +142,37 @@ export const SignInUpWorkspaceCreationForm = () => {
isMultiWorkspaceEnabledState,
);
- const setIsOnboardingV2 = useSetAtomState(isOnboardingV2State);
-
- const [isSubmitting, setIsSubmitting] = useState(false);
+ const isCreatingWorkspace = useAtomStateValue(isCreatingWorkspaceState);
+ const setIsCreatingWorkspace = useSetAtomState(isCreatingWorkspaceState);
const [logo, setLogo] = useState(undefined);
const [logoPreviewUrl, setLogoPreviewUrl] = useState(
undefined,
);
+ const hiddenFileInputRef = useRef(null);
const {
workspaceName,
subdomain,
status,
errorMessage,
- suggestion,
+ suggestions,
isAvailable,
handleWorkspaceNameChange,
handleSubdomainChange,
- applySuggestion,
+ applySuggestionValue,
} = useWorkspaceSubdomainField({
isSubdomainEnabled: isMultiWorkspaceEnabled,
});
const isContinueDisabled =
workspaceName.trim() === '' ||
- isSubmitting ||
+ isCreatingWorkspace ||
(isMultiWorkspaceEnabled && !isAvailable);
+ const openFilePicker = () => {
+ hiddenFileInputRef.current?.click();
+ };
+
const handleLogoUpload = (file: File) => {
if (!isDefined(file)) {
return;
@@ -111,16 +201,16 @@ export const SignInUpWorkspaceCreationForm = () => {
return;
}
- setIsSubmitting(true);
- setIsOnboardingV2(false);
- try {
- await createWorkspace({
- displayName: workspaceName.trim(),
- ...(isMultiWorkspaceEnabled ? { subdomain } : {}),
- logo,
- });
- } finally {
- setIsSubmitting(false);
+ setIsCreatingWorkspace(true);
+
+ const isWorkspaceCreated = await createWorkspace({
+ displayName: workspaceName.trim(),
+ ...(isMultiWorkspaceEnabled ? { subdomain } : {}),
+ logo,
+ });
+
+ if (!isWorkspaceCreated) {
+ setIsCreatingWorkspace(false);
}
};
@@ -144,75 +234,122 @@ export const SignInUpWorkspaceCreationForm = () => {
: undefined;
return (
-
-
- {isMultiWorkspaceEnabled
- ? t`Pick a name and a web address for your new workspace.`
- : t`Pick a name and a logo for your new workspace.`}
-
-
- {t`Workspace logo`}
-
-
-
-
-
- {isMultiWorkspaceEnabled && (
-
+
+
+
+ {t`Create your workspace`}
+
+
+
+ {t`Move work forward across teams and agents`}
+
+
+
+
+
+
+
+ {
+ const file = event.target.files?.[0];
+ if (isDefined(file)) {
+ handleLogoUpload(file);
+ }
+ event.target.value = '';
+ }}
+ />
+
+
+
+
+
+
+
- {status === 'checking' && {t`Checking…`}}
- {status === 'available' && (
-
- {t`This address is available`}
-
- )}
- {status === 'unavailable' && (
-
- {subdomainError}
- {isDefined(suggestion) && (
-
- {t`Use ${suggestion} instead`}
-
- )}
-
- )}
-
- )}
-
+
+ {isMultiWorkspaceEnabled && (
+
+
+
+
+
+
+ {t`Subdomain already in use, here are some alternatives:`}
+
+
+ {suggestions.map((alternative) => (
+ applySuggestionValue(alternative)}
+ >
+
+
+
+ {alternative}
+
+ ))}
+
+
+
+
+
+ )}
+
+
(isSubmitting ? : null)}
fullWidth
/>
-
-
+
+
);
};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationFormV2.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationFormV2.tsx
deleted file mode 100644
index 3c59b0acb6..0000000000
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationFormV2.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-import { StyledOnboardingContentContainer } from '@/auth/components/StyledOnboardingContentContainer';
-import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
-import { useWorkspaceSubdomainField } from '@/auth/sign-in-up/hooks/useWorkspaceSubdomainField';
-import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState';
-import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State';
-import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
-import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
-import { TextInput } from '@/ui/input/components/TextInput';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-import { styled } from '@linaria/react';
-import { useLingui } from '@lingui/react/macro';
-import { isNonEmptyString } from '@sniptt/guards';
-import { useEffect, useRef, useState } from 'react';
-import { Key } from 'ts-key-enum';
-import { isDefined } from 'twenty-shared/utils';
-import { Avatar } from 'twenty-ui/data-display';
-import { IconTrash, IconUpload } from 'twenty-ui/icon';
-import { Button, LightIconButton, MainButton } from 'twenty-ui/input';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-
-const StyledHeading = styled.div`
- margin-bottom: ${themeCssVariables.spacing[6]};
-`;
-
-const StyledTitle = styled.div`
- color: ${themeCssVariables.font.color.primary};
- font-size: ${themeCssVariables.font.size.xl};
- font-weight: ${themeCssVariables.font.weight.semiBold};
-`;
-
-const StyledSubtitle = styled.div`
- color: ${themeCssVariables.font.color.secondary};
- font-size: ${themeCssVariables.font.size.sm};
- margin-top: ${themeCssVariables.spacing[1]};
-`;
-
-const StyledSection = styled.div`
- margin-top: ${themeCssVariables.spacing[4]};
- width: 100%;
-`;
-
-const StyledLogoRow = styled.div`
- align-items: center;
- display: flex;
- gap: ${themeCssVariables.spacing[2]};
-`;
-
-const StyledHiddenFileInput = styled.input`
- display: none;
-`;
-
-const StyledButtonContainer = styled.div`
- margin-top: ${themeCssVariables.spacing[6]};
- width: 100%;
-`;
-
-const StyledAlternativesBox = styled.div`
- border: 1px solid ${themeCssVariables.border.color.medium};
- border-radius: ${themeCssVariables.border.radius.sm};
- display: flex;
- flex-direction: column;
- gap: ${themeCssVariables.spacing[2]};
- margin-top: ${themeCssVariables.spacing[2]};
- padding: ${themeCssVariables.spacing[3]};
-`;
-
-const StyledAlternativesLabel = styled.span`
- color: ${themeCssVariables.font.color.secondary};
- font-size: ${themeCssVariables.font.size.xs};
-`;
-
-const StyledAlternativeRow = styled.button`
- align-items: center;
- background: transparent;
- border: none;
- color: ${themeCssVariables.color.green};
- cursor: pointer;
- display: flex;
- font-size: ${themeCssVariables.font.size.sm};
- gap: ${themeCssVariables.spacing[2]};
- padding: 0;
- text-align: left;
-`;
-
-const StyledAvailabilityDot = styled.div`
- background-color: ${themeCssVariables.color.green};
- border-radius: 50%;
- flex-shrink: 0;
- height: 6px;
- width: 6px;
-`;
-
-export const SignInUpWorkspaceCreationFormV2 = () => {
- const { t } = useLingui();
- const { createWorkspace } = useSignUpInNewWorkspace();
- const { frontDomain } = useAtomStateValue(domainConfigurationState);
- const isMultiWorkspaceEnabled = useAtomStateValue(
- isMultiWorkspaceEnabledState,
- );
-
- const isCreatingWorkspace = useAtomStateValue(isCreatingWorkspaceState);
- const setIsCreatingWorkspace = useSetAtomState(isCreatingWorkspaceState);
- const setIsOnboardingV2 = useSetAtomState(isOnboardingV2State);
- const [logo, setLogo] = useState(undefined);
- const [logoPreviewUrl, setLogoPreviewUrl] = useState(
- undefined,
- );
- const hiddenFileInputRef = useRef(null);
-
- const {
- workspaceName,
- subdomain,
- status,
- errorMessage,
- suggestions,
- isAvailable,
- handleWorkspaceNameChange,
- handleSubdomainChange,
- applySuggestionValue,
- } = useWorkspaceSubdomainField({
- isSubdomainEnabled: isMultiWorkspaceEnabled,
- });
-
- const isContinueDisabled =
- workspaceName.trim() === '' ||
- isCreatingWorkspace ||
- (isMultiWorkspaceEnabled && !isAvailable);
-
- const openFilePicker = () => {
- hiddenFileInputRef.current?.click();
- };
-
- const handleLogoUpload = (file: File) => {
- if (!isDefined(file)) {
- return;
- }
- setLogo(file);
- setLogoPreviewUrl(URL.createObjectURL(file));
- };
-
- const handleLogoRemove = () => {
- setLogo(undefined);
- setLogoPreviewUrl(undefined);
- };
-
- useEffect(() => {
- if (!isDefined(logoPreviewUrl)) {
- return;
- }
-
- return () => {
- URL.revokeObjectURL(logoPreviewUrl);
- };
- }, [logoPreviewUrl]);
-
- const handleSubmit = async () => {
- if (isContinueDisabled) {
- return;
- }
-
- setIsCreatingWorkspace(true);
- setIsOnboardingV2(true);
-
- const isWorkspaceCreated = await createWorkspace({
- displayName: workspaceName.trim(),
- ...(isMultiWorkspaceEnabled ? { subdomain } : {}),
- logo,
- });
-
- if (!isWorkspaceCreated) {
- setIsCreatingWorkspace(false);
- }
- };
-
- const handleKeyDown = (event: React.KeyboardEvent) => {
- if (event.nativeEvent.isComposing || event.keyCode === 229) {
- return;
- }
- if (event.key === Key.Enter) {
- event.preventDefault();
- handleSubmit();
- }
- };
-
- const subdomainError =
- status === 'invalid'
- ? errorMessage
- : status === 'unavailable'
- ? t`This address is already taken`
- : status === 'error'
- ? t`Couldn't check availability. Please try again.`
- : undefined;
-
- return (
-
-
- {t`Create your workspace`}
-
- {t`Move work forward across teams and agents`}
-
-
-
-
-
- {
- const file = event.target.files?.[0];
- if (isDefined(file)) {
- handleLogoUpload(file);
- }
- event.target.value = '';
- }}
- />
-
-
-
-
-
-
-
- {isMultiWorkspaceEnabled && (
-
-
- {status === 'unavailable' && (
-
-
- {t`Subdomain already in use, here are some alternatives:`}
-
- {suggestions.map((alternative) => (
- applySuggestionValue(alternative)}
- >
-
- {alternative}
-
- ))}
-
- )}
-
- )}
-
-
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationForm.test.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationForm.test.tsx
index 0eb1272c4f..bcb02ce1c6 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationForm.test.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationForm.test.tsx
@@ -6,6 +6,7 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { ThemeProvider } from 'twenty-ui/theme-constants';
import { SignInUpWorkspaceCreationForm } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm';
+import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import {
jotaiStore,
@@ -14,7 +15,7 @@ import {
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
const createWorkspaceMock = jest.fn();
-const applySuggestionMock = jest.fn();
+const applySuggestionValueMock = jest.fn();
const handleSubdomainChangeMock = jest.fn();
const handleWorkspaceNameChangeMock = jest.fn();
const useWorkspaceSubdomainFieldMock = jest.fn();
@@ -56,11 +57,11 @@ describe('SignInUpWorkspaceCreationForm', () => {
subdomain: 'apple',
status: 'available',
errorMessage: undefined,
- suggestion: undefined,
+ suggestions: [],
isAvailable: true,
handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
handleSubdomainChange: handleSubdomainChangeMock,
- applySuggestion: applySuggestionMock,
+ applySuggestionValue: applySuggestionValueMock,
});
});
@@ -69,35 +70,18 @@ describe('SignInUpWorkspaceCreationForm', () => {
setMultiWorkspaceEnabled(true);
});
- it('keeps Continue disabled until a workspace name is entered', () => {
- useWorkspaceSubdomainFieldMock.mockReturnValue({
- workspaceName: '',
- subdomain: '',
- status: 'idle',
- errorMessage: undefined,
- suggestion: undefined,
- isAvailable: false,
- handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
- handleSubdomainChange: handleSubdomainChangeMock,
- applySuggestion: applySuggestionMock,
+ it('creates the workspace with the chosen name and subdomain', async () => {
+ createWorkspaceMock.mockResolvedValue(true);
+
+ renderForm();
+
+ const createButton = screen.getByRole('button', {
+ name: 'Create workspace',
});
-
- renderForm();
-
- expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
- });
-
- it('creates the workspace with the chosen name and address in the same tab', async () => {
- createWorkspaceMock.mockResolvedValue(undefined);
-
- renderForm();
-
- const continueButton = screen.getByRole('button', { name: 'Continue' });
-
- expect(continueButton).toBeEnabled();
+ expect(createButton).toBeEnabled();
await act(async () => {
- fireEvent.click(continueButton);
+ fireEvent.click(createButton);
});
expect(createWorkspaceMock).toHaveBeenCalledWith({
@@ -107,61 +91,73 @@ describe('SignInUpWorkspaceCreationForm', () => {
});
});
- it('passes the picked logo file when creating the workspace', async () => {
- createWorkspaceMock.mockResolvedValue(undefined);
+ it('keeps the loader on through a successful creation, until the redirect', async () => {
+ let resolveCreateWorkspace: () => void = () => {};
+ createWorkspaceMock.mockReturnValue(
+ new Promise((resolve) => {
+ resolveCreateWorkspace = () => resolve(true);
+ }),
+ );
- const { container } = renderForm();
-
- const fileInput = container.querySelector(
- 'input[type="file"]',
- ) as HTMLInputElement;
- const logoFile = new File(['logo'], 'logo.png', { type: 'image/png' });
-
- await act(async () => {
- fireEvent.change(fileInput, { target: { files: [logoFile] } });
- });
-
- await act(async () => {
- fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
- });
-
- expect(createWorkspaceMock).toHaveBeenCalledWith({
- displayName: 'Apple',
- subdomain: 'apple',
- logo: logoFile,
- });
- });
-
- it('routes name edits back through the field hook', () => {
renderForm();
- fireEvent.change(screen.getByLabelText('Workspace name'), {
- target: { value: 'Acme' },
+ await act(async () => {
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Create workspace' }),
+ );
});
- expect(handleWorkspaceNameChangeMock).toHaveBeenCalledWith('Acme');
+ expect(jotaiStore.get(isCreatingWorkspaceState.atom)).toBe(true);
+ expect(createWorkspaceMock).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveCreateWorkspace();
+ });
+
+ expect(jotaiStore.get(isCreatingWorkspaceState.atom)).toBe(true);
});
- it('offers a one-click suggestion when the address is taken', () => {
+ it('returns to the form when workspace creation fails', async () => {
+ createWorkspaceMock.mockResolvedValue(false);
+
+ renderForm();
+
+ await act(async () => {
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Create workspace' }),
+ );
+ });
+
+ expect(jotaiStore.get(isCreatingWorkspaceState.atom)).toBe(false);
+ });
+
+ it('lists available alternatives and applies the picked one when the subdomain is taken', () => {
useWorkspaceSubdomainFieldMock.mockReturnValue({
- workspaceName: 'Apple',
- subdomain: 'apple',
+ workspaceName: 'Stripe',
+ subdomain: 'stripe',
status: 'unavailable',
errorMessage: undefined,
- suggestion: 'apple-2',
+ suggestions: ['stripe-2', 'mystripe', 'stripeeinc'],
isAvailable: false,
handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
handleSubdomainChange: handleSubdomainChangeMock,
- applySuggestion: applySuggestionMock,
+ applySuggestionValue: applySuggestionValueMock,
});
renderForm();
- expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled();
+ expect(
+ screen.getByText(
+ 'Subdomain already in use, here are some alternatives:',
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'Create workspace' }),
+ ).toBeDisabled();
- fireEvent.click(screen.getByText('Use apple-2 instead'));
+ fireEvent.click(screen.getByRole('button', { name: 'mystripe' }));
- expect(applySuggestionMock).toHaveBeenCalledTimes(1);
+ expect(applySuggestionValueMock).toHaveBeenCalledWith('mystripe');
});
});
@@ -170,40 +166,18 @@ describe('SignInUpWorkspaceCreationForm', () => {
setMultiWorkspaceEnabled(false);
});
- it('hides the workspace address field', () => {
- renderForm();
-
- expect(screen.getByLabelText('Workspace name')).toBeInTheDocument();
- expect(
- screen.queryByLabelText('Workspace address'),
- ).not.toBeInTheDocument();
- });
-
- it('enables Continue based on the name only and creates without a subdomain', async () => {
- createWorkspaceMock.mockResolvedValue(undefined);
-
- // An unavailable subdomain status must not block submission when the
- // address field is hidden.
- useWorkspaceSubdomainFieldMock.mockReturnValue({
- workspaceName: 'Apple',
- subdomain: 'apple',
- status: 'unavailable',
- errorMessage: undefined,
- suggestion: 'apple-2',
- isAvailable: false,
- handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
- handleSubdomainChange: handleSubdomainChangeMock,
- applySuggestion: applySuggestionMock,
- });
+ it('hides the subdomain field and creates without a subdomain', async () => {
+ createWorkspaceMock.mockResolvedValue(true);
renderForm();
- const continueButton = screen.getByRole('button', { name: 'Continue' });
-
- expect(continueButton).toBeEnabled();
+ expect(screen.getByLabelText('Name')).toBeInTheDocument();
+ expect(screen.queryByLabelText('Subdomain')).not.toBeInTheDocument();
await act(async () => {
- fireEvent.click(continueButton);
+ fireEvent.click(
+ screen.getByRole('button', { name: 'Create workspace' }),
+ );
});
expect(createWorkspaceMock).toHaveBeenCalledWith({
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationFormV2.test.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationFormV2.test.tsx
deleted file mode 100644
index 8f4a4111dc..0000000000
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/__tests__/SignInUpWorkspaceCreationFormV2.test.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-import { i18n } from '@lingui/core';
-import { I18nProvider } from '@lingui/react';
-import { act, fireEvent, render, screen } from '@testing-library/react';
-import { Provider as JotaiProvider } from 'jotai';
-import { SOURCE_LOCALE } from 'twenty-shared/translations';
-import { ThemeProvider } from 'twenty-ui/theme-constants';
-
-import { SignInUpWorkspaceCreationFormV2 } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationFormV2';
-import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState';
-import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
-import {
- jotaiStore,
- resetJotaiStore,
-} from '@/ui/utilities/state/jotai/jotaiStore';
-import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
-
-const createWorkspaceMock = jest.fn();
-const applySuggestionValueMock = jest.fn();
-const handleSubdomainChangeMock = jest.fn();
-const handleWorkspaceNameChangeMock = jest.fn();
-const useWorkspaceSubdomainFieldMock = jest.fn();
-
-jest.mock('@/auth/sign-in-up/hooks/useSignUpInNewWorkspace', () => ({
- useSignUpInNewWorkspace: () => ({ createWorkspace: createWorkspaceMock }),
-}));
-
-jest.mock('@/auth/sign-in-up/hooks/useWorkspaceSubdomainField', () => ({
- useWorkspaceSubdomainField: () => useWorkspaceSubdomainFieldMock(),
-}));
-
-global.URL.createObjectURL = jest.fn(() => 'blob:logo-preview');
-global.URL.revokeObjectURL = jest.fn();
-
-dynamicActivate(SOURCE_LOCALE);
-
-const setMultiWorkspaceEnabled = (isEnabled: boolean) => {
- jotaiStore.set(isMultiWorkspaceEnabledState.atom, isEnabled);
-};
-
-const renderForm = () =>
- render(
-
-
-
-
-
-
- ,
- );
-
-describe('SignInUpWorkspaceCreationFormV2', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- resetJotaiStore();
- useWorkspaceSubdomainFieldMock.mockReturnValue({
- workspaceName: 'Apple',
- subdomain: 'apple',
- status: 'available',
- errorMessage: undefined,
- suggestions: [],
- isAvailable: true,
- handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
- handleSubdomainChange: handleSubdomainChangeMock,
- applySuggestionValue: applySuggestionValueMock,
- });
- });
-
- describe('multi-workspace', () => {
- beforeEach(() => {
- setMultiWorkspaceEnabled(true);
- });
-
- it('creates the workspace with the chosen name and subdomain', async () => {
- createWorkspaceMock.mockResolvedValue(true);
-
- renderForm();
-
- const createButton = screen.getByRole('button', {
- name: 'Create workspace',
- });
- expect(createButton).toBeEnabled();
-
- await act(async () => {
- fireEvent.click(createButton);
- });
-
- expect(createWorkspaceMock).toHaveBeenCalledWith({
- displayName: 'Apple',
- subdomain: 'apple',
- logo: undefined,
- });
- });
-
- it('keeps the loader on through a successful creation, until the redirect', async () => {
- let resolveCreateWorkspace: () => void = () => {};
- createWorkspaceMock.mockReturnValue(
- new Promise((resolve) => {
- resolveCreateWorkspace = () => resolve(true);
- }),
- );
-
- renderForm();
-
- await act(async () => {
- fireEvent.click(
- screen.getByRole('button', { name: 'Create workspace' }),
- );
- });
-
- expect(jotaiStore.get(isCreatingWorkspaceState.atom)).toBe(true);
- expect(createWorkspaceMock).toHaveBeenCalledTimes(1);
-
- await act(async () => {
- resolveCreateWorkspace();
- });
-
- expect(jotaiStore.get(isCreatingWorkspaceState.atom)).toBe(true);
- });
-
- it('returns to the form when workspace creation fails', async () => {
- createWorkspaceMock.mockResolvedValue(false);
-
- renderForm();
-
- await act(async () => {
- fireEvent.click(
- screen.getByRole('button', { name: 'Create workspace' }),
- );
- });
-
- expect(jotaiStore.get(isCreatingWorkspaceState.atom)).toBe(false);
- });
-
- it('lists available alternatives and applies the picked one when the subdomain is taken', () => {
- useWorkspaceSubdomainFieldMock.mockReturnValue({
- workspaceName: 'Stripe',
- subdomain: 'stripe',
- status: 'unavailable',
- errorMessage: undefined,
- suggestions: ['stripe-2', 'mystripe', 'stripeeinc'],
- isAvailable: false,
- handleWorkspaceNameChange: handleWorkspaceNameChangeMock,
- handleSubdomainChange: handleSubdomainChangeMock,
- applySuggestionValue: applySuggestionValueMock,
- });
-
- renderForm();
-
- expect(
- screen.getByText(
- 'Subdomain already in use, here are some alternatives:',
- ),
- ).toBeInTheDocument();
- expect(
- screen.getByRole('button', { name: 'Create workspace' }),
- ).toBeDisabled();
-
- fireEvent.click(screen.getByRole('button', { name: 'mystripe' }));
-
- expect(applySuggestionValueMock).toHaveBeenCalledWith('mystripe');
- });
- });
-
- describe('single-workspace', () => {
- beforeEach(() => {
- setMultiWorkspaceEnabled(false);
- });
-
- it('hides the subdomain field and creates without a subdomain', async () => {
- createWorkspaceMock.mockResolvedValue(true);
-
- renderForm();
-
- expect(screen.getByLabelText('Name')).toBeInTheDocument();
- expect(screen.queryByLabelText('Subdomain')).not.toBeInTheDocument();
-
- await act(async () => {
- fireEvent.click(
- screen.getByRole('button', { name: 'Create workspace' }),
- );
- });
-
- expect(createWorkspaceMock).toHaveBeenCalledWith({
- displayName: 'Apple',
- logo: undefined,
- });
- expect(createWorkspaceMock.mock.calls[0][0]).not.toHaveProperty(
- 'subdomain',
- );
- });
- });
-});
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx
index b371525d05..a7afcf6461 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useWorkspaceSubdomainField.test.tsx
@@ -140,7 +140,6 @@ describe('useWorkspaceSubdomainField', () => {
'taken-3',
'taken-4',
]);
- expect(result.current.suggestion).toBe('taken-2');
expect(result.current.isAvailable).toBe(false);
});
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts
index 237f636903..6fedcafcde 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts
@@ -1,5 +1,4 @@
import { useAuth } from '@/auth/hooks/useAuth';
-import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -14,7 +13,6 @@ import {
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { useLingui } from '@lingui/react/macro';
-import { useStore } from 'jotai';
export const useSignUpInNewWorkspace = () => {
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
@@ -22,7 +20,6 @@ export const useSignUpInNewWorkspace = () => {
const isMultiWorkspaceEnabled = useAtomStateValue(
isMultiWorkspaceEnabledState,
);
- const store = useStore();
const { enqueueErrorSnackBar } = useSnackBar();
const { t } = useLingui();
@@ -76,11 +73,9 @@ export const useSignUpInNewWorkspace = () => {
return true;
}
- const isOnboardingV2 = store.get(isOnboardingV2State.atom);
-
await redirectToWorkspaceDomain(
getWorkspaceUrl(data.signUpInNewWorkspace.workspace.workspaceUrls),
- isOnboardingV2 ? AppPath.VerifyV2 : AppPath.Verify,
+ AppPath.Verify,
{ loginToken },
'_self',
);
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts
index 3b388a8c3a..e7b5bec08d 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceSubdomainField.ts
@@ -166,27 +166,15 @@ export const useWorkspaceSubdomainField = ({
debouncedAvailabilityCheck(value, { adoptSuggestion: false });
};
- const suggestion: string | undefined = suggestions[0];
-
- const applySuggestion = () => {
- if (!isDefined(suggestion)) {
- return;
- }
-
- applySuggestionValue(suggestion);
- };
-
return {
workspaceName,
subdomain,
status,
errorMessage,
- suggestion,
suggestions,
isAvailable: status === 'available',
handleWorkspaceNameChange,
handleSubdomainChange,
- applySuggestion,
applySuggestionValue,
};
};
diff --git a/packages/twenty-front/src/modules/auth/states/__tests__/isOnboardingV2State.test.ts b/packages/twenty-front/src/modules/auth/states/__tests__/isOnboardingV2State.test.ts
deleted file mode 100644
index 0201b574b8..0000000000
--- a/packages/twenty-front/src/modules/auth/states/__tests__/isOnboardingV2State.test.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { createStore } from 'jotai';
-
-import { type isOnboardingV2State as IsOnboardingV2State } from '@/auth/states/isOnboardingV2State';
-
-const loadAtom = (): typeof IsOnboardingV2State => {
- let state: typeof IsOnboardingV2State | undefined;
-
- jest.isolateModules(() => {
- state = require('@/auth/states/isOnboardingV2State').isOnboardingV2State;
- });
-
- if (state === undefined) {
- throw new Error('Failed to load isOnboardingV2State');
- }
-
- return state;
-};
-
-describe('isOnboardingV2State', () => {
- afterEach(() => {
- sessionStorage.clear();
- jest.resetModules();
- });
-
- it('hydrates from sessionStorage on load so the flag survives the email-connect OAuth round-trip', () => {
- sessionStorage.setItem('isOnboardingV2State', JSON.stringify(true));
-
- const isOnboardingV2State = loadAtom();
-
- expect(createStore().get(isOnboardingV2State.atom)).toBe(true);
- });
-
- it('writes through to sessionStorage when set', () => {
- const isOnboardingV2State = loadAtom();
- const store = createStore();
-
- store.set(isOnboardingV2State.atom, true);
-
- expect(sessionStorage.getItem('isOnboardingV2State')).toBe(
- JSON.stringify(true),
- );
- });
-
- it('defaults to false when nothing is persisted', () => {
- const isOnboardingV2State = loadAtom();
-
- expect(createStore().get(isOnboardingV2State.atom)).toBe(false);
- });
-});
diff --git a/packages/twenty-front/src/modules/auth/states/isOnboardingV2State.ts b/packages/twenty-front/src/modules/auth/states/isOnboardingV2State.ts
deleted file mode 100644
index dd35a5437c..0000000000
--- a/packages/twenty-front/src/modules/auth/states/isOnboardingV2State.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
-
-export const isOnboardingV2State = createAtomState({
- key: 'isOnboardingV2State',
- defaultValue: false,
- useSessionStorage: true,
-});
diff --git a/packages/twenty-front/src/modules/auth/utils/getAuthModalConfig.ts b/packages/twenty-front/src/modules/auth/utils/getAuthModalConfig.ts
deleted file mode 100644
index 50f3b60287..0000000000
--- a/packages/twenty-front/src/modules/auth/utils/getAuthModalConfig.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AUTH_MODAL_CONFIG } from '@/auth/constants/AuthModalConfig';
-import { type Location } from 'react-router-dom';
-import { AppPath } from 'twenty-shared/types';
-import { isDefined } from 'twenty-shared/utils';
-import { isMatchingLocation } from '~/utils/isMatchingLocation';
-
-export const getAuthModalConfig = (location: Location) => {
- for (const path of Object.values(AppPath)) {
- if (
- isMatchingLocation(location, path) &&
- isDefined(AUTH_MODAL_CONFIG[path])
- ) {
- return AUTH_MODAL_CONFIG[path];
- }
- }
-
- return AUTH_MODAL_CONFIG.default;
-};
diff --git a/packages/twenty-front/src/modules/auth/utils/isOnOnboardingTransitionPath.ts b/packages/twenty-front/src/modules/auth/utils/isOnOnboardingTransitionPath.ts
new file mode 100644
index 0000000000..3a3a5fa6b4
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/utils/isOnOnboardingTransitionPath.ts
@@ -0,0 +1,9 @@
+import { matchPath } from 'react-router-dom';
+import { isDefined } from 'twenty-shared/utils';
+
+import { ONBOARDING_TRANSITION_PATHS } from '@/auth/constants/OnboardingTransitionPaths';
+
+export const isOnOnboardingTransitionPath = (pathname: string) =>
+ ONBOARDING_TRANSITION_PATHS.some((onboardingPath) =>
+ isDefined(matchPath(onboardingPath, pathname)),
+ );
diff --git a/packages/twenty-front/src/modules/captcha/components/RequestFreshCaptchaTokenEffect.tsx b/packages/twenty-front/src/modules/captcha/components/RequestFreshCaptchaTokenEffect.tsx
new file mode 100644
index 0000000000..73635ad34c
--- /dev/null
+++ b/packages/twenty-front/src/modules/captcha/components/RequestFreshCaptchaTokenEffect.tsx
@@ -0,0 +1,21 @@
+import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+
+import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
+import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
+import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+
+export const RequestFreshCaptchaTokenEffect = () => {
+ const location = useLocation();
+ const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
+ const isCaptchaScriptLoaded = useAtomStateValue(isCaptchaScriptLoadedState);
+
+ useEffect(() => {
+ if (isCaptchaScriptLoaded && isCaptchaRequiredForPath(location.pathname)) {
+ requestFreshCaptchaToken();
+ }
+ }, [isCaptchaScriptLoaded, location.pathname, requestFreshCaptchaToken]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/captcha/constants/CaptchaProtectedPaths.ts b/packages/twenty-front/src/modules/captcha/constants/CaptchaProtectedPaths.ts
index b71a6d6a0d..a48182f145 100644
--- a/packages/twenty-front/src/modules/captcha/constants/CaptchaProtectedPaths.ts
+++ b/packages/twenty-front/src/modules/captcha/constants/CaptchaProtectedPaths.ts
@@ -2,9 +2,7 @@ import { AppPath } from 'twenty-shared/types';
export const CAPTCHA_PROTECTED_PATHS: string[] = [
AppPath.SignInUp,
- AppPath.SignInUpV2,
AppPath.Verify,
- AppPath.VerifyV2,
AppPath.VerifyEmail,
AppPath.ResetPassword,
AppPath.Invite,
diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts
index 9564aed9a6..44a83f4da6 100644
--- a/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts
+++ b/packages/twenty-front/src/modules/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates.ts
@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
-import { isOnboardingV2State } from '@/auth/states/isOnboardingV2State';
import { returnToPathState } from '@/auth/states/returnToPathState';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import { isNonEmptyString } from '@sniptt/guards';
@@ -12,7 +11,6 @@ export const useBuildSearchParamsFromUrlSyncedStates = () => {
const buildSearchParamsFromUrlSyncedStates = useCallback(async () => {
const billingCheckoutSession = store.get(billingCheckoutSessionState.atom);
const returnToPath = store.get(returnToPathState.atom);
- const isOnboardingV2 = store.get(isOnboardingV2State.atom);
const output = {
...(billingCheckoutSession !== BILLING_CHECKOUT_SESSION_DEFAULT_VALUE
@@ -21,7 +19,6 @@ export const useBuildSearchParamsFromUrlSyncedStates = () => {
}
: {}),
...(isNonEmptyString(returnToPath) ? { returnToPath } : {}),
- ...(isOnboardingV2 ? { onboardingV2: 'true' } : {}),
};
return output;
diff --git a/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGate.tsx b/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGate.tsx
new file mode 100644
index 0000000000..1ecde38589
--- /dev/null
+++ b/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGate.tsx
@@ -0,0 +1,20 @@
+import { Outlet } from 'react-router-dom';
+
+import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
+import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
+
+export const MinimalMetadataGate = () => {
+ const isMinimalMetadataReady = useAtomStateValue(isMinimalMetadataReadyState);
+
+ if (!isMinimalMetadataReady) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx b/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx
deleted file mode 100644
index 705ab42a25..0000000000
--- a/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-
-import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
-import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { UserContext } from '@/users/contexts/UserContext';
-import { useLocation } from 'react-router-dom';
-import { AppPath } from 'twenty-shared/types';
-import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
-import { isMatchingLocation } from '~/utils/isMatchingLocation';
-
-export const MinimalMetadataGater = ({ children }: React.PropsWithChildren) => {
- const isMinimalMetadataReady = useAtomStateValue(isMinimalMetadataReadyState);
- const location = useLocation();
-
- const { dateFormat, timeFormat, timeZone } = useDateTimeFormat();
-
- const isOnExcludedPath =
- isMatchingLocation(location, AppPath.Verify) ||
- isMatchingLocation(location, AppPath.VerifyV2) ||
- isMatchingLocation(location, AppPath.VerifyEmail) ||
- isMatchingLocation(location, AppPath.SignInUp) ||
- isMatchingLocation(location, AppPath.SignInUpV2) ||
- isMatchingLocation(location, AppPath.Invite) ||
- isMatchingLocation(location, AppPath.ResetPassword) ||
- isMatchingLocation(location, AppPath.WorkspaceActivation) ||
- isMatchingLocation(location, AppPath.WorkspaceActivationV2) ||
- isMatchingLocation(location, AppPath.PlanRequired) ||
- isMatchingLocation(location, AppPath.PlanRequiredSuccess) ||
- isMatchingLocation(location, AppPath.Authorize);
-
- const shouldShowLoader = !isMinimalMetadataReady && !isOnExcludedPath;
-
- if (shouldShowLoader) {
- return ;
- }
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx
index 2f11cd147c..04b7d188ea 100644
--- a/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx
+++ b/packages/twenty-front/src/modules/metadata-store/effect-components/MinimalMetadataLoadEffect.tsx
@@ -1,4 +1,5 @@
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
+import { useIsOnAuthOrOnboardingPage } from '@/auth/hooks/useIsOnAuthOrOnboardingPage';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
@@ -21,8 +22,11 @@ export const MinimalMetadataLoadEffect = () => {
const { loadMinimalMetadata } = useLoadMinimalMetadata();
const { loadStaleMetadataEntities } = useLoadStaleMetadataEntities();
+ const isOnAuthOrOnboardingPage = useIsOnAuthOrOnboardingPage();
+
const isActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
- const shouldLoadRealMetadata = hasAccessTokenPair && isActiveWorkspace;
+ const shouldLoadRealMetadata =
+ hasAccessTokenPair && isActiveWorkspace && !isOnAuthOrOnboardingPage;
useEffect(() => {
if (!isCurrentUserLoaded && !isDefined(currentUser)) {
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationOutlet.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationOutlet.tsx
new file mode 100644
index 0000000000..79af43ed4d
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationOutlet.tsx
@@ -0,0 +1,23 @@
+import { Outlet } from 'react-router-dom';
+
+import { OnboardingActivationStepsProgress } from '@/onboarding/components/OnboardingActivationStepsProgress';
+import { OnboardingVerifyLayout } from '@/onboarding/components/OnboardingVerifyLayout';
+import { onboardingActivationFailedState } from '@/onboarding/states/onboardingActivationFailedState';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+
+export const OnboardingActivationOutlet = () => {
+ const onboardingActivationFailed = useAtomStateValue(
+ onboardingActivationFailedState,
+ );
+
+ return (
+ <>
+ {!onboardingActivationFailed && (
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationSteps.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationSteps.tsx
new file mode 100644
index 0000000000..ff9cd75ddf
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationSteps.tsx
@@ -0,0 +1,62 @@
+import { SubTitle } from '@/auth/components/SubTitle';
+import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
+import { styled } from '@linaria/react';
+import { type MessageDescriptor } from '@lingui/core';
+import { useLingui } from '@lingui/react/macro';
+import { motion } from 'framer-motion';
+
+const STEP_OPACITIES = [1, 0.4, 0.12];
+const VISIBLE_STEP_COUNT = STEP_OPACITIES.length;
+const STEP_HEIGHT_IN_PX = 28;
+const STEPS_CONTAINER_HEIGHT_IN_PX = STEP_HEIGHT_IN_PX * VISIBLE_STEP_COUNT;
+
+const StyledStepsContainer = styled.div`
+ height: ${STEPS_CONTAINER_HEIGHT_IN_PX}px;
+ position: relative;
+ width: 100%;
+`;
+
+const StyledStepBase = styled.div`
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+`;
+
+const StyledStep = motion.create(StyledStepBase);
+
+type OnboardingActivationStepsProps = {
+ messages: MessageDescriptor[];
+ messageIndex: number;
+};
+
+export const OnboardingActivationSteps = ({
+ messages,
+ messageIndex,
+}: OnboardingActivationStepsProps) => {
+ const { i18n } = useLingui();
+ const transition = useOnboardingMotionTransition();
+
+ return (
+
+ {messages.map((message, index) => {
+ const stepOffset = index - messageIndex;
+ const isVisible = stepOffset >= 0 && stepOffset < VISIBLE_STEP_COUNT;
+
+ return (
+
+ {i18n._(message)}
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationStepsEffect.tsx
similarity index 55%
rename from packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect.tsx
rename to packages/twenty-front/src/modules/onboarding/components/OnboardingActivationStepsEffect.tsx
index aba5a9f0de..33f3b07e13 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpWorkspaceActivationV2Effect.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationStepsEffect.tsx
@@ -1,20 +1,20 @@
-import { WORKSPACE_ACTIVATION_MESSAGES } from '@/auth/sign-in-up/constants/WorkspaceActivationMessages';
import { type Dispatch, type SetStateAction, useEffect } from 'react';
const MESSAGE_INTERVAL_IN_MS = 1000;
-type SignInUpWorkspaceActivationV2EffectProps = {
+type OnboardingActivationStepsEffectProps = {
messageIndex: number;
setMessageIndex: Dispatch>;
+ messageCount: number;
};
-export const SignInUpWorkspaceActivationV2Effect = ({
+export const OnboardingActivationStepsEffect = ({
messageIndex,
setMessageIndex,
-}: SignInUpWorkspaceActivationV2EffectProps) => {
+ messageCount,
+}: OnboardingActivationStepsEffectProps) => {
useEffect(() => {
- const isLastMessage =
- messageIndex >= WORKSPACE_ACTIVATION_MESSAGES.length - 1;
+ const isLastMessage = messageIndex >= messageCount - 1;
if (isLastMessage) {
return;
@@ -25,7 +25,7 @@ export const SignInUpWorkspaceActivationV2Effect = ({
}, MESSAGE_INTERVAL_IN_MS);
return () => clearTimeout(timeout);
- }, [messageIndex, setMessageIndex]);
+ }, [messageIndex, setMessageIndex, messageCount]);
return <>>;
};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationStepsProgress.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationStepsProgress.tsx
new file mode 100644
index 0000000000..65f7407e5a
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingActivationStepsProgress.tsx
@@ -0,0 +1,57 @@
+import { useEffect, useState } from 'react';
+import { useMatch } from 'react-router-dom';
+
+import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState';
+import { OnboardingActivationSteps } from '@/onboarding/components/OnboardingActivationSteps';
+import { OnboardingActivationStepsEffect } from '@/onboarding/components/OnboardingActivationStepsEffect';
+import { ONBOARDING_ACTIVATION_MESSAGES } from '@/onboarding/constants/OnboardingActivationMessages';
+import { WORKSPACE_ACTIVATION_FIRST_MESSAGE_INDEX } from '@/onboarding/constants/WorkspaceActivationFirstMessageIndex';
+import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { AppPath } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+import { OnboardingStatus } from '~/generated-metadata/graphql';
+
+export const OnboardingActivationStepsProgress = () => {
+ const isActivating = isDefined(useMatch(AppPath.WorkspaceActivation));
+ const isCreatingWorkspace = useAtomStateValue(isCreatingWorkspaceState);
+ const onboardingStatus = useOnboardingStatus();
+
+ const shouldShowWorkspaceActivationMessages =
+ isActivating ||
+ isCreatingWorkspace ||
+ onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION;
+
+ const messages = shouldShowWorkspaceActivationMessages
+ ? ONBOARDING_ACTIVATION_MESSAGES
+ : ONBOARDING_ACTIVATION_MESSAGES.slice(
+ 0,
+ WORKSPACE_ACTIVATION_FIRST_MESSAGE_INDEX,
+ );
+
+ const [messageIndex, setMessageIndex] = useState(
+ isActivating ? WORKSPACE_ACTIVATION_FIRST_MESSAGE_INDEX : 0,
+ );
+
+ useEffect(() => {
+ setMessageIndex(
+ isActivating ? WORKSPACE_ACTIVATION_FIRST_MESSAGE_INDEX : 0,
+ );
+ }, [isActivating]);
+
+ return (
+ <>
+ {isActivating && (
+
+ )}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingAnimatedReveal.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingAnimatedReveal.tsx
new file mode 100644
index 0000000000..b0909dbc42
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingAnimatedReveal.tsx
@@ -0,0 +1,41 @@
+import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
+import { styled } from '@linaria/react';
+import { AnimatePresence, motion } from 'framer-motion';
+import { type ReactNode } from 'react';
+
+const StyledAnimatedRevealBase = styled.div`
+ max-width: 100%;
+ overflow: hidden;
+`;
+
+const StyledAnimatedReveal = motion.create(StyledAnimatedRevealBase);
+
+type OnboardingAnimatedRevealProps = {
+ isVisible: boolean;
+ children: ReactNode;
+ className?: string;
+};
+
+export const OnboardingAnimatedReveal = ({
+ isVisible,
+ children,
+ className,
+}: OnboardingAnimatedRevealProps) => {
+ const transition = useOnboardingMotionTransition();
+
+ return (
+
+ {isVisible && (
+
+ {children}
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Header.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingHeader.tsx
similarity index 78%
rename from packages/twenty-front/src/modules/onboarding/components/OnboardingV2Header.tsx
rename to packages/twenty-front/src/modules/onboarding/components/OnboardingHeader.tsx
index 321926c8a2..ee2d2d218a 100644
--- a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Header.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingHeader.tsx
@@ -1,23 +1,25 @@
+import { useOnboardingContentWidth } from '@/onboarding/hooks/useOnboardingContentWidth';
+import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
import { styled } from '@linaria/react';
+import { motion } from 'framer-motion';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, IconCoins, IconInfoCircle } from 'twenty-ui/icon';
import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants';
-const HEADER_CENTER_WIDTH = 340;
-
const StyledHeader = styled.div`
- align-items: center;
+ align-items: flex-start;
box-sizing: border-box;
display: flex;
justify-content: space-between;
- padding: ${themeCssVariables.spacing[8]};
+ padding: ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]} 1px;
width: 100%;
`;
const StyledSide = styled.div`
align-items: center;
+ box-sizing: border-box;
display: flex;
flex: 1 1 0;
min-width: 0;
@@ -28,19 +30,20 @@ const StyledLeftSide = styled(StyledSide)`
padding-right: ${themeCssVariables.spacing[1]};
`;
-const StyledCenter = styled.div`
+const StyledCenter = styled.div<{ contentWidth: number }>`
align-items: center;
display: flex;
- flex: 0 1 ${HEADER_CENTER_WIDTH}px;
+ flex: 0 1 ${({ contentWidth }) => `${contentWidth}px`};
justify-content: flex-start;
min-width: 0;
`;
const StyledRightSide = styled(StyledSide)`
justify-content: flex-end;
+ padding-left: ${themeCssVariables.spacing[1]};
`;
-const StyledLogo = styled.div`
+const StyledLogoBase = styled.div`
background-image: url('/images/integrations/twenty-logo.svg');
background-size: cover;
height: ${themeCssVariables.spacing[6]};
@@ -48,6 +51,8 @@ const StyledLogo = styled.div`
width: ${themeCssVariables.spacing[6]};
`;
+const StyledLogo = motion.create(StyledLogoBase);
+
const StyledFreeCredits = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
@@ -89,20 +94,23 @@ const StyledInfoTag = styled.div`
display: flex;
height: ${themeCssVariables.spacing[6]};
justify-content: center;
- padding: 0 ${themeCssVariables.spacing['1.5']};
+ padding: 0 ${themeCssVariables.spacing['1.5']} 0
+ ${themeCssVariables.spacing[1]};
`;
-type OnboardingV2HeaderProps = {
+type OnboardingHeaderProps = {
onBack?: () => void;
freeCredits?: number;
};
-export const OnboardingV2Header = ({
+export const OnboardingHeader = ({
onBack,
freeCredits,
-}: OnboardingV2HeaderProps) => {
+}: OnboardingHeaderProps) => {
const { t } = useLingui();
const theme = useTheme();
+ const contentWidth = useOnboardingContentWidth();
+ const transition = useOnboardingMotionTransition();
return (
@@ -111,14 +119,14 @@ export const OnboardingV2Header = ({
)}
-
-
+
+
{isDefined(freeCredits) && (
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Layout.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingLayout.tsx
similarity index 65%
rename from packages/twenty-front/src/modules/onboarding/components/OnboardingV2Layout.tsx
rename to packages/twenty-front/src/modules/onboarding/components/OnboardingLayout.tsx
index b51b6f5f23..757c7316e7 100644
--- a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Layout.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingLayout.tsx
@@ -1,4 +1,4 @@
-import { OnboardingV2Header } from '@/onboarding/components/OnboardingV2Header';
+import { OnboardingHeader } from '@/onboarding/components/OnboardingHeader';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -11,19 +11,19 @@ const StyledBackground = styled.div`
width: 100%;
`;
-type OnboardingV2LayoutProps = {
+type OnboardingLayoutProps = {
children: ReactNode;
onBack?: () => void;
freeCredits?: number;
};
-export const OnboardingV2Layout = ({
+export const OnboardingLayout = ({
children,
onBack,
freeCredits,
-}: OnboardingV2LayoutProps) => (
+}: OnboardingLayoutProps) => (
-
+
{children}
);
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingPageLoader.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingPageLoader.tsx
index 41a924b845..66d694f6b7 100644
--- a/packages/twenty-front/src/modules/onboarding/components/OnboardingPageLoader.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingPageLoader.tsx
@@ -4,7 +4,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
align-items: center;
- background: ${themeCssVariables.background.primary};
+ background: ${themeCssVariables.background.secondary};
display: flex;
flex-direction: column;
height: 100dvh;
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingSkipButton.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingSkipButton.tsx
new file mode 100644
index 0000000000..04a728b284
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingSkipButton.tsx
@@ -0,0 +1,34 @@
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledSkipButton = styled.button`
+ background-color: transparent;
+ border: 1px solid ${themeCssVariables.border.color.light};
+ border-radius: ${themeCssVariables.border.radius.md};
+ color: ${themeCssVariables.font.color.tertiary};
+ cursor: pointer;
+ font-family: ${themeCssVariables.font.family};
+ font-size: ${themeCssVariables.font.size.md};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+ height: ${themeCssVariables.spacing[8]};
+ padding: 0 ${themeCssVariables.spacing[5]};
+`;
+
+type OnboardingSkipButtonProps = {
+ onClick: () => void;
+ disabled?: boolean;
+};
+
+export const OnboardingSkipButton = ({
+ onClick,
+ disabled,
+}: OnboardingSkipButtonProps) => {
+ const { t } = useLingui();
+
+ return (
+
+ {t`Skip`}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingStepAnimatedItem.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingStepAnimatedItem.tsx
new file mode 100644
index 0000000000..47094cf175
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingStepAnimatedItem.tsx
@@ -0,0 +1,44 @@
+import { styled } from '@linaria/react';
+import { motion, useReducedMotion } from 'framer-motion';
+import { type ReactNode } from 'react';
+import { ONBOARDING_MOTION_SLIDE_OFFSET } from '@/onboarding/constants/OnboardingMotionSlideOffset';
+import { ONBOARDING_MOTION_STAGGER_DELAY } from '@/onboarding/constants/OnboardingMotionStaggerDelay';
+import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
+
+const StyledAnimatedItemBase = styled.div`
+ max-width: 100%;
+`;
+
+const StyledAnimatedItem = motion.create(StyledAnimatedItemBase);
+
+type OnboardingStepAnimatedItemProps = {
+ index: number;
+ children: ReactNode;
+ className?: string;
+};
+
+export const OnboardingStepAnimatedItem = ({
+ index,
+ children,
+ className,
+}: OnboardingStepAnimatedItemProps) => {
+ const transition = useOnboardingMotionTransition();
+ const shouldReduceMotion = useReducedMotion();
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingStepLayout.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingStepLayout.tsx
new file mode 100644
index 0000000000..1e18008ef6
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingStepLayout.tsx
@@ -0,0 +1,13 @@
+import { OnboardingLayout } from '@/onboarding/components/OnboardingLayout';
+import { OnboardingTransitionOutlet } from '@/onboarding/components/OnboardingTransitionOutlet';
+import { useOnboardingFreeCreditsTotal } from '@/onboarding/hooks/useOnboardingFreeCreditsTotal';
+
+export const OnboardingStepLayout = () => {
+ const freeCredits = useOnboardingFreeCreditsTotal();
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingStepPageLoader.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingStepPageLoader.tsx
new file mode 100644
index 0000000000..ada09ba9c4
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingStepPageLoader.tsx
@@ -0,0 +1,17 @@
+import { OnboardingPulsingLogo } from '@/onboarding/components/OnboardingPulsingLogo';
+import { styled } from '@linaria/react';
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ min-height: 0;
+ width: 100%;
+`;
+
+export const OnboardingStepPageLoader = () => (
+
+
+
+);
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingSyncEmailsSettingsCard.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingSyncEmailsSettingsCard.tsx
deleted file mode 100644
index b124404da8..0000000000
--- a/packages/twenty-front/src/modules/onboarding/components/OnboardingSyncEmailsSettingsCard.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { ONBOARDING_SYNC_EMAILS_OPTIONS } from '@/onboarding/constants/OnboardingSyncEmailsOptions';
-import { SettingsAccountsRadioSettingsCard } from '@/settings/accounts/components/SettingsAccountsRadioSettingsCard';
-import { SettingsAccountsVisibilityIcon } from '@/settings/accounts/components/SettingsAccountsVisibilityIcon';
-import { styled } from '@linaria/react';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-import { MessageChannelVisibility } from '~/generated/graphql';
-
-type OnboardingSyncEmailsSettingsCardProps = {
- onChange: (nextValue: MessageChannelVisibility) => void;
- value?: MessageChannelVisibility;
-};
-
-const StyledCardMediaContainer = styled.div`
- width: ${themeCssVariables.spacing[10]};
-`;
-
-export const OnboardingSyncEmailsSettingsCard = ({
- onChange,
- value = MessageChannelVisibility.SHARE_EVERYTHING,
-}: OnboardingSyncEmailsSettingsCardProps) => {
- const optionsWithCardMedia = ONBOARDING_SYNC_EMAILS_OPTIONS.map((option) => ({
- ...option,
- cardMedia: (
-
-
-
- ),
- }));
-
- return (
-
- );
-};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2TransitionOutlet.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingTransitionOutlet.tsx
similarity index 61%
rename from packages/twenty-front/src/modules/onboarding/components/OnboardingV2TransitionOutlet.tsx
rename to packages/twenty-front/src/modules/onboarding/components/OnboardingTransitionOutlet.tsx
index 1fc36f265f..b88b75043a 100644
--- a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2TransitionOutlet.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingTransitionOutlet.tsx
@@ -1,8 +1,8 @@
+import { ONBOARDING_MOTION_SLIDE_OFFSET } from '@/onboarding/constants/OnboardingMotionSlideOffset';
+import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
import { styled } from '@linaria/react';
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
-import { useContext } from 'react';
import { useLocation, useOutlet } from 'react-router-dom';
-import { ThemeContext } from 'twenty-ui/theme-constants';
const StyledTransitionContainer = styled.div`
display: flex;
@@ -14,38 +14,32 @@ const StyledTransitionContainer = styled.div`
const StyledTransitionPage = styled(motion.div)`
display: flex;
+ flex-direction: column;
inset: 0;
min-height: 0;
min-width: 0;
position: absolute;
`;
-export const OnboardingV2TransitionOutlet = () => {
+export const OnboardingTransitionOutlet = () => {
const { pathname } = useLocation();
const outlet = useOutlet();
const shouldReduceMotion = useReducedMotion();
- const { theme } = useContext(ThemeContext);
+ const transition = useOnboardingMotionTransition();
return (
{outlet}
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingVerifyLayout.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingVerifyLayout.tsx
new file mode 100644
index 0000000000..abf1734ba1
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingVerifyLayout.tsx
@@ -0,0 +1,27 @@
+import { OnboardingPulsingLogo } from '@/onboarding/components/OnboardingPulsingLogo';
+import { styled } from '@linaria/react';
+import { type ReactNode } from 'react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledContainer = styled.div`
+ align-items: center;
+ background: ${themeCssVariables.background.secondary};
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ width: 100%;
+`;
+
+type OnboardingVerifyLayoutProps = {
+ children: ReactNode;
+};
+
+export const OnboardingVerifyLayout = ({
+ children,
+}: OnboardingVerifyLayoutProps) => (
+
+
+ {children}
+
+);
diff --git a/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepHeading.ts b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepHeading.ts
new file mode 100644
index 0000000000..1ae4c2faed
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepHeading.ts
@@ -0,0 +1,11 @@
+import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
+import { styled } from '@linaria/react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+export const StyledOnboardingStepHeading = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[4]};
+ max-width: 100%;
+ width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
+`;
diff --git a/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepPage.ts b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepPage.ts
new file mode 100644
index 0000000000..32be70d5b8
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepPage.ts
@@ -0,0 +1,16 @@
+import { styled } from '@linaria/react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+export const StyledOnboardingStepPage = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.secondary};
+ box-sizing: border-box;
+ display: flex;
+ flex: 1 1 0;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[14]};
+ min-height: 0;
+ overflow-y: auto;
+ padding: ${themeCssVariables.spacing[16]} ${themeCssVariables.spacing[8]};
+ width: 100%;
+`;
diff --git a/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepSubtitle.ts b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepSubtitle.ts
new file mode 100644
index 0000000000..f44b79e12c
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepSubtitle.ts
@@ -0,0 +1,9 @@
+import { styled } from '@linaria/react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+export const StyledOnboardingStepSubtitle = styled.p`
+ color: ${themeCssVariables.font.color.secondary};
+ font-size: ${themeCssVariables.font.size.md};
+ line-height: 1.4;
+ margin: 0;
+`;
diff --git a/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTagsRow.ts b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTagsRow.ts
new file mode 100644
index 0000000000..c2df9129ef
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTagsRow.ts
@@ -0,0 +1,9 @@
+import { styled } from '@linaria/react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+export const StyledOnboardingStepTagsRow = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: ${themeCssVariables.spacing[1]};
+ padding-top: ${themeCssVariables.spacing[1]};
+`;
diff --git a/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTitle.ts b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTitle.ts
new file mode 100644
index 0000000000..3af5bab0cf
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/StyledOnboardingStepTitle.ts
@@ -0,0 +1,10 @@
+import { styled } from '@linaria/react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+export const StyledOnboardingStepTitle = styled.h1`
+ color: ${themeCssVariables.font.color.primary};
+ font-size: ${themeCssVariables.font.size.xl};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+ line-height: 1.2;
+ margin: 0;
+`;
diff --git a/packages/twenty-front/src/modules/onboarding/components/__stories__/OnboardingActivationSteps.stories.tsx b/packages/twenty-front/src/modules/onboarding/components/__stories__/OnboardingActivationSteps.stories.tsx
new file mode 100644
index 0000000000..f6732b597b
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/__stories__/OnboardingActivationSteps.stories.tsx
@@ -0,0 +1,38 @@
+import { type Meta, type StoryObj } from '@storybook/react-vite';
+
+import { OnboardingActivationSteps } from '@/onboarding/components/OnboardingActivationSteps';
+import { OnboardingActivationStepsEffect } from '@/onboarding/components/OnboardingActivationStepsEffect';
+import { ONBOARDING_ACTIVATION_MESSAGES } from '@/onboarding/constants/OnboardingActivationMessages';
+import { useState } from 'react';
+import { ModalContent } from 'twenty-ui/surfaces';
+import { ComponentDecorator } from 'twenty-ui/testing';
+
+const RenderWithModalContent = () => {
+ const [messageIndex, setMessageIndex] = useState(0);
+
+ return (
+
+
+
+
+ );
+};
+
+const meta: Meta = {
+ title: 'Modules/Onboarding/OnboardingActivationSteps',
+ component: OnboardingActivationSteps,
+ decorators: [ComponentDecorator],
+ render: RenderWithModalContent,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingCreditsRewardTag.tsx b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingCreditsRewardTag.tsx
index f322eb3836..b13462e38d 100644
--- a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingCreditsRewardTag.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingCreditsRewardTag.tsx
@@ -7,7 +7,7 @@ const StyledTag = styled.div`
align-items: center;
background-color: ${themeCssVariables.color.green3};
border: 1px solid ${themeCssVariables.color.green4};
- border-radius: ${themeCssVariables.border.radius.pill};
+ border-radius: ${themeCssVariables.border.radius.xxl};
box-sizing: border-box;
color: ${themeCssVariables.color.green9};
display: flex;
@@ -20,11 +20,13 @@ const StyledTag = styled.div`
const StyledLabel = styled.span`
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
+ line-height: 1.4;
`;
const StyledSuffix = styled.span`
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.regular};
+ line-height: 1.4;
`;
type OnboardingCreditsRewardTagProps = {
diff --git a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreview.tsx b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreview.tsx
index c77aed6bca..87b7c7e8d3 100644
--- a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreview.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreview.tsx
@@ -2,24 +2,25 @@ import { OnboardingImportPreviewCompanies } from '@/onboarding/components/import
import { OnboardingImportPreviewEmails } from '@/onboarding/components/import-contacts/OnboardingImportPreviewEmails';
import { OnboardingImportPreviewSyncBadge } from '@/onboarding/components/import-contacts/OnboardingImportPreviewSyncBadge';
import { OnboardingImportPrivacyNote } from '@/onboarding/components/import-contacts/OnboardingImportPrivacyNote';
+import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-const PREVIEW_WIDTH = 340;
-const PREVIEW_HEIGHT = 200;
+const PREVIEW_HEIGHT = 198;
const StyledCard = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
- border-radius: ${themeCssVariables.border.radius.xl};
+ border-radius: 12px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[3]};
+ max-width: 100%;
overflow: hidden;
padding-bottom: ${themeCssVariables.spacing[3]};
- width: ${PREVIEW_WIDTH}px;
+ width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
`;
const StyledColumns = styled.div`
diff --git a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewEmails.tsx b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewEmails.tsx
index 780b5d3577..d75ab85afd 100644
--- a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewEmails.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewEmails.tsx
@@ -37,7 +37,7 @@ const StyledEmailRow = styled.div<{ isUnread: boolean }>`
: themeCssVariables.font.color.tertiary};
display: flex;
flex-shrink: 0;
- font-size: ${themeCssVariables.font.size.xs};
+ font-size: ${themeCssVariables.font.size.sm};
gap: ${themeCssVariables.spacing[2]};
height: ${EMAIL_ROW_HEIGHT}px;
padding: 0 ${themeCssVariables.spacing[3]};
@@ -46,7 +46,7 @@ const StyledEmailRow = styled.div<{ isUnread: boolean }>`
const StyledEmailCheckbox = styled.div`
border: 1px solid ${themeCssVariables.font.color.light};
- border-radius: ${themeCssVariables.border.radius.xs};
+ border-radius: 1px;
box-sizing: border-box;
flex-shrink: 0;
height: ${EMAIL_CHECKBOX_SIZE}px;
@@ -67,29 +67,30 @@ const StyledEmailSender = styled.span<{ isUnread: boolean }>`
`;
const StyledEmailSubject = styled.span`
- color: ${themeCssVariables.font.color.tertiary};
+ color: ${themeCssVariables.font.color.secondary};
`;
-const StyledEventCard = styled.div<{ color: 'yellow' | 'blue' }>`
+const StyledEventCard = styled.div<{ color: 'orange' | 'sky' }>`
background-color: ${({ color }) =>
- color === 'yellow'
- ? themeCssVariables.color.yellow3
- : themeCssVariables.color.blue3};
+ color === 'orange'
+ ? themeCssVariables.color.orange3
+ : themeCssVariables.color.sky3};
border-left: 2px solid
${({ color }) =>
- color === 'yellow'
- ? themeCssVariables.color.yellow8
- : themeCssVariables.color.blue8};
- border-radius: ${themeCssVariables.border.radius.sm};
- box-shadow: ${themeCssVariables.boxShadow.light};
+ color === 'orange'
+ ? themeCssVariables.color.orange11
+ : themeCssVariables.color.sky11};
+ border-radius: ${themeCssVariables.border.radius.md};
+ box-shadow: ${themeCssVariables.boxShadow.strong};
box-sizing: border-box;
color: ${({ color }) =>
- color === 'yellow'
- ? themeCssVariables.color.yellow11
- : themeCssVariables.color.blue11};
+ color === 'orange'
+ ? themeCssVariables.color.orange11
+ : themeCssVariables.color.sky11};
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[1]};
+ line-height: 1.1;
padding: ${themeCssVariables.spacing[2]};
position: absolute;
width: 160px;
@@ -101,15 +102,15 @@ const StyledEventTitle = styled.span`
`;
const StyledEventTime = styled.span`
- font-size: ${themeCssVariables.font.size.xs};
+ font-size: 10px;
`;
const EVENT_CARD_POSITIONS: Record<
ImportContactsPreviewCalendarEvent['color'],
{ top: number; left: number; rotate: number }
> = {
- blue: { top: -6, left: 41, rotate: 10 },
- yellow: { top: 150, left: 20, rotate: -7 },
+ orange: { top: 160, left: 22, rotate: -7 },
+ sky: { top: -4, left: 44, rotate: 10 },
};
const EmailRow = ({ email }: { email: ImportContactsPreviewEmail }) => (
diff --git a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewSyncBadge.tsx b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewSyncBadge.tsx
index 25a434f261..f151f8cae0 100644
--- a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewSyncBadge.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewSyncBadge.tsx
@@ -9,8 +9,8 @@ const StyledBadge = styled.div`
backdrop-filter: blur(20px);
background-color: ${themeCssVariables.background.transparent.secondary};
border: 1px solid ${themeCssVariables.background.transparent.lighter};
- border-radius: 0 ${themeCssVariables.border.radius.md}
- ${themeCssVariables.border.radius.md} 0;
+ border-left: none;
+ border-radius: ${themeCssVariables.border.radius.md};
box-shadow: ${themeCssVariables.boxShadow.strong};
box-sizing: border-box;
display: flex;
@@ -18,7 +18,7 @@ const StyledBadge = styled.div`
left: 50%;
padding: ${themeCssVariables.spacing[2]};
position: absolute;
- top: 84px;
+ top: 83px;
transform: translateX(-50%);
`;
@@ -29,6 +29,7 @@ const StyledDivider = styled.div`
`;
const StyledTwentyLogo = styled.img`
+ border-radius: ${themeCssVariables.border.radius.xs};
height: ${SYNC_BADGE_LOGO_SIZE}px;
width: ${SYNC_BADGE_LOGO_SIZE}px;
`;
diff --git a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPrivacyNote.tsx b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPrivacyNote.tsx
index cdaf05080c..3b56fe0f56 100644
--- a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPrivacyNote.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPrivacyNote.tsx
@@ -13,6 +13,10 @@ const StyledNote = styled.div`
gap: ${themeCssVariables.spacing[1]};
`;
+const StyledNoteText = styled.span`
+ line-height: 1.4;
+`;
+
export const OnboardingImportPrivacyNote = () => {
const { t } = useLingui();
@@ -22,7 +26,9 @@ export const OnboardingImportPrivacyNote = () => {
size={PRIVACY_NOTE_ICON_SIZE}
color={themeCssVariables.font.color.tertiary}
/>
- {t`Only you will be able to see your emails and events`}
+
+ {t`Only you will be able to see your emails and events`}
+
);
};
diff --git a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingTrustBadges.tsx b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingTrustBadges.tsx
index e4d58eb1a1..cd229345d1 100644
--- a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingTrustBadges.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingTrustBadges.tsx
@@ -16,11 +16,13 @@ const TRUSTED_BY_LOGOS = [
const StyledRow = styled.div`
align-items: center;
display: flex;
+ flex-wrap: wrap;
gap: ${themeCssVariables.spacing[2]};
justify-content: center;
+ max-width: 100%;
`;
-const StyledBadge = styled.div`
+const StyledBadge = styled.div<{ hasClusterLeading: boolean }>`
align-items: center;
background-color: ${themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.medium};
@@ -32,13 +34,18 @@ const StyledBadge = styled.div`
font-weight: ${themeCssVariables.font.weight.semiBold};
gap: ${themeCssVariables.spacing[1]};
height: ${themeCssVariables.spacing[7]};
- padding: 0 ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[1]};
+ overflow: hidden;
+ padding: 0 10px 0
+ ${({ hasClusterLeading }) =>
+ hasClusterLeading
+ ? themeCssVariables.spacing['0.5']
+ : themeCssVariables.spacing[1]};
`;
const StyledSeal = styled.img`
- height: 20px;
+ height: 21px;
object-fit: contain;
- width: 20px;
+ width: 21px;
`;
const StyledLogoCluster = styled.div`
@@ -48,9 +55,9 @@ const StyledLogoCluster = styled.div`
`;
const StyledClusterLogo = styled.img`
- height: 18px;
+ height: ${themeCssVariables.spacing[6]};
object-fit: contain;
- width: auto;
+ width: ${themeCssVariables.spacing[6]};
`;
const StyledBadgeLabel = styled.span`
@@ -58,12 +65,17 @@ const StyledBadgeLabel = styled.span`
`;
type TrustBadgeProps = {
+ hasClusterLeading?: boolean;
label: string;
leading: ReactNode;
};
-const TrustBadge = ({ label, leading }: TrustBadgeProps) => (
-
+const TrustBadge = ({
+ hasClusterLeading = false,
+ label,
+ leading,
+}: TrustBadgeProps) => (
+
{leading}
{label}
@@ -77,6 +89,7 @@ export const OnboardingTrustBadges = () => (
/>
{TRUSTED_BY_LOGOS.map((logo) => (
diff --git a/packages/twenty-front/src/modules/onboarding/components/upgrade-free-trial/OnboardingPlanCard.tsx b/packages/twenty-front/src/modules/onboarding/components/upgrade-free-trial/OnboardingPlanCard.tsx
index 42f2f5fceb..ba2c943f93 100644
--- a/packages/twenty-front/src/modules/onboarding/components/upgrade-free-trial/OnboardingPlanCard.tsx
+++ b/packages/twenty-front/src/modules/onboarding/components/upgrade-free-trial/OnboardingPlanCard.tsx
@@ -15,27 +15,32 @@ const StyledCard = styled.div`
width: 100%;
`;
-const StyledHeader = styled.button<{ hasBody: boolean }>`
- align-items: flex-start;
+const StyledHeader = styled.button<{ hasBody: boolean; hasNote: boolean }>`
+ align-items: center;
background-color: transparent;
border: none;
border-bottom: ${({ hasBody }) =>
hasBody ? `1px solid ${themeCssVariables.border.color.light}` : 'none'};
cursor: pointer;
display: flex;
- gap: ${themeCssVariables.spacing[2]};
- justify-content: space-between;
- padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]};
+ gap: ${themeCssVariables.spacing[1]};
+ padding: ${({ hasNote }) =>
+ hasNote
+ ? `${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]}`
+ : themeCssVariables.spacing[3]};
+ position: relative;
text-align: left;
width: 100%;
`;
-const StyledHeaderLeft = styled.div`
+const StyledHeaderLeft = styled.div<{ hasNote: boolean }>`
display: flex;
flex: 1 1 0;
flex-direction: column;
- gap: ${themeCssVariables.spacing[2]};
+ gap: ${themeCssVariables.spacing[4]};
min-width: 0;
+ padding-right: ${({ hasNote }) =>
+ hasNote ? themeCssVariables.spacing[8] : '0'};
`;
const StyledTitleRow = styled.div`
@@ -48,35 +53,61 @@ const StyledTitle = styled.span`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
+ line-height: 1.4;
`;
-const StyledTitleSuffix = styled.span`
- color: ${themeCssVariables.font.color.tertiary};
- font-size: ${themeCssVariables.font.size.sm};
- font-weight: ${themeCssVariables.font.weight.regular};
+const StyledTitleSuffix = styled.span<{ isEmphasized: boolean }>`
+ color: ${({ isEmphasized }) =>
+ isEmphasized
+ ? themeCssVariables.font.color.tertiary
+ : themeCssVariables.font.color.extraLight};
+ font-size: ${({ isEmphasized }) =>
+ isEmphasized
+ ? themeCssVariables.font.size.md
+ : themeCssVariables.font.size.sm};
+ font-weight: ${({ isEmphasized }) =>
+ isEmphasized
+ ? themeCssVariables.font.weight.medium
+ : themeCssVariables.font.weight.regular};
+ line-height: 1.4;
`;
const StyledNote = styled.span`
color: ${themeCssVariables.font.color.light};
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.regular};
+ line-height: 1.4;
`;
const StyledHeaderRight = styled.div`
align-items: center;
display: flex;
- gap: ${themeCssVariables.spacing[2]};
+ gap: ${themeCssVariables.spacing[1]};
+ min-height: ${themeCssVariables.spacing[6]};
`;
const StyledBadge = styled.span`
align-items: center;
- background-color: ${themeCssVariables.background.tertiary};
+ background-color: ${themeCssVariables.grayScale.gray3};
border-radius: ${themeCssVariables.border.radius.pill};
+ box-sizing: border-box;
color: ${themeCssVariables.font.color.tertiary};
display: flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
- padding: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
+ height: ${themeCssVariables.spacing[5]};
+ padding: 0 ${themeCssVariables.spacing[2]};
+`;
+
+const StyledRadioContainer = styled.div`
+ align-items: center;
+ display: flex;
+ height: ${themeCssVariables.spacing[6]};
+ justify-content: center;
+ position: absolute;
+ right: ${themeCssVariables.spacing[2]};
+ top: ${themeCssVariables.spacing[2]};
+ width: ${themeCssVariables.spacing[6]};
`;
const StyledBody = styled.div`
@@ -105,23 +136,37 @@ export const OnboardingPlanCard = ({
children,
}: OnboardingPlanCardProps) => {
const hasBody = isValidElement(children);
+ const hasNote = isDefined(note);
return (
-
-
+
+
{title}
{isDefined(titleSuffix) && (
- {titleSuffix}
+
+ {titleSuffix}
+
)}
- {isDefined(note) && {note}}
+ {hasNote && {note}}
-
- {isDefined(badge) && {badge}}
-
-
+ {hasNote ? (
+
+
+
+ ) : (
+
+ {isDefined(badge) && {badge}}
+
+
+ )}
{hasBody && {children}}
diff --git a/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts
index daec8ac036..717cf98eea 100644
--- a/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts
+++ b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts
@@ -2,7 +2,7 @@ export type ImportContactsPreviewCalendarEvent = {
id: string;
title: string;
time: string;
- color: 'yellow' | 'blue';
+ color: 'orange' | 'sky';
};
export const IMPORT_CONTACTS_PREVIEW_CALENDAR_EVENTS = [
@@ -10,12 +10,12 @@ export const IMPORT_CONTACTS_PREVIEW_CALENDAR_EVENTS = [
id: 'tim-apple-anthropic',
title: 'Tim Apple x Anthropic',
time: '10:00am',
- color: 'yellow',
+ color: 'orange',
},
{
id: 'dario-amodei',
title: 'Dario Amodei',
time: '3:00pm',
- color: 'blue',
+ color: 'sky',
},
] satisfies ImportContactsPreviewCalendarEvent[];
diff --git a/packages/twenty-front/src/modules/onboarding/constants/OnboardingActivationMessages.ts b/packages/twenty-front/src/modules/onboarding/constants/OnboardingActivationMessages.ts
new file mode 100644
index 0000000000..33e01c23eb
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/OnboardingActivationMessages.ts
@@ -0,0 +1,7 @@
+import { WORKSPACE_ACTIVATION_MESSAGES } from '@/auth/sign-in-up/constants/WorkspaceActivationMessages';
+import { msg } from '@lingui/core/macro';
+
+export const ONBOARDING_ACTIVATION_MESSAGES = [
+ msg`Verifying your login token`,
+ ...WORKSPACE_ACTIVATION_MESSAGES,
+];
diff --git a/packages/twenty-front/src/modules/onboarding/constants/OnboardingContentBlockWidth.ts b/packages/twenty-front/src/modules/onboarding/constants/OnboardingContentBlockWidth.ts
new file mode 100644
index 0000000000..76b4cdb197
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/OnboardingContentBlockWidth.ts
@@ -0,0 +1 @@
+export const ONBOARDING_CONTENT_BLOCK_WIDTH = 340;
diff --git a/packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionSlideOffset.ts b/packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionSlideOffset.ts
new file mode 100644
index 0000000000..0e3a158ea3
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionSlideOffset.ts
@@ -0,0 +1 @@
+export const ONBOARDING_MOTION_SLIDE_OFFSET = 12;
diff --git a/packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionStaggerDelay.ts b/packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionStaggerDelay.ts
new file mode 100644
index 0000000000..c14462606c
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/OnboardingMotionStaggerDelay.ts
@@ -0,0 +1 @@
+export const ONBOARDING_MOTION_STAGGER_DELAY = 0.07;
diff --git a/packages/twenty-front/src/modules/onboarding/constants/OnboardingSyncEmailsOptions.ts b/packages/twenty-front/src/modules/onboarding/constants/OnboardingSyncEmailsOptions.ts
deleted file mode 100644
index bd77d80f10..0000000000
--- a/packages/twenty-front/src/modules/onboarding/constants/OnboardingSyncEmailsOptions.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { msg } from '@lingui/core/macro';
-
-import { MessageChannelVisibility } from '~/generated/graphql';
-
-type OnboardingEmailVisibilityProps = {
- metadata: 'active' | 'inactive';
- subject: 'active' | 'inactive';
- body: 'active' | 'inactive';
-};
-
-const { ONBOARDING_SYNC_EMAILS_OPTIONS } = {
- ONBOARDING_SYNC_EMAILS_OPTIONS: [
- {
- title: msg`Everything`,
- description: msg`Your emails and events content will be shared with your team.`,
- value: MessageChannelVisibility.SHARE_EVERYTHING,
- cardMediaProps: {
- metadata: 'active',
- subject: 'active',
- body: 'active',
- } as OnboardingEmailVisibilityProps,
- },
- {
- title: msg`Subject and metadata`,
- description: msg`Your email subjects and meeting titles will be shared with your team.`,
- value: MessageChannelVisibility.SUBJECT,
- cardMediaProps: {
- metadata: 'active',
- subject: 'active',
- body: 'inactive',
- } as OnboardingEmailVisibilityProps,
- },
- {
- title: msg`Metadata`,
- description: msg`Only the timestamp & participants will be shared with your team.`,
- value: MessageChannelVisibility.METADATA,
- cardMediaProps: {
- metadata: 'active',
- subject: 'inactive',
- body: 'inactive',
- } as OnboardingEmailVisibilityProps,
- },
- ],
-};
-
-export { ONBOARDING_SYNC_EMAILS_OPTIONS };
diff --git a/packages/twenty-front/src/modules/onboarding/constants/UpgradeStepContentWidth.ts b/packages/twenty-front/src/modules/onboarding/constants/UpgradeStepContentWidth.ts
new file mode 100644
index 0000000000..aab60fbd2b
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/UpgradeStepContentWidth.ts
@@ -0,0 +1 @@
+export const UPGRADE_STEP_CONTENT_WIDTH = 440;
diff --git a/packages/twenty-front/src/modules/onboarding/constants/WorkspaceActivationFirstMessageIndex.ts b/packages/twenty-front/src/modules/onboarding/constants/WorkspaceActivationFirstMessageIndex.ts
new file mode 100644
index 0000000000..ff145fe10e
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/WorkspaceActivationFirstMessageIndex.ts
@@ -0,0 +1,5 @@
+import { WORKSPACE_ACTIVATION_MESSAGES } from '@/auth/sign-in-up/constants/WorkspaceActivationMessages';
+import { ONBOARDING_ACTIVATION_MESSAGES } from '@/onboarding/constants/OnboardingActivationMessages';
+
+export const WORKSPACE_ACTIVATION_FIRST_MESSAGE_INDEX =
+ ONBOARDING_ACTIVATION_MESSAGES.length - WORKSPACE_ACTIVATION_MESSAGES.length;
diff --git a/packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx b/packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx
deleted file mode 100644
index 3ba89fa758..0000000000
--- a/packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useTriggerInstallAppsOnboardingStep } from '@/onboarding/hooks/useTriggerInstallAppsOnboardingStep';
-import { useEffect, useRef } from 'react';
-
-type InstallAppsAutoSkipEffectProps = {
- onError: () => void;
-};
-
-export const InstallAppsAutoSkipEffect = ({
- onError,
-}: InstallAppsAutoSkipEffectProps) => {
- const triggerInstallAppsOnboardingStep =
- useTriggerInstallAppsOnboardingStep();
-
- // oxlint-disable-next-line twenty/no-state-useref
- const hasSkippedRef = useRef(false);
-
- useEffect(() => {
- if (hasSkippedRef.current) {
- return;
- }
- hasSkippedRef.current = true;
-
- const skip = async () => {
- try {
- await triggerInstallAppsOnboardingStep([]);
- } catch {
- onError();
- }
- };
-
- void skip();
- }, [triggerInstallAppsOnboardingStep, onError]);
-
- return null;
-};
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts b/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts
index eb733449b9..d711ca6322 100644
--- a/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useInviteTeam.ts
@@ -1,5 +1,3 @@
-import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
-import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { onboardingFreeCreditsState } from '@/onboarding/states/onboardingFreeCreditsState';
@@ -17,7 +15,6 @@ import { type SubmitHandler, useFieldArray, useForm } from 'react-hook-form';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { GetInviteSuggestionsDocument } from '~/generated-metadata/graphql';
-import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
import { z } from 'zod';
const validationSchema = z.object({
@@ -28,15 +25,11 @@ type InviteTeamFormInput = z.infer;
export const useInviteTeam = () => {
const { t } = useLingui();
- const { copyToClipboard } = useCopyToClipboard();
const { enqueueSuccessSnackBar } = useSnackBar();
const { sendInvitation } = useCreateWorkspaceInvitation();
const setNextOnboardingStatus = useSetNextOnboardingStatus();
const setOnboardingFreeCredits = useSetAtomState(onboardingFreeCreditsState);
const onboardingConfig = useAtomStateValue(onboardingConfigState);
- const currentWorkspace = useAtomStateValue(currentWorkspaceState);
- const calendarBookingPageId = useAtomStateValue(calendarBookingPageIdState);
- const hasCalendarBooking = isDefined(calendarBookingPageId);
const {
control,
@@ -125,13 +118,6 @@ export const useInviteTeam = () => {
return 'craig@apple.com';
};
- const copyInviteLink = () => {
- if (isDefined(currentWorkspace?.inviteHash)) {
- const inviteLink = `${window.location.origin}/invite/${currentWorkspace?.inviteHash}`;
- copyToClipboard(inviteLink, t`Link copied to clipboard`);
- }
- };
-
const onSubmit: SubmitHandler = useCallback(
async (data) => {
const emails = Array.from(
@@ -197,12 +183,8 @@ export const useInviteTeam = () => {
handleSubmit,
onSubmit,
handleSkip,
- copyInviteLink,
getPlaceholder,
- hasPrefilledSuggestions,
- hasCalendarBooking,
isValid,
isSubmitting,
- currentWorkspace,
};
};
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingContentWidth.ts b/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingContentWidth.ts
new file mode 100644
index 0000000000..93142f018b
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingContentWidth.ts
@@ -0,0 +1,13 @@
+import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
+import { UPGRADE_STEP_CONTENT_WIDTH } from '@/onboarding/constants/UpgradeStepContentWidth';
+import { useLocation } from 'react-router-dom';
+import { AppPath } from 'twenty-shared/types';
+import { isMatchingLocation } from '~/utils/isMatchingLocation';
+
+export const useOnboardingContentWidth = () => {
+ const location = useLocation();
+
+ return isMatchingLocation(location, AppPath.PlanRequired)
+ ? UPGRADE_STEP_CONTENT_WIDTH
+ : ONBOARDING_CONTENT_BLOCK_WIDTH;
+};
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingMotionTransition.ts b/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingMotionTransition.ts
new file mode 100644
index 0000000000..5f68611fbf
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useOnboardingMotionTransition.ts
@@ -0,0 +1,11 @@
+import { type Transition, useReducedMotion } from 'framer-motion';
+import { useTheme } from 'twenty-ui/theme-constants';
+
+export const useOnboardingMotionTransition = (): Transition => {
+ const theme = useTheme();
+ const shouldReduceMotion = useReducedMotion();
+
+ return shouldReduceMotion
+ ? { duration: 0 }
+ : { duration: theme.animation.duration.normal, ease: 'easeInOut' };
+};
diff --git a/packages/twenty-front/src/modules/onboarding/states/onboardingActivationFailedState.ts b/packages/twenty-front/src/modules/onboarding/states/onboardingActivationFailedState.ts
new file mode 100644
index 0000000000..4fc9f8bf2a
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/states/onboardingActivationFailedState.ts
@@ -0,0 +1,6 @@
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export const onboardingActivationFailedState = createAtomState({
+ key: 'onboardingActivationFailedState',
+ defaultValue: false,
+});
diff --git a/packages/twenty-front/src/modules/settings/billing/components/TrialCard.tsx b/packages/twenty-front/src/modules/settings/billing/components/TrialCard.tsx
deleted file mode 100644
index e0374a484c..0000000000
--- a/packages/twenty-front/src/modules/settings/billing/components/TrialCard.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { styled } from '@linaria/react';
-import { useLingui } from '@lingui/react/macro';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-
-type TrialCardProps = {
- duration: number;
- withCreditCard: boolean;
-};
-
-const StyledTrialCardContainer = styled.div`
- display: flex;
- flex-direction: column;
-`;
-
-const StyledTrialDurationContainer = styled.div`
- color: ${themeCssVariables.font.color.secondary};
- display: flex;
- font-size: ${themeCssVariables.font.size.md};
- margin-bottom: ${themeCssVariables.spacing[2]};
-`;
-
-const StyledCreditCardRequirementContainer = styled.div`
- color: ${themeCssVariables.font.color.tertiary};
- display: flex;
- font-size: ${themeCssVariables.font.size.md};
-`;
-
-export const TrialCard = ({ duration, withCreditCard }: TrialCardProps) => {
- const { t } = useLingui();
- return (
-
- {t`${duration} days trial`}
-
- {withCreditCard ? t`With Credit Card` : t`No Credit Card`}
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/settings/billing/hooks/useStripeAppearance.ts b/packages/twenty-front/src/modules/settings/billing/hooks/useStripeAppearance.ts
index ecfff268a6..fa041fb158 100644
--- a/packages/twenty-front/src/modules/settings/billing/hooks/useStripeAppearance.ts
+++ b/packages/twenty-front/src/modules/settings/billing/hooks/useStripeAppearance.ts
@@ -32,11 +32,42 @@ export const useStripeAppearance = (): Appearance => {
return {
theme: isDark ? 'night' : 'stripe',
variables: {
+ fontFamily: theme.font.family,
+ fontSizeBase: '14px',
colorPrimary: toStripeColor(theme.color.blue),
colorBackground: toStripeColor(theme.background.primary),
colorText: toStripeColor(theme.font.color.primary),
+ colorTextSecondary: toStripeColor(theme.font.color.tertiary),
+ colorTextPlaceholder: toStripeColor(theme.font.color.light),
colorDanger: toStripeColor(theme.font.color.danger),
- borderRadius: '8px',
+ colorIcon: toStripeColor(theme.font.color.tertiary),
+ borderRadius: theme.border.radius.md,
+ spacingGridRow: '12px',
+ },
+ rules: {
+ '.Label': {
+ color: toStripeColor(theme.font.color.secondary),
+ fontWeight: String(theme.font.weight.medium),
+ fontSize: '13px',
+ marginBottom: '6px',
+ },
+ '.Input': {
+ backgroundColor: toStripeColor(theme.background.tertiary),
+ border: `1px solid ${toStripeColor(theme.border.color.medium)}`,
+ boxShadow: 'none',
+ padding: '8px 12px',
+ },
+ '.Input:focus': {
+ border: `1px solid ${toStripeColor(theme.border.color.blue)}`,
+ boxShadow: 'none',
+ },
+ '.Input--invalid': {
+ border: `1px solid ${toStripeColor(theme.border.color.danger)}`,
+ boxShadow: 'none',
+ },
+ '.Input::placeholder': {
+ color: toStripeColor(theme.font.color.light),
+ },
},
};
};
diff --git a/packages/twenty-front/src/modules/settings/billing/components/SubscriptionPaymentForm.tsx b/packages/twenty-front/src/modules/settings/billing/hooks/useSubmitSubscriptionPayment.ts
similarity index 53%
rename from packages/twenty-front/src/modules/settings/billing/components/SubscriptionPaymentForm.tsx
rename to packages/twenty-front/src/modules/settings/billing/hooks/useSubmitSubscriptionPayment.ts
index 23629a68d3..403d3da949 100644
--- a/packages/twenty-front/src/modules/settings/billing/components/SubscriptionPaymentForm.tsx
+++ b/packages/twenty-front/src/modules/settings/billing/hooks/useSubmitSubscriptionPayment.ts
@@ -1,71 +1,39 @@
-import { currentUserState } from '@/auth/states/currentUserState';
-import { useStripeAppearance } from '@/settings/billing/hooks/useStripeAppearance';
-import { useStripePromise } from '@/settings/billing/hooks/useStripePromise';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useMutation } from '@apollo/client/react';
-import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
-import {
- Elements,
- PaymentElement,
- useElements,
- useStripe,
-} from '@stripe/react-stripe-js';
+import { useElements, useStripe } from '@stripe/react-stripe-js';
import { useState } from 'react';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
-import { Info, Loader } from 'twenty-ui/feedback';
-import { MainButton } from 'twenty-ui/input';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
type BillingPlanKey,
type SubscriptionInterval,
CreateSubscriptionPaymentIntentDocument,
} from '~/generated-metadata/graphql';
-type SubscriptionPaymentFormContentProps = {
+type UseSubmitSubscriptionPaymentParams = {
plan: BillingPlanKey;
recurringInterval: SubscriptionInterval;
};
-type SubscriptionPaymentFormProps = SubscriptionPaymentFormContentProps & {
- amount: number;
-};
-
-const StyledFormContainer = styled.div`
- display: flex;
- flex-direction: column;
- gap: ${themeCssVariables.spacing[4]};
- margin-bottom: ${themeCssVariables.spacing[8]};
- width: 100%;
-`;
-
-const StyledButtonContainer = styled.div`
- display: flex;
- justify-content: center;
-`;
-
-const SubscriptionPaymentFormContent = ({
+export const useSubmitSubscriptionPayment = ({
plan,
recurringInterval,
-}: SubscriptionPaymentFormContentProps) => {
+}: UseSubmitSubscriptionPaymentParams) => {
const stripe = useStripe();
const elements = useElements();
const { enqueueErrorSnackBar } = useSnackBar();
const [isSubmitting, setIsSubmitting] = useState(false);
- const customerEmail = useAtomStateValue(currentUserState)?.email;
-
const [createSubscriptionPaymentIntent] = useMutation(
CreateSubscriptionPaymentIntentDocument,
);
const isStripeReady = isDefined(stripe) && isDefined(elements);
- const handleSubmit = async () => {
- if (!isStripeReady) {
+ const submit = async () => {
+ if (!isDefined(stripe) || !isDefined(elements)) {
return;
}
@@ -135,56 +103,5 @@ const SubscriptionPaymentFormContent = ({
}
};
- return (
-
-
-
- (isSubmitting ? : null)}
- disabled={!isStripeReady || isSubmitting}
- />
-
-
- );
-};
-
-export const SubscriptionPaymentForm = ({
- plan,
- recurringInterval,
- amount,
-}: SubscriptionPaymentFormProps) => {
- const stripePromise = useStripePromise();
- const appearance = useStripeAppearance();
-
- if (!isDefined(stripePromise)) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
- );
+ return { submit, isSubmitting, isStripeReady };
};
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/BackgroundMockNavigationDrawer.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/BackgroundMockNavigationDrawer.tsx
deleted file mode 100644
index ab21c5d97a..0000000000
--- a/packages/twenty-front/src/modules/sign-in-background-mock/components/BackgroundMockNavigationDrawer.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { styled } from '@linaria/react';
-import { useLingui } from '@lingui/react/macro';
-import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath } from 'twenty-shared/utils';
-import { IconSearch, IconSettings } from 'twenty-ui/icon';
-import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
-
-import { BACKGROUND_MOCK_WORKSPACE_ITEMS } from '@/sign-in-background-mock/constants/BackgroundMockNavigationItems';
-import { NavigationDrawer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer';
-import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
-import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
-import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
-import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
-
-const StyledMainSectionWrapper = styled.div`
- min-height: fit-content;
-`;
-
-export type BackgroundMockNavigationDrawerProps = {
- className?: string;
-};
-
-export const BackgroundMockNavigationDrawer = ({
- className,
-}: BackgroundMockNavigationDrawerProps) => {
- const isMobile = useIsMobile();
- const { t } = useLingui();
-
- return (
-
- {!isMobile && (
-
-
- {}}
- modifier={{ keyboard: [getOsControlSymbol(), 'K'] }}
- />
- {}}
- Icon={IconSettings}
- />
-
-
- )}
-
-
- {BACKGROUND_MOCK_WORKSPACE_ITEMS.map((item, index) => (
- {}}
- />
- ))}
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockNavigationItems.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockNavigationItems.ts
deleted file mode 100644
index 6ea7ba51c1..0000000000
--- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockNavigationItems.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import {
- IconBuildingSkyscraper,
- IconCalendarEvent,
- IconCheckbox,
- type IconComponent,
- IconFileText,
- IconHeart,
- IconLayoutDashboard,
- IconNotes,
- IconRocket,
- IconStar,
- IconTargetArrow,
- IconUser,
- IconUserCircle,
-} from 'twenty-ui/icon';
-import { type ThemeColor } from 'twenty-ui/theme';
-
-export type BackgroundMockNavigationItem = {
- label: string;
- Icon: IconComponent;
- color: ThemeColor;
-};
-
-export const BACKGROUND_MOCK_WORKSPACE_ITEMS = [
- { label: 'Companies', Icon: IconBuildingSkyscraper, color: 'blue' },
- { label: 'People', Icon: IconUser, color: 'blue' },
- { label: 'Opportunities', Icon: IconTargetArrow, color: 'red' },
- { label: 'Tasks', Icon: IconCheckbox, color: 'turquoise' },
- { label: 'Notes', Icon: IconNotes, color: 'turquoise' },
- { label: 'Dashboards', Icon: IconLayoutDashboard, color: 'orange' },
- { label: 'Workflows', Icon: IconRocket, color: 'pink' },
- { label: 'Rockets', Icon: IconRocket, color: 'sky' },
- { label: 'Pets', Icon: IconHeart, color: 'orange' },
- { label: 'Survey results', Icon: IconStar, color: 'yellow' },
- { label: 'Employment Histories', Icon: IconUserCircle, color: 'green' },
- { label: 'Pet Care Agreements', Icon: IconFileText, color: 'purple' },
- { label: 'Star History', Icon: IconCalendarEvent, color: 'red' },
-] satisfies BackgroundMockNavigationItem[];
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockOtherItems.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockOtherItems.ts
deleted file mode 100644
index b79c1ebb4d..0000000000
--- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/BackgroundMockOtherItems.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { IconFileText, IconSettings } from 'twenty-ui/icon';
-
-import { type BackgroundMockNavigationItem } from '@/sign-in-background-mock/constants/BackgroundMockNavigationItems';
-
-export const BACKGROUND_MOCK_OTHER_ITEMS = [
- { label: 'Settings', Icon: IconSettings, color: 'gray' },
- { label: 'Documentation', Icon: IconFileText, color: 'gray' },
-] satisfies BackgroundMockNavigationItem[];
diff --git a/packages/twenty-front/src/modules/types/PageFocusId.ts b/packages/twenty-front/src/modules/types/PageFocusId.ts
index 674bc75bc2..c2e739fefd 100644
--- a/packages/twenty-front/src/modules/types/PageFocusId.ts
+++ b/packages/twenty-front/src/modules/types/PageFocusId.ts
@@ -3,7 +3,6 @@ export enum PageFocusId {
WorkspaceActivation = 'workspace-activation',
SignInUp = 'sign-in-up',
CreateProfile = 'create-profile',
- CreateProfileV2 = 'create-profile-v2',
InviteTeam = 'invite-team',
SyncEmail = 'sync-email',
PlanRequired = 'plan-required',
diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx
deleted file mode 100644
index 8c8306b46f..0000000000
--- a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { renderHook } from '@testing-library/react';
-import * as reactRouterDom from 'react-router-dom';
-
-import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
-import { AppPath } from 'twenty-shared/types';
-import { isMatchingLocation } from '~/utils/isMatchingLocation';
-
-jest.mock('react-router-dom', () => ({
- useLocation: jest.fn(),
-}));
-
-const mockUseLocation = reactRouterDom.useLocation as jest.Mock;
-
-jest.mock('~/utils/isMatchingLocation');
-const mockIsMatchingLocation = jest.mocked(isMatchingLocation);
-
-const setupMockIsMatchingLocation = (pathname: string) => {
- mockUseLocation.mockReturnValue({ pathname });
- mockIsMatchingLocation.mockImplementation(
- (_location, path) => path === pathname,
- );
-};
-
-const getResult = () =>
- renderHook(() => {
- return useShowAuthModal();
- });
-
-const testCases = [
- { loc: AppPath.Verify, res: true },
- { loc: AppPath.VerifyEmail, res: true },
- { loc: AppPath.SignInUp, res: true },
- { loc: AppPath.Invite, res: true },
- { loc: AppPath.ResetPassword, res: true },
- { loc: AppPath.WorkspaceActivation, res: true },
- { loc: AppPath.SyncEmails, res: true },
- { loc: AppPath.InviteTeam, res: true },
- { loc: AppPath.PlanRequired, res: true },
- { loc: AppPath.PlanRequiredSuccess, res: true },
- { loc: AppPath.BookCallDecision, res: true },
- { loc: AppPath.BookCall, res: true },
-
- { loc: AppPath.Index, res: false },
- { loc: AppPath.RecordIndexPage, res: false },
- { loc: AppPath.RecordShowPage, res: false },
- { loc: AppPath.SettingsCatchAll, res: false },
- { loc: AppPath.DevelopersCatchAll, res: false },
- { loc: AppPath.Authorize, res: false },
- { loc: AppPath.NotFoundWildcard, res: false },
- { loc: AppPath.NotFound, res: false },
-];
-
-describe('useShowAuthModal', () => {
- testCases.forEach((testCase) => {
- it(`testCase for location ${testCase.loc} should return ${testCase.res}`, () => {
- setupMockIsMatchingLocation(testCase.loc);
- const { result } = getResult();
- if (testCase.res) {
- expect(result.current).toBeTruthy();
- } else {
- expect(result.current).toBeFalsy();
- }
- });
- });
-});
diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts b/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts
deleted file mode 100644
index 4e678cb62f..0000000000
--- a/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useMemo } from 'react';
-
-import { useLocation } from 'react-router-dom';
-import { AppPath } from 'twenty-shared/types';
-import { isMatchingLocation } from '~/utils/isMatchingLocation';
-
-export const useShowAuthModal = () => {
- const location = useLocation();
-
- return useMemo(() => {
- if (
- isMatchingLocation(location, AppPath.Invite) ||
- isMatchingLocation(location, AppPath.InviteTeam) ||
- isMatchingLocation(location, AppPath.CreateProfile) ||
- isMatchingLocation(location, AppPath.SyncEmails) ||
- isMatchingLocation(location, AppPath.ResetPassword) ||
- isMatchingLocation(location, AppPath.VerifyEmail) ||
- isMatchingLocation(location, AppPath.Verify) ||
- isMatchingLocation(location, AppPath.SignInUp) ||
- isMatchingLocation(location, AppPath.WorkspaceActivation) ||
- isMatchingLocation(location, AppPath.PlanRequired) ||
- isMatchingLocation(location, AppPath.PlanRequiredSuccess) ||
- isMatchingLocation(location, AppPath.BookCallDecision) ||
- isMatchingLocation(location, AppPath.BookCall)
- ) {
- return true;
- }
-
- return false;
- }, [location]);
-};
diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/AuthFlowLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/AuthFlowLayout.tsx
new file mode 100644
index 0000000000..bffa9b94cd
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/page/components/AuthFlowLayout.tsx
@@ -0,0 +1,17 @@
+import { styled } from '@linaria/react';
+import { Outlet } from 'react-router-dom';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledBackground = styled.div`
+ background: ${themeCssVariables.background.secondary};
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ width: 100%;
+`;
+
+export const AuthFlowLayout = () => (
+
+
+
+);
diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
index 6e585793dd..9b7b214e6c 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx
@@ -1,4 +1,3 @@
-import { AuthModal } from '@/auth/components/AuthModal';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
import { AppPageErrorFallback } from '@/error-handler/components/AppPageErrorFallback';
@@ -9,19 +8,9 @@ import { LayoutCustomizationBar } from '@/layout-customization/components/Layout
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
import { PageDragDropProvider } from '@/navigation-menu-item/display/dnd/providers/PageDragDropProvider';
-import { BackgroundMockNavigationDrawer } from '@/sign-in-background-mock/components/BackgroundMockNavigationDrawer';
-import { Suspense, lazy } from 'react';
-
-const BackgroundMockPage = lazy(() =>
- import('@/sign-in-background-mock/components/BackgroundMockPage').then(
- (module) => ({ default: module.BackgroundMockPage }),
- ),
-);
import { useShowFullscreen } from '@/ui/layout/fullscreen/hooks/useShowFullscreen';
-import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { styled } from '@linaria/react';
-import { AnimatePresence, LayoutGroup } from 'framer-motion';
import { Outlet } from 'react-router-dom';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledLayout = styled.div`
@@ -85,7 +74,6 @@ const StyledMainContainer = styled.div`
export const DefaultLayout = () => {
const isMobile = useIsMobile();
- const showAuthModal = useShowAuthModal();
const useShowFullScreen = useShowFullscreen();
return (
@@ -97,41 +85,20 @@ export const DefaultLayout = () => {
- {!showAuthModal && }
- {showAuthModal ? (
-
-
-
- ) : useShowFullScreen ? null : (
+
+ {useShowFullScreen ? null : (
)}
- {showAuthModal ? (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ) : (
-
-
-
-
-
- )}
+
+
+
+
+
- {isMobile && !showAuthModal && }
+ {isMobile && }
diff --git a/packages/twenty-front/src/modules/users/components/UserContextProvider.tsx b/packages/twenty-front/src/modules/users/components/UserContextProvider.tsx
new file mode 100644
index 0000000000..c96b9023e4
--- /dev/null
+++ b/packages/twenty-front/src/modules/users/components/UserContextProvider.tsx
@@ -0,0 +1,16 @@
+import { type PropsWithChildren } from 'react';
+
+import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
+import { UserContext } from '@/users/contexts/UserContext';
+
+type UserContextProviderProps = PropsWithChildren;
+
+export const UserContextProvider = ({ children }: UserContextProviderProps) => {
+ const { dateFormat, timeFormat, timeZone } = useDateTimeFormat();
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx
index 1a023a33d9..683fa1bbd8 100644
--- a/packages/twenty-front/src/pages/auth/SignInUp.tsx
+++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx
@@ -1,5 +1,6 @@
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
+import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState';
import {
SignInUpStep,
signInUpStepState,
@@ -10,21 +11,20 @@ import { styled } from '@linaria/react';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-import { Logo } from '@/auth/components/Logo';
-import { Title } from '@/auth/components/Title';
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
-import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
+import { SignInUpStandardContent } from '@/auth/sign-in-up/components/SignInUpStandardContent';
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
-import { WorkspaceSelectionFooter } from '@/auth/sign-in-up/components/WorkspaceSelectionFooter';
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/internal/SignInUpSSOIdentityProviderSelection';
+import { OnboardingLayout } from '@/onboarding/components/OnboardingLayout';
+import { StyledOnboardingStepPage } from '@/onboarding/components/StyledOnboardingStepPage';
import { SignInUpWorkspaceCreationForm } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationForm';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceScopeFormEffect';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
-import { type JSX, useMemo } from 'react';
+import { useMemo } from 'react';
import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect';
import { SignInUpTwoFactorAuthenticationProvision } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationProvision';
@@ -37,8 +37,6 @@ import { useSearchParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { Loader } from 'twenty-ui/feedback';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-import { AnimatedEaseIn } from 'twenty-ui/layout';
-import { type PublicWorkspaceData } from '~/generated-metadata/graphql';
const StyledLoaderContainer = styled.div`
align-items: center;
@@ -49,48 +47,20 @@ const StyledLoaderContainer = styled.div`
width: 100%;
`;
-const StandardContent = ({
- workspacePublicData,
- signInUpForm,
- signInUpStep,
- title,
- onClickOnLogo,
-}: {
- workspacePublicData: PublicWorkspaceData | null;
- signInUpForm: JSX.Element | null;
- signInUpStep: SignInUpStep;
- title: string;
- onClickOnLogo: () => void;
-}) => {
- return (
-
-
-
-
- {title}
- {signInUpForm}
- {signInUpStep === SignInUpStep.WorkspaceSelection && (
-
- )}
- {![
- SignInUpStep.Password,
- SignInUpStep.TwoFactorAuthenticationProvision,
- SignInUpStep.TwoFactorAuthenticationVerification,
- SignInUpStep.WorkspaceSelection,
- SignInUpStep.WorkspaceCreation,
- ].includes(signInUpStep) && }
-
- );
-};
+const StyledBackground = styled.div`
+ background: ${themeCssVariables.background.secondary};
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ overflow-y: auto;
+ width: 100%;
+`;
export const SignInUp = () => {
const { t } = useLingui();
const setSignInUpStep = useSetAtomState(signInUpStepState);
const clientConfigApiStatus = useAtomStateValue(clientConfigApiStatusState);
+ const isCreatingWorkspace = useAtomStateValue(isCreatingWorkspaceState);
const { form } = useSignInUpForm();
const { signInUpStep } = useSignInUp(form);
@@ -105,12 +75,22 @@ export const SignInUp = () => {
const { workspaceInviteHash, workspace: workspaceFromInviteHash } =
useWorkspaceFromInviteHash();
- const [searchParams] = useSearchParams();
+ const [searchParams, setSearchParams] = useSearchParams();
const onClickOnLogo = () => {
setSignInUpStep(SignInUpStep.Init);
};
+ const onBackFromWorkspaceCreation = () => {
+ if (searchParams.has('action')) {
+ const nextSearchParams = new URLSearchParams(searchParams);
+ nextSearchParams.delete('action');
+ setSearchParams(nextSearchParams, { replace: true });
+ }
+
+ setSignInUpStep(SignInUpStep.WorkspaceSelection);
+ };
+
const isGlobalScope = isDefaultDomain && isMultiWorkspaceEnabled;
const title = useMemo(() => {
@@ -220,21 +200,27 @@ export const SignInUp = () => {
workspacePublicData,
]);
- if (signInUpStep === SignInUpStep.EmailVerification) {
- return (
-
-
-
- );
- }
-
- return (
-
+ return signInUpStep === SignInUpStep.WorkspaceCreation ? (
+
+ {signInUpForm}
+
+ ) : (
+
+ {signInUpStep === SignInUpStep.EmailVerification ? (
+
+
+
+ ) : (
+
+ )}
+
);
};
diff --git a/packages/twenty-front/src/pages/auth/SignInUpV2.tsx b/packages/twenty-front/src/pages/auth/SignInUpV2.tsx
deleted file mode 100644
index 1feac10cd8..0000000000
--- a/packages/twenty-front/src/pages/auth/SignInUpV2.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
-import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
-import { isCreatingWorkspaceState } from '@/auth/states/isCreatingWorkspaceState';
-import {
- SignInUpStep,
- signInUpStepState,
-} from '@/auth/states/signInUpStepState';
-import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
-import { styled } from '@linaria/react';
-
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-
-import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
-import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
-import { SignInUpV2StandardContent } from '@/auth/sign-in-up/components/SignInUpV2StandardContent';
-import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
-import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/internal/SignInUpSSOIdentityProviderSelection';
-import { OnboardingV2Layout } from '@/onboarding/components/OnboardingV2Layout';
-import { SignInUpWorkspaceCreationFormV2 } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceCreationFormV2';
-import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpWorkspaceScopeFormEffect';
-import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
-import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain';
-import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
-import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
-import { useMemo } from 'react';
-
-import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect';
-import { SignInUpTwoFactorAuthenticationProvision } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationProvision';
-import { SignInUpTOTPVerification } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification';
-import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
-import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
-import { ModalContent } from 'twenty-ui/surfaces';
-import { useLingui } from '@lingui/react/macro';
-import { useSearchParams } from 'react-router-dom';
-import { isDefined } from 'twenty-shared/utils';
-import { Loader } from 'twenty-ui/feedback';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-
-const StyledLoaderContainer = styled.div`
- align-items: center;
- display: flex;
- justify-content: center;
- margin-bottom: ${themeCssVariables.spacing[8]};
- margin-top: ${themeCssVariables.spacing[8]};
- width: 100%;
-`;
-
-const StyledBackground = styled.div`
- background: ${themeCssVariables.background.secondary};
- display: flex;
- flex-direction: column;
- height: 100dvh;
- overflow-y: auto;
- width: 100%;
-`;
-
-export const SignInUpV2 = () => {
- const { t } = useLingui();
- const setSignInUpStep = useSetAtomState(signInUpStepState);
- const clientConfigApiStatus = useAtomStateValue(clientConfigApiStatusState);
- const isCreatingWorkspace = useAtomStateValue(isCreatingWorkspaceState);
-
- const { form } = useSignInUpForm();
- const { signInUpStep } = useSignInUp(form);
- const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
- const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
- const workspacePublicData = useAtomStateValue(workspacePublicDataState);
- const { loading: getPublicWorkspaceDataLoading } =
- useGetPublicWorkspaceDataByDomain();
- const isMultiWorkspaceEnabled = useAtomStateValue(
- isMultiWorkspaceEnabledState,
- );
- const { workspaceInviteHash, workspace: workspaceFromInviteHash } =
- useWorkspaceFromInviteHash();
-
- const [searchParams] = useSearchParams();
-
- const onClickOnLogo = () => {
- setSignInUpStep(SignInUpStep.Init);
- };
-
- const isGlobalScope = isDefaultDomain && isMultiWorkspaceEnabled;
-
- const title = useMemo(() => {
- if (isDefined(workspaceInviteHash)) {
- const workspaceName = workspaceFromInviteHash?.displayName ?? '';
- return t`Join ${workspaceName} team`;
- }
-
- if (signInUpStep === SignInUpStep.WorkspaceSelection) {
- return t`Choose a Workspace`;
- }
-
- if (signInUpStep === SignInUpStep.WorkspaceCreation) {
- return t`Create your workspace`;
- }
-
- if (signInUpStep === SignInUpStep.TwoFactorAuthenticationProvision) {
- return t`Setup your 2FA`;
- }
-
- if (signInUpStep === SignInUpStep.TwoFactorAuthenticationVerification) {
- return t`Verify code from the app`;
- }
-
- if (isGlobalScope) {
- return t`Welcome to Twenty`;
- }
-
- const workspaceName = workspacePublicData?.displayName;
-
- if (!workspaceName) {
- return t`Welcome to your workspace`;
- }
-
- return t`Welcome, ${workspaceName}.`;
- }, [
- workspaceInviteHash,
- signInUpStep,
- workspacePublicData?.displayName,
- isGlobalScope,
- t,
- workspaceFromInviteHash?.displayName,
- ]);
-
- const signInUpForm = useMemo(() => {
- if (getPublicWorkspaceDataLoading || !clientConfigApiStatus.isLoadedOnce) {
- return (
-
-
-
- );
- }
-
- // The workspace creation form is shared by both multi-workspace and
- // single-workspace self-host, so it must render regardless of domain or
- // workspace scope.
- if (signInUpStep === SignInUpStep.WorkspaceCreation) {
- return ;
- }
-
- if (isDefaultDomain && isMultiWorkspaceEnabled) {
- return (
- <>
-
-
- >
- );
- }
-
- if (
- isOnAWorkspace &&
- signInUpStep === SignInUpStep.SSOIdentityProviderSelection
- ) {
- return ;
- }
-
- if (signInUpStep === SignInUpStep.TwoFactorAuthenticationProvision) {
- return ;
- }
-
- if (signInUpStep === SignInUpStep.TwoFactorAuthenticationVerification) {
- return ;
- }
-
- if (isDefined(workspacePublicData) && isOnAWorkspace) {
- return (
- <>
-
-
- >
- );
- }
-
- return (
- <>
-
-
- >
- );
- }, [
- clientConfigApiStatus.isLoadedOnce,
- isDefaultDomain,
- isMultiWorkspaceEnabled,
- isOnAWorkspace,
- getPublicWorkspaceDataLoading,
- signInUpStep,
- workspacePublicData,
- ]);
-
- return signInUpStep === SignInUpStep.WorkspaceCreation ? (
-
-
- {signInUpForm}
-
-
- ) : (
-
- {signInUpStep === SignInUpStep.EmailVerification ? (
-
-
-
- ) : (
-
- )}
-
- );
-};
diff --git a/packages/twenty-front/src/pages/auth/__stories__/SignInUpV2.stories.tsx b/packages/twenty-front/src/pages/auth/__stories__/SignInUpV2.stories.tsx
deleted file mode 100644
index bc10152a29..0000000000
--- a/packages/twenty-front/src/pages/auth/__stories__/SignInUpV2.stories.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { getOperationName } from '~/utils/getOperationName';
-import { type Meta, type StoryObj } from '@storybook/react-vite';
-import { HttpResponse, graphql } from 'msw';
-import { useEffect } from 'react';
-import { fireEvent, within } from 'storybook/test';
-
-import { captchaTokenState } from '@/captcha/states/captchaTokenState';
-import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
-import {
- PageDecorator,
- type PageDecoratorArgs,
-} from '~/testing/decorators/PageDecorator';
-import { graphqlMocks } from '~/testing/graphqlMocks';
-
-import { AppPath } from 'twenty-shared/types';
-import { SignInUpV2 } from '~/pages/auth/SignInUpV2';
-
-const CaptchaTokenSetterEffect = () => {
- const setCaptchaToken = useSetAtomState(captchaTokenState);
-
- useEffect(() => {
- setCaptchaToken('MOCKED_CAPTCHA_TOKEN');
- }, [setCaptchaToken]);
-
- return null;
-};
-
-const SignInUpV2WithCaptcha = () => {
- return (
- <>
-
-
- >
- );
-};
-
-const meta: Meta = {
- title: 'Pages/Auth/SignInUpV2',
- component: SignInUpV2WithCaptcha,
- decorators: [PageDecorator],
- args: { routePath: AppPath.SignInUpV2 },
- parameters: {
- msw: {
- handlers: [
- graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
- return HttpResponse.json({
- data: null,
- errors: [
- {
- message: 'Unauthorized',
- extensions: {
- code: 'UNAUTHENTICATED',
- response: {
- statusCode: 401,
- message: 'Unauthorized',
- },
- },
- },
- ],
- });
- }),
- graphqlMocks.handlers,
- ],
- },
- cookie: '',
- },
-};
-
-export default meta;
-
-export type Story = StoryObj;
-
-export const Default: Story = {
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement.ownerDocument.body);
- const continueWithEmailButton = await canvas.findByText(
- 'Continue with Email',
- {},
- { timeout: 3000 },
- );
-
- await fireEvent.click(continueWithEmailButton);
- },
-};
diff --git a/packages/twenty-front/src/pages/onboarding/BookCall.tsx b/packages/twenty-front/src/pages/onboarding/BookCall.tsx
index ad75614ed3..01e4d08721 100644
--- a/packages/twenty-front/src/pages/onboarding/BookCall.tsx
+++ b/packages/twenty-front/src/pages/onboarding/BookCall.tsx
@@ -5,14 +5,14 @@ import { Link } from 'react-router-dom';
import { currentUserState } from '@/auth/states/currentUserState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
-import { ModalContent, ModalFooter } from 'twenty-ui/surfaces';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
+import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { AppPath } from 'twenty-shared/types';
import { IconChevronLeft, IconChevronRightPipe } from 'twenty-ui/icon';
import { LightButton } from 'twenty-ui/input';
-import { ThemeContext } from 'twenty-ui/theme-constants';
+import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { useMutation } from '@apollo/client/react';
import {
@@ -20,6 +20,32 @@ import {
SkipBookOnboardingStepDocument,
} from '~/generated-metadata/graphql';
+const StyledPage = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-height: 0;
+ width: 100%;
+`;
+
+const StyledContent = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ min-height: 0;
+ overflow: hidden;
+ width: 100%;
+`;
+
+const StyledFooter = styled.div`
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ padding: ${themeCssVariables.spacing[2]};
+ width: 100%;
+`;
+
export const BookCall = () => {
const { colorScheme } = useContext(ThemeContext);
@@ -41,15 +67,10 @@ export const BookCall = () => {
};
return (
- <>
-
+
+
{
}}
/>
-
-
+
+
{isPlanRequired ? (
@@ -75,7 +96,7 @@ export const BookCall = () => {
onClick={handleCompleteOnboarding}
/>
)}
-
- >
+
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx b/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx
index a43338b56f..948072bb66 100644
--- a/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx
+++ b/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx
@@ -1,7 +1,6 @@
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
-import { ModalContent } from 'twenty-ui/surfaces';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
import { Link } from 'react-router-dom';
@@ -11,9 +10,21 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useMutation } from '@apollo/client/react';
import { SkipBookOnboardingStepDocument } from '~/generated-metadata/graphql';
+const StyledPage = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[8]};
+ justify-content: center;
+ min-height: 0;
+ width: 100%;
+`;
+
const StyledCoverImage = styled.img`
border-radius: ${themeCssVariables.border.radius.sm};
height: 204px;
+ max-width: 100%;
object-fit: cover;
width: 320px;
`;
@@ -52,7 +63,7 @@ export const BookCallDecision = () => {
};
return (
-
+
Book your onboarding
@@ -73,6 +84,6 @@ export const BookCallDecision = () => {
-
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx
index 8b43615502..b872330029 100644
--- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx
+++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx
@@ -1,25 +1,22 @@
-import { ModalContent } from 'twenty-ui/surfaces';
-import { styled } from '@linaria/react';
-import { isDefined } from 'twenty-shared/utils';
-import { ChooseYourPlanContent } from '~/pages/onboarding/internal/ChooseYourPlanContent';
import { billingState } from '@/client-config/states/billingState';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
+import { OnboardingPageLoader } from '@/onboarding/components/OnboardingPageLoader';
import { usePlans } from '@/settings/billing/hooks/usePlans';
-
-const StyledChooseYourPlanPlaceholder = styled.div`
- height: 566px;
-`;
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { isDefined } from 'twenty-shared/utils';
+import { UpgradeFreeTrial } from '~/pages/onboarding/UpgradeFreeTrial';
export const ChooseYourPlan = () => {
const { isPlansLoaded } = usePlans();
const billing = useAtomStateValue(billingState);
- return (
-
- {isDefined(billing) && isPlansLoaded ? (
-
- ) : (
-
- )}
-
+ const onboardingConfig = useAtomStateValue(onboardingConfigState);
+
+ return isDefined(billing) && isPlansLoaded ? (
+
+ ) : (
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlanV2.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlanV2.tsx
deleted file mode 100644
index 4604b24b08..0000000000
--- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlanV2.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { billingState } from '@/client-config/states/billingState';
-import { onboardingConfigState } from '@/client-config/states/onboardingConfigState';
-import { OnboardingV2Layout } from '@/onboarding/components/OnboardingV2Layout';
-import { useOnboardingFreeCreditsTotal } from '@/onboarding/hooks/useOnboardingFreeCreditsTotal';
-import { usePlans } from '@/settings/billing/hooks/usePlans';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { styled } from '@linaria/react';
-import { isDefined } from 'twenty-shared/utils';
-import { UpgradeFreeTrial } from '~/pages/onboarding/UpgradeFreeTrial';
-
-const StyledPlaceholder = styled.div`
- flex: 1 1 0;
-`;
-
-export const ChooseYourPlanV2 = () => {
- const { isPlansLoaded } = usePlans();
- const billing = useAtomStateValue(billingState);
- const onboardingConfig = useAtomStateValue(onboardingConfigState);
- const freeCreditsTotal = useOnboardingFreeCreditsTotal();
-
- return (
-
- {isDefined(billing) && isPlansLoaded ? (
-
- ) : (
-
- )}
-
- );
-};
diff --git a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
index 5bc3bcbd4c..583fc1e718 100644
--- a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
+++ b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
@@ -1,69 +1,75 @@
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { styled } from '@linaria/react';
-import { useCallback, useState } from 'react';
-import { Controller, type SubmitHandler, useForm } from 'react-hook-form';
-import { Key } from 'ts-key-enum';
-import { z } from 'zod';
-
-import { SubTitle } from '@/auth/components/SubTitle';
-import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
+import { OnboardingProfilePictureUploader } from '@/onboarding/components/OnboardingProfilePictureUploader';
+import { OnboardingStepAnimatedItem } from '@/onboarding/components/OnboardingStepAnimatedItem';
+import { StyledOnboardingStepHeading } from '@/onboarding/components/StyledOnboardingStepHeading';
+import { StyledOnboardingStepPage } from '@/onboarding/components/StyledOnboardingStepPage';
+import { StyledOnboardingStepSubtitle } from '@/onboarding/components/StyledOnboardingStepSubtitle';
+import { StyledOnboardingStepTitle } from '@/onboarding/components/StyledOnboardingStepTitle';
+import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/OnboardingContentBlockWidth';
import { usePrefetchInviteSuggestions } from '@/onboarding/hooks/usePrefetchInviteSuggestions';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { useUpdateWorkspaceMemberSettings } from '@/settings/profile/hooks/useUpdateWorkspaceMemberSettings';
-import { WorkspaceMemberPictureUploader } from '@/settings/workspace-member/components/WorkspaceMemberPictureUploader';
import { PageFocusId } from '@/types/PageFocusId';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { styled } from '@linaria/react';
import { i18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
-import { Trans, useLingui } from '@lingui/react/macro';
+import { useLingui } from '@lingui/react/macro';
+import { useCallback, useState } from 'react';
+import { Controller, type SubmitHandler, useForm } from 'react-hook-form';
+import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
-import { H2Title } from 'twenty-ui/typography';
import { MainButton } from 'twenty-ui/input';
-import { ModalContent } from 'twenty-ui/surfaces';
import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { z } from 'zod';
-const StyledContentContainer = styled.div`
+const StyledForm = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[8]};
+ max-width: 100%;
+ padding-bottom: ${themeCssVariables.spacing[4]};
+ width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
+`;
+
+const StyledNameRow = styled.div`
+ align-items: flex-end;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
width: 100%;
`;
-const StyledSectionContainer = styled.div`
- margin-top: ${themeCssVariables.spacing[8]};
+const StyledNameField = styled.div`
+ flex: 1 1 0;
+ min-width: 0;
`;
const StyledButtonContainer = styled.div`
- margin-top: ${themeCssVariables.spacing[8]};
- width: 200px;
-`;
-
-const StyledComboInputContainer = styled.div`
display: flex;
- flex-direction: row;
- > * + * {
- margin-left: ${themeCssVariables.spacing[4]};
- }
+ max-width: 100%;
+ width: ${ONBOARDING_CONTENT_BLOCK_WIDTH}px;
`;
const firstNameErrorMessage = msg`First name can not be empty`;
const lastNameErrorMessage = msg`Last name can not be empty`;
-const validationSchema = z
- .object({
- firstName: z.string().min(1, {
- error: i18n._(firstNameErrorMessage),
- }),
- lastName: z.string().min(1, {
- error: i18n._(lastNameErrorMessage),
- }),
- })
- .required();
+const validationSchema = z.object({
+ firstName: z.string().min(1, {
+ error: i18n._(firstNameErrorMessage),
+ }),
+ lastName: z.string().min(1, {
+ error: i18n._(lastNameErrorMessage),
+ }),
+ jobTitle: z.string(),
+});
type Form = z.infer;
@@ -81,17 +87,16 @@ export const CreateProfile = () => {
);
const { updateWorkspaceMemberSettings } = useUpdateWorkspaceMemberSettings();
- // Form
const {
control,
handleSubmit,
formState: { isValid, isSubmitting },
- getValues,
} = useForm