poc - cal.com integration in onboarding flow (#12530)

This commit is contained in:
nitin
2025-06-19 15:27:38 +05:30
committed by GitHub
parent e4d44e9c39
commit a8fb039e65
36 changed files with 526 additions and 34 deletions

View File

@ -19,6 +19,8 @@ import { SignInUp } from '~/pages/auth/SignInUp';
import { NotFound } from '~/pages/not-found/NotFound';
import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage';
import { RecordShowPage } from '~/pages/object-record/RecordShowPage';
import { BookCall } from '~/pages/onboarding/BookCall';
import { BookCallDecision } from '~/pages/onboarding/BookCallDecision';
import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan';
import { CreateProfile } from '~/pages/onboarding/CreateProfile';
import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace';
@ -53,6 +55,11 @@ export const useCreateAppRouter = (
path={AppPath.PlanRequiredSuccess}
element={<PaymentSuccess />}
/>
<Route
path={AppPath.BookCallDecision}
element={<BookCallDecision />}
/>
<Route path={AppPath.BookCall} element={<BookCall />} />
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />

View File

@ -1,9 +1,11 @@
import { AuthModalMountEffect } from '@/auth/components/AuthModalMountEffect';
import { AUTH_MODAL_ID } from '@/auth/constants/AuthModalId';
import { getAuthModalConfig } from '@/auth/utils/getAuthModalConfig';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import React from 'react';
import { useLocation } from 'react-router-dom';
const StyledContent = styled.div`
align-items: center;
@ -14,13 +16,27 @@ type AuthModalProps = {
children: React.ReactNode;
};
export const AuthModal = ({ children }: AuthModalProps) => (
<>
<AuthModalMountEffect />
<Modal modalId={AUTH_MODAL_ID} padding={'none'} modalVariant="primary">
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>{children}</StyledContent>
</ScrollWrapper>
</Modal>
</>
);
export const AuthModal = ({ children }: AuthModalProps) => {
const location = useLocation();
const config = getAuthModalConfig(location);
return (
<>
<AuthModalMountEffect />
<Modal
modalId={AUTH_MODAL_ID}
padding={'none'}
size={config.size}
modalVariant={config.variant}
>
{config.showScrollWrapper ? (
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>{children}</StyledContent>
</ScrollWrapper>
) : (
<>{children}</>
)}
</Modal>
</>
);
};

View File

@ -0,0 +1,24 @@
import { AppPath } from '@/types/AppPath';
import { ModalSize, ModalVariants } from '@/ui/layout/modal/components/Modal';
type AuthModalConfigType = {
size: ModalSize;
variant: ModalVariants;
showScrollWrapper: boolean;
};
export const AUTH_MODAL_CONFIG: {
default: AuthModalConfigType;
[key: string]: AuthModalConfigType;
} = {
default: {
size: 'medium',
variant: 'primary',
showScrollWrapper: true,
},
[AppPath.BookCall]: {
size: 'extraLarge',
variant: 'transparent',
showScrollWrapper: false,
},
};

View File

@ -0,0 +1,18 @@
import { AUTH_MODAL_CONFIG } from '@/auth/constants/AuthModalConfig';
import { AppPath } from '@/types/AppPath';
import { Location } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const getAuthModalConfig = (location: Location) => {
for (const path of Object.values(AppPath)) {
if (
isMatchingLocation(location, path) &&
isDefined(AUTH_MODAL_CONFIG[path])
) {
return AUTH_MODAL_CONFIG[path];
}
}
return AUTH_MODAL_CONFIG.default;
};

View File

@ -2,6 +2,7 @@ import { useClientConfig } from '@/client-config/hooks/useClientConfig';
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { captchaState } from '@/client-config/states/captchaState';
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
@ -85,6 +86,10 @@ export const ClientConfigProviderEffect = () => {
isConfigVariablesInDbEnabledState,
);
const setCalendarBookingPageId = useSetRecoilState(
calendarBookingPageIdState,
);
const { data, loading, error, fetchClientConfig } = useClientConfig();
useEffect(() => {
@ -173,6 +178,8 @@ export const ClientConfigProviderEffect = () => {
...currentStatus,
isSaved: true,
}));
setCalendarBookingPageId(data?.clientConfig?.calendarBookingPageId ?? null);
}, [
data,
loading,
@ -198,6 +205,7 @@ export const ClientConfigProviderEffect = () => {
setGoogleCalendarEnabled,
setIsAttachmentPreviewEnabled,
setIsConfigVariablesInDbEnabled,
setCalendarBookingPageId,
]);
return <></>;

View File

@ -62,6 +62,7 @@ export const GET_CLIENT_CONFIG = gql`
isGoogleMessagingEnabled
isGoogleCalendarEnabled
isConfigVariablesInDbEnabled
calendarBookingPageId
}
}
`;

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui/utilities';
export const calendarBookingPageIdState = createState<string | null>({
key: 'calendarBookingPageIdState',
defaultValue: null,
});

View File

@ -0,0 +1 @@
export const BOOK_CALL_MODAL_ID = 'book-call-modal';

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const SKIP_BOOK_ONBOARDING_STEP = gql`
mutation SkipBookOnboardingStep {
skipBookOnboardingStep {
success
}
}
`;

View File

@ -5,12 +5,14 @@ import {
CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { isDefined } from 'twenty-shared/utils';
import { OnboardingStatus } from '~/generated/graphql';
const getNextOnboardingStatus = (
currentUser: CurrentUser | null,
currentWorkspace: CurrentWorkspace | null,
calendarBookingPageId: string | null,
) => {
if (currentUser?.onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION) {
return OnboardingStatus.PROFILE_CREATION;
@ -25,12 +27,21 @@ const getNextOnboardingStatus = (
) {
return OnboardingStatus.INVITE_TEAM;
}
if (currentUser?.onboardingStatus === OnboardingStatus.INVITE_TEAM) {
return isDefined(calendarBookingPageId)
? OnboardingStatus.BOOK_ONBOARDING
: OnboardingStatus.COMPLETED;
}
if (currentUser?.onboardingStatus === OnboardingStatus.BOOK_ONBOARDING) {
return OnboardingStatus.COMPLETED;
}
return OnboardingStatus.COMPLETED;
};
export const useSetNextOnboardingStatus = () => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const calendarBookingPageId = useRecoilValue(calendarBookingPageIdState);
return useRecoilCallback(
({ set }) =>
@ -38,6 +49,7 @@ export const useSetNextOnboardingStatus = () => {
const nextOnboardingStatus = getNextOnboardingStatus(
currentUser,
currentWorkspace,
calendarBookingPageId,
);
set(currentUserState, (current) => {
if (isDefined(current)) {
@ -49,6 +61,6 @@ export const useSetNextOnboardingStatus = () => {
return current;
});
},
[currentWorkspace, currentUser],
[currentWorkspace, currentUser, calendarBookingPageId],
);
};

View File

@ -13,6 +13,8 @@ export enum AppPath {
InviteTeam = '/invite-team',
PlanRequired = '/plan-required',
PlanRequiredSuccess = '/plan-required/payment-success',
BookCallDecision = '/book-call-decision',
BookCall = '/book-call',
// Onboarded
Index = '/',

View File

@ -43,6 +43,8 @@ const testCases = [
{ loc: AppPath.InviteTeam, res: true },
{ loc: AppPath.PlanRequired, res: true },
{ loc: AppPath.PlanRequiredSuccess, res: true },
{ loc: AppPath.BookCallDecision, res: true },
{ loc: AppPath.BookCall, res: true },
{ loc: AppPath.Index, res: false },
{ loc: AppPath.RecordIndexPage, res: false },

View File

@ -19,7 +19,9 @@ export const useShowAuthModal = () => {
isMatchingLocation(location, AppPath.SignInUp) ||
isMatchingLocation(location, AppPath.CreateWorkspace) ||
isMatchingLocation(location, AppPath.PlanRequired) ||
isMatchingLocation(location, AppPath.PlanRequiredSuccess)
isMatchingLocation(location, AppPath.PlanRequiredSuccess) ||
isMatchingLocation(location, AppPath.BookCallDecision) ||
isMatchingLocation(location, AppPath.BookCall)
) {
return true;
}

View File

@ -25,11 +25,14 @@ const StyledModalDiv = styled(motion.div)<{
box-shadow: ${({ theme, modalVariant }) =>
modalVariant === 'primary'
? theme.boxShadow.superHeavy
: theme.boxShadow.strong};
background: ${({ theme }) => theme.background.primary};
: modalVariant === 'transparent'
? 'none'
: theme.boxShadow.strong};
background: ${({ theme, modalVariant }) =>
modalVariant === 'transparent' ? 'transparent' : theme.background.primary};
color: ${({ theme }) => theme.font.color.primary};
border-radius: ${({ theme, isMobile }) => {
if (isMobile) return `0`;
border-radius: ${({ theme, isMobile, modalVariant }) => {
if (isMobile || modalVariant === 'transparent') return `0`;
return theme.border.radius.md;
}};
overflow-x: hidden;
@ -123,7 +126,7 @@ const StyledBackDrop = styled(motion.div)<{
}>`
align-items: center;
background: ${({ theme, modalVariant }) =>
modalVariant === 'primary'
modalVariant === 'primary' || modalVariant === 'transparent'
? theme.background.overlayPrimary
: modalVariant === 'secondary'
? theme.background.overlaySecondary
@ -177,7 +180,11 @@ const ModalFooter = ({ children, className }: ModalFooterProps) => (
export type ModalSize = 'small' | 'medium' | 'large' | 'extraLarge';
export type ModalPadding = 'none' | 'small' | 'medium' | 'large';
export type ModalVariants = 'primary' | 'secondary' | 'tertiary';
export type ModalVariants =
| 'primary'
| 'secondary'
| 'tertiary'
| 'transparent';
export type ModalProps = React.PropsWithChildren & {
modalId: string;