Update ChooseYourPlan page with new trial period options (#9628)

### Context
- Update /plan-required page to let users get free trial without credit
card plan
- Update usePageChangeEffectNavigateLocation to redirect paused and
canceled subscription (suspended workspace) to /settings/billing page

### To do

- [x] Update usePageChangeEffectNavigateLocation test
- [x] Update ChooseYourPlan sb test



closes #9520

---------

Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
This commit is contained in:
Etienne
2025-01-16 11:10:36 +01:00
committed by GitHub
parent c79cb14132
commit 26058f3e25
40 changed files with 722 additions and 596 deletions

View File

@ -1,41 +0,0 @@
import styled from '@emotion/styled';
import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice';
import { capitalize } from 'twenty-shared';
type SubscriptionCardProps = {
type?: string;
price: number;
info: string;
};
const StyledSubscriptionCardContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledTypeContainer = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
display: flex;
`;
const StyledInfoContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
display: flex;
`;
export const SubscriptionCard = ({
type,
price,
info,
}: SubscriptionCardProps) => {
return (
<StyledSubscriptionCardContainer>
<StyledTypeContainer>{capitalize(type || '')}</StyledTypeContainer>
<SubscriptionCardPrice price={price} />
<StyledInfoContainer>{info}</StyledInfoContainer>
</StyledSubscriptionCardContainer>
);
};

View File

@ -1,33 +0,0 @@
import styled from '@emotion/styled';
type SubscriptionCardPriceProps = {
price: number;
};
const StyledSubscriptionCardPriceContainer = styled.div`
align-items: baseline;
display: flex;
gap: ${({ theme }) => theme.betweenSiblingsGap};
margin: ${({ theme }) => theme.spacing(1)} 0
${({ theme }) => theme.spacing(2)};
`;
const StyledPriceSpan = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;
const StyledSeatSpan = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
export const SubscriptionCardPrice = ({
price,
}: SubscriptionCardPriceProps) => {
return (
<StyledSubscriptionCardPriceContainer>
<StyledPriceSpan>${price}</StyledPriceSpan>
<StyledSeatSpan>/</StyledSeatSpan>
<StyledSeatSpan>seat</StyledSeatSpan>
</StyledSubscriptionCardPriceContainer>
);
};

View File

@ -0,0 +1,29 @@
import styled from '@emotion/styled';
import { SubscriptionInterval } from '~/generated-metadata/graphql';
type SubscriptionPriceProps = {
type: SubscriptionInterval;
price: number;
};
const StyledPriceSpan = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xxl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
`;
const StyledPriceUnitSpan = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
export const SubscriptionPrice = ({ type, price }: SubscriptionPriceProps) => {
return (
<>
<StyledPriceSpan>{`$${price}`}</StyledPriceSpan>
<StyledPriceUnitSpan>{`seat / ${type.toLocaleLowerCase()}`}</StyledPriceUnitSpan>
</>
);
};

View File

@ -0,0 +1,33 @@
import styled from '@emotion/styled';
type TrialCardProps = {
duration: number;
withCreditCard: boolean;
};
const StyledTrialCardContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledTrialDurationContainer = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
display: flex;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledCreditCardRequirementContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
display: flex;
`;
export const TrialCard = ({ duration, withCreditCard }: TrialCardProps) => {
return (
<StyledTrialCardContainer>
<StyledTrialDurationContainer>{`${duration} days trial`}</StyledTrialDurationContainer>
<StyledCreditCardRequirementContainer>{`${withCreditCard ? 'With Credit Card' : 'Without Credit Card'}`}</StyledCreditCardRequirementContainer>
</StyledTrialCardContainer>
);
};

View File

@ -0,0 +1,11 @@
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import {
BillingPlanKey,
SubscriptionInterval,
} from '~/generated-metadata/graphql';
export const BILLING_CHECKOUT_SESSION_DEFAULT_VALUE: BillingCheckoutSession = {
plan: BillingPlanKey.Pro,
interval: SubscriptionInterval.Month,
requirePaymentMethod: true,
};

View File

@ -0,0 +1,50 @@
import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useState } from 'react';
import {
BillingPlanKey,
SubscriptionInterval,
} from '~/generated-metadata/graphql';
import { useCheckoutSessionMutation } from '~/generated/graphql';
export const useHandleCheckoutSession = ({
recurringInterval,
plan,
requirePaymentMethod,
}: {
recurringInterval: SubscriptionInterval;
plan: BillingPlanKey;
requirePaymentMethod: boolean;
}) => {
const { enqueueSnackBar } = useSnackBar();
const [checkoutSession] = useCheckoutSessionMutation();
const [isSubmitting, setIsSubmitting] = useState(false);
const handleCheckoutSession = async () => {
setIsSubmitting(true);
const { data } = await checkoutSession({
variables: {
recurringInterval,
successUrlPath: `${AppPath.Settings}/${SettingsPath.Billing}`,
plan,
requirePaymentMethod,
},
});
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 { isSubmitting, handleCheckoutSession };
};