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

@ -11,7 +11,7 @@ const StyledContent = styled(UIModal.Content)`
type AuthModalProps = { children: React.ReactNode };
export const AuthModal = ({ children }: AuthModalProps) => (
<UIModal isOpen={true}>
<UIModal isOpen={true} padding={'none'}>
<StyledContent>{children}</StyledContent>
</UIModal>
);

View File

@ -5,24 +5,30 @@ import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEase
type TitleProps = React.PropsWithChildren & {
animate?: boolean;
withMarginTop?: boolean;
};
const StyledTitle = styled.div`
const StyledTitle = styled.div<Pick<TitleProps, 'withMarginTop'>>`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(4)};
margin-top: ${({ theme }) => theme.spacing(4)};
margin-top: ${({ theme, withMarginTop }) =>
withMarginTop ? theme.spacing(4) : 0};
`;
export const Title = ({ children, animate = false }: TitleProps) => {
export const Title = ({
children,
animate = false,
withMarginTop = true,
}: TitleProps) => {
if (animate) {
return (
<StyledTitle>
<StyledTitle withMarginTop={withMarginTop}>
<AnimatedEaseIn>{children}</AnimatedEaseIn>
</StyledTitle>
);
}
return <StyledTitle>{children}</StyledTitle>;
return <StyledTitle withMarginTop={withMarginTop}>{children}</StyledTitle>;
};

View File

@ -8,6 +8,7 @@ const StyledContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
max-width: 280px;
text-align: center;
`;

View File

@ -30,10 +30,6 @@ const StyledContentContainer = styled.div`
width: 200px;
`;
const StyledFooterNote = styled(FooterNote)`
max-width: 280px;
`;
const StyledForm = styled.form`
align-items: center;
display: flex;
@ -89,12 +85,8 @@ export const SignInUpForm = () => {
return 'Continue';
}
return signInUpMode === SignInUpMode.SignIn
? 'Sign in'
: form.formState.isSubmitting
? 'Creating workspace'
: 'Sign up';
}, [signInUpMode, signInUpStep, form.formState.isSubmitting]);
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
}, [signInUpMode, signInUpStep]);
const title = useMemo(() => {
if (signInUpMode === SignInUpMode.Invite) {
@ -242,10 +234,10 @@ export const SignInUpForm = () => {
Forgot your password?
</ActionLink>
) : (
<StyledFooterNote>
<FooterNote>
By using Twenty, you agree to the Terms of Service and Data Processing
Agreement.
</StyledFooterNote>
</FooterNote>
)}
</>
);

View File

@ -0,0 +1,36 @@
import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/display/icon';
const StyledBenefitContainer = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledCheckContainer = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: 50%;
display: flex;
height: 16px;
justify-content: center;
width: 16px;
`;
type SubscriptionBenefitProps = {
children: React.ReactNode;
};
export const SubscriptionBenefit = ({ children }: SubscriptionBenefitProps) => {
const theme = useTheme();
return (
<StyledBenefitContainer>
<StyledCheckContainer>
<IconCheck color={theme.grayScale.gray50} size={14} />
</StyledCheckContainer>
{children}
</StyledBenefitContainer>
);
};

View File

@ -0,0 +1,41 @@
import styled from '@emotion/styled';
import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice.tsx';
import { capitalize } from '~/utils/string/capitalize.ts';
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

@ -0,0 +1,33 @@
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,12 @@
import { gql } from '@apollo/client';
export const CHECKOUT = gql`
mutation Checkout($recurringInterval: String!, $successUrlPath: String) {
checkout(
recurringInterval: $recurringInterval
successUrlPath: $successUrlPath
) {
url
}
}
`;

View File

@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const GET_PRODUCT_PRICES = gql`
query GetProductPrices($product: String!) {
getProductPrices(product: $product) {
productPrices {
created
recurringInterval
stripePriceId
unitAmount
}
}
}
`;

View File

@ -10,6 +10,7 @@ export const GET_CLIENT_CONFIG = gql`
billing {
isBillingEnabled
billingUrl
billingFreeTrialDurationInDays
}
signInPrefilled
signUpDisabled

View File

@ -10,6 +10,7 @@ export enum AppPath {
CreateWorkspace = '/create/workspace',
CreateProfile = '/create/profile',
PlanRequired = '/plan-required',
PlanRequiredSuccess = '/plan-required/payment-success',
// Onboarded
Index = '/',

View File

@ -9,11 +9,14 @@ type Variant = 'primary' | 'secondary';
type Props = {
title: string;
fullWidth?: boolean;
width?: number;
variant?: Variant;
soon?: boolean;
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<Pick<Props, 'fullWidth' | 'variant'>>`
const StyledButton = styled.button<
Pick<Props, 'fullWidth' | 'width' | 'variant'>
>`
align-items: center;
background: ${({ theme, variant, disabled }) => {
if (disabled) {
@ -75,7 +78,8 @@ const StyledButton = styled.button<Pick<Props, 'fullWidth' | 'variant'>>`
justify-content: center;
outline: none;
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
width: ${({ fullWidth, width }) =>
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
${({ theme, variant }) => {
switch (variant) {
case 'secondary':
@ -101,6 +105,7 @@ type MainButtonProps = Props & {
export const MainButton = ({
Icon,
title,
width,
fullWidth = false,
variant = 'primary',
type,
@ -112,7 +117,7 @@ export const MainButton = ({
return (
<StyledButton
className={className}
{...{ disabled, fullWidth, onClick, type, variant }}
{...{ disabled, fullWidth, width, onClick, type, variant }}
>
{Icon && <Icon size={theme.icon.size.sm} />}
{title}

View File

@ -42,6 +42,10 @@ export const FullWidth: Story = {
args: { fullWidth: true },
};
export const Width: Story = {
args: { width: 200 },
};
export const Secondary: Story = {
args: { title: 'A secondary Button', variant: 'secondary' },
};

View File

@ -0,0 +1,44 @@
import React from 'react';
import styled from '@emotion/styled';
import { Radio } from '@/ui/input/components/Radio.tsx';
const StyledSubscriptionCardContainer = styled.button`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
padding: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(3)};
position: relative;
width: 100%;
:hover {
cursor: pointer;
background: ${({ theme }) => theme.background.tertiary};
}
`;
const StyledRadioContainer = styled.div`
position: absolute;
right: ${({ theme }) => theme.spacing(2)};
top: ${({ theme }) => theme.spacing(2)};
`;
type CardPickerProps = {
children: React.ReactNode;
handleChange?: () => void;
checked?: boolean;
};
export const CardPicker = ({
children,
checked,
handleChange,
}: CardPickerProps) => {
return (
<StyledSubscriptionCardContainer onClick={handleChange}>
<StyledRadioContainer>
<Radio checked={checked} />
</StyledRadioContainer>
{children}
</StyledSubscriptionCardContainer>
);
};

View File

@ -74,7 +74,6 @@ export const DefaultLayout = ({ children }: DefaultLayoutProps) => {
const theme = useTheme();
const widowsWidth = useScreenSize().width;
const isMatchingLocation = useIsMatchingLocation();
const showAuthModal = useMemo(() => {
return (
(onboardingStatus && onboardingStatus !== OnboardingStatus.Completed) ||

View File

@ -3,4 +3,5 @@ export type FeatureFlagKey =
| 'IS_CALENDAR_ENABLED'
| 'IS_MESSAGING_ENABLED'
| 'IS_NEW_RECORD_BOARD_ENABLED'
| 'IS_QUICK_ACTIONS_ENABLED';
| 'IS_QUICK_ACTIONS_ENABLED'
| 'IS_SELF_BILLING_ENABLED';