poc - cal.com integration in onboarding flow (#12530)
This commit is contained in:
@ -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 />} />
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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 <></>;
|
||||
|
||||
@ -62,6 +62,7 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
isGoogleMessagingEnabled
|
||||
isGoogleCalendarEnabled
|
||||
isConfigVariablesInDbEnabled
|
||||
calendarBookingPageId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const calendarBookingPageIdState = createState<string | null>({
|
||||
key: 'calendarBookingPageIdState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export const BOOK_CALL_MODAL_ID = 'book-call-modal';
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SKIP_BOOK_ONBOARDING_STEP = gql`
|
||||
mutation SkipBookOnboardingStep {
|
||||
skipBookOnboardingStep {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -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],
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = '/',
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user