Skip to content

Commit

Permalink
🛂 Add new yearly plans and graduated pricing
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Stripe environment variables have changed. New ones are required. Check out the new Stripe configuration in the
docs.

Closes #457
  • Loading branch information
baptisteArno committed Apr 13, 2023
1 parent 39d0dba commit 2cbf834
Show file tree
Hide file tree
Showing 33 changed files with 1,257 additions and 1,399 deletions.
70 changes: 0 additions & 70 deletions apps/builder/src/features/billing/api/cancelSubscription.ts

This file was deleted.

11 changes: 5 additions & 6 deletions apps/builder/src/features/billing/api/createCheckoutSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const createCheckoutSession = authenticatedProcedure
value: z.string(),
})
.optional(),
isYearly: z.boolean(),
})
)
.output(
Expand All @@ -52,14 +53,11 @@ export const createCheckoutSession = authenticatedProcedure
returnUrl,
additionalChats,
additionalStorage,
isYearly,
},
ctx: { user },
}) => {
if (
!process.env.STRIPE_SECRET_KEY ||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID ||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)
if (!process.env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
Expand Down Expand Up @@ -120,7 +118,8 @@ export const createCheckoutSession = authenticatedProcedure
line_items: parseSubscriptionItems(
plan,
additionalChats,
additionalStorage
additionalStorage,
isYearly
),
})

Expand Down
54 changes: 30 additions & 24 deletions apps/builder/src/features/billing/api/getSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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'

export const getSubscription = authenticatedProcedure
.meta({
Expand All @@ -23,15 +24,11 @@ export const getSubscription = authenticatedProcedure
)
.output(
z.object({
subscription: subscriptionSchema,
subscription: subscriptionSchema.or(z.null()),
})
)
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
if (
!process.env.STRIPE_SECRET_KEY ||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID ||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)
if (!process.env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
Expand All @@ -43,10 +40,9 @@ export const getSubscription = authenticatedProcedure
},
})
if (!workspace?.stripeId)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
return {
subscription: null,
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
Expand All @@ -58,24 +54,34 @@ export const getSubscription = authenticatedProcedure
const subscription = subscriptions?.data.shift()

if (!subscription)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Subscription not found',
})
return {
subscription: null,
}

return {
subscription: {
additionalChatsIndex:
subscription?.items.data.find(
(item) =>
item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
)?.quantity ?? 0,
additionalStorageIndex:
subscription.items.data.find(
(item) =>
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)?.quantity ?? 0,
isYearly: subscription.items.data.some((item) => {
return (
priceIds.STARTER.chats.yearly === item.price.id ||
priceIds.STARTER.storage.yearly === item.price.id ||
priceIds.PRO.chats.yearly === item.price.id ||
priceIds.PRO.storage.yearly === item.price.id
)
}),
currency: subscription.currency as 'usd' | 'eur',
cancelDate: subscription.cancel_at
? new Date(subscription.cancel_at * 1000)
: undefined,
},
}
})

export const chatPriceIds = [priceIds.STARTER.chats.monthly]
.concat(priceIds.STARTER.chats.yearly)
.concat(priceIds.PRO.chats.monthly)
.concat(priceIds.PRO.chats.yearly)

export const storagePriceIds = [priceIds.STARTER.storage.monthly]
.concat(priceIds.STARTER.storage.yearly)
.concat(priceIds.PRO.storage.monthly)
.concat(priceIds.PRO.storage.yearly)
2 changes: 0 additions & 2 deletions apps/builder/src/features/billing/api/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { router } from '@/helpers/server/trpc'
import { cancelSubscription } from './cancelSubscription'
import { createCheckoutSession } from './createCheckoutSession'
import { getBillingPortalUrl } from './getBillingPortalUrl'
import { getSubscription } from './getSubscription'
Expand All @@ -10,7 +9,6 @@ import { updateSubscription } from './updateSubscription'
export const billingRouter = router({
getBillingPortalUrl,
listInvoices,
cancelSubscription,
createCheckoutSession,
updateSubscription,
getSubscription,
Expand Down
57 changes: 35 additions & 22 deletions apps/builder/src/features/billing/api/updateSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { workspaceSchema } from '@typebot.io/schemas'
import Stripe from 'stripe'
import { isDefined } from '@typebot.io/lib'
import { z } from 'zod'
import {
getChatsLimit,
getStorageLimit,
priceIds,
} from '@typebot.io/lib/pricing'
import { chatPriceIds, storagePriceIds } from './getSubscription'

export const updateSubscription = authenticatedProcedure
.meta({
Expand All @@ -25,6 +31,7 @@ export const updateSubscription = authenticatedProcedure
additionalChats: z.number(),
additionalStorage: z.number(),
currency: z.enum(['usd', 'eur']),
isYearly: z.boolean(),
})
)
.output(
Expand All @@ -40,14 +47,11 @@ export const updateSubscription = authenticatedProcedure
additionalChats,
additionalStorage,
currency,
isYearly,
},
ctx: { user },
}) => {
if (
!process.env.STRIPE_SECRET_KEY ||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID ||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)
if (!process.env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
Expand All @@ -70,42 +74,48 @@ export const updateSubscription = authenticatedProcedure
customer: workspace.stripeId,
})
const subscription = data[0] as Stripe.Subscription | undefined
const currentStarterPlanItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_STARTER_PRICE_ID
)?.id
const currentProPlanItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_PRO_PRICE_ID
const currentPlanItemId = subscription?.items.data.find((item) =>
[
process.env.STRIPE_STARTER_PRODUCT_ID,
process.env.STRIPE_PRO_PRODUCT_ID,
].includes(item.price.product.toString())
)?.id
const currentAdditionalChatsItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
(item) => chatPriceIds.includes(item.price.id)
)?.id
const currentAdditionalStorageItemId = subscription?.items.data.find(
(item) =>
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
(item) => storagePriceIds.includes(item.price.id)
)?.id
const frequency = isYearly ? 'yearly' : 'monthly'

const items = [
{
id: currentStarterPlanItemId ?? currentProPlanItemId,
price:
plan === Plan.STARTER
? process.env.STRIPE_STARTER_PRICE_ID
: process.env.STRIPE_PRO_PRICE_ID,
id: currentPlanItemId,
price: priceIds[plan].base[frequency],
quantity: 1,
},
additionalChats === 0 && !currentAdditionalChatsItemId
? undefined
: {
id: currentAdditionalChatsItemId,
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
quantity: additionalChats,
price: priceIds[plan].chats[frequency],
quantity: getChatsLimit({
plan,
additionalChatsIndex: additionalChats,
customChatsLimit: null,
}),
deleted: subscription ? additionalChats === 0 : undefined,
},
additionalStorage === 0 && !currentAdditionalStorageItemId
? undefined
: {
id: currentAdditionalStorageItemId,
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
quantity: additionalStorage,
price: priceIds[plan].storage[frequency],
quantity: getStorageLimit({
plan,
additionalStorageIndex: additionalStorage,
customStorageLimit: null,
}),
deleted: subscription ? additionalStorage === 0 : undefined,
},
].filter(isDefined)
Expand All @@ -126,6 +136,9 @@ export const updateSubscription = authenticatedProcedure
items,
currency,
default_payment_method: paymentMethods[0].id,
automatic_tax: {
enabled: true,
},
})
}

Expand Down
Loading

4 comments on commit 2cbf834

@vercel
Copy link

@vercel vercel bot commented on 2cbf834 Apr 13, 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

filmylogy.com
goldorayo.com
rabbit.cr8.ai
signup.cr8.ai
start.taxt.co
turkey.cr8.ai
vhpage.cr8.ai
vitamyway.com
am.nigerias.io
abbonamento.bwell.it
assistent.m-vogel.de
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
menu.numero-primo.it
menukb.wpwakanda.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
bot.robertohairlab.it
bot.sharemyreview.net
bot.truongnguyen.live
botz.cloudsiteapp.com
cdd.searchcube.com.sg
chat.missarkansas.org
chatbot.ownacademy.co
chats.maisefetivo.com
criar.somaperuzzo.com
homerun.wpwakanda.com
sbutton.wpwakanda.com
zillabot.saaszilla.co
815639944.21000000.one
83720273.bouclidom.com
type.dericsoncalari.com.br
bot.pinpointinteractive.com
bot.polychromes-project.com
bot.seidinembroseanchetu.it
chat.semanalimpanome.com.br
designguide.techyscouts.com
liveconvert2.kandalearn.com
presente.empresarias.com.mx
register.algorithmpress.com
sell.sellthemotorhome.co.uk
anamnese.odontopavani.com.br
austin.channelautomation.com
bot.marketingplusmindset.com
bot.seidibergamoseanchetu.it
desabafe.sergiolimajr.com.br
download.venturemarketing.in
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
bot.cabin-rentals-of-georgia.net
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 2cbf834 Apr 13, 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-typebot-io.vercel.app
app.typebot.io
builder-v2-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 2cbf834 Apr 13, 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

@vercel
Copy link

@vercel vercel bot commented on 2cbf834 Apr 13, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.