From 8f7d6c24ddfb0e67535074f944ec6cdb464ce774 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?=
<71827178+bosiraphael@users.noreply.github.com>
Date: Fri, 26 Jun 2026 14:54:52 +0200
Subject: [PATCH] Add v2 onboarding import contacts page and unify the
onboarding v2 shell (#22212)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds the new v2 onboarding **Import contacts** step (email + calendar
import), shown right after workspace creation in the v2 flow. The
presentational page was designed in a previous PR; this wires it in and
unifies the shell.
**What changed**
- Reuses and unifies the existing v2 onboarding shell: extracts
`OnboardingV2Layout` + `OnboardingV2Header` (the back + logo header, now
with the free-credits pill), and the `SignInUpV2` workspace-creation
step renders through it (old `SignInUpV2Header` removed).
- New `SyncEmailsV2` route (`/sync/emails-v2`) under `BlankLayout`,
wired to the same OAuth/skip hooks as v1 `SyncEmails`.
- The `SYNC_EMAIL` step routes to the new page only when
`isOnboardingV2` is set (mirrors the existing `WorkspaceActivation` →
`WorkspaceActivationV2` branch); the v1 modal is unchanged for the
non-v2 flow.
- No backend changes — reuses the `SYNC_EMAIL` status and
`skipSyncEmailOnboardingStep` mutation.
**Reviewer notes**
- Connect defaults to `METADATA` (private) visibility to match the "Only
you will be able to see your emails and events" note (v1 had a selector
defaulting to `SHARE_EVERYTHING`).
- The header free-credits pill shows `0` for now (no current-workspace
credits source on the frontend yet).
- The back button is hidden on the import page (no meaningful "back"
after workspace creation); unchanged on the workspace-creation step.
---
.../images/onboarding/trust-badges/bayer.png | Bin 0 -> 2419 bytes
.../trust-badges/french-republic.png | Bin 0 -> 971 bytes
.../images/onboarding/trust-badges/gdpr.png | Bin 0 -> 3842 bytes
.../images/onboarding/trust-badges/pwc.png | Bin 0 -> 1284 bytes
.../images/onboarding/trust-badges/soc2.png | Bin 0 -> 2689 bytes
...sePageChangeEffectNavigateLocation.test.ts | 29 +++-
.../usePageChangeEffectNavigateLocation.ts | 4 +-
.../modules/app/hooks/useCreateAppRouter.tsx | 14 ++
.../modules/auth/constants/OnboardingPaths.ts | 1 +
.../components/internal/SignInUpV2Header.tsx | 70 --------
.../components/OnboardingV2Header.tsx | 145 ++++++++++++++++
.../components/OnboardingV2Layout.tsx | 29 ++++
.../OnboardingCreditsRewardTag.tsx | 50 ++++++
.../OnboardingImportPreview.tsx | 43 +++++
.../OnboardingImportPreviewCompanies.tsx | 98 +++++++++++
.../OnboardingImportPreviewEmails.tsx | 162 ++++++++++++++++++
.../OnboardingImportPreviewSyncBadge.tsx | 51 ++++++
.../OnboardingImportPrivacyNote.tsx | 28 +++
.../import-contacts/OnboardingTrustBadges.tsx | 93 ++++++++++
.../ImportContactsPreviewCalendarEvents.ts | 21 +++
.../ImportContactsPreviewCompanies.ts | 14 ++
.../constants/ImportContactsPreviewEmails.ts | 54 ++++++
.../SyncEmailsAutoSkipEffect.tsx | 34 ++++
.../hooks/useSkipSyncEmailOnboardingStep.ts | 16 ++
.../src/pages/auth/SignInUpV2.tsx | 21 ++-
.../src/pages/onboarding/ImportContacts.tsx | 148 ++++++++++++++++
.../src/pages/onboarding/SyncEmailsV2.tsx | 86 ++++++++++
.../__stories__/ImportContacts.stories.tsx | 43 +++++
.../__stories__/SyncEmailsV2.stories.tsx | 49 ++++++
.../src/testing/constants/UntestedAppPaths.ts | 1 +
packages/twenty-shared/src/types/AppPath.ts | 1 +
31 files changed, 1218 insertions(+), 87 deletions(-)
create mode 100644 packages/twenty-front/public/images/onboarding/trust-badges/bayer.png
create mode 100644 packages/twenty-front/public/images/onboarding/trust-badges/french-republic.png
create mode 100644 packages/twenty-front/public/images/onboarding/trust-badges/gdpr.png
create mode 100644 packages/twenty-front/public/images/onboarding/trust-badges/pwc.png
create mode 100644 packages/twenty-front/public/images/onboarding/trust-badges/soc2.png
delete mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpV2Header.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingV2Header.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/OnboardingV2Layout.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingCreditsRewardTag.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreview.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewCompanies.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewEmails.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewSyncBadge.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPrivacyNote.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingTrustBadges.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCompanies.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewEmails.ts
create mode 100644 packages/twenty-front/src/modules/onboarding/effect-components/SyncEmailsAutoSkipEffect.tsx
create mode 100644 packages/twenty-front/src/modules/onboarding/hooks/useSkipSyncEmailOnboardingStep.ts
create mode 100644 packages/twenty-front/src/pages/onboarding/ImportContacts.tsx
create mode 100644 packages/twenty-front/src/pages/onboarding/SyncEmailsV2.tsx
create mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/ImportContacts.stories.tsx
create mode 100644 packages/twenty-front/src/pages/onboarding/__stories__/SyncEmailsV2.stories.tsx
diff --git a/packages/twenty-front/public/images/onboarding/trust-badges/bayer.png b/packages/twenty-front/public/images/onboarding/trust-badges/bayer.png
new file mode 100644
index 0000000000000000000000000000000000000000..29195168b1f6ade4d1f0e763d0151444a6f3ce05
GIT binary patch
literal 2419
zcmV-(35@oMP)_62^
z0}*A~uV1~Y`n$UN>#F~%7HKpZy}7wLOog7V0@ZC+iqu&sugig^QH45PHN+w@tR*to
zOFn|k(idK~{iuY&PT~fk5a|h~46M^BaHsq*?v@=!eWMaW;AInrUb1L(k&ZO^nRba8
zNL(-7gN*Xyv@*54+}_*`ULE>EW)%P%i6caG>8WUR8dPdaP^!L%yxQk5)0gwlNXey~N}-={WsrDO3%mtZ!*%hn^iLqKCuZ;cOMiWY_OMLVi(6^UG~JVDz9c
z)-Klq8H$IvRrWh-wW+WbaU_Er#$%YKUFM;$>?=mDB^GS@
z5h*wBA+%c{8gx3`eE0-Amd`=t@PVzyU9GIfq5MR6nf(lFk%FyWwBI3Cw*_%q!s{iw
zc~TCRUYO;dgb0@fZ;>*h(?`zuB=}=Q^zH#0D+_2e+PB6Gbg;*PkXT6Ve?w;7C@8eA
zk?>+Bsx+@mBW56FC5dxHMe+D?l!@Q_C5H{RcZ+@cUMG&2C-soWr?RRp93nT@kG
z8&OFscJ9?G=7w5AVr(cIigE7sD%O@-c;Jgp|0dr(1|^eqMV7$sy~=alBZ1L
z%{A0%I)q}=-lOCq8WB8UHZL=*T12nzCeP84OmJ{VAf~g*e|(vgoR-H&xuU#Yt3zEo>~k^vDO??5m-KD#PHbVtgfk5V@RK#tZneaTMr~)
zMNBjvJk7zzpN`~K8Ekz?}GO(-s{z?Jx&@T8#rC^ru!FnRzmG*TCU76$bgx|J4T%N@vrb1tV?7@@Cw;q~Tk;we!h<=p-$q|JhjcBdiy!Ns
zBhp*qN*;sjD3FO5k2o0uO?h-dHxfiL{
z;%5rIw0;=;Z2T~-a{`3o*tt}CQD!UI=7nGL1WiR+6)=EvsmSa{At}Jd9$9stqpUuc
zk5fW${eBkGGoMiw>%x~0L24yecP7j1VN1j&y8<`Wk?d@ktxUy-pqg~CfVRU}I#^pl
zD2~11>WC&al{ipz_v_FD8MJDhDt<$cj$c41I@;O7*F*TB)`$yqczVRSQ%q}`hGiG4
zeaS^Azr%CYQ^;%6Ar#sM9aQSNRz(Q5=5w_#E%Y!)uFX8er_}MZmLXZW1b&6v$p{i5
z5{XTkbV%P2%JZ#IBan;{hl?qDZql#Se$8?|wZ2#Ze0CogZ?^%9eH`hyHs7R)4dKP$
z?~G{aVB13;eajHC#QZvLwO$NpaZ}#{a~>1P6uth-+bi@Lm8bNkVD9?==$#`b>fMJ<
zX7$*4t4w;1j(Pe|(z*AU{i)Z#tZjY2)qZWEbS#M6PlDoKSe^YGuWE0b^yPhny5PpI
zKj4oa*5N8SPkD=925q6b0f-{kce9%7Z}SIy{&FCbwCqT!_rDNsc~4a@MNP&YZ1T@)
zb*yR&2Cr|-U?YbQdhd1?(6&vZW5JawuD^y1Z?V_g(=U*fKEOm1y@|=c
zI*kO+mr>G4aV<>&G`91kD`ljS&P0ad@lXc{f=H=U>TPUnAQqeE>5Zsr;I`<6n$sI2
ltZ7FB$@D|)Ij8xd_&?vk{^Wc&`Q88k002ovPDHLkV1fv)l^y^9
literal 0
HcmV?d00001
diff --git a/packages/twenty-front/public/images/onboarding/trust-badges/french-republic.png b/packages/twenty-front/public/images/onboarding/trust-badges/french-republic.png
new file mode 100644
index 0000000000000000000000000000000000000000..c6b61cea25abcdc2cf2f51776d7c5ad1ea2ebbec
GIT binary patch
literal 971
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
U26jJUkpX3Rn=qjlLq_
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 {
- const setupWorkspaceActivationV2Case = (loc: AppPath) => {
+ const setupOnboardingV2Case = (
+ loc: AppPath,
+ onboardingStatus: OnboardingStatus,
+ ) => {
setupMockIsMatchingLocation(loc);
- setupMockOnboardingStatus(OnboardingStatus.WORKSPACE_ACTIVATION);
+ setupMockOnboardingStatus(onboardingStatus);
setupMockIsWorkspaceActivationStatusEqualsTo(false);
setupMockHasAccessTokenPair(true);
setupMockIsOnAWorkspace(true);
@@ -477,7 +480,10 @@ describe('usePageChangeEffectNavigateLocation — onboarding V2', () => {
};
it('routes to WorkspaceActivationV2 when onboardingV2 is active and status is WORKSPACE_ACTIVATION', () => {
- setupWorkspaceActivationV2Case(AppPath.SignInUpV2);
+ setupOnboardingV2Case(
+ AppPath.SignInUpV2,
+ OnboardingStatus.WORKSPACE_ACTIVATION,
+ );
expect(usePageChangeEffectNavigateLocation()).toEqual(
AppPath.WorkspaceActivationV2,
@@ -485,7 +491,22 @@ describe('usePageChangeEffectNavigateLocation — onboarding V2', () => {
});
it('does not redirect away from the WorkspaceActivationV2 page during activation', () => {
- setupWorkspaceActivationV2Case(AppPath.WorkspaceActivationV2);
+ setupOnboardingV2Case(
+ AppPath.WorkspaceActivationV2,
+ OnboardingStatus.WORKSPACE_ACTIVATION,
+ );
+
+ expect(usePageChangeEffectNavigateLocation()).toBeUndefined();
+ });
+
+ it('routes to SyncEmailsV2 when onboardingV2 is active and status is SYNC_EMAIL', () => {
+ setupOnboardingV2Case(AppPath.Index, OnboardingStatus.SYNC_EMAIL);
+
+ expect(usePageChangeEffectNavigateLocation()).toEqual(AppPath.SyncEmailsV2);
+ });
+
+ it('does not redirect away from the SyncEmailsV2 page', () => {
+ setupOnboardingV2Case(AppPath.SyncEmailsV2, OnboardingStatus.SYNC_EMAIL);
expect(usePageChangeEffectNavigateLocation()).toBeUndefined();
});
diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts
index fa569582d7..84c4b6fe71 100644
--- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts
+++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts
@@ -140,9 +140,9 @@ export const usePageChangeEffectNavigateLocation = () => {
if (
onboardingStatus === OnboardingStatus.SYNC_EMAIL &&
- !isMatchingLocation(location, AppPath.SyncEmails)
+ !someMatchingLocationOf([AppPath.SyncEmails, AppPath.SyncEmailsV2])
) {
- return AppPath.SyncEmails;
+ return isOnboardingV2 ? AppPath.SyncEmailsV2 : AppPath.SyncEmails;
}
if (
diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx
index fceea53e90..b84f9c2cc9 100644
--- a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx
+++ b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx
@@ -78,6 +78,12 @@ const SyncEmails = lazy(() =>
})),
);
+const SyncEmailsV2 = lazy(() =>
+ import('~/pages/onboarding/SyncEmailsV2').then((module) => ({
+ default: module.SyncEmailsV2,
+ })),
+);
+
const InviteTeam = lazy(() =>
import('~/pages/onboarding/InviteTeam').then((module) => ({
default: module.InviteTeam,
@@ -285,6 +291,14 @@ export const useCreateAppRouter = (
}
/>
+
+
+
+ }
+ />
void;
-};
-
-export const SignInUpV2Header = ({ onBack }: SignInUpV2HeaderProps) => {
- const { t } = useLingui();
-
- return (
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Header.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Header.tsx
new file mode 100644
index 0000000000..321926c8a2
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Header.tsx
@@ -0,0 +1,145 @@
+import { styled } from '@linaria/react';
+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;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: space-between;
+ padding: ${themeCssVariables.spacing[8]};
+ width: 100%;
+`;
+
+const StyledSide = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1 1 0;
+ min-width: 0;
+`;
+
+const StyledLeftSide = styled(StyledSide)`
+ justify-content: flex-end;
+ padding-right: ${themeCssVariables.spacing[1]};
+`;
+
+const StyledCenter = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 0 1 ${HEADER_CENTER_WIDTH}px;
+ justify-content: flex-start;
+ min-width: 0;
+`;
+
+const StyledRightSide = styled(StyledSide)`
+ justify-content: flex-end;
+`;
+
+const StyledLogo = styled.div`
+ background-image: url('/images/integrations/twenty-logo.svg');
+ background-size: cover;
+ height: ${themeCssVariables.spacing[6]};
+ opacity: 0.4;
+ width: ${themeCssVariables.spacing[6]};
+`;
+
+const StyledFreeCredits = styled.div`
+ align-items: center;
+ color: ${themeCssVariables.font.color.tertiary};
+ display: flex;
+`;
+
+const StyledCreditsTag = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.tertiary};
+ border-bottom: 1px solid ${themeCssVariables.border.color.light};
+ border-bottom-left-radius: ${themeCssVariables.border.radius.pill};
+ border-left: 1px solid ${themeCssVariables.border.color.light};
+ border-top: 1px solid ${themeCssVariables.border.color.light};
+ border-top-left-radius: ${themeCssVariables.border.radius.pill};
+ box-sizing: border-box;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ height: ${themeCssVariables.spacing[6]};
+ padding: 0 ${themeCssVariables.spacing[2]} 0
+ ${themeCssVariables.spacing['1.5']};
+`;
+
+const StyledCreditsCount = styled.span`
+ font-size: ${themeCssVariables.font.size.md};
+ font-weight: ${themeCssVariables.font.weight.medium};
+`;
+
+const StyledCreditsLabel = styled.span`
+ font-size: ${themeCssVariables.font.size.sm};
+`;
+
+const StyledInfoTag = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.tertiary};
+ border: 1px solid ${themeCssVariables.border.color.light};
+ border-bottom-right-radius: ${themeCssVariables.border.radius.rounded};
+ border-top-right-radius: ${themeCssVariables.border.radius.rounded};
+ box-sizing: border-box;
+ display: flex;
+ height: ${themeCssVariables.spacing[6]};
+ justify-content: center;
+ padding: 0 ${themeCssVariables.spacing['1.5']};
+`;
+
+type OnboardingV2HeaderProps = {
+ onBack?: () => void;
+ freeCredits?: number;
+};
+
+export const OnboardingV2Header = ({
+ onBack,
+ freeCredits,
+}: OnboardingV2HeaderProps) => {
+ const { t } = useLingui();
+ const theme = useTheme();
+
+ return (
+
+
+ {isDefined(onBack) && (
+
+ )}
+
+
+
+
+
+ {isDefined(freeCredits) && (
+
+
+
+ {freeCredits}
+ {t`free credits`}
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Layout.tsx b/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Layout.tsx
new file mode 100644
index 0000000000..b51b6f5f23
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/OnboardingV2Layout.tsx
@@ -0,0 +1,29 @@
+import { OnboardingV2Header } from '@/onboarding/components/OnboardingV2Header';
+import { styled } from '@linaria/react';
+import { type ReactNode } from 'react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledBackground = styled.div`
+ background: ${themeCssVariables.background.secondary};
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ width: 100%;
+`;
+
+type OnboardingV2LayoutProps = {
+ children: ReactNode;
+ onBack?: () => void;
+ freeCredits?: number;
+};
+
+export const OnboardingV2Layout = ({
+ children,
+ onBack,
+ freeCredits,
+}: OnboardingV2LayoutProps) => (
+
+
+ {children}
+
+);
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
new file mode 100644
index 0000000000..862b32fa4e
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingCreditsRewardTag.tsx
@@ -0,0 +1,50 @@
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import { IconCoins } from 'twenty-ui/icon';
+import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants';
+
+const StyledTag = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.color.green3};
+ border: 1px solid ${themeCssVariables.color.green4};
+ border-radius: ${themeCssVariables.border.radius.pill};
+ box-sizing: border-box;
+ color: ${themeCssVariables.color.green9};
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ height: ${themeCssVariables.spacing[6]};
+ padding: 0 ${themeCssVariables.spacing[2]} 0
+ ${themeCssVariables.spacing['1.5']};
+`;
+
+const StyledLabel = styled.span`
+ font-size: ${themeCssVariables.font.size.md};
+ font-weight: ${themeCssVariables.font.weight.medium};
+`;
+
+const StyledSuffix = styled.span`
+ font-size: ${themeCssVariables.font.size.sm};
+ font-weight: ${themeCssVariables.font.weight.regular};
+`;
+
+type OnboardingCreditsRewardTagProps = {
+ amount: number;
+};
+
+export const OnboardingCreditsRewardTag = ({
+ amount,
+}: OnboardingCreditsRewardTagProps) => {
+ const { t } = useLingui();
+ const theme = useTheme();
+
+ return (
+
+
+ {t`Earn +${amount}`}
+ {t`free credits`}
+
+ );
+};
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
new file mode 100644
index 0000000000..c77aed6bca
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreview.tsx
@@ -0,0 +1,43 @@
+import { OnboardingImportPreviewCompanies } from '@/onboarding/components/import-contacts/OnboardingImportPreviewCompanies';
+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 { styled } from '@linaria/react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const PREVIEW_WIDTH = 340;
+const PREVIEW_HEIGHT = 200;
+
+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};
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[3]};
+ overflow: hidden;
+ padding-bottom: ${themeCssVariables.spacing[3]};
+ width: ${PREVIEW_WIDTH}px;
+`;
+
+const StyledColumns = styled.div`
+ background-color: ${themeCssVariables.border.color.medium};
+ display: flex;
+ gap: 1px;
+ height: ${PREVIEW_HEIGHT}px;
+ position: relative;
+ width: 100%;
+`;
+
+export const OnboardingImportPreview = () => (
+
+
+
+
+
+
+
+
+);
diff --git a/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewCompanies.tsx b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewCompanies.tsx
new file mode 100644
index 0000000000..6df7d2653a
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewCompanies.tsx
@@ -0,0 +1,98 @@
+import { IMPORT_CONTACTS_PREVIEW_COMPANIES } from '@/onboarding/constants/ImportContactsPreviewCompanies';
+import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import { getLogoUrlFromDomainName } from 'twenty-shared/utils';
+import {
+ Avatar,
+ Chip,
+ ChipAccent,
+ ChipSize,
+ ChipVariant,
+} from 'twenty-ui/data-display';
+import { IconBuildingSkyscraper, IconPlus } from 'twenty-ui/icon';
+import { Checkbox } from 'twenty-ui/input';
+import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants';
+
+const PREVIEW_ROW_HEIGHT = 32;
+
+const StyledColumn = styled.div`
+ background-color: ${themeCssVariables.border.color.light};
+ box-sizing: border-box;
+ display: flex;
+ flex: 1 1 0;
+ flex-direction: column;
+ gap: 1px;
+ height: 100%;
+ min-width: 0;
+ overflow: hidden;
+`;
+
+const StyledRow = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.primary};
+ box-sizing: border-box;
+ display: flex;
+ flex-shrink: 0;
+ gap: ${themeCssVariables.spacing[1]};
+ height: ${PREVIEW_ROW_HEIGHT}px;
+ padding: 0 ${themeCssVariables.spacing[1]};
+`;
+
+const StyledHeaderTitle = styled.div`
+ align-items: center;
+ color: ${themeCssVariables.font.color.tertiary};
+ display: flex;
+ flex: 1 1 0;
+ font-size: ${themeCssVariables.font.size.md};
+ font-weight: ${themeCssVariables.font.weight.medium};
+ gap: ${themeCssVariables.spacing[1]};
+ min-width: 0;
+`;
+
+export const OnboardingImportPreviewCompanies = () => {
+ const { t } = useLingui();
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+ {t`Companies`}
+
+
+
+ {IMPORT_CONTACTS_PREVIEW_COMPANIES.map((company) => (
+
+
+
+ }
+ />
+
+ ))}
+
+ );
+};
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
new file mode 100644
index 0000000000..780b5d3577
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewEmails.tsx
@@ -0,0 +1,162 @@
+import {
+ type ImportContactsPreviewCalendarEvent,
+ IMPORT_CONTACTS_PREVIEW_CALENDAR_EVENTS,
+} from '@/onboarding/constants/ImportContactsPreviewCalendarEvents';
+import {
+ type ImportContactsPreviewEmail,
+ IMPORT_CONTACTS_PREVIEW_EMAILS,
+} from '@/onboarding/constants/ImportContactsPreviewEmails';
+import { styled } from '@linaria/react';
+import { IconStar } from 'twenty-ui/icon';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const EMAIL_ROW_HEIGHT = 32;
+const EMAIL_CHECKBOX_SIZE = 12;
+const EMAIL_STAR_SIZE = 11;
+
+const StyledColumn = styled.div`
+ background-color: ${themeCssVariables.border.color.light};
+ box-sizing: border-box;
+ display: flex;
+ flex: 1 1 0;
+ flex-direction: column;
+ gap: 1px;
+ height: 100%;
+ min-width: 0;
+ overflow: hidden;
+ position: relative;
+`;
+
+const StyledEmailRow = styled.div<{ isUnread: boolean }>`
+ align-items: center;
+ background-color: ${themeCssVariables.background.primary};
+ box-sizing: border-box;
+ color: ${({ isUnread }) =>
+ isUnread
+ ? themeCssVariables.font.color.primary
+ : themeCssVariables.font.color.tertiary};
+ display: flex;
+ flex-shrink: 0;
+ font-size: ${themeCssVariables.font.size.xs};
+ gap: ${themeCssVariables.spacing[2]};
+ height: ${EMAIL_ROW_HEIGHT}px;
+ padding: 0 ${themeCssVariables.spacing[3]};
+ white-space: nowrap;
+`;
+
+const StyledEmailCheckbox = styled.div`
+ border: 1px solid ${themeCssVariables.font.color.light};
+ border-radius: ${themeCssVariables.border.radius.xs};
+ box-sizing: border-box;
+ flex-shrink: 0;
+ height: ${EMAIL_CHECKBOX_SIZE}px;
+ width: ${EMAIL_CHECKBOX_SIZE}px;
+`;
+
+const StyledEmailStar = styled.div`
+ align-items: center;
+ display: flex;
+ flex-shrink: 0;
+`;
+
+const StyledEmailSender = styled.span<{ isUnread: boolean }>`
+ font-weight: ${({ isUnread }) =>
+ isUnread
+ ? themeCssVariables.font.weight.medium
+ : themeCssVariables.font.weight.regular};
+`;
+
+const StyledEmailSubject = styled.span`
+ color: ${themeCssVariables.font.color.tertiary};
+`;
+
+const StyledEventCard = styled.div<{ color: 'yellow' | 'blue' }>`
+ background-color: ${({ color }) =>
+ color === 'yellow'
+ ? themeCssVariables.color.yellow3
+ : themeCssVariables.color.blue3};
+ border-left: 2px solid
+ ${({ color }) =>
+ color === 'yellow'
+ ? themeCssVariables.color.yellow8
+ : themeCssVariables.color.blue8};
+ border-radius: ${themeCssVariables.border.radius.sm};
+ box-shadow: ${themeCssVariables.boxShadow.light};
+ box-sizing: border-box;
+ color: ${({ color }) =>
+ color === 'yellow'
+ ? themeCssVariables.color.yellow11
+ : themeCssVariables.color.blue11};
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[1]};
+ padding: ${themeCssVariables.spacing[2]};
+ position: absolute;
+ width: 160px;
+`;
+
+const StyledEventTitle = styled.span`
+ font-size: ${themeCssVariables.font.size.sm};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+`;
+
+const StyledEventTime = styled.span`
+ font-size: ${themeCssVariables.font.size.xs};
+`;
+
+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 },
+};
+
+const EmailRow = ({ email }: { email: ImportContactsPreviewEmail }) => (
+
+
+
+
+
+
+ {email.sender}
+
+ {email.subject}
+
+);
+
+const EventCard = ({
+ event,
+}: {
+ event: ImportContactsPreviewCalendarEvent;
+}) => {
+ const position = EVENT_CARD_POSITIONS[event.color];
+
+ return (
+
+ {event.title}
+ {event.time}
+
+ );
+};
+
+export const OnboardingImportPreviewEmails = () => (
+
+ {IMPORT_CONTACTS_PREVIEW_EMAILS.map((email) => (
+
+ ))}
+ {IMPORT_CONTACTS_PREVIEW_CALENDAR_EVENTS.map((event) => (
+
+ ))}
+
+);
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
new file mode 100644
index 0000000000..25a434f261
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPreviewSyncBadge.tsx
@@ -0,0 +1,51 @@
+import { styled } from '@linaria/react';
+import { IconArrowRight, IconGoogle, IconMicrosoft } from 'twenty-ui/icon';
+import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants';
+
+const SYNC_BADGE_LOGO_SIZE = 16;
+
+const StyledBadge = styled.div`
+ align-items: center;
+ 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;
+ box-shadow: ${themeCssVariables.boxShadow.strong};
+ box-sizing: border-box;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+ left: 50%;
+ padding: ${themeCssVariables.spacing[2]};
+ position: absolute;
+ top: 84px;
+ transform: translateX(-50%);
+`;
+
+const StyledDivider = styled.div`
+ align-self: stretch;
+ background-color: ${themeCssVariables.border.color.medium};
+ width: 1px;
+`;
+
+const StyledTwentyLogo = styled.img`
+ height: ${SYNC_BADGE_LOGO_SIZE}px;
+ width: ${SYNC_BADGE_LOGO_SIZE}px;
+`;
+
+export const OnboardingImportPreviewSyncBadge = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+
+ );
+};
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
new file mode 100644
index 0000000000..cdaf05080c
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingImportPrivacyNote.tsx
@@ -0,0 +1,28 @@
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import { IconLock } from 'twenty-ui/icon';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const PRIVACY_NOTE_ICON_SIZE = 12;
+
+const StyledNote = styled.div`
+ align-items: center;
+ color: ${themeCssVariables.font.color.tertiary};
+ display: flex;
+ font-size: ${themeCssVariables.font.size.sm};
+ gap: ${themeCssVariables.spacing[1]};
+`;
+
+export const OnboardingImportPrivacyNote = () => {
+ const { t } = useLingui();
+
+ return (
+
+
+ {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
new file mode 100644
index 0000000000..e4d58eb1a1
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/components/import-contacts/OnboardingTrustBadges.tsx
@@ -0,0 +1,93 @@
+import { styled } from '@linaria/react';
+import { type ReactNode } from 'react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const BADGE_ASSET_PATH = '/images/onboarding/trust-badges';
+
+const TRUSTED_BY_LOGOS = [
+ { name: 'Bayer', src: `${BADGE_ASSET_PATH}/bayer.png` },
+ { name: 'PwC', src: `${BADGE_ASSET_PATH}/pwc.png` },
+ {
+ name: 'République Française',
+ src: `${BADGE_ASSET_PATH}/french-republic.png`,
+ },
+];
+
+const StyledRow = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+ justify-content: center;
+`;
+
+const StyledBadge = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.primary};
+ border: 1px solid ${themeCssVariables.border.color.medium};
+ border-radius: ${themeCssVariables.border.radius.pill};
+ box-sizing: border-box;
+ color: ${themeCssVariables.font.color.light};
+ display: flex;
+ font-size: ${themeCssVariables.font.size.xs};
+ font-weight: ${themeCssVariables.font.weight.semiBold};
+ gap: ${themeCssVariables.spacing[1]};
+ height: ${themeCssVariables.spacing[7]};
+ padding: 0 ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[1]};
+`;
+
+const StyledSeal = styled.img`
+ height: 20px;
+ object-fit: contain;
+ width: 20px;
+`;
+
+const StyledLogoCluster = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+`;
+
+const StyledClusterLogo = styled.img`
+ height: 18px;
+ object-fit: contain;
+ width: auto;
+`;
+
+const StyledBadgeLabel = styled.span`
+ white-space: nowrap;
+`;
+
+type TrustBadgeProps = {
+ label: string;
+ leading: ReactNode;
+};
+
+const TrustBadge = ({ label, leading }: TrustBadgeProps) => (
+
+ {leading}
+ {label}
+
+);
+
+export const OnboardingTrustBadges = () => (
+
+ }
+ />
+
+ {TRUSTED_BY_LOGOS.map((logo) => (
+
+ ))}
+
+ }
+ />
+ }
+ />
+
+);
diff --git a/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts
new file mode 100644
index 0000000000..daec8ac036
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCalendarEvents.ts
@@ -0,0 +1,21 @@
+export type ImportContactsPreviewCalendarEvent = {
+ id: string;
+ title: string;
+ time: string;
+ color: 'yellow' | 'blue';
+};
+
+export const IMPORT_CONTACTS_PREVIEW_CALENDAR_EVENTS = [
+ {
+ id: 'tim-apple-anthropic',
+ title: 'Tim Apple x Anthropic',
+ time: '10:00am',
+ color: 'yellow',
+ },
+ {
+ id: 'dario-amodei',
+ title: 'Dario Amodei',
+ time: '3:00pm',
+ color: 'blue',
+ },
+] satisfies ImportContactsPreviewCalendarEvent[];
diff --git a/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCompanies.ts b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCompanies.ts
new file mode 100644
index 0000000000..159e5a4ec7
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewCompanies.ts
@@ -0,0 +1,14 @@
+export type ImportContactsPreviewCompany = {
+ id: string;
+ name: string;
+ domainName: string;
+};
+
+export const IMPORT_CONTACTS_PREVIEW_COMPANIES = [
+ { id: 'figma', name: 'Figma', domainName: 'figma.com' },
+ { id: 'anthropic', name: 'Anthropic', domainName: 'anthropic.com' },
+ { id: 'notion', name: 'Notion', domainName: 'notion.so' },
+ { id: 'airbnb', name: 'Airbnb', domainName: 'airbnb.com' },
+ { id: 'linkedin', name: 'LinkedIn', domainName: 'linkedin.com' },
+ { id: 'slack', name: 'Slack', domainName: 'slack.com' },
+] satisfies ImportContactsPreviewCompany[];
diff --git a/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewEmails.ts b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewEmails.ts
new file mode 100644
index 0000000000..d8b0843208
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/constants/ImportContactsPreviewEmails.ts
@@ -0,0 +1,54 @@
+export type ImportContactsPreviewEmail = {
+ id: string;
+ sender: string;
+ subject: string;
+ date: string;
+ isUnread: boolean;
+};
+
+export const IMPORT_CONTACTS_PREVIEW_EMAILS = [
+ {
+ id: 'acme',
+ sender: 'Acme Inc.',
+ subject: 'Insights: The latest in industrial equipment and tools',
+ date: 'Feb, 26',
+ isUnread: false,
+ },
+ {
+ id: 'dylan-field',
+ sender: 'Dylan Field',
+ subject:
+ 'Lorem ipsum dolor sit amet consectetur. Ac eget eu eget ullamcorper tellus sem scelerisque sit ante.',
+ date: '12/12/23',
+ isUnread: false,
+ },
+ {
+ id: 'dario-amodei',
+ sender: 'Dario Amodei',
+ subject:
+ 'Delta Weekly News: Learn about important safety tips before you fly!',
+ date: 'Jan, 26',
+ isUnread: true,
+ },
+ {
+ id: 'ivan-zhao',
+ sender: 'Ivan Zhao',
+ subject: 'Our latest Adventures and Destinations',
+ date: 'March, 26',
+ isUnread: false,
+ },
+ {
+ id: 'brian-chesky',
+ sender: 'Brian Chesky',
+ subject: 'Insights: Industry trends and best practices',
+ date: 'Jan, 26',
+ isUnread: false,
+ },
+ {
+ id: 'stewart-butterfield',
+ sender: 'Stewart Butterfield',
+ subject: 'Our Complete list of Recipe Ideas and Restaurant Reviews!',
+ date: 'Jan, 26',
+ isUnread: false,
+ },
+] satisfies ImportContactsPreviewEmail[];
diff --git a/packages/twenty-front/src/modules/onboarding/effect-components/SyncEmailsAutoSkipEffect.tsx b/packages/twenty-front/src/modules/onboarding/effect-components/SyncEmailsAutoSkipEffect.tsx
new file mode 100644
index 0000000000..d72ddcaaae
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/effect-components/SyncEmailsAutoSkipEffect.tsx
@@ -0,0 +1,34 @@
+import { useSkipSyncEmailOnboardingStep } from '@/onboarding/hooks/useSkipSyncEmailOnboardingStep';
+import { useEffect, useRef } from 'react';
+
+type SyncEmailsAutoSkipEffectProps = {
+ onError: () => void;
+};
+
+export const SyncEmailsAutoSkipEffect = ({
+ onError,
+}: SyncEmailsAutoSkipEffectProps) => {
+ const skipSyncEmailOnboardingStep = useSkipSyncEmailOnboardingStep();
+
+ // 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 skipSyncEmailOnboardingStep();
+ } catch {
+ onError();
+ }
+ };
+
+ void skip();
+ }, [skipSyncEmailOnboardingStep, onError]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/onboarding/hooks/useSkipSyncEmailOnboardingStep.ts b/packages/twenty-front/src/modules/onboarding/hooks/useSkipSyncEmailOnboardingStep.ts
new file mode 100644
index 0000000000..714f102dbc
--- /dev/null
+++ b/packages/twenty-front/src/modules/onboarding/hooks/useSkipSyncEmailOnboardingStep.ts
@@ -0,0 +1,16 @@
+import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
+import { useMutation } from '@apollo/client/react';
+import { useCallback } from 'react';
+import { SkipSyncEmailOnboardingStepDocument } from '~/generated-metadata/graphql';
+
+export const useSkipSyncEmailOnboardingStep = () => {
+ const setNextOnboardingStatus = useSetNextOnboardingStatus();
+ const [skipSyncEmailOnboardingStepMutation] = useMutation(
+ SkipSyncEmailOnboardingStepDocument,
+ );
+
+ return useCallback(async () => {
+ await skipSyncEmailOnboardingStepMutation();
+ setNextOnboardingStatus();
+ }, [skipSyncEmailOnboardingStepMutation, setNextOnboardingStatus]);
+};
diff --git a/packages/twenty-front/src/pages/auth/SignInUpV2.tsx b/packages/twenty-front/src/pages/auth/SignInUpV2.tsx
index 8333c6fe1d..186c14ae33 100644
--- a/packages/twenty-front/src/pages/auth/SignInUpV2.tsx
+++ b/packages/twenty-front/src/pages/auth/SignInUpV2.tsx
@@ -16,7 +16,7 @@ import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGl
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 { SignInUpV2Header } from '@/auth/sign-in-up/components/internal/SignInUpV2Header';
+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';
@@ -188,21 +188,20 @@ export const SignInUpV2 = () => {
workspacePublicData,
]);
- return (
+ return signInUpStep === SignInUpStep.WorkspaceCreation ? (
+
+
+ {signInUpForm}
+
+
+ ) : (
{signInUpStep === SignInUpStep.EmailVerification ? (
- ) : signInUpStep === SignInUpStep.WorkspaceCreation ? (
- <>
- {!isCreatingWorkspace ? (
-
- ) : null}
-
- {signInUpForm}
-
- >
) : (
void;
+ onContinueWithMicrosoft?: () => void;
+ onSkip?: () => void;
+};
+
+export const ImportContacts = ({
+ creditsReward = DEFAULT_CREDITS_REWARD,
+ onContinueWithGoogle,
+ onContinueWithMicrosoft,
+ onSkip,
+}: ImportContactsProps) => {
+ const { t } = useLingui();
+ const theme = useTheme();
+
+ return (
+
+
+ {t`Import your contacts`}
+
+ {t`Connect your email and calendar to see your entire network instantly. Takes only 30 seconds.`}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isDefined(onContinueWithMicrosoft) && (
+ }
+ />
+ )}
+ {isDefined(onContinueWithGoogle) && (
+ }
+ />
+ )}
+
+ {isDefined(onSkip) && (
+
+ {t`Skip`}
+
+ )}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/onboarding/SyncEmailsV2.tsx b/packages/twenty-front/src/pages/onboarding/SyncEmailsV2.tsx
new file mode 100644
index 0000000000..af9b387b59
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/SyncEmailsV2.tsx
@@ -0,0 +1,86 @@
+import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
+import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
+import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState';
+import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicrosoftCalendarEnabledState';
+import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
+import { OnboardingV2Layout } from '@/onboarding/components/OnboardingV2Layout';
+import { SyncEmailsAutoSkipEffect } from '@/onboarding/effect-components/SyncEmailsAutoSkipEffect';
+import { useSkipSyncEmailOnboardingStep } from '@/onboarding/hooks/useSkipSyncEmailOnboardingStep';
+import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useCallback, useState } from 'react';
+import { AppPath, ConnectedAccountProvider } from 'twenty-shared/types';
+import { ImportContacts } from '~/pages/onboarding/ImportContacts';
+import {
+ CalendarChannelVisibility,
+ MessageChannelVisibility,
+} from '~/generated/graphql';
+
+const IMPORT_CONTACTS_FREE_CREDITS = 0;
+
+export const SyncEmailsV2 = () => {
+ const { triggerApisOAuth } = useTriggerApisOAuth();
+ const skipSyncEmailOnboardingStep = useSkipSyncEmailOnboardingStep();
+ const [hasAutoSkipFailed, setHasAutoSkipFailed] = useState(false);
+
+ const isGoogleMessagingEnabled = useAtomStateValue(
+ isGoogleMessagingEnabledState,
+ );
+ const isMicrosoftMessagingEnabled = useAtomStateValue(
+ isMicrosoftMessagingEnabledState,
+ );
+ const isGoogleCalendarEnabled = useAtomStateValue(
+ isGoogleCalendarEnabledState,
+ );
+ const isMicrosoftCalendarEnabled = useAtomStateValue(
+ isMicrosoftCalendarEnabledState,
+ );
+
+ const isGoogleProviderEnabled =
+ isGoogleMessagingEnabled || isGoogleCalendarEnabled;
+ const isMicrosoftProviderEnabled =
+ isMicrosoftMessagingEnabled || isMicrosoftCalendarEnabled;
+ const hasProviderEnabled =
+ isGoogleProviderEnabled || isMicrosoftProviderEnabled;
+ const isClientConfigLoaded = useAtomStateValue(
+ clientConfigApiStatusState,
+ ).isLoadedOnce;
+
+ const connectWithProvider = (provider: ConnectedAccountProvider) =>
+ triggerApisOAuth(provider, {
+ redirectLocation: AppPath.Index,
+ messageVisibility: MessageChannelVisibility.METADATA,
+ calendarVisibility: CalendarChannelVisibility.METADATA,
+ skipMessageChannelConfiguration: true,
+ });
+
+ const handleAutoSkipError = useCallback(() => {
+ setHasAutoSkipFailed(true);
+ }, []);
+
+ if (!isClientConfigLoaded) {
+ return null;
+ }
+
+ if (!hasProviderEnabled && !hasAutoSkipFailed) {
+ return ;
+ }
+
+ return (
+
+ connectWithProvider(ConnectedAccountProvider.GOOGLE)
+ : undefined
+ }
+ onContinueWithMicrosoft={
+ isMicrosoftProviderEnabled
+ ? () => connectWithProvider(ConnectedAccountProvider.MICROSOFT)
+ : undefined
+ }
+ onSkip={skipSyncEmailOnboardingStep}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/onboarding/__stories__/ImportContacts.stories.tsx b/packages/twenty-front/src/pages/onboarding/__stories__/ImportContacts.stories.tsx
new file mode 100644
index 0000000000..74625a05ea
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/__stories__/ImportContacts.stories.tsx
@@ -0,0 +1,43 @@
+import { styled } from '@linaria/react';
+import { type Meta, type StoryObj } from '@storybook/react-vite';
+import { action } from 'storybook/actions';
+import { within } from 'storybook/test';
+
+import { ImportContacts } from '~/pages/onboarding/ImportContacts';
+
+const StyledViewport = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ width: 100%;
+`;
+
+const meta: Meta = {
+ title: 'Pages/Onboarding/ImportContacts',
+ component: ImportContacts,
+ parameters: { layout: 'fullscreen' },
+ args: {
+ creditsReward: 2,
+ onContinueWithGoogle: action('continue-with-google'),
+ onContinueWithMicrosoft: action('continue-with-microsoft'),
+ onSkip: action('skip'),
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await canvas.findByText('Import your contacts');
+ },
+};
diff --git a/packages/twenty-front/src/pages/onboarding/__stories__/SyncEmailsV2.stories.tsx b/packages/twenty-front/src/pages/onboarding/__stories__/SyncEmailsV2.stories.tsx
new file mode 100644
index 0000000000..5a3f03448c
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/__stories__/SyncEmailsV2.stories.tsx
@@ -0,0 +1,49 @@
+import { getOperationName } from '~/utils/getOperationName';
+import { type Meta, type StoryObj } from '@storybook/react-vite';
+import { HttpResponse, graphql } from 'msw';
+import { within } from 'storybook/test';
+import { AppPath } from 'twenty-shared/types';
+
+import { OnboardingStatus } from '~/generated-metadata/graphql';
+import { GET_CURRENT_USER } from '~/modules/users/graphql/queries/getCurrentUser';
+import { SyncEmailsV2 } from '~/pages/onboarding/SyncEmailsV2';
+import {
+ PageDecorator,
+ type PageDecoratorArgs,
+} from '~/testing/decorators/PageDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
+import { mockedOnboardingUserData } from '~/testing/mock-data/users';
+
+const meta: Meta = {
+ title: 'Pages/Onboarding/SyncEmailsV2',
+ component: SyncEmailsV2,
+ decorators: [PageDecorator],
+ args: { routePath: AppPath.SyncEmailsV2 },
+ parameters: {
+ msw: {
+ handlers: [
+ graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
+ return HttpResponse.json({
+ data: {
+ currentUser: mockedOnboardingUserData(
+ OnboardingStatus.SYNC_EMAIL,
+ ),
+ },
+ });
+ }),
+ graphqlMocks.handlers,
+ ],
+ },
+ },
+};
+
+export default meta;
+
+export type Story = StoryObj;
+
+export const Default: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement.ownerDocument.body);
+ await canvas.findByText('Import your contacts');
+ },
+};
diff --git a/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts b/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts
index b3aa851250..db7a492c3a 100644
--- a/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts
+++ b/packages/twenty-front/src/testing/constants/UntestedAppPaths.ts
@@ -4,4 +4,5 @@ export const UNTESTED_APP_PATHS = [
AppPath.Settings,
AppPath.Developers,
AppPath.WorkspaceActivationV2,
+ AppPath.SyncEmailsV2,
];
diff --git a/packages/twenty-shared/src/types/AppPath.ts b/packages/twenty-shared/src/types/AppPath.ts
index 9da7e99096..b1c8eaa405 100644
--- a/packages/twenty-shared/src/types/AppPath.ts
+++ b/packages/twenty-shared/src/types/AppPath.ts
@@ -12,6 +12,7 @@ export enum AppPath {
WorkspaceActivationV2 = '/workspace-activation-v2',
CreateProfile = '/create/profile',
SyncEmails = '/sync/emails',
+ SyncEmailsV2 = '/sync/emails-v2',
InviteTeam = '/invite-team',
PlanRequired = '/plan-required',
PlanRequiredSuccess = '/plan-required/payment-success',