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:
133
packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx
Normal file
133
packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx
Normal 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} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
53
packages/twenty-front/src/pages/auth/PaymentSuccess.tsx
Normal file
53
packages/twenty-front/src/pages/auth/PaymentSuccess.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user