2
0

🐛 (typebotLink) Fix linked typebot fetching error

Closes #429
This commit is contained in:
Baptiste Arnaud
2023-03-30 10:30:26 +02:00
parent 70416c0d14
commit 684e6338e2
25 changed files with 2716 additions and 605 deletions

View File

@@ -32,7 +32,7 @@ export const ImageUploadContent = ({
onClose, onClose,
}: Props) => { }: Props) => {
const [currentTab, setCurrentTab] = useState<Tabs>( const [currentTab, setCurrentTab] = useState<Tabs>(
isEmojiEnabled ? 'emoji' : 'upload' isEmojiEnabled ? 'emoji' : 'link'
) )
const handleSubmit = (url: string) => { const handleSubmit = (url: string) => {

View File

@@ -4,7 +4,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { Typebot, Webhook } from '@typebot.io/schemas' import { Typebot, Webhook } from '@typebot.io/schemas'
import { z } from 'zod' import { z } from 'zod'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/helpers/getLinkedTypebots' import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots'
import { parseResultExample } from '../helpers/parseResultExample' import { parseResultExample } from '../helpers/parseResultExample'
export const getResultExample = authenticatedProcedure export const getResultExample = authenticatedProcedure
@@ -63,7 +63,7 @@ export const getResultExample = authenticatedProcedure
if (!block) if (!block)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Block not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Block not found' })
const linkedTypebots = await getLinkedTypebots(typebot, user) const linkedTypebots = await fetchLinkedTypebots(typebot, user)
return { return {
resultExample: await parseResultExample( resultExample: await parseResultExample(

View File

@@ -0,0 +1,66 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Typebot, typebotSchema } from '@typebot.io/schemas'
import { z } from 'zod'
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
export const getLinkedTypebots = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/linkedTypebots',
protect: true,
summary: 'Get linked typebots',
tags: ['Typebot'],
},
})
.input(
z.object({
workspaceId: z.string(),
typebotIds: z.string().describe('Comma separated list of typebot ids'),
})
)
.output(
z.object({
typebots: z.array(
typebotSchema.pick({
id: true,
groups: true,
variables: true,
name: true,
})
),
})
)
.query(async ({ input: { workspaceId, typebotIds }, ctx: { user } }) => {
const typebotIdsArray = typebotIds.split(',')
const workspace = await prisma.workspace.findUnique({
where: { id: workspaceId },
select: { members: true },
})
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
if (userRole === undefined)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
const typebots = (await prisma.typebot.findMany({
where: {
isArchived: { not: true },
id: { in: typebotIdsArray },
workspaceId,
},
orderBy: { createdAt: 'desc' },
select: {
id: true,
groups: true,
variables: true,
name: true,
},
})) as Pick<Typebot, 'id' | 'groups' | 'variables' | 'name'>[]
if (!typebots)
throw new TRPCError({ code: 'NOT_FOUND', message: 'No typebots found' })
return {
typebots,
}
})

View File

@@ -1,27 +1,22 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { canReadTypebots } from '@/helpers/databaseRules' import { canReadTypebots } from '@/helpers/databaseRules'
import { User } from '@typebot.io/prisma' import { User } from '@typebot.io/prisma'
import { import { LogicBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas'
LogicBlockType,
PublicTypebot,
Typebot,
TypebotLinkBlock,
} from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
export const getLinkedTypebots = async ( export const fetchLinkedTypebots = async (
typebot: Pick<PublicTypebot, 'groups'>, typebot: Pick<PublicTypebot, 'groups'>,
user?: User user?: User
): Promise<(Typebot | PublicTypebot)[]> => { ): Promise<(Typebot | PublicTypebot)[]> => {
const linkedTypebotIds = ( const linkedTypebotIds = typebot.groups
typebot.groups .flatMap((group) => group.blocks)
.flatMap((g) => g.blocks) .reduce<string[]>((typebotIds, block) => {
.filter( if (block.type !== LogicBlockType.TYPEBOT_LINK) return typebotIds
(s) => const typebotId = block.options.typebotId
s.type === LogicBlockType.TYPEBOT_LINK && if (!typebotId) return typebotIds
isDefined(s.options.typebotId) return typebotIds.includes(typebotId)
) as TypebotLinkBlock[] ? typebotIds
).map((s) => s.options.typebotId as string) : [...typebotIds, typebotId]
}, [])
if (linkedTypebotIds.length === 0) return [] if (linkedTypebotIds.length === 0) return []
const typebots = (await ('typebotId' in typebot const typebots = (await ('typebotId' in typebot
? prisma.publicTypebot.findMany({ ? prisma.publicTypebot.findMany({
@@ -36,6 +31,6 @@ export const getLinkedTypebots = async (
], ],
} }
: { id: { in: linkedTypebotIds } }, : { id: { in: linkedTypebotIds } },
}))) as unknown as (Typebot | PublicTypebot)[] }))) as (Typebot | PublicTypebot)[]
return typebots return typebots
} }

View File

@@ -57,7 +57,7 @@ test('should be configurable', async ({ page }) => {
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await page.getByTestId('selected-item-label').first().click({ force: true }) await page.getByTestId('selected-item-label').first().click({ force: true })
await page.click('button >> text=Current typebot') await page.click('button >> text=Current typebot')
await page.getByPlaceholder('Select a block').click() await page.getByRole('textbox').nth(1).click()
await page.click('button >> text=Hello') await page.click('button >> text=Hello')
await page.click('text=Preview') await page.click('text=Preview')

View File

@@ -24,7 +24,6 @@ import { dequal } from 'dequal'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { useTypebotQuery } from '@/hooks/useTypebotQuery' import { useTypebotQuery } from '@/hooks/useTypebotQuery'
import { useUndo } from '../hooks/useUndo' import { useUndo } from '../hooks/useUndo'
import { useLinkedTypebots } from '@/hooks/useLinkedTypebots'
import { updateTypebotQuery } from '../queries/updateTypebotQuery' import { updateTypebotQuery } from '../queries/updateTypebotQuery'
import { updateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/updateWebhookQuery' import { updateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/updateWebhookQuery'
import { useAutoSave } from '@/hooks/useAutoSave' import { useAutoSave } from '@/hooks/useAutoSave'
@@ -39,6 +38,7 @@ import { areTypebotsEqual } from '@/features/publish/helpers/areTypebotsEqual'
import { isPublished as isPublishedHelper } from '@/features/publish/helpers/isPublished' import { isPublished as isPublishedHelper } from '@/features/publish/helpers/isPublished'
import { convertTypebotToPublicTypebot } from '@/features/publish/helpers/convertTypebotToPublicTypebot' import { convertTypebotToPublicTypebot } from '@/features/publish/helpers/convertTypebotToPublicTypebot'
import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/convertPublicTypebotToTypebot' import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/convertPublicTypebotToTypebot'
import { trpc } from '@/lib/trpc'
const autoSaveTimeout = 10000 const autoSaveTimeout = 10000
@@ -65,7 +65,7 @@ const typebotContext = createContext<
{ {
typebot?: Typebot typebot?: Typebot
publishedTypebot?: PublicTypebot publishedTypebot?: PublicTypebot
linkedTypebots?: Typebot[] linkedTypebots?: Pick<Typebot, 'id' | 'groups' | 'variables' | 'name'>[]
webhooks: Webhook[] webhooks: Webhook[]
isReadOnly?: boolean isReadOnly?: boolean
isPublished: boolean isPublished: boolean
@@ -132,16 +132,21 @@ export const TypebotProvider = ({
[] []
) ?? [] ) ?? []
const { typebots: linkedTypebots } = useLinkedTypebots({ const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
workspaceId: localTypebot?.workspaceId ?? undefined, {
typebotId, workspaceId: localTypebot?.workspaceId as string,
typebotIds: linkedTypebotIds, typebotIds: linkedTypebotIds.join(','),
onError: (error) => },
showToast({ {
title: 'Error while fetching linkedTypebots', enabled:
description: error.message, isDefined(localTypebot?.workspaceId) && linkedTypebotIds.length > 0,
}), onError: (error) =>
}) showToast({
title: 'Error while fetching linkedTypebots',
description: error.message,
}),
}
)
useEffect(() => { useEffect(() => {
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined) if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
@@ -385,7 +390,7 @@ export const TypebotProvider = ({
value={{ value={{
typebot: localTypebot, typebot: localTypebot,
publishedTypebot, publishedTypebot,
linkedTypebots, linkedTypebots: linkedTypebotsData?.typebots ?? [],
webhooks: webhooks ?? [], webhooks: webhooks ?? [],
isReadOnly, isReadOnly,
isSavingLoading, isSavingLoading,

View File

@@ -223,6 +223,7 @@ test.describe.parallel('Theme page', () => {
}) })
await page.goto(`/typebots/${typebotId}/theme`) await page.goto(`/typebots/${typebotId}/theme`)
await expect(page.locator('button >> text="Go"')).toBeVisible() await expect(page.locator('button >> text="Go"')).toBeVisible()
await page.getByRole('button', { name: 'Templates New!' }).click()
await page.getByRole('button', { name: 'Save current theme' }).click() await page.getByRole('button', { name: 'Save current theme' }).click()
await page.getByPlaceholder('My template').fill('My awesome theme') await page.getByPlaceholder('My template').fill('My awesome theme')
await page.getByRole('button', { name: 'Save' }).click() await page.getByRole('button', { name: 'Save' }).click()

View File

@@ -5,6 +5,7 @@ import { WorkspaceRole } from '@typebot.io/prisma'
import { PublicTypebot, Typebot, typebotSchema } from '@typebot.io/schemas' import { PublicTypebot, Typebot, typebotSchema } from '@typebot.io/schemas'
import { omit } from '@typebot.io/lib' import { omit } from '@typebot.io/lib'
import { z } from 'zod' import { z } from 'zod'
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
export const listTypebots = authenticatedProcedure export const listTypebots = authenticatedProcedure
.meta({ .meta({
@@ -31,33 +32,27 @@ export const listTypebots = authenticatedProcedure
}) })
) )
.query(async ({ input: { workspaceId, folderId }, ctx: { user } }) => { .query(async ({ input: { workspaceId, folderId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findUnique({
where: { id: workspaceId },
select: { members: true },
})
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
if (userRole === undefined)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
const typebots = (await prisma.typebot.findMany({ const typebots = (await prisma.typebot.findMany({
where: { where: {
OR: [ isArchived: { not: true },
{ folderId:
isArchived: { not: true }, userRole === WorkspaceRole.GUEST
folderId: folderId === 'root' ? null : folderId, ? undefined
workspace: { : folderId === 'root'
id: workspaceId, ? null
members: { : folderId,
some: { workspaceId,
userId: user.id, collaborators:
role: { not: WorkspaceRole.GUEST }, userRole === WorkspaceRole.GUEST
}, ? { some: { userId: user.id } }
}, : undefined,
},
},
{
isArchived: { not: true },
workspace: {
id: workspaceId,
members: {
some: { userId: user.id, role: WorkspaceRole.GUEST },
},
},
collaborators: { some: { userId: user.id } },
},
],
}, },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
select: { select: {

View File

@@ -1,5 +1,6 @@
import { billingRouter } from '@/features/billing/api/router' import { billingRouter } from '@/features/billing/api/router'
import { webhookRouter } from '@/features/blocks/integrations/webhook/api/router' import { webhookRouter } from '@/features/blocks/integrations/webhook/api/router'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api/getLinkedTypebots'
import { credentialsRouter } from '@/features/credentials/api/router' import { credentialsRouter } from '@/features/credentials/api/router'
import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure' import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure'
import { resultsRouter } from '@/features/results/api/router' import { resultsRouter } from '@/features/results/api/router'
@@ -12,6 +13,7 @@ import { router } from '../../trpc'
export const trpcRouter = router({ export const trpcRouter = router({
getAppVersionProcedure, getAppVersionProcedure,
processTelemetryEvent, processTelemetryEvent,
getLinkedTypebots,
workspace: workspaceRouter, workspace: workspaceRouter,
typebot: typebotRouter, typebot: typebotRouter,
webhook: webhookRouter, webhook: webhookRouter,

View File

@@ -1,37 +0,0 @@
import { fetcher } from '@/helpers/fetcher'
import { Typebot } from '@typebot.io/schemas'
import { stringify } from 'qs'
import useSWR from 'swr'
export const useLinkedTypebots = ({
workspaceId,
typebotId,
typebotIds,
onError,
}: {
workspaceId?: string
typebotId?: string
typebotIds: string[]
onError: (error: Error) => void
}) => {
const params = stringify({ typebotIds, workspaceId }, { indices: false })
const { data, error, mutate } = useSWR<
{
typebots: Typebot[]
},
Error
>(
workspaceId && typebotIds.length > 0
? typebotIds.every((id) => typebotId === id)
? undefined
: `/api/typebots?${params}`
: null,
fetcher
)
if (error) onError(error)
return {
typebots: data?.typebots,
isLoading: !error && !data,
mutate,
}
}

View File

@@ -1,42 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from '@typebot.io/lib/api'
import Stripe from 'stripe'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import prisma from '@/lib/prisma'
import { WorkspaceRole } from '@typebot.io/prisma'
// TO-DO: Delete
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const session = await stripe.billingPortal.sessions.create({
customer: workspace.stripeId,
return_url: req.headers.referer,
})
res.redirect(session.url)
return
}
return methodNotAllowed(res)
}
export default handler

View File

@@ -1,49 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from '@typebot.io/lib/api'
import Stripe from 'stripe'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import prisma from '@/lib/prisma'
import { WorkspaceRole } from '@typebot.io/prisma'
// TODO: Delete
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const invoices = await stripe.invoices.list({
customer: workspace.stripeId,
})
res.send({
invoices: invoices.data.map((i) => ({
id: i.number,
url: i.invoice_pdf,
amount: i.subtotal,
currency: i.currency,
date: i.status_transitions.paid_at,
})),
})
return
}
return methodNotAllowed(res)
}
export default handler

View File

@@ -1,261 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { isDefined } from '@typebot.io/lib'
import {
badRequest,
forbidden,
methodNotAllowed,
notAuthenticated,
} from '@typebot.io/lib/api'
import Stripe from 'stripe'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import prisma from '@/lib/prisma'
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
// TODO: Delete
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET')
return res.send(await getSubscriptionDetails(req, res)(user.id))
if (req.method === 'POST') {
const session = await createCheckoutSession(req)
return res.send({ sessionId: session.id })
}
if (req.method === 'PUT') {
await updateSubscription(req)
return res.send({ message: 'success' })
}
if (req.method === 'DELETE') {
await cancelSubscription(req, res)(user.id)
return res.send({ message: 'success' })
}
return methodNotAllowed(res)
}
const getSubscriptionDetails =
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const subscriptions = await stripe.subscriptions.list({
customer: workspace.stripeId,
limit: 1,
})
return {
additionalChatsIndex:
subscriptions.data[0]?.items.data.find(
(item) =>
item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
)?.quantity ?? 0,
additionalStorageIndex:
subscriptions.data[0]?.items.data.find(
(item) =>
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)?.quantity ?? 0,
currency: subscriptions.data[0]?.currency,
}
}
const createCheckoutSession = (req: NextApiRequest) => {
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const {
email,
currency,
plan,
workspaceId,
href,
additionalChats,
additionalStorage,
} = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
return stripe.checkout.sessions.create({
success_url: `${href}?stripe=${plan}&success=true`,
cancel_url: `${href}?stripe=cancel`,
allow_promotion_codes: true,
customer_email: email,
mode: 'subscription',
metadata: { workspaceId, plan, additionalChats, additionalStorage },
currency,
automatic_tax: { enabled: true },
line_items: parseSubscriptionItems(
plan,
additionalChats,
additionalStorage
),
})
}
const updateSubscription = async (req: NextApiRequest) => {
const {
stripeId,
plan,
workspaceId,
additionalChats,
additionalStorage,
currency,
} = (typeof req.body === 'string' ? JSON.parse(req.body) : req.body) as {
stripeId: string
workspaceId: string
additionalChats: number
additionalStorage: number
plan: 'STARTER' | 'PRO'
currency: 'eur' | 'usd'
}
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const { data } = await stripe.subscriptions.list({
customer: stripeId,
})
const subscription = data[0] as Stripe.Subscription | undefined
const currentStarterPlanItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_STARTER_PRICE_ID
)?.id
const currentProPlanItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_PRO_PRICE_ID
)?.id
const currentAdditionalChatsItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
)?.id
const currentAdditionalStorageItemId = subscription?.items.data.find(
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
)?.id
const items = [
{
id: currentStarterPlanItemId ?? currentProPlanItemId,
price:
plan === Plan.STARTER
? process.env.STRIPE_STARTER_PRICE_ID
: process.env.STRIPE_PRO_PRICE_ID,
quantity: 1,
},
additionalChats === 0 && !currentAdditionalChatsItemId
? undefined
: {
id: currentAdditionalChatsItemId,
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
quantity: additionalChats,
deleted: subscription ? additionalChats === 0 : undefined,
},
additionalStorage === 0 && !currentAdditionalStorageItemId
? undefined
: {
id: currentAdditionalStorageItemId,
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
quantity: additionalStorage,
deleted: subscription ? additionalStorage === 0 : undefined,
},
].filter(isDefined)
if (subscription) {
await stripe.subscriptions.update(subscription.id, {
items,
})
} else {
await stripe.subscriptions.create({
customer: stripeId,
items,
currency,
})
}
await prisma.workspace.update({
where: { id: workspaceId },
data: {
plan,
additionalChatsIndex: additionalChats,
additionalStorageIndex: additionalStorage,
chatsLimitFirstEmailSentAt: null,
chatsLimitSecondEmailSentAt: null,
storageLimitFirstEmailSentAt: null,
storageLimitSecondEmailSentAt: null,
},
})
}
const cancelSubscription =
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
const stripeId = req.query.stripeId as string | undefined
if (!stripeId) return badRequest(res)
if (!process.env.STRIPE_SECRET_KEY)
throw Error('STRIPE_SECRET_KEY var is missing')
const workspace = await prisma.workspace.findFirst({
where: {
stripeId,
members: { some: { userId, role: WorkspaceRole.ADMIN } },
},
})
if (!workspace?.stripeId) return forbidden(res)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const existingSubscription = await stripe.subscriptions.list({
customer: workspace.stripeId,
})
const currentSubscriptionId = existingSubscription.data[0]?.id
if (currentSubscriptionId)
await stripe.subscriptions.del(currentSubscriptionId)
await prisma.workspace.update({
where: { id: workspace.id },
data: {
plan: Plan.FREE,
additionalChatsIndex: 0,
additionalStorageIndex: 0,
},
})
}
const parseSubscriptionItems = (
plan: Plan,
additionalChats: number,
additionalStorage: number
) =>
[
{
price:
plan === Plan.STARTER
? process.env.STRIPE_STARTER_PRICE_ID
: process.env.STRIPE_PRO_PRICE_ID,
quantity: 1,
},
]
.concat(
additionalChats > 0
? [
{
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
quantity: additionalChats,
},
]
: []
)
.concat(
additionalStorage > 0
? [
{
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
quantity: additionalStorage,
},
]
: []
)
export default handler

View File

@@ -2,7 +2,6 @@ import { Plan } from '@typebot.io/prisma'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { import {
badRequest,
methodNotAllowed, methodNotAllowed,
notAuthenticated, notAuthenticated,
notFound, notFound,
@@ -19,37 +18,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req) const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res) if (!user) return notAuthenticated(res)
try { try {
if (req.method === 'GET') {
const workspaceId = req.query.workspaceId as string | undefined
if (!workspaceId) return badRequest(res)
const typebotIds = req.query.typebotIds as string[]
const typebots = await prisma.typebot.findMany({
where:
process.env.ADMIN_EMAIL === user.email
? undefined
: {
OR: [
{
workspace: { members: { some: { userId: user.id } } },
id: { in: typebotIds },
isArchived: { not: true },
},
{
id: { in: typebotIds },
collaborators: {
some: {
userId: user.id,
},
},
isArchived: { not: true },
},
],
},
orderBy: { createdAt: 'desc' },
select: { name: true, id: true, groups: true, variables: true },
})
return res.send({ typebots })
}
if (req.method === 'POST') { if (req.method === 'POST') {
const workspace = await prisma.workspace.findFirst({ const workspace = await prisma.workspace.findFirst({
where: { id: req.body.workspaceId }, where: { id: req.body.workspaceId },

View File

@@ -1,8 +0,0 @@
// TODO: Remove when all clients are up to date
import { NextApiRequest, NextApiResponse } from 'next'
const handler = async (_req: NextApiRequest, res: NextApiResponse) => {
return res.send({ commitSha: process.env.VERCEL_GIT_COMMIT_SHA })
}
export default handler

View File

@@ -1,65 +0,0 @@
import prisma from '@/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
// TODO: Delete
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req)
if (!user) return notAuthenticated(res)
if (req.method === 'GET') {
const workspaceId = req.query.workspaceId as string
const now = new Date()
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const firstDayOfNextMonth = new Date(
now.getFullYear(),
now.getMonth() + 1,
1
)
const [
totalChatsUsed,
{
_sum: { storageUsed: totalStorageUsed },
},
] = await prisma.$transaction(async (tx) => {
const typebots = await tx.typebot.findMany({
where: {
workspace: {
id: workspaceId,
members: { some: { userId: user.id } },
},
},
})
return Promise.all([
prisma.result.count({
where: {
typebotId: { in: typebots.map((typebot) => typebot.id) },
hasStarted: true,
createdAt: {
gte: firstDayOfMonth,
lt: firstDayOfNextMonth,
},
},
}),
prisma.answer.aggregate({
where: {
storageUsed: { gt: 0 },
result: {
typebotId: { in: typebots.map((typebot) => typebot.id) },
},
},
_sum: { storageUsed: true },
}),
])
})
return res.send({
totalChatsUsed,
totalStorageUsed,
})
}
methodNotAllowed(res)
}
export default handler

View File

@@ -27,7 +27,7 @@ Fork the repository
5. Change the build command to: 5. Change the build command to:
```sh ```sh
cd ../.. && pnpm prisma generate && pnpm build:builder && pnpm db:migrate cd ../.. && pnpm build:builder && pnpm db:migrate
``` ```
6. Add the required environment variables ([Check out the configuration guide](/self-hosting/configuration)) 6. Add the required environment variables ([Check out the configuration guide](/self-hosting/configuration))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
import { canReadTypebots } from '@/helpers/api/dbRules'
import prisma from '@/lib/prisma'
import { User } from '@typebot.io/prisma'
import { PublicTypebot, Typebot } from '@typebot.io/schemas'
type Props = {
isPreview?: boolean
typebotIds: string[]
user?: User
}
export const fetchLinkedTypebots = async ({
user,
isPreview,
typebotIds,
}: Props) => {
const linkedTypebots = (
isPreview
? await prisma.typebot.findMany({
where: user
? {
AND: [
{ id: { in: typebotIds } },
canReadTypebots(typebotIds, user as User),
],
}
: { id: { in: typebotIds } },
})
: await prisma.publicTypebot.findMany({
where: { id: { in: typebotIds } },
})
) as (Typebot | PublicTypebot)[]
return linkedTypebots
}

View File

@@ -1,20 +0,0 @@
import prisma from '@/lib/prisma'
import { PublicTypebot, Typebot } from '@typebot.io/schemas'
type Props = {
isPreview: boolean
typebotIds: string[]
}
export const getLinkedTypebots = async ({ isPreview, typebotIds }: Props) => {
const linkedTypebots = (
isPreview
? await prisma.typebot.findMany({
where: { id: { in: typebotIds } },
})
: await prisma.publicTypebot.findMany({
where: { id: { in: typebotIds } },
})
) as (Typebot | PublicTypebot)[]
return linkedTypebots
}

View File

@@ -1,5 +1,3 @@
import prisma from '@/lib/prisma'
import { canReadTypebots } from '@/helpers/api/dbRules'
import { User } from '@typebot.io/prisma' import { User } from '@typebot.io/prisma'
import { import {
LogicBlockType, LogicBlockType,
@@ -8,6 +6,7 @@ import {
TypebotLinkBlock, TypebotLinkBlock,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { fetchLinkedTypebots } from './fetchLinkedTypebots'
type Props = { type Props = {
typebots: Pick<PublicTypebot, 'groups'>[] typebots: Pick<PublicTypebot, 'groups'>[]
@@ -15,7 +14,7 @@ type Props = {
isPreview?: boolean isPreview?: boolean
} }
export const getLinkedTypebotsChildren = export const getPreviouslyLinkedTypebots =
({ typebots, user, isPreview }: Props) => ({ typebots, user, isPreview }: Props) =>
async ( async (
capturedLinkedBots: (Typebot | PublicTypebot)[] capturedLinkedBots: (Typebot | PublicTypebot)[]
@@ -39,23 +38,12 @@ export const getLinkedTypebotsChildren =
) )
.filter(isDefined) .filter(isDefined)
if (linkedTypebotIds.length === 0) return capturedLinkedBots if (linkedTypebotIds.length === 0) return capturedLinkedBots
const linkedTypebots = ( const linkedTypebots = (await fetchLinkedTypebots({
isPreview user,
? await prisma.typebot.findMany({ typebotIds: linkedTypebotIds,
where: user isPreview,
? { })) as (Typebot | PublicTypebot)[]
AND: [ return getPreviouslyLinkedTypebots({
{ id: { in: linkedTypebotIds } },
canReadTypebots(linkedTypebotIds, user as User),
],
}
: { id: { in: linkedTypebotIds } },
})
: await prisma.publicTypebot.findMany({
where: { id: { in: linkedTypebotIds } },
})
) as (Typebot | PublicTypebot)[]
return getLinkedTypebotsChildren({
typebots: linkedTypebots, typebots: linkedTypebots,
user, user,
isPreview, isPreview,

View File

@@ -21,8 +21,8 @@ import Cors from 'cors'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { parseVariables } from '@/features/variables/parseVariables' import { parseVariables } from '@/features/variables/parseVariables'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult' import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/getLinkedTypebots' import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/fetchLinkedTypebots'
import { getLinkedTypebotsChildren } from '@/features/blocks/logic/typebotLink/getLinkedTypebotsChildren' import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { saveErrorLog } from '@/features/logs/saveErrorLog' import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog' import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
@@ -127,11 +127,11 @@ export const executeWebhook =
convertKeyValueTableToObject(webhook.queryParams, variables) convertKeyValueTableToObject(webhook.queryParams, variables)
) )
const contentType = headers ? headers['Content-Type'] : undefined const contentType = headers ? headers['Content-Type'] : undefined
const linkedTypebotsParents = await getLinkedTypebots({ const linkedTypebotsParents = await fetchLinkedTypebots({
isPreview: !('typebotId' in typebot), isPreview: !('typebotId' in typebot),
typebotIds: parentTypebotIds, typebotIds: parentTypebotIds,
}) })
const linkedTypebotsChildren = await getLinkedTypebotsChildren({ const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({
isPreview: !('typebotId' in typebot), isPreview: !('typebotId' in typebot),
typebots: [typebot], typebots: [typebot],
})([]) })([])

View File

@@ -3,7 +3,7 @@ import prisma from '@/lib/prisma'
import { Typebot } from '@typebot.io/schemas' import { Typebot } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from '@typebot.io/lib/api' import { methodNotAllowed } from '@typebot.io/lib/api'
import { getLinkedTypebotsChildren } from '@/features/blocks/logic/typebotLink/getLinkedTypebotsChildren' import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult' import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@@ -23,7 +23,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
.flatMap((g) => g.blocks) .flatMap((g) => g.blocks)
.find((s) => s.id === blockId) .find((s) => s.id === blockId)
if (!block) return res.status(404).send({ message: 'Group not found' }) if (!block) return res.status(404).send({ message: 'Group not found' })
const linkedTypebots = await getLinkedTypebotsChildren({ const linkedTypebots = await getPreviouslyLinkedTypebots({
isPreview: true, isPreview: true,
typebots: [typebot], typebots: [typebot],
user, user,

View File

@@ -3,7 +3,7 @@ import { Typebot } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from '@typebot.io/lib/api' import { methodNotAllowed } from '@typebot.io/lib/api'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult' import { parseSampleResult } from '@/features/blocks/integrations/webhook/parseSampleResult'
import { getLinkedTypebotsChildren } from '@/features/blocks/logic/typebotLink/getLinkedTypebotsChildren' import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { authenticateUser } from '@/helpers/authenticateUser' import { authenticateUser } from '@/helpers/authenticateUser'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@@ -19,7 +19,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}, },
})) as unknown as Typebot | undefined })) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const linkedTypebots = await getLinkedTypebotsChildren({ const linkedTypebots = await getPreviouslyLinkedTypebots({
isPreview: true, isPreview: true,
typebots: [typebot], typebots: [typebot],
user, user,

View File

@@ -15,7 +15,7 @@ import Mail from 'nodemailer/lib/mailer'
import { DefaultBotNotificationEmail } from '@typebot.io/emails' import { DefaultBotNotificationEmail } from '@typebot.io/emails'
import { render } from '@faire/mjml-react/utils/render' import { render } from '@faire/mjml-react/utils/render'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { getLinkedTypebotsChildren } from '@/features/blocks/logic/typebotLink/getLinkedTypebotsChildren' import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { saveErrorLog } from '@/features/logs/saveErrorLog' import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog' import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
@@ -194,7 +194,7 @@ const getEmailBody = async ({
where: { typebotId }, where: { typebotId },
})) as unknown as PublicTypebot })) as unknown as PublicTypebot
if (!typebot) return if (!typebot) return
const linkedTypebots = await getLinkedTypebotsChildren({ const linkedTypebots = await getPreviouslyLinkedTypebots({
typebots: [typebot], typebots: [typebot],
})([]) })([])
const answers = parseAnswers(typebot, linkedTypebots)(resultValues) const answers = parseAnswers(typebot, linkedTypebots)(resultValues)