Skip to content

Commit

Permalink
🚸 (billing) Make sure customer is not created before launching checko…
Browse files Browse the repository at this point in the history
…ut page
  • Loading branch information
baptisteArno committed Aug 22, 2023
1 parent c08e0cd commit 53dd7ba
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 43 deletions.
23 changes: 20 additions & 3 deletions apps/builder/src/features/billing/api/createCheckoutSession.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
import { Plan } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { parseSubscriptionItems } from '../helpers/parseSubscriptionItems'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'

export const createCheckoutSession = authenticatedProcedure
.meta({
Expand Down Expand Up @@ -64,14 +65,30 @@ export const createCheckoutSession = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
select: {
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (!workspace)

if (!workspace || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})

if (workspace.stripeId)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Customer already exists, use updateSubscription endpoint.',
})

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
import { Plan } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'

export const createCustomCheckoutSession = authenticatedProcedure
.meta({
Expand Down Expand Up @@ -38,15 +39,23 @@ export const createCustomCheckoutSession = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
include: {
select: {
stripeId: true,
claimableCustomPlan: true,
name: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (
!workspace?.claimableCustomPlan ||
workspace.claimableCustomPlan.claimedAt
workspace.claimableCustomPlan.claimedAt ||
isAdminWriteWorkspaceForbidden(workspace, user)
)
throw new TRPCError({
code: 'NOT_FOUND',
Expand Down
11 changes: 8 additions & 3 deletions apps/builder/src/features/billing/api/getBillingPortalUrl.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { WorkspaceRole } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'

export const getBillingPortalUrl = authenticatedProcedure
.meta({
Expand Down Expand Up @@ -34,13 +34,18 @@ export const getBillingPortalUrl = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
select: {
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (!workspace?.stripeId)
if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
Expand Down
16 changes: 14 additions & 2 deletions apps/builder/src/features/billing/api/getSubscription.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { WorkspaceRole } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
import { priceIds } from '@typebot.io/lib/pricing'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'

export const getSubscription = authenticatedProcedure
.meta({
Expand Down Expand Up @@ -36,9 +36,21 @@ export const getSubscription = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
select: {
stripeId: true,
members: {
select: {
userId: true,
},
},
},
})
if (!workspace || isReadWorkspaceFobidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
if (!workspace?.stripeId)
return {
subscription: null,
Expand Down
14 changes: 11 additions & 3 deletions apps/builder/src/features/billing/api/listInvoices.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { WorkspaceRole } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { isDefined } from '@typebot.io/lib'
import { z } from 'zod'
import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'

export const listInvoices = authenticatedProcedure
.meta({
Expand Down Expand Up @@ -36,10 +36,18 @@ export const listInvoices = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
select: {
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (!workspace?.stripeId)
if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
Expand Down
18 changes: 15 additions & 3 deletions apps/builder/src/features/billing/api/updateSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEven
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
import { Plan } from '@typebot.io/prisma'
import { workspaceSchema } from '@typebot.io/schemas'
import Stripe from 'stripe'
import { isDefined } from '@typebot.io/lib'
Expand All @@ -14,6 +14,7 @@ import {
} from '@typebot.io/lib/pricing'
import { chatPriceIds, storagePriceIds } from './getSubscription'
import { createCheckoutSessionUrl } from './createCheckoutSession'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'

export const updateSubscription = authenticatedProcedure
.meta({
Expand Down Expand Up @@ -63,10 +64,21 @@ export const updateSubscription = authenticatedProcedure
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
select: {
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (!workspace?.stripeId)
if (
!workspace?.stripeId ||
isAdminWriteWorkspaceForbidden(workspace, user)
)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
Expand Down
4 changes: 3 additions & 1 deletion apps/builder/src/features/billing/billing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ test('plan changes should work', async ({ page }) => {
page.waitForNavigation(),
page.click('text="Billing portal"'),
])
await expect(page.getByText('$247.00 per month')).toBeVisible()
await expect(page.getByText('$247.00 per month')).toBeVisible({
timeout: 10000,
})
await expect(page.getByText('(×25000)')).toBeVisible()
await expect(page.getByText('(×15)')).toBeVisible()
await expect(page.locator('text="Add payment method"')).toBeVisible()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UsageProgressBars } from './UsageProgressBars'
import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'

export const BillingSettingsLayout = () => {
const { workspace, refreshWorkspace } = useWorkspace()
const { workspace } = useWorkspace()

if (!workspace) return null
return (
Expand All @@ -20,10 +20,7 @@ export const BillingSettingsLayout = () => {
workspace.plan !== Plan.LIFETIME &&
workspace.plan !== Plan.UNLIMITED &&
workspace.plan !== Plan.OFFERED && (
<ChangePlanForm
workspace={workspace}
onUpgradeSuccess={refreshWorkspace}
/>
<ChangePlanForm workspace={workspace} />
)}
</Stack>

Expand Down
10 changes: 6 additions & 4 deletions apps/builder/src/features/billing/components/ChangePlanForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ import { StripeClimateLogo } from './StripeClimateLogo'

type Props = {
workspace: Workspace
onUpgradeSuccess: () => void
}

export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
export const ChangePlanForm = ({ workspace }: Props) => {
const scopedT = useScopedI18n('billing')

const { user } = useUser()
Expand All @@ -28,7 +27,9 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
useState<PreCheckoutModalProps['selectedSubscription']>()
const [isYearly, setIsYearly] = useState(true)

const { data } = trpc.billing.getSubscription.useQuery(
const trpcContext = trpc.useContext()

const { data, refetch } = trpc.billing.getSubscription.useQuery(
{
workspaceId: workspace.id,
},
Expand All @@ -52,7 +53,8 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
window.location.href = checkoutUrl
return
}
onUpgradeSuccess()
refetch()
trpcContext.workspace.getWorkspace.invalidate()
showToast({
status: 'success',
description: scopedT('updateSuccessToast.description', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const ChangePlanModal = ({
type,
}: ChangePlanModalProps) => {
const t = useI18n()
const { workspace, refreshWorkspace } = useWorkspace()
const { workspace } = useWorkspace()
return (
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
<ModalOverlay />
Expand All @@ -36,12 +36,7 @@ export const ChangePlanModal = ({
{t('billing.upgradeLimitLabel', { type: type })}
</AlertInfo>
)}
{workspace && (
<ChangePlanForm
workspace={workspace}
onUpgradeSuccess={refreshWorkspace}
/>
)}
{workspace && <ChangePlanForm workspace={workspace} />}
</ModalBody>

<ModalFooter>
Expand Down
7 changes: 0 additions & 7 deletions apps/builder/src/features/workspace/WorkspaceProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const workspaceContext = createContext<{
createWorkspace: (name?: string) => Promise<void>
updateWorkspace: (updates: { icon?: string; name?: string }) => void
deleteCurrentWorkspace: () => Promise<void>
refreshWorkspace: () => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
Expand Down Expand Up @@ -166,11 +165,6 @@ export const WorkspaceProvider = ({
await deleteWorkspaceMutation.mutateAsync({ workspaceId })
}

const refreshWorkspace = () => {
trpcContext.workspace.getWorkspace.invalidate()
trpcContext.billing.getSubscription.invalidate()
}

return (
<workspaceContext.Provider
value={{
Expand All @@ -181,7 +175,6 @@ export const WorkspaceProvider = ({
createWorkspace,
updateWorkspace,
deleteCurrentWorkspace,
refreshWorkspace,
}}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { MemberInWorkspace, User } from '@typebot.io/prisma'

export const isAdminWriteWorkspaceForbidden = (
workspace: {
members: MemberInWorkspace[]
members: Pick<MemberInWorkspace, 'role' | 'userId'>[]
},
user: Pick<User, 'email' | 'id'>
) => {
Expand Down
13 changes: 13 additions & 0 deletions apps/builder/src/pages/api/stripe/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
const { data } = await stripe.subscriptions.list({
customer: subscription.customer as string,
limit: 1,
status: 'active',
})
const existingSubscription = data[0] as
| Stripe.Subscription
| undefined
if (existingSubscription)
return res.send({
message:
'An active subscription still exists. Skipping downgrade.',
})
const workspace = await prisma.workspace.update({
where: {
stripeId: subscription.customer as string,
Expand Down

4 comments on commit 53dd7ba

@vercel
Copy link

@vercel vercel bot commented on 53dd7ba Aug 22, 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 53dd7ba Aug 22, 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

metodoelev.com.br
nutriandreia.shop
ov1.wpwakanda.com
ov2.wpwakanda.com
ov3.wpwakanda.com
pcb.drapamela.com
redeemchatgpt.com
softwarelucra.com
support.triplo.ai
test.eqfeqfeq.com
viewer.typebot.io
welcome.triplo.ai
www.thegymgame.it
zeropendencia.com
1988.bouclidom.com
a.onewebcenter.com
amancarseat.online
amostra-safe.click
andreimayer.com.br
bebesemcolicas.com
bot.innovacion.fun
bot.lucide.contact
bot.neferlopez.com
bot.photonative.de
bot.samplehunt.com
bot.sinalcerto.com
bot.wphelpchat.com
bots.robomotion.io
cadu.uninta.edu.br
chat.hand-made.one
chat.tuanpakya.com
chat.webisharp.com
chatbotforthat.com
descobrindotudo.me
dicanatural.online
digitalhelp.com.au
draraquelnutri.com
drcarlosyoshi.site
goalsettingbot.com
leads.gecoelho.com
noticiasnet.online
novoappespiao.site
omarcodosanjos.com
pant.maxbot.com.br
pantherview.cr8.ai
positivobra.com.br
rollingball.cr8.ai
speciallife.com.br
sub.yolozeeeer.com
se.onewebcenter.com
secretespiao.online
start.belenmotz.com
support.wawplus.com
survey1.digienge.io
surveys.essiell.com
test.botscientis.us
test.getreview.help
test.reventepro.com
typebot.stillio.app
typebot.stillio.com
viewer-v2-typebot-io.vercel.app
mdb.assessoria.desideri.progenbr.com
mdb.assessoria.fernanda.progenbr.com
mdb.assessoria.jbatista.progenbr.com
mdb.assessoria.mauricio.progenbr.com
mdb.evento.autocadastro.progenbr.com
form.shopmercedesbenzsouthorlando.com
mdb.evento.equipeinterna.progenbr.com
bot.studiotecnicoimmobiliaremerelli.it
mdb.assessoria.boaventura.progenbr.com
mdb.assessoria.jtrebesqui.progenbr.com
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
gabinete.baleia.formulario.progenbr.com
mdb.assessoria.carreirinha.progenbr.com
chrome-os-inquiry-system.itschromeos.com
mdb.assessoria.paulomarques.progenbr.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com
mdb.assessoria.qrcode.ademir.progenbr.com
mdb.assessoria.qrcode.arthur.progenbr.com
mdb.assessoria.qrcode.danilo.progenbr.com
mdb.assessoria.qrcode.marcao.progenbr.com
mdb.assessoria.qrcode.marcio.progenbr.com
mdb.assessoria.qrcode.aloisio.progenbr.com
mdb.assessoria.qrcode.girotto.progenbr.com
mdb.assessoria.qrcode.marinho.progenbr.com
mdb.assessoria.qrcode.rodrigo.progenbr.com
mdb.assessoria.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.desideri.progenbr.com
mdb.assessoria.qrcode.fernanda.progenbr.com
mdb.assessoria.qrcode.jbatista.progenbr.com
mdb.assessoria.qrcode.mauricio.progenbr.com
mdb.assessoria.fernanda.regional.progenbr.com
mdb.assessoria.qrcode.boaventura.progenbr.com
mdb.assessoria.qrcode.jtrebesqui.progenbr.com
mdb.assessoria.qrcode.carreirinha.progenbr.com
mdb.assessoria.qrcode.paulomarques.progenbr.com
mdb.assessoria.qrcode.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.fernanda.regional.progenbr.com

@vercel
Copy link

@vercel vercel bot commented on 53dd7ba Aug 22, 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

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

@vercel
Copy link

@vercel vercel bot commented on 53dd7ba Aug 22, 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.vercel.app
docs-git-main-typebot-io.vercel.app
docs.typebot.io

Please sign in to comment.