Skip to content

Commit

Permalink
🚸 (billing) Add precheckout form
Browse files Browse the repository at this point in the history
Collects required company name and email and create the customer before redirecting to checkout
  • Loading branch information
baptisteArno committed Mar 6, 2023
1 parent c1a636b commit 26e5d9c
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const createCheckoutSession = authenticatedProcedure
})
.input(
z.object({
email: z.string(),
company: z.string(),
workspaceId: z.string(),
prefilledEmail: z.string().optional(),
currency: z.enum(['usd', 'eur']),
Expand All @@ -35,8 +37,9 @@ export const createCheckoutSession = authenticatedProcedure
.mutation(
async ({
input: {
email,
company,
workspaceId,
prefilledEmail,
currency,
plan,
returnUrl,
Expand Down Expand Up @@ -69,11 +72,21 @@ export const createCheckoutSession = authenticatedProcedure
apiVersion: '2022-11-15',
})

const customer = await stripe.customers.create({
email,
name: company,
metadata: { workspaceId },
})

const session = await stripe.checkout.sessions.create({
success_url: `${returnUrl}?stripe=${plan}&success=true`,
cancel_url: `${returnUrl}?stripe=cancel`,
allow_promotion_codes: true,
customer_email: prefilledEmail,
customer: customer.id,
customer_update: {
address: 'auto',
name: 'auto',
},
mode: 'subscription',
metadata: { workspaceId, plan, additionalChats, additionalStorage },
currency,
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/src/features/billing/billing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ test('plan changes should work', async ({ page }) => {
await page.click('button >> text="4"')
await expect(page.locator('text="$73"')).toBeVisible()
await page.click('button >> text=Upgrade >> nth=0')
await page.getByLabel('Company name').fill('Company LLC')
await page.getByRole('button', { name: 'Go to checkout' }).click()
await page.waitForNavigation()
expect(page.url()).toContain('https://checkout.stripe.com')
await expect(page.locator('text=$73.00 >> nth=0')).toBeVisible()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,25 @@ import { TextLink } from '@/components/TextLink'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import { guessIfUserIsEuropean } from 'utils/pricing'
import { useRouter } from 'next/router'
import { Workspace } from 'models'
import { PreCheckoutModal, PreCheckoutModalProps } from '../PreCheckoutModal'
import { useState } from 'react'

type Props = {
workspace: Pick<Workspace, 'id' | 'stripeId' | 'plan'>
onUpgradeSuccess: () => void
}

export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
const router = useRouter()
const { user } = useUser()
const { showToast } = useToast()
const [preCheckoutPlan, setPreCheckoutPlan] =
useState<PreCheckoutModalProps['selectedSubscription']>()

const { data } = trpc.billing.getSubscription.useQuery({
workspaceId: workspace.id,
})

const { mutate: createCheckoutSession, isLoading: isCreatingCheckout } =
trpc.billing.createCheckoutSession.useMutation({
onError: (error) => {
showToast({
description: error.message,
})
},
onSuccess: ({ checkoutUrl }) => {
router.push(checkoutUrl)
},
})

const { mutate: updateSubscription, isLoading: isUpdatingSubscription } =
trpc.billing.updateSubscription.useMutation({
onError: (error) => {
Expand Down Expand Up @@ -79,15 +70,20 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
if (workspace.stripeId) {
updateSubscription(newSubscription)
} else {
createCheckoutSession({
...newSubscription,
returnUrl: window.location.href,
})
setPreCheckoutPlan(newSubscription)
}
}

return (
<Stack spacing={6}>
{!workspace.stripeId && (
<PreCheckoutModal
selectedSubscription={preCheckoutPlan}
existingEmail={user?.email ?? undefined}
existingCompany={user?.company ?? undefined}
onClose={() => setPreCheckoutPlan(undefined)}
/>
)}
<HStack alignItems="stretch" spacing="4" w="full">
<StarterPlanContent
initialChatsLimitIndex={
Expand All @@ -103,7 +99,7 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
onPayClick={(props) =>
handlePayClick({ ...props, plan: Plan.STARTER })
}
isLoading={isCreatingCheckout || isUpdatingSubscription}
isLoading={isUpdatingSubscription}
currency={data?.subscription.currency}
/>

Expand All @@ -119,7 +115,7 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
: 0
}
onPayClick={(props) => handlePayClick({ ...props, plan: Plan.PRO })}
isLoading={isCreatingCheckout || isUpdatingSubscription}
isLoading={isUpdatingSubscription}
currency={data?.subscription.currency}
/>
</HStack>
Expand Down
111 changes: 111 additions & 0 deletions apps/builder/src/features/billing/components/PreCheckoutModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { TextInput } from '@/components/inputs'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import {
Button,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
Stack,
} from '@chakra-ui/react'
import { useRouter } from 'next/router'
import React, { FormEvent, useState } from 'react'
import { isDefined } from 'utils'

export type PreCheckoutModalProps = {
selectedSubscription:
| {
plan: 'STARTER' | 'PRO'
workspaceId: string
additionalChats: number
additionalStorage: number
currency: 'eur' | 'usd'
}
| undefined
existingCompany?: string
existingEmail?: string
onClose: () => void
}

export const PreCheckoutModal = ({
selectedSubscription,
existingCompany,
existingEmail,
onClose,
}: PreCheckoutModalProps) => {
const router = useRouter()
const { showToast } = useToast()
const { mutate: createCheckoutSession, isLoading: isCreatingCheckout } =
trpc.billing.createCheckoutSession.useMutation({
onError: (error) => {
showToast({
description: error.message,
})
},
onSuccess: ({ checkoutUrl }) => {
router.push(checkoutUrl)
},
})

const [customer, setCustomer] = useState({
company: existingCompany ?? '',
email: existingEmail ?? '',
})

const updateCustomerCompany = (company: string) => {
setCustomer((customer) => ({ ...customer, company }))
}

const updateCustomerEmail = (email: string) => {
setCustomer((customer) => ({ ...customer, email }))
}

const createCustomer = (e: FormEvent) => {
e.preventDefault()
if (!selectedSubscription) return
createCheckoutSession({
...selectedSubscription,
email: customer.email,
company: customer.company,
returnUrl: window.location.href,
})
}

return (
<Modal isOpen={isDefined(selectedSubscription)} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalBody py="8">
<Stack as="form" onSubmit={createCustomer} spacing="4">
<TextInput
isRequired
label="Company name"
defaultValue={customer.company}
onChange={updateCustomerCompany}
withVariableButton={false}
debounceTimeout={0}
/>
<TextInput
isRequired
type="email"
label="Email"
defaultValue={customer.email}
onChange={updateCustomerEmail}
withVariableButton={false}
debounceTimeout={0}
/>
<Button
type="submit"
isLoading={isCreatingCheckout}
colorScheme="blue"
isDisabled={customer.company === '' || customer.email === ''}
>
Go to checkout
</Button>
</Stack>
</ModalBody>
</ModalContent>
</Modal>
)
}
29 changes: 17 additions & 12 deletions apps/builder/src/features/dashboard/components/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Seo } from '@/components/Seo'
import { useUser } from '@/features/account'
import {
PreCheckoutModal,
PreCheckoutModalProps,
} from '@/features/billing/components/PreCheckoutModal'
import { TypebotDndProvider, FolderContent } from '@/features/folders'
import { useWorkspace } from '@/features/workspace'
import { trpc } from '@/lib/trpc'
import { Stack, VStack, Spinner, Text } from '@chakra-ui/react'
import { Plan } from 'db'
import { useRouter } from 'next/router'
Expand All @@ -12,15 +15,11 @@ import { DashboardHeader } from './DashboardHeader'

export const DashboardPage = () => {
const [isLoading, setIsLoading] = useState(false)
const { query, push } = useRouter()
const { query } = useRouter()
const { user } = useUser()
const { workspace } = useWorkspace()
const { mutate: createCheckoutSession } =
trpc.billing.createCheckoutSession.useMutation({
onSuccess: (data) => {
push(data.checkoutUrl)
},
})
const [preCheckoutPlan, setPreCheckoutPlan] =
useState<PreCheckoutModalProps['selectedSubscription']>()

useEffect(() => {
const { subscribePlan, chats, storage } = query as {
Expand All @@ -30,22 +29,28 @@ export const DashboardPage = () => {
}
if (workspace && subscribePlan && user && workspace.plan === 'FREE') {
setIsLoading(true)
createCheckoutSession({
setPreCheckoutPlan({
plan: subscribePlan as 'PRO' | 'STARTER',
workspaceId: workspace.id,
additionalChats: chats ? parseInt(chats) : 0,
additionalStorage: storage ? parseInt(storage) : 0,
returnUrl: window.location.href,
currency: guessIfUserIsEuropean() ? 'eur' : 'usd',
prefilledEmail: user.email ?? undefined,
})
}
}, [createCheckoutSession, query, user, workspace])
}, [query, user, workspace])

return (
<Stack minH="100vh">
<Seo title={workspace?.name ?? 'My typebots'} />
<DashboardHeader />
{!workspace?.stripeId && (
<PreCheckoutModal
selectedSubscription={preCheckoutPlan}
existingEmail={user?.email ?? undefined}
existingCompany={workspace?.name ?? undefined}
onClose={() => setPreCheckoutPlan(undefined)}
/>
)}
<TypebotDndProvider>
{isLoading ? (
<VStack w="full" justifyContent="center" pt="10" spacing={6}>
Expand Down

4 comments on commit 26e5d9c

@vercel
Copy link

@vercel vercel bot commented on 26e5d9c Mar 6, 2023

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

bot.winglabs.com.br
carsalesenquiry.com
chat.marius.digital
chatbot.matthesv.de
chatbot.repplai.com
demo.botscientis.us
demo.wemakebots.xyz
forms.webisharp.com
kbsub.wpwakanda.com
live.botscientis.us
mentoria.omelhor.vc
nutrisamirbayde.com
order.maitempah.com
quest.wpwakanda.com
support.wawplus.com
survey1.digienge.io
surveys.essiell.com
test.botscientis.us
test.reventepro.com
typebot.stillio.com
wordsandimagery.com
88584434.therpm.club
92109660.therpm.club
abbonamento.bwell.it
bium.gratirabbit.com
bot.ansuraniphone.my
bot.barrettamario.it
bot.cotemeuplano.com
bot.leadbooster.help
bot.mycompay.reviews
chat.hayurihijab.com
chatbee.agfunnel.com
click.sevenoways.com
connect.growthguy.in
forms.bonanza.design
hello.advergreen.com
kuiz.sistemniaga.com
offer.botscientis.us
sellmycarglasgow.com
talkbot.agfunnel.com
tenorioadvogados.com
uppity.wpwakanda.com
abutton.wpwakanda.com
acelera.maxbot.com.br
aidigitalmarketing.kr
bbutton.wpwakanda.com
bot.coachayongzul.com
bot.digitalpointer.id
bot.eikju.photography
bot.incusservices.com
bot.meuesocial.com.br
bot.mycompany.reviews
bot.outstandbrand.com
bot.ramonmatos.com.br
newsletter.itshcormeos.com
tarian.theiofoundation.org
ted.meujalecobrasil.com.br
type.dericsoncalari.com.br
bot.pinpointinteractive.com
bot.polychromes-project.com
bot.seidinembroseanchetu.it
chatbot.berbelanjabiz.trade
designguide.techyscouts.com
jcapp.virtuesocialmedia.com
liveconvert2.kandalearn.com
presente.empresarias.com.mx
sell.sellthemotorhome.co.uk
anamnese.odontopavani.com.br
austin.channelautomation.com
bot.marketingplusmindset.com
bot.seidibergamoseanchetu.it
desabafe.sergiolimajr.com.br
download.venturemarketing.in
jc-app.virtuesocialmedia.com
piazzatorre.barrettamario.it
type.cookieacademyonline.com
upload.atlasoutfittersk9.com
bot.brigadeirosemdrama.com.br
forms.escoladeautomacao.com.br
onboarding.libertydreamcare.ie
type.talitasouzamarques.com.br
agendamento.sergiolimajr.com.br
anamnese.clinicamegasjdr.com.br
bookings.littlepartymonkeys.com
bot.comercializadoraomicron.com
elevateyourmind.groovepages.com
viewer-v2-typebot-io.vercel.app
yourfeedback.comebackreward.com
gerador.verificadordehospedes.com
personal-trainer.barrettamario.it
preagendamento.sergiolimajr.com.br
studiotecnicoimmobiliaremerelli.it
download.thailandmicespecialist.com
register.thailandmicespecialist.com
bot.studiotecnicoimmobiliaremerelli.it
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
chrome-os-inquiry-system.itschromeos.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com

@vercel
Copy link

@vercel vercel bot commented on 26e5d9c Mar 6, 2023

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:

docs – ./apps/docs

docs.typebot.io
docs-typebot-io.vercel.app
docs-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 26e5d9c Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 26e5d9c Mar 6, 2023

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

app.typebot.io
builder-v2-git-main-typebot-io.vercel.app
builder-v2-typebot-io.vercel.app

Please sign in to comment.