Add v2 onboarding import contacts page and unify the onboarding v2 shell (#22212)
<img width="3024" height="1668" alt="CleanShot 2026-06-26 at 13 29 28@2x" src="https://github.com/user-attachments/assets/9bdd0029-45eb-4ddb-859f-eaab9bb61406" /> 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.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 971 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
+25
-4
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 = (
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.SyncEmailsV2}
|
||||
element={
|
||||
<LazyRoute fallback={null}>
|
||||
<SyncEmailsV2 />
|
||||
</LazyRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={AppPath.Authorize}
|
||||
element={
|
||||
|
||||
@@ -5,6 +5,7 @@ export const ONBOARDING_PATHS = [
|
||||
AppPath.WorkspaceActivationV2,
|
||||
AppPath.CreateProfile,
|
||||
AppPath.SyncEmails,
|
||||
AppPath.SyncEmailsV2,
|
||||
AppPath.InviteTeam,
|
||||
AppPath.PlanRequired,
|
||||
AppPath.PlanRequiredSuccess,
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconChevronLeft } from 'twenty-ui/icon';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } 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 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]};
|
||||
`;
|
||||
|
||||
type SignInUpV2HeaderProps = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const SignInUpV2Header = ({ onBack }: SignInUpV2HeaderProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<StyledHeader>
|
||||
<StyledLeftSide>
|
||||
<LightIconButton
|
||||
Icon={IconChevronLeft}
|
||||
accent="tertiary"
|
||||
size="medium"
|
||||
onClick={onBack}
|
||||
aria-label={t`Go back`}
|
||||
/>
|
||||
</StyledLeftSide>
|
||||
<StyledCenter>
|
||||
<StyledLogo />
|
||||
</StyledCenter>
|
||||
<StyledSide />
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<StyledHeader>
|
||||
<StyledLeftSide>
|
||||
{isDefined(onBack) && (
|
||||
<LightIconButton
|
||||
Icon={IconChevronLeft}
|
||||
accent="tertiary"
|
||||
size="medium"
|
||||
onClick={onBack}
|
||||
aria-label={t`Go back`}
|
||||
/>
|
||||
)}
|
||||
</StyledLeftSide>
|
||||
<StyledCenter>
|
||||
<StyledLogo />
|
||||
</StyledCenter>
|
||||
<StyledRightSide>
|
||||
{isDefined(freeCredits) && (
|
||||
<StyledFreeCredits>
|
||||
<StyledCreditsTag>
|
||||
<IconCoins
|
||||
size={theme.icon.size.md}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
<StyledCreditsCount>{freeCredits}</StyledCreditsCount>
|
||||
<StyledCreditsLabel>{t`free credits`}</StyledCreditsLabel>
|
||||
</StyledCreditsTag>
|
||||
<StyledInfoTag>
|
||||
<IconInfoCircle
|
||||
size={theme.icon.size.md}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
</StyledInfoTag>
|
||||
</StyledFreeCredits>
|
||||
)}
|
||||
</StyledRightSide>
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
@@ -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) => (
|
||||
<StyledBackground>
|
||||
<OnboardingV2Header onBack={onBack} freeCredits={freeCredits} />
|
||||
{children}
|
||||
</StyledBackground>
|
||||
);
|
||||
+50
@@ -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 (
|
||||
<StyledTag>
|
||||
<IconCoins
|
||||
size={theme.icon.size.md}
|
||||
color={themeCssVariables.color.green9}
|
||||
/>
|
||||
<StyledLabel>{t`Earn +${amount}`}</StyledLabel>
|
||||
<StyledSuffix>{t`free credits`}</StyledSuffix>
|
||||
</StyledTag>
|
||||
);
|
||||
};
|
||||
+43
@@ -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 = () => (
|
||||
<StyledCard>
|
||||
<StyledColumns>
|
||||
<OnboardingImportPreviewEmails />
|
||||
<OnboardingImportPreviewCompanies />
|
||||
<OnboardingImportPreviewSyncBadge />
|
||||
</StyledColumns>
|
||||
<OnboardingImportPrivacyNote />
|
||||
</StyledCard>
|
||||
);
|
||||
+98
@@ -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 (
|
||||
<StyledColumn>
|
||||
<StyledRow>
|
||||
<Checkbox checked={false} hoverable />
|
||||
<StyledHeaderTitle>
|
||||
<IconBuildingSkyscraper
|
||||
size={theme.icon.size.md}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
{t`Companies`}
|
||||
</StyledHeaderTitle>
|
||||
<IconPlus
|
||||
size={theme.icon.size.md}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
</StyledRow>
|
||||
{IMPORT_CONTACTS_PREVIEW_COMPANIES.map((company) => (
|
||||
<StyledRow key={company.id}>
|
||||
<Checkbox checked={false} hoverable />
|
||||
<Chip
|
||||
label={company.name}
|
||||
size={ChipSize.Small}
|
||||
variant={ChipVariant.Transparent}
|
||||
accent={ChipAccent.TextPrimary}
|
||||
clickable={false}
|
||||
leftComponent={
|
||||
<Avatar
|
||||
type="squared"
|
||||
size="md"
|
||||
placeholder={company.name}
|
||||
placeholderColorSeed={company.id}
|
||||
avatarUrl={getAbsoluteImageUrl(
|
||||
getLogoUrlFromDomainName(company.domainName),
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledRow>
|
||||
))}
|
||||
</StyledColumn>
|
||||
);
|
||||
};
|
||||
+162
@@ -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 }) => (
|
||||
<StyledEmailRow isUnread={email.isUnread}>
|
||||
<StyledEmailCheckbox />
|
||||
<StyledEmailStar>
|
||||
<IconStar
|
||||
size={EMAIL_STAR_SIZE}
|
||||
color={themeCssVariables.font.color.light}
|
||||
/>
|
||||
</StyledEmailStar>
|
||||
<StyledEmailSender isUnread={email.isUnread}>
|
||||
{email.sender}
|
||||
</StyledEmailSender>
|
||||
<StyledEmailSubject>{email.subject}</StyledEmailSubject>
|
||||
</StyledEmailRow>
|
||||
);
|
||||
|
||||
const EventCard = ({
|
||||
event,
|
||||
}: {
|
||||
event: ImportContactsPreviewCalendarEvent;
|
||||
}) => {
|
||||
const position = EVENT_CARD_POSITIONS[event.color];
|
||||
|
||||
return (
|
||||
<StyledEventCard
|
||||
color={event.color}
|
||||
style={{
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
transform: `rotate(${position.rotate}deg)`,
|
||||
}}
|
||||
>
|
||||
<StyledEventTitle>{event.title}</StyledEventTitle>
|
||||
<StyledEventTime>{event.time}</StyledEventTime>
|
||||
</StyledEventCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const OnboardingImportPreviewEmails = () => (
|
||||
<StyledColumn>
|
||||
{IMPORT_CONTACTS_PREVIEW_EMAILS.map((email) => (
|
||||
<EmailRow key={email.id} email={email} />
|
||||
))}
|
||||
{IMPORT_CONTACTS_PREVIEW_CALENDAR_EVENTS.map((event) => (
|
||||
<EventCard key={event.id} event={event} />
|
||||
))}
|
||||
</StyledColumn>
|
||||
);
|
||||
+51
@@ -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 (
|
||||
<StyledBadge>
|
||||
<IconGoogle size={theme.icon.size.md} />
|
||||
<IconMicrosoft size={theme.icon.size.md} />
|
||||
<StyledDivider />
|
||||
<IconArrowRight
|
||||
size={theme.icon.size.md}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
<StyledTwentyLogo src="/images/integrations/twenty-logo.svg" alt="" />
|
||||
</StyledBadge>
|
||||
);
|
||||
};
|
||||
+28
@@ -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 (
|
||||
<StyledNote>
|
||||
<IconLock
|
||||
size={PRIVACY_NOTE_ICON_SIZE}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
{t`Only you will be able to see your emails and events`}
|
||||
</StyledNote>
|
||||
);
|
||||
};
|
||||
+93
@@ -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) => (
|
||||
<StyledBadge>
|
||||
{leading}
|
||||
<StyledBadgeLabel>{label}</StyledBadgeLabel>
|
||||
</StyledBadge>
|
||||
);
|
||||
|
||||
export const OnboardingTrustBadges = () => (
|
||||
<StyledRow>
|
||||
<TrustBadge
|
||||
label="SOC2"
|
||||
leading={<StyledSeal src={`${BADGE_ASSET_PATH}/soc2.png`} alt="" />}
|
||||
/>
|
||||
<TrustBadge
|
||||
label="+10k"
|
||||
leading={
|
||||
<StyledLogoCluster>
|
||||
{TRUSTED_BY_LOGOS.map((logo) => (
|
||||
<StyledClusterLogo key={logo.name} src={logo.src} alt={logo.name} />
|
||||
))}
|
||||
</StyledLogoCluster>
|
||||
}
|
||||
/>
|
||||
<TrustBadge
|
||||
label="GDPR"
|
||||
leading={<StyledSeal src={`${BADGE_ASSET_PATH}/gdpr.png`} alt="" />}
|
||||
/>
|
||||
</StyledRow>
|
||||
);
|
||||
+21
@@ -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[];
|
||||
+14
@@ -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[];
|
||||
@@ -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[];
|
||||
+34
@@ -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;
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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 ? (
|
||||
<OnboardingV2Layout
|
||||
onBack={!isCreatingWorkspace ? onClickOnLogo : undefined}
|
||||
>
|
||||
<ModalContent isVerticallyCentered isHorizontallyCentered>
|
||||
{signInUpForm}
|
||||
</ModalContent>
|
||||
</OnboardingV2Layout>
|
||||
) : (
|
||||
<StyledBackground>
|
||||
{signInUpStep === SignInUpStep.EmailVerification ? (
|
||||
<ModalContent isVerticallyCentered isHorizontallyCentered>
|
||||
<EmailVerificationSent email={searchParams.get('email')} />
|
||||
</ModalContent>
|
||||
) : signInUpStep === SignInUpStep.WorkspaceCreation ? (
|
||||
<>
|
||||
{!isCreatingWorkspace ? (
|
||||
<SignInUpV2Header onBack={onClickOnLogo} />
|
||||
) : null}
|
||||
<ModalContent isVerticallyCentered isHorizontallyCentered>
|
||||
{signInUpForm}
|
||||
</ModalContent>
|
||||
</>
|
||||
) : (
|
||||
<SignInUpV2StandardContent
|
||||
workspacePublicData={workspacePublicData}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { OnboardingCreditsRewardTag } from '@/onboarding/components/import-contacts/OnboardingCreditsRewardTag';
|
||||
import { OnboardingImportPreview } from '@/onboarding/components/import-contacts/OnboardingImportPreview';
|
||||
import { OnboardingTrustBadges } from '@/onboarding/components/import-contacts/OnboardingTrustBadges';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconGoogle, IconMicrosoft } from 'twenty-ui/icon';
|
||||
import { MainButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants';
|
||||
|
||||
const CONTENT_BLOCK_WIDTH = 340;
|
||||
const DEFAULT_CREDITS_REWARD = 2;
|
||||
|
||||
const StyledPage = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[14]};
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: ${themeCssVariables.spacing[16]} ${themeCssVariables.spacing[8]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
width: ${CONTENT_BLOCK_WIDTH}px;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h1`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.xl};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const StyledSubtitle = styled.p`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const StyledCreditsRow = styled.div`
|
||||
display: flex;
|
||||
padding-top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledMiddle = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[5]};
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
width: ${CONTENT_BLOCK_WIDTH}px;
|
||||
`;
|
||||
|
||||
const StyledButtons = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSkipButton = styled.button`
|
||||
background-color: transparent;
|
||||
border: 1px solid ${themeCssVariables.border.color.light};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
padding: 0 ${themeCssVariables.spacing[5]};
|
||||
`;
|
||||
|
||||
type ImportContactsProps = {
|
||||
creditsReward?: number;
|
||||
onContinueWithGoogle?: () => void;
|
||||
onContinueWithMicrosoft?: () => void;
|
||||
onSkip?: () => void;
|
||||
};
|
||||
|
||||
export const ImportContacts = ({
|
||||
creditsReward = DEFAULT_CREDITS_REWARD,
|
||||
onContinueWithGoogle,
|
||||
onContinueWithMicrosoft,
|
||||
onSkip,
|
||||
}: ImportContactsProps) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledPage>
|
||||
<StyledHeading>
|
||||
<StyledTitle>{t`Import your contacts`}</StyledTitle>
|
||||
<StyledSubtitle>
|
||||
{t`Connect your email and calendar to see your entire network instantly. Takes only 30 seconds.`}
|
||||
</StyledSubtitle>
|
||||
<StyledCreditsRow>
|
||||
<OnboardingCreditsRewardTag amount={creditsReward} />
|
||||
</StyledCreditsRow>
|
||||
</StyledHeading>
|
||||
|
||||
<StyledMiddle>
|
||||
<OnboardingTrustBadges />
|
||||
<OnboardingImportPreview />
|
||||
</StyledMiddle>
|
||||
|
||||
<StyledFooter>
|
||||
<StyledButtons>
|
||||
{isDefined(onContinueWithMicrosoft) && (
|
||||
<MainButton
|
||||
title={t`Continue with Microsoft`}
|
||||
fullWidth
|
||||
onClick={onContinueWithMicrosoft}
|
||||
Icon={() => <IconMicrosoft size={theme.icon.size.sm} />}
|
||||
/>
|
||||
)}
|
||||
{isDefined(onContinueWithGoogle) && (
|
||||
<MainButton
|
||||
title={t`Continue with Google`}
|
||||
fullWidth
|
||||
onClick={onContinueWithGoogle}
|
||||
Icon={() => <IconGoogle size={theme.icon.size.sm} />}
|
||||
/>
|
||||
)}
|
||||
</StyledButtons>
|
||||
{isDefined(onSkip) && (
|
||||
<StyledSkipButton type="button" onClick={onSkip}>
|
||||
{t`Skip`}
|
||||
</StyledSkipButton>
|
||||
)}
|
||||
</StyledFooter>
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
@@ -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 <SyncEmailsAutoSkipEffect onError={handleAutoSkipError} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<OnboardingV2Layout freeCredits={IMPORT_CONTACTS_FREE_CREDITS}>
|
||||
<ImportContacts
|
||||
onContinueWithGoogle={
|
||||
isGoogleProviderEnabled
|
||||
? () => connectWithProvider(ConnectedAccountProvider.GOOGLE)
|
||||
: undefined
|
||||
}
|
||||
onContinueWithMicrosoft={
|
||||
isMicrosoftProviderEnabled
|
||||
? () => connectWithProvider(ConnectedAccountProvider.MICROSOFT)
|
||||
: undefined
|
||||
}
|
||||
onSkip={skipSyncEmailOnboardingStep}
|
||||
/>
|
||||
</OnboardingV2Layout>
|
||||
);
|
||||
};
|
||||
@@ -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<typeof ImportContacts> = {
|
||||
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) => (
|
||||
<StyledViewport>
|
||||
<Story />
|
||||
</StyledViewport>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ImportContacts>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText('Import your contacts');
|
||||
},
|
||||
};
|
||||
@@ -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<PageDecoratorArgs> = {
|
||||
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<typeof SyncEmailsV2>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement.ownerDocument.body);
|
||||
await canvas.findByText('Import your contacts');
|
||||
},
|
||||
};
|
||||
@@ -4,4 +4,5 @@ export const UNTESTED_APP_PATHS = [
|
||||
AppPath.Settings,
|
||||
AppPath.Developers,
|
||||
AppPath.WorkspaceActivationV2,
|
||||
AppPath.SyncEmailsV2,
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user