📄 Add Commercial License for ee folder (#1532)
This commit is contained in:
8
LICENSE
8
LICENSE
@ -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
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
@ -110,4 +110,4 @@ Made with [contrib.rocks](https://contrib.rocks).
|
|||||||
|
|
||||||
## License
|
## 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).
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
|
||||||
import { Plan } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import Stripe from 'stripe'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
import { createCheckoutSession as createCheckoutSessionHandler } from '@typebot.io/billing/api/createCheckoutSession'
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
|
|
||||||
export const createCheckoutSession = authenticatedProcedure
|
export const createCheckoutSession = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -38,130 +34,6 @@ export const createCheckoutSession = authenticatedProcedure
|
|||||||
checkoutUrl: z.string(),
|
checkoutUrl: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(
|
.mutation(async ({ input, ctx: { user } }) =>
|
||||||
async ({
|
createCheckoutSessionHandler({ ...input, user })
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
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 { z } from 'zod'
|
||||||
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
import { createCustomCheckoutSession as createCustomCheckoutSessionHandler } from '@typebot.io/billing/api/createCustomCheckoutSession'
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
|
|
||||||
export const createCustomCheckoutSession = authenticatedProcedure
|
export const createCustomCheckoutSession = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -30,154 +25,9 @@ export const createCustomCheckoutSession = authenticatedProcedure
|
|||||||
checkoutUrl: z.string(),
|
checkoutUrl: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(
|
.mutation(async ({ input, ctx: { user } }) =>
|
||||||
async ({ input: { email, workspaceId, returnUrl }, ctx: { user } }) => {
|
createCustomCheckoutSessionHandler({
|
||||||
if (!env.STRIPE_SECRET_KEY)
|
...input,
|
||||||
throw new TRPCError({
|
user,
|
||||||
code: 'INTERNAL_SERVER_ERROR',
|
|
||||||
message: 'Stripe environment variables are missing',
|
|
||||||
})
|
})
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
|
||||||
import Stripe from 'stripe'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
import { getBillingPortalUrl as getBillingPortalUrlHandler } from '@typebot.io/billing/api/getBillingPortalUrl'
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
|
|
||||||
export const getBillingPortalUrl = authenticatedProcedure
|
export const getBillingPortalUrl = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -26,39 +22,6 @@ export const getBillingPortalUrl = authenticatedProcedure
|
|||||||
billingPortalUrl: z.string(),
|
billingPortalUrl: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
|
||||||
if (!env.STRIPE_SECRET_KEY)
|
getBillingPortalUrlHandler({ workspaceId, user })
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
|
||||||
import Stripe from 'stripe'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
|
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
|
||||||
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
import { getSubscription as getSubscriptionHandler } from '@typebot.io/billing/api/getSubscription'
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
|
|
||||||
export const getSubscription = authenticatedProcedure
|
export const getSubscription = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -27,65 +23,6 @@ export const getSubscription = authenticatedProcedure
|
|||||||
subscription: subscriptionSchema.or(z.null().openapi({ type: 'string' })),
|
subscription: subscriptionSchema.or(z.null().openapi({ type: 'string' })),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
|
||||||
if (!env.STRIPE_SECRET_KEY)
|
getSubscriptionHandler({ workspaceId, user })
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
import { getUsage as getUsageHandler } from '@typebot.io/billing/api/getUsage'
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
import Stripe from 'stripe'
|
|
||||||
|
|
||||||
export const getUsage = authenticatedProcedure
|
export const getUsage = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -26,87 +22,6 @@ export const getUsage = authenticatedProcedure
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.output(z.object({ totalChatsUsed: z.number(), resetsAt: z.date() }))
|
.output(z.object({ totalChatsUsed: z.number(), resetsAt: z.date() }))
|
||||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
|
||||||
const workspace = await prisma.workspace.findFirst({
|
getUsageHandler({ workspaceId, user })
|
||||||
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
|
|
||||||
)
|
)
|
||||||
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),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
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 { z } from 'zod'
|
||||||
import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
|
import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
|
||||||
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
import { listInvoices as listInvoicesHandler } from '@typebot.io/billing/api/listInvoices'
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
|
|
||||||
export const listInvoices = authenticatedProcedure
|
export const listInvoices = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -32,49 +27,6 @@ export const listInvoices = authenticatedProcedure
|
|||||||
invoices: z.array(invoiceSchema),
|
invoices: z.array(invoiceSchema),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
.query(async ({ input: { workspaceId }, ctx: { user } }) =>
|
||||||
if (!env.STRIPE_SECRET_KEY)
|
listInvoicesHandler({ workspaceId, user })
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
.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,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
import prisma from '@typebot.io/lib/prisma'
|
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
|
||||||
import { Plan } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import { workspaceSchema } from '@typebot.io/schemas'
|
import { workspaceSchema } from '@typebot.io/schemas'
|
||||||
import Stripe from 'stripe'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createCheckoutSessionUrl } from './createCheckoutSession'
|
import { updateSubscription as updateSubscriptionHandler } from '@typebot.io/billing/api/updateSubscription'
|
||||||
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
|
|
||||||
|
|
||||||
export const updateSubscription = authenticatedProcedure
|
export const updateSubscription = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -34,140 +28,9 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
checkoutUrl: z.string().nullish(),
|
checkoutUrl: z.string().nullish(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(
|
.mutation(async ({ input, ctx: { user } }) =>
|
||||||
async ({
|
updateSubscriptionHandler({
|
||||||
input: { workspaceId, plan, currency, returnUrl },
|
...input,
|
||||||
ctx: { user },
|
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: {
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
@ -11,7 +11,7 @@ import { StarterPlanPricingCard } from './StarterPlanPricingCard'
|
|||||||
import { ProPlanPricingCard } from './ProPlanPricingCard'
|
import { ProPlanPricingCard } from './ProPlanPricingCard'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import { StripeClimateLogo } from './StripeClimateLogo'
|
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'
|
import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import { proChatTiers } from '@typebot.io/billing/constants'
|
import { proChatTiers } from '@typebot.io/billing/constants'
|
||||||
import { formatPrice } from '@typebot.io/billing/formatPrice'
|
import { formatPrice } from '@typebot.io/billing/helpers/formatPrice'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
@ -17,8 +17,8 @@ import {
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import React, { FormEvent, useState } from 'react'
|
import React, { FormEvent, useState } from 'react'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { taxIdTypes } from '../taxIdTypes'
|
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
import { taxIdTypes } from '@typebot.io/billing/taxIdTypes'
|
||||||
|
|
||||||
export type PreCheckoutModalProps = {
|
export type PreCheckoutModalProps = {
|
||||||
selectedSubscription:
|
selectedSubscription:
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
import { Plan } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import { FeaturesList } from './FeaturesList'
|
import { FeaturesList } from './FeaturesList'
|
||||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
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 { ChatsProTiersModal } from './ChatsProTiersModal'
|
||||||
import { prices } from '@typebot.io/billing/constants'
|
import { prices } from '@typebot.io/billing/constants'
|
||||||
import { T, useTranslate } from '@tolgee/react'
|
import { T, useTranslate } from '@tolgee/react'
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
import { Plan } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import { FeaturesList } from './FeaturesList'
|
import { FeaturesList } from './FeaturesList'
|
||||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
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 { prices } from '@typebot.io/billing/constants'
|
||||||
import { T, useTranslate } from '@tolgee/react'
|
import { T, useTranslate } from '@tolgee/react'
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import { AlertIcon } from '@/components/icons'
|
|||||||
import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider'
|
import { WorkspaceInApp } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { parseNumberWithCommas } from '@typebot.io/lib'
|
import { parseNumberWithCommas } from '@typebot.io/lib'
|
||||||
import { defaultQueryOptions, trpc } from '@/lib/trpc'
|
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'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -14,7 +14,7 @@ import { FolderContent } from '@/features/folders/components/FolderContent'
|
|||||||
import { TypebotDndProvider } from '@/features/folders/TypebotDndProvider'
|
import { TypebotDndProvider } from '@/features/folders/TypebotDndProvider'
|
||||||
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
|
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
|
||||||
import { trpc } from '@/lib/trpc'
|
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'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
|
||||||
export const DashboardPage = () => {
|
export const DashboardPage = () => {
|
||||||
|
@ -19,7 +19,7 @@ import { updateInvitationQuery } from '../queries/updateInvitationQuery'
|
|||||||
import { updateMemberQuery } from '../queries/updateMemberQuery'
|
import { updateMemberQuery } from '../queries/updateMemberQuery'
|
||||||
import { Member } from '../types'
|
import { Member } from '../types'
|
||||||
import { useWorkspace } from '../WorkspaceProvider'
|
import { useWorkspace } from '../WorkspaceProvider'
|
||||||
import { getSeatsLimit } from '@typebot.io/billing/getSeatsLimit'
|
import { getSeatsLimit } from '@typebot.io/billing/helpers/getSeatsLimit'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
|
||||||
export const MembersList = () => {
|
export const MembersList = () => {
|
||||||
|
@ -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 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 { RequestHandler } from 'next/dist/server/next'
|
||||||
import { Settings } from '@typebot.io/schemas'
|
import { webhookHandler } from '@typebot.io/billing/api/webhookHandler'
|
||||||
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',
|
|
||||||
})
|
|
||||||
|
|
||||||
const cors = Cors({
|
const cors = Cors({
|
||||||
allowMethods: ['POST', 'HEAD'],
|
allowMethods: ['POST', 'HEAD'],
|
||||||
})
|
})
|
||||||
|
|
||||||
const webhookSecret = env.STRIPE_WEBHOOK_SECRET as string
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
bodyParser: false,
|
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)
|
export default cors(webhookHandler as RequestHandler)
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@typebot.io/lib/api'
|
} from '@typebot.io/lib/api'
|
||||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||||
import { sendWorkspaceMemberInvitationEmail } from '@typebot.io/emails'
|
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'
|
import { env } from '@typebot.io/env'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
@ -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.
|
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)">
|
<Accordion title="Vercel (custom domains)">
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
@ -333,14 +308,6 @@ The related environment variables are listed here but you are probably not inter
|
|||||||
|
|
||||||
</Accordion>
|
</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">
|
<Accordion title="Telemetry">
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Parameter | Default | Description |
|
||||||
|
@ -3,7 +3,7 @@ title: Overview
|
|||||||
icon: server
|
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>
|
<Note>
|
||||||
The easiest way to get started with Typebot is with [the official managed
|
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
|
## 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.
|
- 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 back to the open source project
|
||||||
- Prevent corporations from offering Typebot as a service without contributing 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
|
- 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:
|
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">
|
<Accordion title="You'd like to commercialize your own version of Typebot">
|
||||||
<ol>
|
<ol>
|
||||||
<li>You need to open-source your modifications.</li>{' '}
|
<li>You need to open-source your fork of the project.</li>
|
||||||
<li>
|
<li>
|
||||||
After your users registration, you should provide a prominent mention and
|
After your users registration, you should provide a prominent mention and
|
||||||
link to the original project (https://typebot.io). You should clearly
|
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
|
Typebot. It would be also a good place to explain your version advantages
|
||||||
comparing to the original project.
|
comparing to the original project.
|
||||||
</li>
|
</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>
|
<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>
|
</ol>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</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:
|
Typebot is composed of 2 Next.js applications you need to deploy:
|
||||||
|
|
||||||
- the builder, where you build your typebots
|
- the builder, where you build your typebots
|
||||||
|
@ -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.
|
|
@ -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
34
ee/LICENSE
Normal 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
36
ee/README.md
Normal 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
Reference in New Issue
Block a user