5622 add a syncemail onboarding step (#5689)
- add sync email onboarding step - refactor calendar and email visibility enums - add a new table `keyValuePair` in `core` schema - add a new resolved boolean field `skipSyncEmail` in current user https://github.com/twentyhq/twenty/assets/29927851/de791475-5bfe-47f9-8e90-76c349fba56f
This commit is contained in:
@ -1,187 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString, isNumber } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit';
|
||||
import { SubscriptionCard } from '@/billing/components/SubscriptionCard';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { CardPicker } from '@/ui/input/components/CardPicker';
|
||||
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
|
||||
import { CAL_LINK } from '@/ui/navigation/link/constants/Cal';
|
||||
import {
|
||||
ProductPriceEntity,
|
||||
useCheckoutSessionMutation,
|
||||
useGetProductPricesQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledChoosePlanContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
margin: ${({ theme }) => theme.spacing(8)} 0
|
||||
${({ theme }) => theme.spacing(2)};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledBenefitsContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
padding: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(3)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledLinkGroup = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
> span {
|
||||
background-color: ${({ theme }) => theme.font.color.light};
|
||||
border-radius: 50%;
|
||||
height: 2px;
|
||||
width: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
const benefits = [
|
||||
'Full access',
|
||||
'Unlimited contacts',
|
||||
'Email integration',
|
||||
'Custom objects',
|
||||
'API & Webhooks',
|
||||
'Frequent updates',
|
||||
'And much more',
|
||||
];
|
||||
|
||||
export const ChooseYourPlan = () => {
|
||||
const billing = useRecoilValue(billingState);
|
||||
|
||||
const [planSelected, setPlanSelected] = useState('month');
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const { data: prices } = useGetProductPricesQuery({
|
||||
variables: { product: 'base-plan' },
|
||||
});
|
||||
|
||||
const [checkoutSession] = useCheckoutSessionMutation();
|
||||
|
||||
const handlePlanChange = (type?: string) => {
|
||||
return () => {
|
||||
if (isNonEmptyString(type) && planSelected !== type) {
|
||||
setPlanSelected(type);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const { signOut } = useAuth();
|
||||
|
||||
const computeInfo = (
|
||||
price: ProductPriceEntity,
|
||||
prices: ProductPriceEntity[],
|
||||
): string => {
|
||||
if (price.recurringInterval !== 'year') {
|
||||
return 'Cancel anytime';
|
||||
}
|
||||
const monthPrice = prices.filter(
|
||||
(price) => price.recurringInterval === 'month',
|
||||
)?.[0];
|
||||
if (
|
||||
isDefined(monthPrice) &&
|
||||
isNumber(monthPrice.unitAmount) &&
|
||||
monthPrice.unitAmount > 0 &&
|
||||
isNumber(price.unitAmount) &&
|
||||
price.unitAmount > 0
|
||||
) {
|
||||
return `Save $${(12 * monthPrice.unitAmount - price.unitAmount) / 100}`;
|
||||
}
|
||||
return 'Cancel anytime';
|
||||
};
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
setIsSubmitting(true);
|
||||
const { data } = await checkoutSession({
|
||||
variables: {
|
||||
recurringInterval: planSelected,
|
||||
successUrlPath: AppPath.PlanRequiredSuccess,
|
||||
},
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
if (!data?.checkoutSession.url) {
|
||||
enqueueSnackBar(
|
||||
'Checkout session error. Please retry or contact Twenty team',
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.location.replace(data.checkoutSession.url);
|
||||
};
|
||||
|
||||
return (
|
||||
prices?.getProductPrices?.productPrices && (
|
||||
<>
|
||||
<Title withMarginTop={false}>Choose your Plan</Title>
|
||||
<SubTitle>
|
||||
Enjoy a {billing?.billingFreeTrialDurationInDays}-day free trial
|
||||
</SubTitle>
|
||||
<StyledChoosePlanContainer>
|
||||
{prices.getProductPrices.productPrices.map((price, index) => (
|
||||
<CardPicker
|
||||
checked={price.recurringInterval === planSelected}
|
||||
handleChange={handlePlanChange(price.recurringInterval)}
|
||||
key={index}
|
||||
>
|
||||
<SubscriptionCard
|
||||
type={price.recurringInterval}
|
||||
price={price.unitAmount / 100}
|
||||
info={computeInfo(price, prices.getProductPrices.productPrices)}
|
||||
/>
|
||||
</CardPicker>
|
||||
))}
|
||||
</StyledChoosePlanContainer>
|
||||
<StyledBenefitsContainer>
|
||||
{benefits.map((benefit, index) => (
|
||||
<SubscriptionBenefit key={index}>{benefit}</SubscriptionBenefit>
|
||||
))}
|
||||
</StyledBenefitsContainer>
|
||||
<MainButton
|
||||
title="Continue"
|
||||
onClick={handleButtonClick}
|
||||
width={200}
|
||||
Icon={() => isSubmitting && <Loader />}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<StyledLinkGroup>
|
||||
<ActionLink onClick={signOut}>Log out</ActionLink>
|
||||
<span />
|
||||
<ActionLink href={CAL_LINK} target="_blank" rel="noreferrer">
|
||||
Book a Call
|
||||
</ActionLink>
|
||||
</StyledLinkGroup>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -1,220 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSectionContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
const StyledComboInputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
> * + * {
|
||||
margin-left: ${({ theme }) => theme.spacing(4)};
|
||||
}
|
||||
`;
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
firstName: z.string().min(1, { message: 'First name can not be empty' }),
|
||||
lastName: z.string().min(1, { message: 'Last name can not be empty' }),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = z.infer<typeof validationSchema>;
|
||||
|
||||
export const CreateProfile = () => {
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord<WorkspaceMember>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
});
|
||||
|
||||
// Form
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isValid, isSubmitting },
|
||||
getValues,
|
||||
} = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
firstName: currentWorkspaceMember?.name?.firstName ?? '',
|
||||
lastName: currentWorkspaceMember?.name?.lastName ?? '',
|
||||
},
|
||||
resolver: zodResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<Form> = useCallback(
|
||||
async (data) => {
|
||||
try {
|
||||
if (!currentWorkspaceMember?.id) {
|
||||
throw new Error('User is not logged in');
|
||||
}
|
||||
if (!data.firstName || !data.lastName) {
|
||||
throw new Error('First name or last name is missing');
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
idToUpdate: currentWorkspaceMember?.id,
|
||||
updateOneRecordInput: {
|
||||
name: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
},
|
||||
colorScheme: 'System',
|
||||
},
|
||||
});
|
||||
|
||||
setCurrentWorkspaceMember(
|
||||
(current) =>
|
||||
({
|
||||
...current,
|
||||
name: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
},
|
||||
colorScheme: 'System',
|
||||
}) as any,
|
||||
);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
currentWorkspaceMember?.id,
|
||||
enqueueSnackBar,
|
||||
setCurrentWorkspaceMember,
|
||||
updateOneRecord,
|
||||
],
|
||||
);
|
||||
|
||||
const [isEditingMode, setIsEditingMode] = useState(false);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
if (isEditingMode) {
|
||||
onSubmit(getValues());
|
||||
}
|
||||
},
|
||||
PageHotkeyScope.CreateProfile,
|
||||
);
|
||||
|
||||
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title withMarginTop={false}>Create profile</Title>
|
||||
<SubTitle>How you'll be identified on the app.</SubTitle>
|
||||
<StyledContentContainer>
|
||||
<StyledSectionContainer>
|
||||
<H2Title title="Picture" />
|
||||
<ProfilePictureUploader />
|
||||
</StyledSectionContainer>
|
||||
<StyledSectionContainer>
|
||||
<H2Title
|
||||
title="Name"
|
||||
description="Your name as it will be displayed on the app"
|
||||
/>
|
||||
{/* TODO: When react-web-hook-form is added to edit page we should create a dedicated component with context */}
|
||||
<StyledComboInputContainer>
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<TextInputV2
|
||||
autoFocus
|
||||
label="First Name"
|
||||
value={value}
|
||||
onFocus={() => setIsEditingMode(true)}
|
||||
onBlur={() => {
|
||||
onBlur();
|
||||
setIsEditingMode(false);
|
||||
}}
|
||||
onChange={onChange}
|
||||
placeholder="Tim"
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="lastName"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<TextInputV2
|
||||
label="Last Name"
|
||||
value={value}
|
||||
onFocus={() => setIsEditingMode(true)}
|
||||
onBlur={() => {
|
||||
onBlur();
|
||||
setIsEditingMode(false);
|
||||
}}
|
||||
onChange={onChange}
|
||||
placeholder="Cook"
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledComboInputContainer>
|
||||
</StyledSectionContainer>
|
||||
</StyledContentContainer>
|
||||
<StyledButtonContainer>
|
||||
<MainButton
|
||||
title="Continue"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={!isValid || isSubmitting}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,170 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { useActivateWorkspaceMutation } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSectionContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, { message: 'Name can not be empty' }),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = z.infer<typeof validationSchema>;
|
||||
|
||||
export const CreateWorkspace = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
|
||||
const [activateWorkspace] = useActivateWorkspaceMutation();
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const setIsCurrentUserLoaded = useSetRecoilState(isCurrentUserLoadedState);
|
||||
|
||||
// Form
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isValid, isSubmitting },
|
||||
} = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
resolver: zodResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<Form> = useCallback(
|
||||
async (data) => {
|
||||
try {
|
||||
const result = await activateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
displayName: data.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
setIsCurrentUserLoaded(false);
|
||||
|
||||
await apolloMetadataClient?.refetchQueries({
|
||||
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
|
||||
});
|
||||
|
||||
if (isDefined(result.errors)) {
|
||||
throw result.errors ?? new Error('Unknown error');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
navigate(AppPath.CreateProfile);
|
||||
}, 20);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
activateWorkspace,
|
||||
setIsCurrentUserLoaded,
|
||||
apolloMetadataClient,
|
||||
navigate,
|
||||
enqueueSnackBar,
|
||||
],
|
||||
);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === Key.Enter) {
|
||||
event.preventDefault();
|
||||
handleSubmit(onSubmit)();
|
||||
}
|
||||
};
|
||||
|
||||
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceActivation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title withMarginTop={false}>Create your workspace</Title>
|
||||
<SubTitle>
|
||||
A shared environment where you will be able to manage your customer
|
||||
relations with your team.
|
||||
</SubTitle>
|
||||
<StyledContentContainer>
|
||||
<StyledSectionContainer>
|
||||
<H2Title title="Workspace logo" />
|
||||
<WorkspaceLogoUploader />
|
||||
</StyledSectionContainer>
|
||||
<StyledSectionContainer>
|
||||
<H2Title
|
||||
title="Workspace name"
|
||||
description="The name of your organization"
|
||||
/>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<TextInputV2
|
||||
autoFocus
|
||||
value={value}
|
||||
placeholder="Apple"
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
error={error?.message}
|
||||
onKeyDown={handleKeyDown}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledSectionContainer>
|
||||
</StyledContentContainer>
|
||||
<StyledButtonContainer>
|
||||
<MainButton
|
||||
title="Continue"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={!isValid || isSubmitting}
|
||||
Icon={() => isSubmitting && <Loader />}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCheck, RGBA } from 'twenty-ui';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||
|
||||
const StyledCheckContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: 2px solid ${(props) => props.color};
|
||||
border-radius: ${({ theme }) => theme.border.radius.rounded};
|
||||
box-shadow: ${(props) =>
|
||||
props.color && `-4px 4px 0 -2px ${RGBA(props.color, 1)}`};
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
export const PaymentSuccess = () => {
|
||||
const theme = useTheme();
|
||||
const color =
|
||||
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
<StyledCheckContainer color={color}>
|
||||
<IconCheck color={color} size={24} stroke={3} />
|
||||
</StyledCheckContainer>
|
||||
</AnimatedEaseIn>
|
||||
<Title>All set!</Title>
|
||||
<SubTitle>Your account has been activated.</SubTitle>
|
||||
<StyledButtonContainer>
|
||||
<UndecoratedLink to={AppPath.CreateWorkspace}>
|
||||
<MainButton title="Start" width={200} />
|
||||
</UndecoratedLink>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,75 +0,0 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
import { graphql, HttpResponse } from 'msw';
|
||||
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
import { ChooseYourPlan } from '../ChooseYourPlan';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Auth/ChooseYourPlan',
|
||||
component: ChooseYourPlan,
|
||||
decorators: [PageDecorator],
|
||||
args: { routePath: AppPath.PlanRequired },
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
currentUser: mockedOnboardingUsersData[0],
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('GetProductPrices', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
getProductPrices: {
|
||||
__typename: 'ProductPricesEntity',
|
||||
productPrices: [
|
||||
{
|
||||
__typename: 'ProductPriceEntity',
|
||||
created: 1699860608,
|
||||
recurringInterval: 'month',
|
||||
stripePriceId: 'monthly8usd',
|
||||
unitAmount: 900,
|
||||
},
|
||||
{
|
||||
__typename: 'ProductPriceEntity',
|
||||
created: 1701874964,
|
||||
recurringInterval: 'year',
|
||||
stripePriceId: 'priceId',
|
||||
unitAmount: 9000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof ChooseYourPlan>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
sleep(100);
|
||||
|
||||
await canvas.findByText('Choose your Plan');
|
||||
},
|
||||
};
|
||||
@ -1,47 +0,0 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
import { graphql, HttpResponse } from 'msw';
|
||||
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
import { CreateProfile } from '../CreateProfile';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Auth/CreateProfile',
|
||||
component: CreateProfile,
|
||||
decorators: [PageDecorator],
|
||||
args: { routePath: AppPath.CreateProfile },
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
currentUser: mockedOnboardingUsersData[0],
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof CreateProfile>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText('Create profile');
|
||||
},
|
||||
};
|
||||
@ -1,56 +0,0 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
import { graphql, HttpResponse } from 'msw';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
import { CreateWorkspace } from '../CreateWorkspace';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Auth/CreateWorkspace',
|
||||
component: CreateWorkspace,
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
setCurrentWorkspace(mockedOnboardingUsersData[1].defaultWorkspace);
|
||||
return <Story />;
|
||||
},
|
||||
PageDecorator,
|
||||
],
|
||||
args: { routePath: AppPath.CreateWorkspace },
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
currentUser: mockedOnboardingUsersData[1],
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof CreateWorkspace>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText('Create your workspace');
|
||||
},
|
||||
};
|
||||
@ -1,48 +0,0 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
import { graphql, HttpResponse } from 'msw';
|
||||
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
import { PaymentSuccess } from '../PaymentSuccess';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Auth/PaymentSuccess',
|
||||
component: PaymentSuccess,
|
||||
decorators: [PageDecorator],
|
||||
args: { routePath: AppPath.PlanRequiredSuccess },
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
currentUser: mockedOnboardingUsersData[0],
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof PaymentSuccess>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Start');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user