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 0000000000..29195168b1 Binary files /dev/null and b/packages/twenty-front/public/images/onboarding/trust-badges/bayer.png differ 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 0000000000..c6b61cea25 Binary files /dev/null and b/packages/twenty-front/public/images/onboarding/trust-badges/french-republic.png differ diff --git a/packages/twenty-front/public/images/onboarding/trust-badges/gdpr.png b/packages/twenty-front/public/images/onboarding/trust-badges/gdpr.png new file mode 100644 index 0000000000..05356805e7 Binary files /dev/null and b/packages/twenty-front/public/images/onboarding/trust-badges/gdpr.png differ diff --git a/packages/twenty-front/public/images/onboarding/trust-badges/pwc.png b/packages/twenty-front/public/images/onboarding/trust-badges/pwc.png new file mode 100644 index 0000000000..dfa683ecb9 Binary files /dev/null and b/packages/twenty-front/public/images/onboarding/trust-badges/pwc.png differ diff --git a/packages/twenty-front/public/images/onboarding/trust-badges/soc2.png b/packages/twenty-front/public/images/onboarding/trust-badges/soc2.png new file mode 100644 index 0000000000..38b7040b8d Binary files /dev/null and b/packages/twenty-front/public/images/onboarding/trust-badges/soc2.png differ diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index 20fcbcfa50..688a6565cd 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -458,9 +458,12 @@ describe('usePageChangeEffectNavigateLocation — authenticated with no current }); describe('usePageChangeEffectNavigateLocation — onboarding V2', () => { - 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',