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:
@ -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>
|
||||
);
|
||||
|
||||
@ -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>;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
`;
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -10,6 +10,7 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
billing {
|
||||
isBillingEnabled
|
||||
billingUrl
|
||||
billingFreeTrialDurationInDays
|
||||
}
|
||||
signInPrefilled
|
||||
signUpDisabled
|
||||
|
||||
@ -10,6 +10,7 @@ export enum AppPath {
|
||||
CreateWorkspace = '/create/workspace',
|
||||
CreateProfile = '/create/profile',
|
||||
PlanRequired = '/plan-required',
|
||||
PlanRequiredSuccess = '/plan-required/payment-success',
|
||||
|
||||
// Onboarded
|
||||
Index = '/',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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' },
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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) ||
|
||||
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user