Skip to content

Commit

Permalink
feat(dashboard): 🛂 Limit create folder to Pro user
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Feb 12, 2022
1 parent b1f54b7 commit 3a7b9a0
Show file tree
Hide file tree
Showing 14 changed files with 333 additions and 10 deletions.
10 changes: 3 additions & 7 deletions apps/builder/components/dashboard/FolderContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { DashboardFolder } from '.prisma/client'
import {
Button,
Flex,
Heading,
HStack,
Expand All @@ -11,14 +10,14 @@ import {
useToast,
Wrap,
} from '@chakra-ui/react'
import { FolderPlusIcon } from 'assets/icons'
import { useTypebotDnd } from 'contexts/TypebotDndContext'
import { Typebot } from 'models'
import React, { useState } from 'react'
import { createFolder, useFolders } from 'services/folders'
import { patchTypebot, useTypebots } from 'services/typebots'
import { BackButton } from './FolderContent/BackButton'
import { CreateBotButton } from './FolderContent/CreateBotButton'
import { CreateFolderButton } from './FolderContent/CreateFolderButton'
import { ButtonSkeleton, FolderButton } from './FolderContent/FolderButton'
import { TypebotButton } from './FolderContent/TypebotButton'
import { TypebotCardOverlay } from './FolderContent/TypebotButtonOverlay'
Expand Down Expand Up @@ -152,13 +151,10 @@ export const FolderContent = ({ folder }: Props) => {
<Stack>
<HStack>
{folder && <BackButton id={folder.parentFolderId} />}
<Button
leftIcon={<FolderPlusIcon />}
<CreateFolderButton
onClick={handleCreateFolder}
isLoading={isCreatingFolder || isFolderLoading}
>
Create a folder
</Button>
/>
</HStack>
<Wrap spacing={4}>
<CreateBotButton
Expand Down
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>
)
}
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 apps/builder/components/shared/modals/UpgradeModal./Card.tsx
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 apps/builder/components/shared/modals/UpgradeModal./CardBadge.tsx
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>
)
}
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 apps/builder/components/shared/modals/UpgradeModal./UpgradeModal.tsx
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>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { UpgradeModal } from './UpgradeModal'
1 change: 1 addition & 0 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@giphy/react-components": "^5.4.0",
"@googleapis/drive": "^2.1.0",
"@next-auth/prisma-adapter": "1.0.1",
"@stripe/stripe-js": "^1.22.0",
"@udecode/plate-basic-marks": "^10.0.0",
"@udecode/plate-common": "^7.0.2",
"@udecode/plate-core": "^10.0.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/builder/pages/api/stripe/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const createCheckoutSession = async (
},
],
})
res.status(201).json(session)
res.status(201).send({ sessionId: session.id })
}
return methodNotAllowed(res)
}
Expand Down
15 changes: 15 additions & 0 deletions apps/builder/playwright/tests/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import test, { expect, Page } from '@playwright/test'
import path from 'path'
import { generate } from 'short-uuid'
import { createFolders, createTypebots } from '../services/database'
import { deleteButtonInConfirmDialog } from '../services/selectorUtils'
Expand Down Expand Up @@ -72,6 +73,20 @@ test.describe('Dashboard page', () => {
await page.click('a:has-text("Back")')
await expect(typebotButton).toBeVisible()
})

test.describe('Free user', () => {
test.use({
storageState: path.join(__dirname, '../freeUser.json'),
})
test("create folder shouldn't be available", async ({ page }) => {
await page.goto('/typebots')
await page.click('text=Create a folder')
await expect(
page.locator('text="You can\'t create folders with the basic plan"')
).toBeVisible()
await expect(page.locator('text=Upgrade now')).toBeVisible()
})
})
})

const waitForNextApiCall = (page: Page, path?: string) =>
Expand Down
18 changes: 18 additions & 0 deletions apps/builder/services/stripe.ts
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,
})
}
Loading

3 comments on commit 3a7b9a0

@vercel
Copy link

@vercel vercel bot commented on 3a7b9a0 Feb 12, 2022

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

@vercel
Copy link

@vercel vercel bot commented on 3a7b9a0 Feb 12, 2022

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

@vercel
Copy link

@vercel vercel bot commented on 3a7b9a0 Feb 12, 2022

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

Please sign in to comment.