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:
martmull
2024-06-05 18:16:53 +02:00
committed by GitHub
parent fda0d2a170
commit 9f6a6c3282
92 changed files with 2707 additions and 1246 deletions

View File

@ -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>
</>
)
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -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');
},
};

View File

@ -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');
},
};

View File

@ -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');
},
};

View File

@ -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');
},
};