-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dashboard): 🛂 Limit create folder to Pro user
- Loading branch information
1 parent
b1f54b7
commit 3a7b9a0
Showing
14 changed files
with
333 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { Button, HStack, Tag, useDisclosure, Text } from '@chakra-ui/react' | ||
import { FolderPlusIcon } from 'assets/icons' | ||
import { UpgradeModal } from 'components/shared/modals/UpgradeModal.' | ||
import { LimitReached } from 'components/shared/modals/UpgradeModal./UpgradeModal' | ||
import { useUser } from 'contexts/UserContext' | ||
import React from 'react' | ||
import { isFreePlan } from 'services/user' | ||
|
||
type Props = { isLoading: boolean; onClick: () => void } | ||
|
||
export const CreateFolderButton = ({ isLoading, onClick }: Props) => { | ||
const { user } = useUser() | ||
const { isOpen, onOpen, onClose } = useDisclosure() | ||
|
||
const handleClick = () => { | ||
if (isFreePlan(user)) return onOpen() | ||
onClick() | ||
} | ||
return ( | ||
<Button | ||
leftIcon={<FolderPlusIcon />} | ||
onClick={handleClick} | ||
isLoading={isLoading} | ||
> | ||
<HStack> | ||
<Text>Create a folder</Text> | ||
{isFreePlan(user) && <Tag colorScheme="orange">Pro</Tag>} | ||
</HStack> | ||
{user && ( | ||
<UpgradeModal | ||
isOpen={isOpen} | ||
onClose={onClose} | ||
user={user} | ||
type={LimitReached.FOLDER} | ||
/> | ||
)} | ||
</Button> | ||
) | ||
} |
13 changes: 13 additions & 0 deletions
13
apps/builder/components/shared/modals/UpgradeModal./ActionButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Button, ButtonProps } from '@chakra-ui/react' | ||
import * as React from 'react' | ||
|
||
export const ActionButton = (props: ButtonProps) => ( | ||
<Button | ||
colorScheme="blue" | ||
size="lg" | ||
w="full" | ||
fontWeight="extrabold" | ||
py={{ md: '8' }} | ||
{...props} | ||
/> | ||
) |
28 changes: 28 additions & 0 deletions
28
apps/builder/components/shared/modals/UpgradeModal./Card.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Box, BoxProps, useColorModeValue } from '@chakra-ui/react' | ||
import * as React from 'react' | ||
import { CardBadge } from './CardBadge' | ||
|
||
export interface CardProps extends BoxProps { | ||
isPopular?: boolean | ||
} | ||
|
||
export const Card = (props: CardProps) => { | ||
const { children, isPopular, ...rest } = props | ||
return ( | ||
<Box | ||
bg={useColorModeValue('white', 'gray.700')} | ||
position="relative" | ||
px="6" | ||
pb="6" | ||
pt="16" | ||
overflow="hidden" | ||
shadow="lg" | ||
maxW="md" | ||
width="100%" | ||
{...rest} | ||
> | ||
{isPopular && <CardBadge>Popular</CardBadge>} | ||
{children} | ||
</Box> | ||
) | ||
} |
30 changes: 30 additions & 0 deletions
30
apps/builder/components/shared/modals/UpgradeModal./CardBadge.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { Flex, FlexProps, Text, useColorModeValue } from '@chakra-ui/react' | ||
import * as React from 'react' | ||
|
||
export const CardBadge = (props: FlexProps) => { | ||
const { children, ...flexProps } = props | ||
return ( | ||
<Flex | ||
bg={useColorModeValue('green.500', 'green.200')} | ||
position="absolute" | ||
right={-20} | ||
top={6} | ||
width="240px" | ||
transform="rotate(45deg)" | ||
py={2} | ||
justifyContent="center" | ||
alignItems="center" | ||
{...flexProps} | ||
> | ||
<Text | ||
fontSize="xs" | ||
textTransform="uppercase" | ||
fontWeight="bold" | ||
letterSpacing="wider" | ||
color={useColorModeValue('white', 'gray.800')} | ||
> | ||
{children} | ||
</Text> | ||
</Flex> | ||
) | ||
} |
68 changes: 68 additions & 0 deletions
68
apps/builder/components/shared/modals/UpgradeModal./PricingCard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { | ||
Flex, | ||
Heading, | ||
List, | ||
ListIcon, | ||
ListItem, | ||
Text, | ||
useColorModeValue, | ||
VStack, | ||
} from '@chakra-ui/react' | ||
import { CheckIcon } from 'assets/icons' | ||
import * as React from 'react' | ||
import { Card, CardProps } from './Card' | ||
|
||
export interface PricingCardData { | ||
features: string[] | ||
name: string | ||
price: string | ||
} | ||
|
||
interface PricingCardProps extends CardProps { | ||
data: PricingCardData | ||
button: React.ReactElement | ||
} | ||
|
||
export const PricingCard = (props: PricingCardProps) => { | ||
const { data, button, ...rest } = props | ||
const { features, price, name } = data | ||
const accentColor = useColorModeValue('blue.500', 'blue.200') | ||
|
||
return ( | ||
<Card rounded={{ sm: 'xl' }} {...rest}> | ||
<VStack spacing={6}> | ||
<Heading size="md" fontWeight="extrabold"> | ||
{name} | ||
</Heading> | ||
</VStack> | ||
<Flex | ||
align="flex-end" | ||
justify="center" | ||
fontWeight="extrabold" | ||
color={accentColor} | ||
my="8" | ||
> | ||
<Heading size="3xl" fontWeight="inherit" lineHeight="0.9em"> | ||
{price} | ||
</Heading> | ||
<Text fontWeight="inherit" fontSize="2xl"> | ||
/ mo | ||
</Text> | ||
</Flex> | ||
<List spacing="4" mb="8" maxW="30ch" mx="auto"> | ||
{features.map((feature, index) => ( | ||
<ListItem fontWeight="medium" key={index}> | ||
<ListIcon | ||
fontSize="xl" | ||
as={CheckIcon} | ||
marginEnd={2} | ||
color={accentColor} | ||
/> | ||
{feature} | ||
</ListItem> | ||
))} | ||
</List> | ||
{button} | ||
</Card> | ||
) | ||
} |
106 changes: 106 additions & 0 deletions
106
apps/builder/components/shared/modals/UpgradeModal./UpgradeModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { useEffect, useState } from 'react' | ||
import { | ||
Alert, | ||
AlertIcon, | ||
Modal, | ||
ModalBody, | ||
ModalCloseButton, | ||
ModalContent, | ||
ModalFooter, | ||
ModalHeader, | ||
ModalOverlay, | ||
Stack, | ||
} from '@chakra-ui/react' | ||
import { PricingCard } from './PricingCard' | ||
import { ActionButton } from './ActionButton' | ||
import { User } from 'db' | ||
import { pay } from 'services/stripe' | ||
|
||
export enum LimitReached { | ||
BRAND = 'Remove branding', | ||
CUSTOM_DOMAIN = 'Custom domain', | ||
FOLDER = 'Create folders', | ||
ANALYTICS = 'Unlock analytics', | ||
} | ||
|
||
type UpgradeModalProps = { | ||
user: User | ||
type: LimitReached | ||
isOpen: boolean | ||
onClose: () => void | ||
} | ||
|
||
export const UpgradeModal = ({ | ||
type, | ||
user, | ||
onClose, | ||
isOpen, | ||
}: UpgradeModalProps) => { | ||
const [payLoading, setPayLoading] = useState(false) | ||
const [userLanguage, setUserLanguage] = useState<string>('en') | ||
|
||
useEffect(() => { | ||
setUserLanguage(navigator.language.toLowerCase()) | ||
}, []) | ||
|
||
let limitLabel | ||
switch (type) { | ||
case LimitReached.BRAND: { | ||
limitLabel = "You can't hide Typebot brand on the Basic plan" | ||
break | ||
} | ||
case LimitReached.CUSTOM_DOMAIN: { | ||
limitLabel = "You can't add your domain with the Basic plan" | ||
break | ||
} | ||
case LimitReached.FOLDER: { | ||
limitLabel = "You can't create folders with the basic plan" | ||
} | ||
} | ||
|
||
const handlePayClick = async () => { | ||
if (!user) return | ||
setPayLoading(true) | ||
await pay(user) | ||
} | ||
|
||
return ( | ||
<Modal isOpen={isOpen} onClose={onClose} size="xl"> | ||
<ModalOverlay /> | ||
<ModalContent> | ||
<ModalHeader>Upgrade to Pro plan</ModalHeader> | ||
<ModalCloseButton /> | ||
<ModalBody as={Stack} spacing={6} alignItems="center"> | ||
{limitLabel && ( | ||
<Alert status="warning" rounded="md"> | ||
<AlertIcon /> | ||
{limitLabel} | ||
</Alert> | ||
)} | ||
<PricingCard | ||
data={{ | ||
price: userLanguage.includes('fr') ? '25€' : '$30', | ||
name: 'Pro plan', | ||
features: [ | ||
'Branding removed', | ||
'View incomplete submissions', | ||
'In-depth drop off analytics', | ||
'Custom domains', | ||
'Organize typebots in folders', | ||
'Unlimited uploads', | ||
'Custom Google Analytics events', | ||
], | ||
}} | ||
button={ | ||
<ActionButton onClick={handlePayClick} isLoading={payLoading}> | ||
Upgrade now | ||
</ActionButton> | ||
} | ||
/> | ||
</ModalBody> | ||
|
||
<ModalFooter /> | ||
</ModalContent> | ||
</Modal> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { UpgradeModal } from './UpgradeModal' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { User } from 'db' | ||
import { loadStripe } from '@stripe/stripe-js' | ||
import { sendRequest } from 'utils' | ||
|
||
export const pay = async (user: User) => { | ||
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY) | ||
throw new Error('NEXT_PUBLIC_STRIPE_PUBLIC_KEY is missing in env') | ||
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY) | ||
const { data, error } = await sendRequest<{ sessionId: string }>({ | ||
method: 'POST', | ||
url: '/api/stripe/checkout', | ||
body: { email: user.email }, | ||
}) | ||
if (error || !data) return | ||
return stripe?.redirectToCheckout({ | ||
sessionId: data?.sessionId, | ||
}) | ||
} |
Oops, something went wrong.
3a7b9a0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
landing-page-v2 – ./apps/landing-page
landing-page-v2-typebot-io.vercel.app
landing-page-v2-git-main-typebot-io.vercel.app
landing-page-v2-jade.vercel.app
3a7b9a0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
builder-v2 – ./apps/builder
builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app
next.typebot.io
3a7b9a0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
viewer-v2 – ./apps/viewer
viewer-v2-git-main-typebot-io.vercel.app
typebot-viewer.vercel.app
viewer-v2-typebot-io.vercel.app