39 create subscription and success modale (#4208)

* Init add choose your plan page component

* Update price format

* Add billing refund trial duration env variable

* Add billing benefits

* Add Button

* Call checkout endpoint

* Fix theme color

* Add Payment success modale

* Add loader to createWorkspace submit button

* Fix lint

* Fix dark mode

* Code review returns

* Use a resolver for front requests

* Fix 'create workspace' loader at sign up

* Fix 'create workspace' with enter key bug
This commit is contained in:
martmull
2024-02-28 19:51:04 +01:00
committed by GitHub
parent e0bf8e43d1
commit 9ca3dbeb70
38 changed files with 761 additions and 164 deletions

View File

@ -0,0 +1,133 @@
import React, { useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { SubTitle } from '@/auth/components/SubTitle.tsx';
import { Title } from '@/auth/components/Title.tsx';
import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit.tsx';
import { SubscriptionCard } from '@/billing/components/SubscriptionCard.tsx';
import { billingState } from '@/client-config/states/billingState.ts';
import { AppPath } from '@/types/AppPath.ts';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar.tsx';
import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
import { CardPicker } from '@/ui/input/components/CardPicker.tsx';
import {
ProductPriceEntity,
useCheckoutMutation,
useGetProductPricesQuery,
} from '~/generated/graphql.tsx';
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)};
`;
export const ChooseYourPlan = () => {
const billing = useRecoilValue(billingState);
const [planSelected, setPlanSelected] = useState('month');
const { enqueueSnackBar } = useSnackBar();
const { data: prices } = useGetProductPricesQuery({
variables: { product: 'base-plan' },
});
const [checkout] = useCheckoutMutation();
const handlePlanChange = (type?: string) => {
return () => {
if (type && planSelected !== type) {
setPlanSelected(type);
}
};
};
const computeInfo = (
price: ProductPriceEntity,
prices: ProductPriceEntity[],
): string => {
if (price.recurringInterval !== 'year') {
return 'Cancel anytime';
}
const monthPrice = prices.filter(
(price) => price.recurringInterval === 'month',
)?.[0];
if (monthPrice && monthPrice.unitAmount && price.unitAmount) {
return `Save $${(12 * monthPrice.unitAmount - price.unitAmount) / 100}`;
}
return 'Cancel anytime';
};
const handleButtonClick = async () => {
const { data } = await checkout({
variables: {
recurringInterval: planSelected,
successUrlPath: AppPath.PlanRequiredSuccess,
},
});
if (!data?.checkout.url) {
enqueueSnackBar(
'Checkout session error. Please retry or contact Twenty team',
{
variant: 'error',
},
);
return;
}
window.location.replace(data.checkout.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>
<SubscriptionBenefit>Full access</SubscriptionBenefit>
<SubscriptionBenefit>Unlimited contacts</SubscriptionBenefit>
<SubscriptionBenefit>Email integration</SubscriptionBenefit>
<SubscriptionBenefit>Custom objects</SubscriptionBenefit>
<SubscriptionBenefit>API & Webhooks</SubscriptionBenefit>
<SubscriptionBenefit>Frequent updates</SubscriptionBenefit>
<SubscriptionBenefit>And much more</SubscriptionBenefit>
</StyledBenefitsContainer>
<MainButton title="Continue" onClick={handleButtonClick} width={200} />
</>
)
);
};

View File

@ -141,7 +141,7 @@ export const CreateProfile = () => {
return (
<>
<Title>Create profile</Title>
<Title withMarginTop={false}>Create profile</Title>
<SubTitle>How you'll be identified on the app.</SubTitle>
<StyledContentContainer>
<StyledSectionContainer>

View File

@ -3,6 +3,7 @@ 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 { Key } from 'ts-key-enum';
import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle';
@ -13,12 +14,11 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queri
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Loader } from '@/ui/feedback/loader/components/Loader.tsx';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { useActivateWorkspaceMutation } from '~/generated/graphql';
@ -57,7 +57,6 @@ export const CreateWorkspace = () => {
control,
handleSubmit,
formState: { isValid, isSubmitting },
getValues,
} = useForm<Form>({
mode: 'onChange',
defaultValues: {
@ -99,28 +98,19 @@ export const CreateWorkspace = () => {
);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
if (event.key === Key.Enter) {
event.preventDefault();
handleSubmit(onSubmit)();
}
};
useScopedHotkeys(
'enter',
() => {
onSubmit(getValues());
},
PageHotkeyScope.CreateWokspace,
[onSubmit],
);
if (onboardingStatus !== OnboardingStatus.OngoingWorkspaceActivation) {
return null;
}
return (
<>
<Title>Create your workspace</Title>
<Title withMarginTop={false}>Create your workspace</Title>
<SubTitle>
A shared environment where you will be able to manage your customer
relations with your team.
@ -162,6 +152,7 @@ export const CreateWorkspace = () => {
title="Continue"
onClick={handleSubmit(onSubmit)}
disabled={!isValid || isSubmitting}
Icon={() => isSubmitting && <Loader />}
fullWidth
/>
</StyledButtonContainer>

View File

@ -0,0 +1,53 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { SubTitle } from '@/auth/components/SubTitle.tsx';
import { Title } from '@/auth/components/Title.tsx';
import { AppPath } from '@/types/AppPath.ts';
import { IconCheck } from '@/ui/display/icon';
import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
import { RGBA } from '@/ui/theme/constants/Rgba.ts';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn.tsx';
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 navigate = useNavigate();
const theme = useTheme();
const handleButtonClick = () => {
navigate(AppPath.CreateWorkspace);
};
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>
<MainButton title="Start" onClick={handleButtonClick} width={200} />
</StyledButtonContainer>
</>
);
};

View File

@ -1,3 +1,4 @@
import React from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
@ -6,13 +7,12 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { billingState } from '@/client-config/states/billingState';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
const StyledButtonContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)};
width: 200px;
`;
export const PlanRequired = () => {
@ -36,7 +36,11 @@ export const PlanRequired = () => {
Please select a subscription plan before proceeding to sign in.
</SubTitle>
<StyledButtonContainer>
<MainButton title="Get started" onClick={handleButtonClick} fullWidth />
<MainButton
title="Get started"
onClick={handleButtonClick}
width={200}
/>
</StyledButtonContainer>
</>
);