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 CleanShot 2026-06-26 at 13 29
28@2x 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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 { - 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',