2
0

📄 Add Commercial License for ee folder (#1532)

This commit is contained in:
Baptiste Arnaud
2024-05-23 10:42:23 +02:00
committed by GitHub
parent 5680829906
commit 0eacbebbbe
246 changed files with 1472 additions and 1588 deletions

View File

@ -1,3 +1,11 @@
Copyright (c) 2020-present Typebot
Portions of this software are licensed as follows:
- All content that resides under https://github.com/baptisteArno/typebot.io/tree/main/ee directory of this repository is licensed under the license defined in [ee/LICENSE](./ee/LICENSE).
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007

View File

@ -110,4 +110,4 @@ Made with [contrib.rocks](https://contrib.rocks).
## License
Typebot is open-source under the GNU Affero General Public License Version 3 (AGPLv3). You will find more information about the license and how to comply with it [here](https://docs.typebot.io/self-hosting#license-requirements).
Most of Typebot's code is open-source under the GNU Affero General Public License Version 3 (AGPLv3). You will find more information about the license and how to comply with it [here](https://docs.typebot.io/self-hosting#license-requirements).

View File

@ -1,11 +1,7 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Plan } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
import { createCheckoutSession as createCheckoutSessionHandler } from '@typebot.io/billing/api/createCheckoutSession'
export const createCheckoutSession = authenticatedProcedure
.meta({
@ -38,130 +34,6 @@ export const createCheckoutSession = authenticatedProcedure
checkoutUrl: z.string(),
})
)
.mutation(
async ({
input: { vat, email, company, workspaceId, currency, plan, returnUrl },
ctx: { user },
}) => {
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
})
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
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(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
await prisma.user.updateMany({
where: {
id: user.id,
},
data: {
company,
},
})
const customer = await stripe.customers.create({
email,
name: company,
metadata: { workspaceId },
tax_id_data: vat
? [vat as Stripe.CustomerCreateParams.TaxIdDatum]
: undefined,
})
const checkoutUrl = await createCheckoutSessionUrl(stripe)({
customerId: customer.id,
userId: user.id,
workspaceId,
currency,
plan,
returnUrl,
})
if (!checkoutUrl)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe checkout session creation failed',
})
return {
checkoutUrl,
}
}
.mutation(async ({ input, ctx: { user } }) =>
createCheckoutSessionHandler({ ...input, user })
)
type Props = {
customerId: string
workspaceId: string
currency: 'usd' | 'eur'
plan: 'STARTER' | 'PRO'
returnUrl: string
userId: string
}
export const createCheckoutSessionUrl =
(stripe: Stripe) =>
async ({ customerId, workspaceId, currency, plan, returnUrl }: Props) => {
const session = await stripe.checkout.sessions.create({
success_url: `${returnUrl}?stripe=${plan}&success=true`,
cancel_url: `${returnUrl}?stripe=cancel`,
allow_promotion_codes: true,
customer: customerId,
customer_update: {
address: 'auto',
name: 'never',
},
mode: 'subscription',
metadata: {
workspaceId,
plan,
},
currency,
billing_address_collection: 'required',
automatic_tax: { enabled: true },
line_items: [
{
price:
plan === 'STARTER'
? env.STRIPE_STARTER_PRICE_ID
: env.STRIPE_PRO_PRICE_ID,
quantity: 1,
},
{
price:
plan === 'STARTER'
? env.STRIPE_STARTER_CHATS_PRICE_ID
: env.STRIPE_PRO_CHATS_PRICE_ID,
},
],
})
return session.url
}

View File

@ -1,11 +1,6 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Plan } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
import { createCustomCheckoutSession as createCustomCheckoutSessionHandler } from '@typebot.io/billing/api/createCustomCheckoutSession'
export const createCustomCheckoutSession = authenticatedProcedure
.meta({
@ -30,154 +25,9 @@ export const createCustomCheckoutSession = authenticatedProcedure
checkoutUrl: z.string(),
})
)
.mutation(
async ({ input: { email, workspaceId, returnUrl }, ctx: { user } }) => {
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
.mutation(async ({ input, ctx: { user } }) =>
createCustomCheckoutSessionHandler({
...input,
user,
})
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
stripeId: true,
claimableCustomPlan: true,
name: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (
!workspace?.claimableCustomPlan ||
workspace.claimableCustomPlan.claimedAt ||
isAdminWriteWorkspaceForbidden(workspace, user)
)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Custom plan not found',
})
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const vat =
workspace.claimableCustomPlan.vatValue &&
workspace.claimableCustomPlan.vatType
? ({
type: workspace.claimableCustomPlan.vatType,
value: workspace.claimableCustomPlan.vatValue,
} as Stripe.CustomerCreateParams.TaxIdDatum)
: undefined
const customer = workspace.stripeId
? await stripe.customers.retrieve(workspace.stripeId)
: await stripe.customers.create({
email,
name: workspace.claimableCustomPlan.companyName ?? workspace.name,
metadata: { workspaceId },
tax_id_data: vat ? [vat] : undefined,
})
const session = await stripe.checkout.sessions.create({
success_url: `${returnUrl}?stripe=${Plan.CUSTOM}&success=true`,
cancel_url: `${returnUrl}?stripe=cancel`,
allow_promotion_codes: true,
customer: customer.id,
customer_update: {
address: 'auto',
name: 'never',
},
mode: 'subscription',
metadata: {
claimableCustomPlanId: workspace.claimableCustomPlan.id,
},
currency: workspace.claimableCustomPlan.currency,
billing_address_collection: 'required',
automatic_tax: { enabled: true },
line_items: [
{
price_data: {
currency: workspace.claimableCustomPlan.currency,
tax_behavior: 'exclusive',
recurring: {
interval: workspace.claimableCustomPlan.isYearly
? 'year'
: 'month',
},
product_data: {
name: workspace.claimableCustomPlan.name,
description:
workspace.claimableCustomPlan.description ?? undefined,
},
unit_amount: workspace.claimableCustomPlan.price * 100,
},
quantity: 1,
},
{
price_data: {
currency: workspace.claimableCustomPlan.currency,
tax_behavior: 'exclusive',
recurring: {
interval: workspace.claimableCustomPlan.isYearly
? 'year'
: 'month',
},
product_data: {
name: 'Included chats per month',
},
unit_amount: 0,
},
quantity: workspace.claimableCustomPlan.chatsLimit,
},
{
price_data: {
currency: workspace.claimableCustomPlan.currency,
tax_behavior: 'exclusive',
recurring: {
interval: workspace.claimableCustomPlan.isYearly
? 'year'
: 'month',
},
product_data: {
name: 'Included storage per month',
},
unit_amount: 0,
},
quantity: workspace.claimableCustomPlan.storageLimit,
},
{
price_data: {
currency: workspace.claimableCustomPlan.currency,
tax_behavior: 'exclusive',
recurring: {
interval: workspace.claimableCustomPlan.isYearly
? 'year'
: 'month',
},
product_data: {
name: 'Included seats',
},
unit_amount: 0,
},
quantity: workspace.claimableCustomPlan.seatsLimit,
},
],
})
if (!session.url)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe checkout session creation failed',
})
return {
checkoutUrl: session.url,
}
}
)

View File

@ -1,10 +1,6 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import Stripe from 'stripe'
import { z } from 'zod'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
import { getBillingPortalUrl as getBillingPortalUrlHandler } from '@typebot.io/billing/api/getBillingPortalUrl'
export const getBillingPortalUrl = authenticatedProcedure
.meta({
@ -26,39 +22,6 @@ export const getBillingPortalUrl = authenticatedProcedure
billingPortalUrl: z.string(),
})
)
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'STRIPE_SECRET_KEY var is missing',
})
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const portalSession = await stripe.billingPortal.sessions.create({
customer: workspace.stripeId,
return_url: `${env.NEXTAUTH_URL}/typebots`,
})
return {
billingPortalUrl: portalSession.url,
}
})
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
getBillingPortalUrlHandler({ workspaceId, user })
)

View File

@ -1,11 +1,7 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import Stripe from 'stripe'
import { z } from 'zod'
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { env } from '@typebot.io/env'
import { getSubscription as getSubscriptionHandler } from '@typebot.io/billing/api/getSubscription'
export const getSubscription = authenticatedProcedure
.meta({
@ -27,65 +23,6 @@ export const getSubscription = authenticatedProcedure
subscription: subscriptionSchema.or(z.null().openapi({ type: 'string' })),
})
)
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
})
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
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,
}
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const subscriptions = await stripe.subscriptions.list({
customer: workspace.stripeId,
})
const currentSubscription = subscriptions.data
.filter((sub) => ['past_due', 'active'].includes(sub.status))
.sort((a, b) => a.created - b.created)
.shift()
if (!currentSubscription)
return {
subscription: null,
}
return {
subscription: {
currentBillingPeriod:
subscriptionSchema.shape.currentBillingPeriod.parse({
start: new Date(currentSubscription.current_period_start),
end: new Date(currentSubscription.current_period_end),
}),
status: subscriptionSchema.shape.status.parse(
currentSubscription.status
),
currency: currentSubscription.currency as 'usd' | 'eur',
cancelDate: currentSubscription.cancel_at
? new Date(currentSubscription.cancel_at * 1000)
: undefined,
},
}
})
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
getSubscriptionHandler({ workspaceId, user })
)

View File

@ -1,10 +1,6 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { env } from '@typebot.io/env'
import Stripe from 'stripe'
import { getUsage as getUsageHandler } from '@typebot.io/billing/api/getUsage'
export const getUsage = authenticatedProcedure
.meta({
@ -26,87 +22,6 @@ export const getUsage = authenticatedProcedure
})
)
.output(z.object({ totalChatsUsed: z.number(), resetsAt: z.date() }))
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
stripeId: true,
plan: true,
members: {
select: {
userId: true,
},
},
typebots: {
select: { id: true },
},
},
})
if (!workspace || isReadWorkspaceFobidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
if (
!env.STRIPE_SECRET_KEY ||
!workspace.stripeId ||
(workspace.plan !== 'STARTER' && workspace.plan !== 'PRO')
) {
const now = new Date()
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const totalChatsUsed = await prisma.result.count({
where: {
typebotId: { in: workspace.typebots.map((typebot) => typebot.id) },
hasStarted: true,
createdAt: {
gte: firstDayOfMonth,
},
},
})
const firstDayOfNextMonth = new Date(
firstDayOfMonth.getFullYear(),
firstDayOfMonth.getMonth() + 1,
1
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
getUsageHandler({ workspaceId, user })
)
return { totalChatsUsed, resetsAt: firstDayOfNextMonth }
}
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const subscriptions = await stripe.subscriptions.list({
customer: workspace.stripeId,
})
const currentSubscription = subscriptions.data
.filter((sub) => ['past_due', 'active'].includes(sub.status))
.sort((a, b) => a.created - b.created)
.shift()
if (!currentSubscription)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: `No subscription found on workspace: ${workspaceId}`,
})
const totalChatsUsed = await prisma.result.count({
where: {
typebotId: { in: workspace.typebots.map((typebot) => typebot.id) },
hasStarted: true,
createdAt: {
gte: new Date(currentSubscription.current_period_start * 1000),
},
},
})
return {
totalChatsUsed,
resetsAt: new Date(currentSubscription.current_period_end * 1000),
}
})

View File

@ -1,12 +1,7 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
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'
import { env } from '@typebot.io/env'
import { listInvoices as listInvoicesHandler } from '@typebot.io/billing/api/listInvoices'
export const listInvoices = authenticatedProcedure
.meta({
@ -32,49 +27,6 @@ export const listInvoices = authenticatedProcedure
invoices: z.array(invoiceSchema),
})
)
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'STRIPE_SECRET_KEY var is missing',
})
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const invoices = await stripe.invoices.list({
customer: workspace.stripeId,
limit: 50,
})
return {
invoices: invoices.data
.filter(
(invoice) => isDefined(invoice.invoice_pdf) && isDefined(invoice.id)
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
listInvoicesHandler({ workspaceId, user })
)
.map((invoice) => ({
id: invoice.number as string,
url: invoice.invoice_pdf as string,
amount: invoice.subtotal,
currency: invoice.currency,
date: invoice.status_transitions.paid_at,
})),
}
})

View File

@ -1,14 +1,8 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Plan } from '@typebot.io/prisma'
import { workspaceSchema } from '@typebot.io/schemas'
import Stripe from 'stripe'
import { z } from 'zod'
import { createCheckoutSessionUrl } from './createCheckoutSession'
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
import { env } from '@typebot.io/env'
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
import { updateSubscription as updateSubscriptionHandler } from '@typebot.io/billing/api/updateSubscription'
export const updateSubscription = authenticatedProcedure
.meta({
@ -34,140 +28,9 @@ export const updateSubscription = authenticatedProcedure
checkoutUrl: z.string().nullish(),
})
)
.mutation(
async ({
input: { workspaceId, plan, currency, returnUrl },
ctx: { user },
}) => {
if (!env.STRIPE_SECRET_KEY)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Stripe environment variables are missing',
.mutation(async ({ input, ctx: { user } }) =>
updateSubscriptionHandler({
...input,
user,
})
const workspace = await prisma.workspace.findFirst({
where: {
id: workspaceId,
},
select: {
isPastDue: true,
stripeId: true,
members: {
select: {
userId: true,
role: true,
},
},
},
})
if (workspace?.isPastDue)
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'You have unpaid invoices. Please head over your billing portal to pay it.',
})
if (
!workspace?.stripeId ||
isAdminWriteWorkspaceForbidden(workspace, user)
)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Workspace not found',
})
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const { data } = await stripe.subscriptions.list({
customer: workspace.stripeId,
limit: 1,
status: 'active',
})
const subscription = data[0] as Stripe.Subscription | undefined
const currentPlanItemId = subscription?.items.data.find((item) =>
[env.STRIPE_STARTER_PRICE_ID, env.STRIPE_PRO_PRICE_ID].includes(
item.price.id
)
)?.id
const currentUsageItemId = subscription?.items.data.find(
(item) =>
item.price.id === env.STRIPE_STARTER_CHATS_PRICE_ID ||
item.price.id === env.STRIPE_PRO_CHATS_PRICE_ID
)?.id
const items = [
{
id: currentPlanItemId,
price:
plan === Plan.STARTER
? env.STRIPE_STARTER_PRICE_ID
: env.STRIPE_PRO_PRICE_ID,
quantity: 1,
},
{
id: currentUsageItemId,
price:
plan === Plan.STARTER
? env.STRIPE_STARTER_CHATS_PRICE_ID
: env.STRIPE_PRO_CHATS_PRICE_ID,
},
]
if (subscription) {
if (plan === 'STARTER') {
const totalChatsUsed = await prisma.result.count({
where: {
typebot: { workspaceId },
hasStarted: true,
createdAt: {
gte: new Date(subscription.current_period_start * 1000),
},
},
})
if (totalChatsUsed >= 4000) {
throw new TRPCError({
code: 'BAD_REQUEST',
message:
"You have collected more than 4000 chats during this billing cycle. You can't downgrade to the Starter.",
})
}
}
await stripe.subscriptions.update(subscription.id, {
items,
proration_behavior: 'always_invoice',
})
} else {
const checkoutUrl = await createCheckoutSessionUrl(stripe)({
customerId: workspace.stripeId,
userId: user.id,
workspaceId,
currency,
plan,
returnUrl,
})
return { checkoutUrl }
}
const updatedWorkspace = await prisma.workspace.update({
where: { id: workspaceId },
data: {
plan,
isQuarantined: false,
},
})
await trackEvents([
{
name: 'Subscription updated',
workspaceId,
userId: user.id,
data: {
plan,
},
},
])
return { workspace: updatedWorkspace }
}
)

View File

@ -11,7 +11,7 @@ import { StarterPlanPricingCard } from './StarterPlanPricingCard'
import { ProPlanPricingCard } from './ProPlanPricingCard'
import { useTranslate } from '@tolgee/react'
import { StripeClimateLogo } from './StripeClimateLogo'
import { guessIfUserIsEuropean } from '@typebot.io/billing/guessIfUserIsEuropean'
import { guessIfUserIsEuropean } from '@typebot.io/billing/helpers/guessIfUserIsEuropean'
import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider'
type Props = {

View File

@ -18,7 +18,7 @@ import {
} from '@chakra-ui/react'
import { useTranslate } from '@tolgee/react'
import { proChatTiers } from '@typebot.io/billing/constants'
import { formatPrice } from '@typebot.io/billing/formatPrice'
import { formatPrice } from '@typebot.io/billing/helpers/formatPrice'
type Props = {
isOpen: boolean

View File

@ -17,8 +17,8 @@ import {
import { useRouter } from 'next/router'
import React, { FormEvent, useState } from 'react'
import { isDefined } from '@typebot.io/lib'
import { taxIdTypes } from '../taxIdTypes'
import { useTranslate } from '@tolgee/react'
import { taxIdTypes } from '@typebot.io/billing/taxIdTypes'
export type PreCheckoutModalProps = {
selectedSubscription:

View File

@ -14,7 +14,7 @@ import {
import { Plan } from '@typebot.io/prisma'
import { FeaturesList } from './FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { formatPrice } from '@typebot.io/billing/formatPrice'
import { formatPrice } from '@typebot.io/billing/helpers/formatPrice'
import { ChatsProTiersModal } from './ChatsProTiersModal'
import { prices } from '@typebot.io/billing/constants'
import { T, useTranslate } from '@tolgee/react'

View File

@ -10,7 +10,7 @@ import {
import { Plan } from '@typebot.io/prisma'
import { FeaturesList } from './FeaturesList'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { formatPrice } from '@typebot.io/billing/formatPrice'
import { formatPrice } from '@typebot.io/billing/helpers/formatPrice'
import { prices } from '@typebot.io/billing/constants'
import { T, useTranslate } from '@tolgee/react'

View File

@ -12,7 +12,7 @@ import { AlertIcon } from '@/components/icons'
import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider'
import { parseNumberWithCommas } from '@typebot.io/lib'
import { defaultQueryOptions, trpc } from '@/lib/trpc'
import { getChatsLimit } from '@typebot.io/billing/getChatsLimit'
import { getChatsLimit } from '@typebot.io/billing/helpers/getChatsLimit'
import { useTranslate } from '@tolgee/react'
type Props = {

View File

@ -14,7 +14,7 @@ import { FolderContent } from '@/features/folders/components/FolderContent'
import { TypebotDndProvider } from '@/features/folders/TypebotDndProvider'
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
import { trpc } from '@/lib/trpc'
import { guessIfUserIsEuropean } from '@typebot.io/billing/guessIfUserIsEuropean'
import { guessIfUserIsEuropean } from '@typebot.io/billing/helpers/guessIfUserIsEuropean'
import { useTranslate } from '@tolgee/react'
export const DashboardPage = () => {

View File

@ -19,7 +19,7 @@ import { updateInvitationQuery } from '../queries/updateInvitationQuery'
import { updateMemberQuery } from '../queries/updateMemberQuery'
import { Member } from '../types'
import { useWorkspace } from '../WorkspaceProvider'
import { getSeatsLimit } from '@typebot.io/billing/getSeatsLimit'
import { getSeatsLimit } from '@typebot.io/billing/helpers/getSeatsLimit'
import { useTranslate } from '@tolgee/react'
export const MembersList = () => {

View File

@ -1,346 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from '@typebot.io/lib/api'
import Stripe from 'stripe'
import Cors from 'micro-cors'
import { buffer } from 'micro'
import prisma from '@typebot.io/lib/prisma'
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
import { RequestHandler } from 'next/dist/server/next'
import { Settings } from '@typebot.io/schemas'
import { env } from '@typebot.io/env'
import { prices } from '@typebot.io/billing/constants'
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
if (!env.STRIPE_SECRET_KEY || !env.STRIPE_WEBHOOK_SECRET)
throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing')
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
import { webhookHandler } from '@typebot.io/billing/api/webhookHandler'
const cors = Cors({
allowMethods: ['POST', 'HEAD'],
})
const webhookSecret = env.STRIPE_WEBHOOK_SECRET as string
export const config = {
api: {
bodyParser: false,
},
}
const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const buf = await buffer(req)
const sig = req.headers['stripe-signature']
if (!sig) return res.status(400).send(`stripe-signature is missing`)
try {
const event = stripe.webhooks.constructEvent(
buf.toString(),
sig.toString(),
webhookSecret
)
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
const metadata = session.metadata as unknown as
| {
plan: 'STARTER' | 'PRO'
workspaceId: string
userId: string
}
| { claimableCustomPlanId: string; userId: string }
if ('plan' in metadata) {
const { workspaceId, plan } = metadata
if (!workspaceId || !plan)
return res
.status(500)
.send({ message: `Couldn't retrieve valid metadata` })
const workspace = await prisma.workspace.update({
where: { id: workspaceId },
data: {
plan,
stripeId: session.customer as string,
isQuarantined: false,
},
include: {
members: {
select: { userId: true },
where: {
role: WorkspaceRole.ADMIN,
},
},
},
})
await trackEvents(
workspace.members.map((m) => ({
name: 'Subscription updated',
workspaceId,
userId: m.userId,
data: {
plan,
},
}))
)
} else {
const { claimableCustomPlanId, userId } = metadata
if (!claimableCustomPlanId)
return res
.status(500)
.send({ message: `Couldn't retrieve valid metadata` })
const { workspaceId, chatsLimit, seatsLimit, storageLimit } =
await prisma.claimableCustomPlan.update({
where: { id: claimableCustomPlanId },
data: { claimedAt: new Date() },
})
await prisma.workspace.updateMany({
where: { id: workspaceId },
data: {
plan: Plan.CUSTOM,
stripeId: session.customer as string,
customChatsLimit: chatsLimit,
customStorageLimit: storageLimit,
customSeatsLimit: seatsLimit,
},
})
await trackEvents([
{
name: 'Subscription updated',
workspaceId,
userId,
data: {
plan: Plan.CUSTOM,
},
},
])
}
return res.status(200).send({ message: 'workspace upgraded in DB' })
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription
if (subscription.status !== 'past_due')
return res.send({ message: 'Not past_due, skipping.' })
const existingWorkspace = await prisma.workspace.findFirst({
where: {
stripeId: subscription.customer as string,
},
select: {
isPastDue: true,
id: true,
members: {
select: { userId: true, role: true },
where: { role: WorkspaceRole.ADMIN },
},
},
})
if (!existingWorkspace) throw new Error('Workspace not found')
if (existingWorkspace?.isPastDue)
return res.send({
message: 'Workspace already past due, skipping.',
})
await prisma.workspace.updateMany({
where: {
id: existingWorkspace.id,
},
data: {
isPastDue: true,
},
})
await trackEvents(
existingWorkspace.members.map((m) => ({
name: 'Workspace past due',
workspaceId: existingWorkspace.id,
userId: m.userId,
}))
)
return res.send({ message: 'Workspace set to past due.' })
}
case 'invoice.paid': {
const invoice = event.data.object as Stripe.Invoice
const workspace = await prisma.workspace.findFirst({
where: {
stripeId: invoice.customer as string,
},
select: {
isPastDue: true,
},
})
if (!workspace?.isPastDue)
return res.send({ message: 'Workspace not past_due, skipping.' })
const outstandingInvoices = await stripe.invoices.list({
customer: invoice.customer as string,
status: 'open',
})
const outstandingInvoicesWithAdditionalUsageCosts =
outstandingInvoices.data.filter(
(invoice) => invoice.amount_due > prices['PRO'] * 100
)
if (outstandingInvoicesWithAdditionalUsageCosts.length > 0)
return res.send({
message: 'Workspace has outstanding invoices, skipping.',
})
const updatedWorkspace = await prisma.workspace.update({
where: {
stripeId: invoice.customer as string,
},
data: {
isPastDue: false,
},
select: {
id: true,
members: {
select: { userId: true },
where: {
role: WorkspaceRole.ADMIN,
},
},
},
})
await trackEvents(
updatedWorkspace.members.map((m) => ({
name: 'Workspace past due status removed',
workspaceId: updatedWorkspace.id,
userId: m.userId,
}))
)
return res.send({ message: 'Workspace was regulated' })
}
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 outstandingInvoices = await stripe.invoices.list({
customer: subscription.customer as string,
status: 'open',
})
const outstandingInvoicesWithAdditionalUsageCosts =
outstandingInvoices.data.filter(
(invoice) => invoice.amount_due > prices['PRO'] * 100
)
const workspaceExist =
(await prisma.workspace.count({
where: {
stripeId: subscription.customer as string,
},
})) > 0
if (!workspaceExist)
return res.send({ message: 'Workspace not found, skipping...' })
const workspace = await prisma.workspace.update({
where: {
stripeId: subscription.customer as string,
},
data: {
plan: Plan.FREE,
customChatsLimit: null,
customStorageLimit: null,
customSeatsLimit: null,
isPastDue: outstandingInvoicesWithAdditionalUsageCosts.length > 0,
},
include: {
members: {
select: { userId: true },
where: {
role: WorkspaceRole.ADMIN,
},
},
},
})
await trackEvents(
workspace.members.map((m) => ({
name: 'Subscription updated',
workspaceId: workspace.id,
userId: m.userId,
data: {
plan: Plan.FREE,
},
}))
)
const typebots = await prisma.typebot.findMany({
where: {
workspaceId: workspace.id,
isArchived: { not: true },
},
include: { publishedTypebot: true },
})
for (const typebot of typebots) {
const settings = typebot.settings as Settings
if (settings.general?.isBrandingEnabled) continue
await prisma.typebot.updateMany({
where: { id: typebot.id },
data: {
settings: {
...settings,
general: {
...settings.general,
isBrandingEnabled: true,
},
whatsApp: settings.whatsApp
? {
...settings.whatsApp,
isEnabled: false,
}
: undefined,
},
},
})
const publishedTypebotSettings = typebot.publishedTypebot
?.settings as Settings | null
if (
!publishedTypebotSettings ||
publishedTypebotSettings?.general?.isBrandingEnabled
)
continue
await prisma.publicTypebot.updateMany({
where: { id: typebot.id },
data: {
settings: {
...publishedTypebotSettings,
general: {
...publishedTypebotSettings.general,
isBrandingEnabled: true,
},
},
},
})
}
return res.send({ message: 'workspace downgraded in DB' })
}
default: {
return res.status(304).send({ message: 'event not handled' })
}
}
} catch (err) {
console.error(err)
if (err instanceof Error) {
console.error(err)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
return res.status(500).send(`Error occured: ${err}`)
}
}
return methodNotAllowed(res)
}
export default cors(webhookHandler as RequestHandler)

View File

@ -8,7 +8,7 @@ import {
} from '@typebot.io/lib/api'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { sendWorkspaceMemberInvitationEmail } from '@typebot.io/emails'
import { getSeatsLimit } from '@typebot.io/billing/getSeatsLimit'
import { getSeatsLimit } from '@typebot.io/billing/helpers/getSeatsLimit'
import { env } from '@typebot.io/env'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@ -298,31 +298,6 @@ The [official Typebot managed service](https://app.typebot.io/) uses other servi
The related environment variables are listed here but you are probably not interested in these if you self-host Typebot.
<Accordion title="Stripe">
| Parameter | Default | Description |
| ----------------------------- | ------- | ----------------------------------------- |
| NEXT_PUBLIC_STRIPE_PUBLIC_KEY | | Stripe public key |
| STRIPE_SECRET_KEY | | Stripe secret key |
| STRIPE_STARTER_PRICE_ID | | Starter plan price id |
| STRIPE_PRO_PRICE_ID | | Pro monthly plan price id |
| STRIPE_STARTER_CHATS_PRICE_ID | | Starter Additional chats monthly price id |
| STRIPE_PRO_CHATS_PRICE_ID | | Pro Additional chats monthly price id |
| STRIPE_WEBHOOK_SECRET | | Stripe Webhook secret |
</Accordion>
<Accordion title="Sentry">
| Parameter | Default | Description |
| ---------------------- | ------- | -------------------------------------- |
| NEXT_PUBLIC_SENTRY_DSN | | Sentry DSN |
| SENTRY_AUTH_TOKEN | | Used to upload sourcemaps on app build |
| SENTRY_PROJECT | | Sentry project name |
| SENTRY_ORG | | Sentry organization name |
</Accordion>
<Accordion title="Vercel (custom domains)">
| Parameter | Default | Description |
@ -333,14 +308,6 @@ The related environment variables are listed here but you are probably not inter
</Accordion>
<Accordion title="Sleekplan">
| Parameter | Default | Description |
| ----------------- | ------- | ------------------------------------------------------------------------ |
| SLEEKPLAN_SSO_KEY | | Sleekplan SSO key used to automatically authenticate a user in Sleekplan |
</Accordion>
<Accordion title="Telemetry">
| Parameter | Default | Description |

View File

@ -3,7 +3,7 @@ title: Overview
icon: server
---
Typebot is 100% open-source and can be self-hosted on your own server. This guide will walk you through the process of setting up your own instance of Typebot.
Typebot is open-source and can be self-hosted on your own server. This guide will walk you through the process of setting up your own instance of Typebot.
<Note>
The easiest way to get started with Typebot is with [the official managed
@ -25,11 +25,16 @@ Typebot is 100% open-source and can be self-hosted on your own server. This guid
## License requirements
Typebot is open-source under the GNU Affero General Public License Version 3 (AGPLv3). You can find it [here](https://raw.githubusercontent.com/baptisteArno/typebot.io/main/LICENSE). The goal of the AGPLv3 license is to:
You can find the full license [here](https://raw.githubusercontent.com/baptisteArno/typebot.io/main/LICENSE).
Here is a summary of the license:
All content that resides under the [ee](https://github.com/baptisteArno/typebot.io/tree/main/ee) directory of this repository is licensed under the license defined in [ee/LICENSE](./ee/LICENSE). The [ee](https://github.com/baptisteArno/typebot.io/tree/main/ee) folder contains the code for the [landing page](https://typebot.io) and the billing feature. We restrict these to a Commercial License to make sure other companies can't simply clone Typebot's homepage and billing system and offer the same service without contributing to the open-source project.
Content outside of this directory is available under the GNU Affero General Public License Version 3 (AGPLv3). The goal of the AGPLv3 license is to:
- Maximize user freedom and to encourage companies to contribute to open source.
- Prevent corporations from taking the code and using it as part of their closed-source proprietary products
- Prevent corporations from offering Typebot as a service without contributing to the open source project
- Prevent corporations from offering Typebot as a service without contributing back to the open source project
- Prevent corporations from confusing people and making them think that the service they sell is in any shape or form approved by the original team
Here are the 3 different possible use cases:
@ -46,7 +51,7 @@ Here are the 3 different possible use cases:
<Accordion title="You'd like to commercialize your own version of Typebot">
<ol>
<li>You need to open-source your modifications.</li>{' '}
<li>You need to open-source your fork of the project.</li>
<li>
After your users registration, you should provide a prominent mention and
link to the original project (https://typebot.io). You should clearly
@ -54,11 +59,19 @@ Here are the 3 different possible use cases:
Typebot. It would be also a good place to explain your version advantages
comparing to the original project.
</li>
<li>If you want to use code that resides under the [ee](https://github.com/baptisteArno/typebot.io/tree/main/ee) folder (landing page or billing feature), you need a [Commercial License key](https://typebot.io/enterprise-lead-form).</li>
<li>You need to provide a link to your forked repository somewhere in the landing page or the builder. This way, interested users can easily access and review the modifications you've made.</li>
</ol>
</Accordion>
</AccordionGroup>
<Note>
Either use case, [sponsoring the
project](https://github.com/sponsors/baptisteArno) is a great way to give back
to the community and to contribute to the long-term sustainability of the
project. ❤️
</Note>
Typebot is composed of 2 Next.js applications you need to deploy:
- the builder, where you build your typebots

View File

@ -1,19 +0,0 @@
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,3 +0,0 @@
# Typebot.io
This repository contains the source code of [Typebot's landing page](https://www.typebot.io)

34
ee/LICENSE Normal file
View File

@ -0,0 +1,34 @@
The Typebot Commercial License (the “Commercial License”)
Copyright (c) 2024-present Typebot
With regard to the Typebot Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have a valid
Typebot Enterprise Edition subscription ("Commercial Subscription"). Subject to the foregoing sentence,
you are free to modify this Software and publish patches to the Software. You agree
that Typebot and/or its licensors (as applicable) retain all right, title and interest in
and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Commercial Subscription for the correct number of hosts.
Notwithstanding the foregoing, you may copy and modify the Software for development
and testing purposes, without requiring a subscription. You agree that Typebot and/or
its licensors (as applicable) retain all right, title and interest in and to all such
modifications. You are not granted any other rights beyond what is expressly stated herein.
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
This Commercial License applies only to the part of this Software that is not distributed under
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

36
ee/README.md Normal file
View File

@ -0,0 +1,36 @@
<br />
<p align="center">
<a href="https://typebot.io/#gh-light-mode-only" target="_blank">
<img src="./.github/images/logo-light.png" alt="Typebot illustration" width="350px">
</a>
<a href="https://typebot.io/#gh-dark-mode-only" target="_blank">
<img src="./.github/images/logo-dark.png" alt="Typebot illustration" width="350px">
</a>
</p>
<br />
<a align="center" href="https://typebot.io/enterprise-lead-form">
Get a license key
</a>
## Enterprise Edition
The /ee subfolder is the place for all the Enterprise Edition features from our hosted plan.
This folder contains all the code associated to the landing page and billing features.
This Enterprise edition exists mainly to prevent people to simply fork the project and re-sell it without providing any modifications.
> ❗ WARNING: This repository is copyrighted (unlike the main repo). You are not allowed to use this code in your self-hosted instance of Typebot without obtaining a proper [license](https://typebot.io/enterprise-lead-form) first❗
### Configure STRIPE
| Parameter | Default | Description |
| ----------------------------- | ------- | ----------------------------------------- |
| NEXT_PUBLIC_STRIPE_PUBLIC_KEY | | Stripe public key |
| STRIPE_SECRET_KEY | | Stripe secret key |
| STRIPE_STARTER_PRICE_ID | | Starter plan price id |
| STRIPE_PRO_PRICE_ID | | Pro monthly plan price id |
| STRIPE_STARTER_CHATS_PRICE_ID | | Starter Additional chats monthly price id |
| STRIPE_PRO_CHATS_PRICE_ID | | Pro Additional chats monthly price id |
| STRIPE_WEBHOOK_SECRET | | Stripe Webhook secret |

Some files were not shown because too many files have changed in this diff Show More