From 898367a33be70d86b765421462d1c288d0c8cc05 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Sat, 17 Sep 2022 16:37:33 +0200 Subject: [PATCH] :sparkles: Add usage-based new pricing plans --- .../emails/invitationToCollaborate.mjml | 7 +- .../assets/emails/invitationToCollaborate.ts | 535 +------------- .../emails/workspaceMemberInvitation.mjml | 37 + .../emails/workspaceMemberInvitation.ts | 536 ++++++++++++++ apps/builder/assets/icons.tsx | 8 + .../components/analytics/AnalyticsContent.tsx | 4 +- .../components/dashboard/FolderContent.tsx | 11 +- .../FolderContent/CreateFolderButton.tsx | 8 +- .../BillingContent/BillingContent.tsx | 34 + .../CurrentSubscriptionContent.tsx | 77 ++ .../BillingContent/InvoicesList.tsx | 97 +++ .../UsageContent/UsageContent.tsx | 158 +++++ .../BillingContent/UsageContent/helpers.ts | 7 + .../BillingContent/UsageContent/index.ts | 1 + .../BillingContent/UsageContent/useUsage.ts | 16 + .../BillingContent/index.ts | 1 + .../queries/cancelSubscriptionQuery.ts | 7 + .../queries/redirectToBillingPortal.ts | 7 + .../queries/useInvoicesQuery.ts | 24 + .../WorkspaceSettingsModal/BillingForm.tsx | 76 -- .../MembersList/MembersList.tsx | 22 +- .../MembersList/helpers.ts | 15 + .../WorkspaceSettingsModal.tsx | 11 +- .../components/results/ResultsContent.tsx | 10 - .../ResultsTable/ResultsActionButtons.tsx | 7 +- .../settings/GeneralSettingsForm.tsx | 10 +- .../shared/ChangePlanForm/ChangePlanForm.tsx | 108 +++ .../shared/ChangePlanForm/ProPlanContent.tsx | 339 +++++++++ .../ChangePlanForm/StarterPlanContent.tsx | 280 ++++++++ .../components/FeaturesList.tsx | 21 + .../shared/ChangePlanForm/helpers.ts | 86 +++ .../components/shared/ChangePlanForm/index.ts | 1 + .../ChangePlanForm/queries/updatePlan.tsx} | 50 +- .../queries/useCurrentSubscriptionInfo.ts | 30 + apps/builder/components/shared/Info.tsx | 23 +- .../components/shared/MoreInfoTooltip.tsx | 2 +- apps/builder/components/shared/PlanTag.tsx | 30 + .../shared/buttons/PublishButton.tsx | 7 +- .../shared/buttons/UpgradeButton.tsx | 11 +- .../shared/buttons/UploadButton.tsx | 2 +- .../components/shared/hooks/useToast.tsx | 2 + .../shared/modals/ChangePlanModal.tsx | 53 ++ .../modals/UpgradeModal/ActionButton.tsx | 13 - .../shared/modals/UpgradeModal/Card.tsx | 28 - .../shared/modals/UpgradeModal/CardBadge.tsx | 30 - .../modals/UpgradeModal/PricingCard.tsx | 68 -- .../modals/UpgradeModal/UpgradeModal.tsx | 217 ------ .../shared/modals/UpgradeModal/index.tsx | 1 - .../builder/components/shared/modals/index.ts | 1 + apps/builder/contexts/ResultsProvider.tsx | 4 - apps/builder/libs/theme.ts | 6 + apps/builder/package.json | 2 +- apps/builder/pages/_document.tsx | 2 +- apps/builder/pages/api/auth/[...nextauth].ts | 9 + apps/builder/pages/api/auth/adapter.ts | 9 +- .../{customer-portal.ts => billing-portal.ts} | 6 +- apps/builder/pages/api/stripe/checkout.ts | 46 -- apps/builder/pages/api/stripe/invoices.ts | 49 ++ apps/builder/pages/api/stripe/subscription.ts | 240 +++++++ .../pages/api/stripe/update-subscription.ts | 46 -- apps/builder/pages/api/stripe/webhook.ts | 34 +- .../api/typebots/[typebotId]/invitations.ts | 14 +- apps/builder/pages/api/workspaces.ts | 5 +- .../workspaces/[workspaceId]/invitations.ts | 38 +- .../api/workspaces/[workspaceId]/usage.ts | 54 ++ apps/builder/pages/typebots.tsx | 12 +- .../pages/typebots/[typebotId]/results.tsx | 77 ++ apps/builder/playwright.config.ts | 6 +- apps/builder/playwright/firstUser.json | 15 + apps/builder/playwright/freeUser.json | 19 - apps/builder/playwright/proUser.json | 18 - apps/builder/playwright/secondUser.json | 15 + apps/builder/playwright/services/browser.ts | 4 +- apps/builder/playwright/services/database.ts | 238 ++++--- .../playwright/tests/accountSettings.spec.ts | 4 - apps/builder/playwright/tests/billing.spec.ts | 175 +++++ .../playwright/tests/bubbles/embed.spec.ts | 3 - .../playwright/tests/bubbles/image.spec.ts | 3 - .../playwright/tests/bubbles/text.spec.ts | 3 - .../playwright/tests/bubbles/video.spec.ts | 3 - .../playwright/tests/collaboration.spec.ts | 5 +- .../playwright/tests/customDomains.spec.ts | 13 +- .../playwright/tests/dashboard.spec.ts | 5 +- apps/builder/playwright/tests/editor.spec.ts | 3 - .../playwright/tests/inputs/buttons.spec.ts | 3 - .../playwright/tests/inputs/date.spec.ts | 3 - .../playwright/tests/inputs/email.spec.ts | 3 - .../playwright/tests/inputs/file.spec.ts | 6 - .../playwright/tests/inputs/number.spec.ts | 3 - .../playwright/tests/inputs/payment.spec.ts | 3 - .../playwright/tests/inputs/phone.spec.ts | 3 - .../playwright/tests/inputs/rating.spec.ts | 3 - .../playwright/tests/inputs/text.spec.ts | 3 - .../playwright/tests/inputs/url.spec.ts | 3 - .../integrations/googleAnalytics.spec.ts | 3 - .../tests/integrations/googleSheets.spec.ts | 3 - .../tests/integrations/sendEmail.spec.ts | 3 - .../tests/integrations/webhook.spec.ts | 3 - .../playwright/tests/logic/code.spec.ts | 3 - .../playwright/tests/logic/condition.spec.ts | 3 - .../playwright/tests/logic/redirect.spec.ts | 3 - .../tests/logic/setVariable.spec.ts | 3 - .../tests/logic/typebotLink.spec.ts | 3 - apps/builder/playwright/tests/results.spec.ts | 7 +- .../builder/playwright/tests/settings.spec.ts | 8 +- .../playwright/tests/templates.spec.ts | 3 - apps/builder/playwright/tests/theme.spec.ts | 3 - .../playwright/tests/workspaces.spec.ts | 108 +-- apps/builder/services/api/utils.ts | 10 +- apps/builder/services/utils/utils.ts | 3 + apps/builder/services/workspace/workspace.ts | 16 +- apps/landing-page/lib/chakraTheme.ts | 5 + .../emails/almostReachedChatsLimitEmail.mjml | 39 + .../emails/almostReachedChatsLimitEmail.ts | 33 + .../almostReachedStorageLimitEmail.mjml | 38 + .../emails/almostReachedStorageLimitEmail.ts | 31 + .../{ => emails}/newLeadEmailContent.ts | 0 .../assets/emails/reachedChatsLimitEmail.mjml | 37 + .../assets/emails/reachedChatsLimitEmail.ts | 33 + .../emails/reachedStorageLimitEmail.mjml | 37 + .../assets/emails/reachedStorageLimitEmail.ts | 31 + apps/viewer/layouts/TypebotPage.tsx | 10 +- apps/viewer/package.json | 3 + .../blocks/[blockId]/storage/upload-url.ts | 128 +++- .../[typebotId]/integrations/email.ts | 2 +- .../pages/api/typebots/[typebotId]/results.ts | 156 +++- .../[typebotId]/results/[resultId]/answers.ts | 13 +- apps/viewer/playwright/services/database.ts | 139 ++-- apps/viewer/playwright/tests/api.spec.ts | 2 +- .../playwright/tests/fileUpload.spec.ts | 49 +- apps/viewer/playwright/tests/limits.spec.ts | 23 + apps/viewer/services/result.ts | 2 +- package.json | 2 +- .../ChatBlock/inputs/FileUploadForm.tsx | 3 +- .../migration.sql | 18 + packages/db/prisma/schema.prisma | 8 +- packages/utils/package.json | 15 +- packages/utils/src/api/index.ts | 1 + .../utils/src/api/sendEmailNotification.ts | 18 +- packages/utils/src/index.ts | 2 + packages/utils/src/playwright.ts | 60 ++ packages/utils/src/pricing.ts | 85 +++ packages/utils/src/utils.ts | 33 +- pnpm-lock.yaml | 671 +++++++++++++++++- 144 files changed, 4633 insertions(+), 1626 deletions(-) create mode 100644 apps/builder/assets/emails/workspaceMemberInvitation.mjml create mode 100644 apps/builder/assets/emails/workspaceMemberInvitation.ts create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/BillingContent.tsx create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/InvoicesList.tsx create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/UsageContent.tsx create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/helpers.ts create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/index.ts create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/useUsage.ts create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/index.ts create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/cancelSubscriptionQuery.ts create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/redirectToBillingPortal.ts create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/useInvoicesQuery.ts delete mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/BillingForm.tsx create mode 100644 apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/helpers.ts create mode 100644 apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx create mode 100644 apps/builder/components/shared/ChangePlanForm/ProPlanContent.tsx create mode 100644 apps/builder/components/shared/ChangePlanForm/StarterPlanContent.tsx create mode 100644 apps/builder/components/shared/ChangePlanForm/components/FeaturesList.tsx create mode 100644 apps/builder/components/shared/ChangePlanForm/helpers.ts create mode 100644 apps/builder/components/shared/ChangePlanForm/index.ts rename apps/builder/{services/stripe.ts => components/shared/ChangePlanForm/queries/updatePlan.tsx} (50%) create mode 100644 apps/builder/components/shared/ChangePlanForm/queries/useCurrentSubscriptionInfo.ts create mode 100644 apps/builder/components/shared/PlanTag.tsx create mode 100644 apps/builder/components/shared/modals/ChangePlanModal.tsx delete mode 100644 apps/builder/components/shared/modals/UpgradeModal/ActionButton.tsx delete mode 100644 apps/builder/components/shared/modals/UpgradeModal/Card.tsx delete mode 100644 apps/builder/components/shared/modals/UpgradeModal/CardBadge.tsx delete mode 100644 apps/builder/components/shared/modals/UpgradeModal/PricingCard.tsx delete mode 100644 apps/builder/components/shared/modals/UpgradeModal/UpgradeModal.tsx delete mode 100644 apps/builder/components/shared/modals/UpgradeModal/index.tsx create mode 100644 apps/builder/components/shared/modals/index.ts rename apps/builder/pages/api/stripe/{customer-portal.ts => billing-portal.ts} (89%) delete mode 100644 apps/builder/pages/api/stripe/checkout.ts create mode 100644 apps/builder/pages/api/stripe/invoices.ts create mode 100644 apps/builder/pages/api/stripe/subscription.ts delete mode 100644 apps/builder/pages/api/stripe/update-subscription.ts create mode 100644 apps/builder/pages/api/workspaces/[workspaceId]/usage.ts create mode 100644 apps/builder/playwright/firstUser.json delete mode 100644 apps/builder/playwright/freeUser.json delete mode 100644 apps/builder/playwright/proUser.json create mode 100644 apps/builder/playwright/secondUser.json create mode 100644 apps/builder/playwright/tests/billing.spec.ts create mode 100644 apps/viewer/assets/emails/almostReachedChatsLimitEmail.mjml create mode 100644 apps/viewer/assets/emails/almostReachedChatsLimitEmail.ts create mode 100644 apps/viewer/assets/emails/almostReachedStorageLimitEmail.mjml create mode 100644 apps/viewer/assets/emails/almostReachedStorageLimitEmail.ts rename apps/viewer/assets/{ => emails}/newLeadEmailContent.ts (100%) create mode 100644 apps/viewer/assets/emails/reachedChatsLimitEmail.mjml create mode 100644 apps/viewer/assets/emails/reachedChatsLimitEmail.ts create mode 100644 apps/viewer/assets/emails/reachedStorageLimitEmail.mjml create mode 100644 apps/viewer/assets/emails/reachedStorageLimitEmail.ts create mode 100644 apps/viewer/playwright/tests/limits.spec.ts create mode 100644 packages/db/prisma/migrations/20220918083055_add_usage_based_pricing/migration.sql rename apps/builder/services/api/emails.ts => packages/utils/src/api/sendEmailNotification.ts (53%) create mode 100644 packages/utils/src/playwright.ts create mode 100644 packages/utils/src/pricing.ts diff --git a/apps/builder/assets/emails/invitationToCollaborate.mjml b/apps/builder/assets/emails/invitationToCollaborate.mjml index 338df2b867..3124f51f24 100644 --- a/apps/builder/assets/emails/invitationToCollaborate.mjml +++ b/apps/builder/assets/emails/invitationToCollaborate.mjml @@ -20,13 +20,14 @@ - You have been invited to collaborate on a typebot created by ${email} - From now on you will see this typebot in your dashboard under the "Shared with me "button 👍 + You have been invited by ${hostEmail} to collaborate on his typebot ${typebotName} + From now on you will see this typebot in your dashboard under the his workspace "${workspaceName}" 👍 + Make sure to log in as ${guestEmail} - See the typebot + Go to typebot diff --git a/apps/builder/assets/emails/invitationToCollaborate.ts b/apps/builder/assets/emails/invitationToCollaborate.ts index 4ee39d22bc..090612b7bb 100644 --- a/apps/builder/assets/emails/invitationToCollaborate.ts +++ b/apps/builder/assets/emails/invitationToCollaborate.ts @@ -1,505 +1,32 @@ -export const invitationToCollaborate = ( - email: string, - typebotUrl: string -) => ` - - - - - - - - - - - - - - - - -
- -
- - - - - - -
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- - - - - - -
- header image -
-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - - - - -
-
- You have been invited to - collaborate on a typebot created - by ${email} -
-
-
- From now on you will have access to this - typebot in their workspace 👍 -
-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - - - - - -
- - - - - - -
- See the typebot -
-
-
-
- -
-
- -
-
- -
- - +type Props = { + workspaceName: string + typebotName: string + url: string + hostEmail: string + guestEmail: string +} + +export const invitationToCollaborate = ({guestEmail, hostEmail, typebotName, url, workspaceName}: Props) => `
header image
You have been invited by ${hostEmail} to collaborate on his typebot ${typebotName}
From now on you will see this typebot in your dashboard under the his workspace "${workspaceName}" üëç
Make sure to log in as ${guestEmail}
` diff --git a/apps/builder/assets/emails/workspaceMemberInvitation.mjml b/apps/builder/assets/emails/workspaceMemberInvitation.mjml new file mode 100644 index 0000000000..873dcb6bf0 --- /dev/null +++ b/apps/builder/assets/emails/workspaceMemberInvitation.mjml @@ -0,0 +1,37 @@ + + + + + + + + + .footer-link { + color: #A0AEC0 + } + + + + + + + + + + + + You have been invited by ${hostEmail} to collaborate on his workspace "${workspaceName}" as a team member. + From now on you will have access to this workspace in your dashboard 👍 + Make sure to log in as ${guestEmail} + + + + + Go the workspace + + + + + + + \ No newline at end of file diff --git a/apps/builder/assets/emails/workspaceMemberInvitation.ts b/apps/builder/assets/emails/workspaceMemberInvitation.ts new file mode 100644 index 0000000000..6f037900a3 --- /dev/null +++ b/apps/builder/assets/emails/workspaceMemberInvitation.ts @@ -0,0 +1,536 @@ +type Props = { + workspaceName: string + url: string, + hostEmail: string, + guestEmail: string +} + +export const workspaceMemberInvitationEmail = ({workspaceName, url, hostEmail, guestEmail}: Props) => ` + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ header image +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + + + + + + + +
+
+ You have been invited by + ${hostEmail} to collaborate on his + workspace "${workspaceName}" as a + team member. +
+
+
+ From now on you will have access + to this workspace in your + dashboard üëç +
+
+
+ Make sure to log in as + ${guestEmail} +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ Go the workspace +
+
+
+
+ +
+
+ +
+
+ +
+ + +` \ No newline at end of file diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx index 0e3b674e64..98f2137cf9 100644 --- a/apps/builder/assets/icons.tsx +++ b/apps/builder/assets/icons.tsx @@ -481,3 +481,11 @@ export const EyeOffIcon = (props: IconProps) => ( ) + +export const AlertIcon = (props: IconProps) => ( + + + + + +) diff --git a/apps/builder/components/analytics/AnalyticsContent.tsx b/apps/builder/components/analytics/AnalyticsContent.tsx index 248c17b694..9078455955 100644 --- a/apps/builder/components/analytics/AnalyticsContent.tsx +++ b/apps/builder/components/analytics/AnalyticsContent.tsx @@ -2,7 +2,7 @@ import { Flex, Spinner, useDisclosure } from '@chakra-ui/react' import { StatsCards } from 'components/analytics/StatsCards' import { Graph } from 'components/shared/Graph' import { useToast } from 'components/shared/hooks/useToast' -import { UpgradeModal } from 'components/shared/modals/UpgradeModal' +import { ChangePlanModal } from 'components/shared/modals/ChangePlanModal' import { GraphProvider, GroupsCoordinatesProvider } from 'contexts/GraphContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { Stats } from 'models' @@ -49,7 +49,7 @@ export const AnalyticsContent = ({ stats }: { stats?: Stats }) => { )} - + ) diff --git a/apps/builder/components/dashboard/FolderContent.tsx b/apps/builder/components/dashboard/FolderContent.tsx index 315446bdfb..fda8edd961 100644 --- a/apps/builder/components/dashboard/FolderContent.tsx +++ b/apps/builder/components/dashboard/FolderContent.tsx @@ -1,4 +1,5 @@ import { DashboardFolder, WorkspaceRole } from 'db' +import { env } from 'utils' import { Flex, Heading, @@ -160,9 +161,13 @@ export const FolderContent = ({ folder }: Props) => { return ( - {typebots && !isTypebotLoading && user && folder === null && ( - - )} + {typebots && + !isTypebotLoading && + user && + folder === null && + env('E2E_TEST') !== 'true' && ( + + )} {folder?.name} diff --git a/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx b/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx index a3ead02c48..e3da3bec12 100644 --- a/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx +++ b/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx @@ -1,7 +1,9 @@ import { Button, HStack, Tag, useDisclosure, Text } from '@chakra-ui/react' import { FolderPlusIcon } from 'assets/icons' -import { UpgradeModal } from 'components/shared/modals/UpgradeModal' -import { LimitReached } from 'components/shared/modals/UpgradeModal/UpgradeModal' +import { + LimitReached, + ChangePlanModal, +} from 'components/shared/modals/ChangePlanModal' import { useWorkspace } from 'contexts/WorkspaceContext' import React from 'react' import { isFreePlan } from 'services/workspace' @@ -26,7 +28,7 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => { Create a folder {isFreePlan(workspace) && Pro} - { + const { workspace, refreshWorkspace } = useWorkspace() + + if (!workspace) return null + return ( + + + refreshWorkspace({ + plan: Plan.FREE, + additionalChatsIndex: 0, + additionalStorageIndex: 0, + }) + } + /> + + {workspace.plan !== Plan.LIFETIME && workspace.plan !== Plan.OFFERED && ( + + )} + {workspace.stripeId && } + + ) +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx new file mode 100644 index 0000000000..b0962ba504 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx @@ -0,0 +1,77 @@ +import { + Text, + HStack, + Link, + Spinner, + Stack, + Flex, + Button, +} from '@chakra-ui/react' +import { PlanTag } from 'components/shared/PlanTag' +import { Plan } from 'db' +import React, { useState } from 'react' +import { cancelSubscriptionQuery } from './queries/cancelSubscriptionQuery' + +type CurrentSubscriptionContentProps = { + plan: Plan + stripeId?: string | null + onCancelSuccess: () => void +} + +export const CurrentSubscriptionContent = ({ + plan, + stripeId, + onCancelSuccess, +}: CurrentSubscriptionContentProps) => { + const [isCancelling, setIsCancelling] = useState(false) + const [isRedirectingToBillingPortal, setIsRedirectingToBillingPortal] = + useState(false) + + const cancelSubscription = async () => { + if (!stripeId) return + setIsCancelling(true) + await cancelSubscriptionQuery(stripeId) + onCancelSuccess() + setIsCancelling(false) + } + + if (isCancelling) return + return ( + + + Current workspace subscription: + + + + {(plan === Plan.STARTER || plan === Plan.PRO) && stripeId && ( + <> + + + Need to change payment method or billing information? Head over to + your billing portal: + + + + + + Cancel my subscription + + + + )} + + ) +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/InvoicesList.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/InvoicesList.tsx new file mode 100644 index 0000000000..1fa753cf61 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/InvoicesList.tsx @@ -0,0 +1,97 @@ +import { + Stack, + Heading, + Checkbox, + Skeleton, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + IconButton, + Text, +} from '@chakra-ui/react' +import { DownloadIcon, FileIcon } from 'assets/icons' +import { NextChakraLink } from 'components/nextChakra/NextChakraLink' +import { Workspace } from 'db' +import React from 'react' +import { useInvoicesQuery } from './queries/useInvoicesQuery' + +type Props = { + workspace: Workspace +} + +export const InvoicesList = ({ workspace }: Props) => { + const { invoices, isLoading } = useInvoicesQuery(workspace.stripeId) + + return ( + + Invoices + {invoices.length === 0 && !isLoading ? ( + No invoices found for this workspace. + ) : ( + + + + + + + + + + + {invoices?.map((invoice) => ( + + + + + + + + ))} + {isLoading && + Array.from({ length: 3 }).map((_, idx) => ( + + + + + + ))} + +
+ #Paid atSubtotal +
+ + {invoice.id}{new Date(invoice.date * 1000).toDateString()}{getFormattedPrice(invoice.amount, invoice.currency)} + } + variant="outline" + href={invoice.url} + isExternal + aria-label={'Download invoice'} + /> +
+ + + + + +
+
+ )} +
+ ) +} + +const getFormattedPrice = (amount: number, currency: string) => { + const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency, + }) + + return formatter.format(amount / 100) +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/UsageContent.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/UsageContent.tsx new file mode 100644 index 0000000000..8385aa8504 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/UsageContent.tsx @@ -0,0 +1,158 @@ +import { + Stack, + Flex, + Heading, + Progress, + Text, + Skeleton, + HStack, + Tooltip, +} from '@chakra-ui/react' +import { AlertIcon } from 'assets/icons' +import { Plan, Workspace } from 'db' +import React from 'react' +import { getChatsLimit, getStorageLimit, parseNumberWithCommas } from 'utils' +import { storageToReadable } from './helpers' +import { useUsage } from './useUsage' + +type Props = { + workspace: Workspace +} + +export const UsageContent = ({ workspace }: Props) => { + const { data, isLoading } = useUsage(workspace.id) + const totalChatsUsed = data?.totalChatsUsed ?? 0 + const totalStorageUsed = data?.totalStorageUsed ?? 0 + + const workspaceChatsLimit = getChatsLimit(workspace) + const workspaceStorageLimit = getStorageLimit(workspace) + const workspaceStorageLimitGigabites = + workspaceStorageLimit * 1024 * 1024 * 1024 + + const chatsPercentage = Math.round( + (totalChatsUsed / workspaceChatsLimit) * 100 + ) + const storagePercentage = Math.round( + (totalStorageUsed / workspaceStorageLimitGigabites) * 100 + ) + + return ( + + Usage + + + + + Chats + + {chatsPercentage >= 80 && ( + + Your typebots are popular! You will soon reach your plan's + chats limit. 🚀 +
+
+ Make sure to update your plan to increase + this limit and continue chatting with your users. + + } + > + + + +
+ )} + + (resets on 1st of every month) + +
+ + + {parseNumberWithCommas(totalChatsUsed)} + + / {parseNumberWithCommas(workspaceChatsLimit)} + +
+ + = workspaceChatsLimit ? 'red' : 'blue'} + /> +
+ {workspace.plan !== Plan.FREE && ( + + + + + Storage + + {storagePercentage >= 80 && ( + + Your typebots are popular! You will soon reach your plan's + storage limit. 🚀 +
+
+ Make sure to update your plan in order to + continue collecting uploaded files. You can also{' '} + delete files to free up space. + + } + > + + + +
+ )} +
+ + + + {storageToReadable(totalStorageUsed)} + + / {workspaceStorageLimit} GB + +
+ = workspaceStorageLimitGigabites + ? 'red' + : 'blue' + } + rounded="full" + hasStripe + isIndeterminate={isLoading} + /> +
+ )} +
+ ) +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/helpers.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/helpers.ts new file mode 100644 index 0000000000..6644ea5e5b --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/helpers.ts @@ -0,0 +1,7 @@ +export const storageToReadable = (bytes: number) => { + if (bytes == 0) { + return '0' + } + const e = Math.floor(Math.log(bytes) / Math.log(1024)) + return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B' +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/index.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/index.ts new file mode 100644 index 0000000000..2ce5fe4a63 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/index.ts @@ -0,0 +1 @@ +export { UsageContent } from './UsageContent' diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/useUsage.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/useUsage.ts new file mode 100644 index 0000000000..b37ca74f28 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/UsageContent/useUsage.ts @@ -0,0 +1,16 @@ +import { fetcher } from 'services/utils' +import useSWR from 'swr' +import { env } from 'utils' + +export const useUsage = (workspaceId?: string) => { + const { data, error } = useSWR< + { totalChatsUsed: number; totalStorageUsed: number }, + Error + >(workspaceId ? `/api/workspaces/${workspaceId}/usage` : null, fetcher, { + dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined, + }) + return { + data, + isLoading: !error && !data, + } +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/index.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/index.ts new file mode 100644 index 0000000000..27b9ed5273 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/index.ts @@ -0,0 +1 @@ +export { BillingContent } from './BillingContent' diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/cancelSubscriptionQuery.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/cancelSubscriptionQuery.ts new file mode 100644 index 0000000000..705249839a --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/cancelSubscriptionQuery.ts @@ -0,0 +1,7 @@ +import { sendRequest } from 'utils' + +export const cancelSubscriptionQuery = (stripeId: string) => + sendRequest({ + url: `api/stripe/subscription?stripeId=${stripeId}`, + method: 'DELETE', + }) diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/redirectToBillingPortal.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/redirectToBillingPortal.ts new file mode 100644 index 0000000000..f40dd5e542 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/redirectToBillingPortal.ts @@ -0,0 +1,7 @@ +import { sendRequest } from 'utils' + +export const redirectToBillingPortal = ({ + workspaceId, +}: { + workspaceId: string +}) => sendRequest(`/api/stripe/billing-portal?workspaceId=${workspaceId}`) diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/useInvoicesQuery.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/useInvoicesQuery.ts new file mode 100644 index 0000000000..184dba0dab --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/queries/useInvoicesQuery.ts @@ -0,0 +1,24 @@ +import { fetcher } from 'services/utils' +import useSWR from 'swr' +import { env } from 'utils' + +type Invoice = { + id: string + url: string + date: number + currency: string + amount: number +} +export const useInvoicesQuery = (stripeId?: string | null) => { + const { data, error } = useSWR<{ invoices: Invoice[] }, Error>( + stripeId ? `/api/stripe/invoices?stripeId=${stripeId}` : null, + fetcher, + { + dedupingInterval: env('E2E_TEST') === 'enabled' ? 0 : undefined, + } + ) + return { + invoices: data?.invoices ?? [], + isLoading: !error && !data, + } +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingForm.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingForm.tsx deleted file mode 100644 index f24acb379c..0000000000 --- a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingForm.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Stack, HStack, Button, Text, Tag } from '@chakra-ui/react' -import { ExternalLinkIcon } from 'assets/icons' -import { NextChakraLink } from 'components/nextChakra/NextChakraLink' -import { UpgradeButton } from 'components/shared/buttons/UpgradeButton' -import { useWorkspace } from 'contexts/WorkspaceContext' -import { Plan } from 'db' -import React from 'react' - -export const BillingForm = () => { - const { workspace } = useWorkspace() - - return ( - - - Current workspace subscription: - - - {workspace && - !([Plan.TEAM, Plan.LIFETIME, Plan.OFFERED] as Plan[]).includes( - workspace.plan - ) && ( - - {workspace?.plan === Plan.FREE && ( - - Upgrade to Pro plan - - )} - {workspace?.plan !== Plan.TEAM && ( - - Upgrade to Team plan - - )} - - )} - {workspace?.stripeId && ( - <> - - To manage your subscription and download invoices, head over to your - Stripe portal: - - - - - )} - - ) -} - -const PlanTag = ({ plan }: { plan?: Plan }) => { - switch (plan) { - case Plan.TEAM: { - return Team - } - case Plan.LIFETIME: - case Plan.OFFERED: - case Plan.PRO: { - return Personal Pro - } - default: { - return Free - } - } -} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/MembersList.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/MembersList.tsx index 459f8b72c4..cba5f9ac0f 100644 --- a/apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/MembersList.tsx +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/MembersList.tsx @@ -2,7 +2,7 @@ import { HStack, SkeletonCircle, SkeletonText, Stack } from '@chakra-ui/react' import { UnlockPlanInfo } from 'components/shared/Info' import { useUser } from 'contexts/UserContext' import { useWorkspace } from 'contexts/WorkspaceContext' -import { Plan, WorkspaceInvitation, WorkspaceRole } from 'db' +import { WorkspaceInvitation, WorkspaceRole } from 'db' import React from 'react' import { deleteInvitation, @@ -13,6 +13,7 @@ import { useMembers, } from 'services/workspace' import { AddMemberForm } from './AddMemberForm' +import { checkCanInviteMember } from './helpers' import { MemberItem } from './MemberItem' export const MembersList = () => { @@ -78,14 +79,19 @@ export const MembersList = () => { }) } + const canInviteNewMember = checkCanInviteMember({ + plan: workspace?.plan, + currentMembersCount: [...(members ?? []), ...(invitations ?? [])].length, + }) + return ( - - {workspace?.plan !== Plan.TEAM && ( + + {!canInviteNewMember && ( )} {workspace?.id && canEdit && ( @@ -94,7 +100,7 @@ export const MembersList = () => { onNewInvitation={handleNewInvitation} onNewMember={handleNewMember} isLoading={isLoading} - isLocked={workspace.plan !== Plan.TEAM} + isLocked={!canInviteNewMember} /> )} {members?.map((member) => ( diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/helpers.ts b/apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/helpers.ts new file mode 100644 index 0000000000..6c14ae0767 --- /dev/null +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/MembersList/helpers.ts @@ -0,0 +1,15 @@ +import { Plan } from 'db' +import { seatsLimit } from 'utils' + +export function checkCanInviteMember({ + plan, + currentMembersCount, +}: { + plan: string | undefined + currentMembersCount?: number +}) { + if (!plan || !currentMembersCount) return false + if (plan !== Plan.STARTER && plan !== Plan.PRO) return false + + return seatsLimit[plan].totalIncluded > currentMembersCount +} diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/WorkspaceSettingsModal.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/WorkspaceSettingsModal.tsx index 26bfa21360..a405695a0d 100644 --- a/apps/builder/components/dashboard/WorkspaceSettingsModal/WorkspaceSettingsModal.tsx +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/WorkspaceSettingsModal.tsx @@ -18,7 +18,7 @@ import { EmojiOrImageIcon } from 'components/shared/EmojiOrImageIcon' import { useWorkspace } from 'contexts/WorkspaceContext' import { User, Workspace } from 'db' import { useState } from 'react' -import { BillingForm } from './BillingForm' +import { BillingContent } from './BillingContent' import { MembersList } from './MembersList' import { MyAccountForm } from './MyAccountForm' import { EditorSettingsForm } from './EditorSettingsForm' @@ -50,13 +50,12 @@ export const WorkspaceSettingsModal = ({ return ( - + @@ -134,7 +133,7 @@ export const WorkspaceSettingsModal = ({ justifyContent="flex-start" pl="4" > - Billing + Billing & Usage )} @@ -174,7 +173,7 @@ const SettingsContent = ({ case 'members': return case 'billing': - return + return default: return null } diff --git a/apps/builder/components/results/ResultsContent.tsx b/apps/builder/components/results/ResultsContent.tsx index 44fda05103..817eea3a91 100644 --- a/apps/builder/components/results/ResultsContent.tsx +++ b/apps/builder/components/results/ResultsContent.tsx @@ -1,10 +1,8 @@ import { Stack } from '@chakra-ui/react' import { SubmissionsTable } from 'components/results/ResultsTable' import React, { useState } from 'react' -import { UnlockPlanInfo } from 'components/shared/Info' import { LogsModal } from './LogsModal' import { useTypebot } from 'contexts/TypebotContext' -import { Plan } from 'db' import { useResults } from 'contexts/ResultsProvider' import { ResultModal } from './ResultModal' @@ -14,7 +12,6 @@ export const ResultsContent = () => { fetchMore, hasMore, resultHeader, - totalHiddenResults, tableData, } = useResults() const { typebot, publishedTypebot } = useTypebot() @@ -46,13 +43,6 @@ export const ResultsContent = () => { overflow="scroll" w="full" > - {totalHiddenResults && ( - - )} {publishedTypebot && ( 0 && selectedResultsId.length === results?.length - ? totalResults - (totalHiddenResults ?? 0) + ? totalResults : selectedResultsId.length const deleteResults = async () => { @@ -87,9 +86,7 @@ export const ResultsActionButtons = ({ const exportResultsToCSV = async () => { setIsExportLoading(true) - const isSelectAll = - totalSelected === 0 || - totalSelected === totalResults - (totalHiddenResults ?? 0) + const isSelectAll = totalSelected === 0 || totalSelected === totalResults const dataToUnparse = isSelectAll ? await getAllTableData() diff --git a/apps/builder/components/settings/GeneralSettingsForm.tsx b/apps/builder/components/settings/GeneralSettingsForm.tsx index 75e387a826..b60e435280 100644 --- a/apps/builder/components/settings/GeneralSettingsForm.tsx +++ b/apps/builder/components/settings/GeneralSettingsForm.tsx @@ -6,7 +6,7 @@ import { Tag, useDisclosure, } from '@chakra-ui/react' -import { UpgradeModal } from 'components/shared/modals/UpgradeModal' +import { ChangePlanModal } from 'components/shared/modals/ChangePlanModal' import { SwitchWithLabel } from 'components/shared/SwitchWithLabel' import { useWorkspace } from 'contexts/WorkspaceContext' import { GeneralSettings } from 'models' @@ -56,7 +56,7 @@ export const GeneralSettingsForm = ({ return ( - + diff --git a/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx b/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx new file mode 100644 index 0000000000..d07c99add6 --- /dev/null +++ b/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx @@ -0,0 +1,108 @@ +import { Stack, HStack, Text } from '@chakra-ui/react' +import { NextChakraLink } from 'components/nextChakra/NextChakraLink' +import { useUser } from 'contexts/UserContext' +import { useWorkspace } from 'contexts/WorkspaceContext' +import { Plan } from 'db' +import { useToast } from '../hooks/useToast' +import { ProPlanContent } from './ProPlanContent' +import { pay } from './queries/updatePlan' +import { useCurrentSubscriptionInfo } from './queries/useCurrentSubscriptionInfo' +import { StarterPlanContent } from './StarterPlanContent' + +export const ChangePlanForm = () => { + const { user } = useUser() + const { workspace, refreshWorkspace } = useWorkspace() + const { showToast } = useToast() + const { data, mutate: refreshCurrentSubscriptionInfo } = + useCurrentSubscriptionInfo({ + stripeId: workspace?.stripeId, + plan: workspace?.plan, + }) + + const handlePayClick = async ({ + plan, + selectedChatsLimitIndex, + selectedStorageLimitIndex, + }: { + plan: 'STARTER' | 'PRO' + selectedChatsLimitIndex: number + selectedStorageLimitIndex: number + }) => { + if ( + !user || + !workspace || + selectedChatsLimitIndex === undefined || + selectedStorageLimitIndex === undefined + ) + return + await pay({ + stripeId: workspace.stripeId ?? undefined, + user, + plan, + workspaceId: workspace.id, + additionalChats: selectedChatsLimitIndex, + additionalStorage: selectedStorageLimitIndex, + }) + refreshCurrentSubscriptionInfo({ + additionalChatsIndex: selectedChatsLimitIndex, + additionalStorageIndex: selectedStorageLimitIndex, + }) + refreshWorkspace({ + plan, + additionalChatsIndex: selectedChatsLimitIndex, + additionalStorageIndex: selectedStorageLimitIndex, + }) + showToast({ + status: 'success', + description: `Workspace ${plan} plan successfully updated 🎉`, + }) + } + + return ( + + + + handlePayClick({ ...props, plan: Plan.STARTER }) + } + /> + + handlePayClick({ ...props, plan: Plan.PRO })} + /> + + + Need custom limits? Specific features?{' '} + + Let me know + + . + + + ) +} diff --git a/apps/builder/components/shared/ChangePlanForm/ProPlanContent.tsx b/apps/builder/components/shared/ChangePlanForm/ProPlanContent.tsx new file mode 100644 index 0000000000..0e778c0b1c --- /dev/null +++ b/apps/builder/components/shared/ChangePlanForm/ProPlanContent.tsx @@ -0,0 +1,339 @@ +import { + Stack, + Heading, + chakra, + HStack, + Menu, + MenuButton, + Button, + MenuList, + MenuItem, + Text, + Tooltip, + Flex, + Tag, +} from '@chakra-ui/react' +import { ChevronLeftIcon } from 'assets/icons' +import { useWorkspace } from 'contexts/WorkspaceContext' +import { Plan } from 'db' +import { useEffect, useState } from 'react' +import { + chatsLimit, + getChatsLimit, + getStorageLimit, + storageLimit, + parseNumberWithCommas, +} from 'utils' +import { MoreInfoTooltip } from '../MoreInfoTooltip' +import { FeaturesList } from './components/FeaturesList' +import { computePrice, formatPrice } from './helpers' + +type ProPlanContentProps = { + initialChatsLimitIndex?: number + initialStorageLimitIndex?: number + onPayClick: (props: { + selectedChatsLimitIndex: number + selectedStorageLimitIndex: number + }) => Promise +} + +export const ProPlanContent = ({ + initialChatsLimitIndex, + initialStorageLimitIndex, + onPayClick, +}: ProPlanContentProps) => { + const { workspace } = useWorkspace() + const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] = + useState() + const [selectedStorageLimitIndex, setSelectedStorageLimitIndex] = + useState() + const [isPaying, setIsPaying] = useState(false) + + useEffect(() => { + if ( + selectedChatsLimitIndex === undefined && + initialChatsLimitIndex !== undefined + ) + setSelectedChatsLimitIndex(initialChatsLimitIndex) + if ( + selectedStorageLimitIndex === undefined && + initialStorageLimitIndex !== undefined + ) + setSelectedStorageLimitIndex(initialStorageLimitIndex) + }, [ + initialChatsLimitIndex, + initialStorageLimitIndex, + selectedChatsLimitIndex, + selectedStorageLimitIndex, + ]) + + const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined + const workspaceStorageLimit = workspace + ? getStorageLimit(workspace) + : undefined + + console.log('workspaceChatsLimit', workspaceChatsLimit) + console.log('workspaceStorageLimit', workspace) + const isCurrentPlan = + chatsLimit[Plan.PRO].totalIncluded + + chatsLimit[Plan.PRO].increaseStep.amount * + (selectedChatsLimitIndex ?? 0) === + workspaceChatsLimit && + storageLimit[Plan.PRO].totalIncluded + + storageLimit[Plan.PRO].increaseStep.amount * + (selectedStorageLimitIndex ?? 0) === + workspaceStorageLimit + + const getButtonLabel = () => { + if ( + selectedChatsLimitIndex === undefined || + selectedStorageLimitIndex === undefined + ) + return '' + if (workspace?.plan === Plan.PRO) { + if (isCurrentPlan) return 'Your current plan' + + if ( + selectedChatsLimitIndex !== initialChatsLimitIndex || + selectedStorageLimitIndex !== initialStorageLimitIndex + ) + return 'Update' + } + return 'Upgrade' + } + + const handlePayClick = async () => { + if ( + selectedChatsLimitIndex === undefined || + selectedStorageLimitIndex === undefined + ) + return + setIsPaying(true) + await onPayClick({ + selectedChatsLimitIndex, + selectedStorageLimitIndex, + }) + setIsPaying(false) + } + + return ( + + + + Most popular + + + + + + Upgrade to Pro + + For agencies & growing startups. + + + + {formatPrice( + computePrice( + Plan.PRO, + selectedChatsLimitIndex ?? 0, + selectedStorageLimitIndex ?? 0 + ) ?? NaN + )} + / month + + + + } + hasArrow + placement="top" + > + + Everything in Starter + + + , plus: + + + + + } + size="sm" + isLoading={selectedChatsLimitIndex === undefined} + > + {parseNumberWithCommas( + chatsLimit.PRO.totalIncluded + + chatsLimit.PRO.increaseStep.amount * + (selectedChatsLimitIndex ?? 0) + )} + + + {selectedChatsLimitIndex !== 0 && ( + setSelectedChatsLimitIndex(0)}> + {parseNumberWithCommas(chatsLimit.PRO.totalIncluded)} + + )} + {selectedChatsLimitIndex !== 1 && ( + setSelectedChatsLimitIndex(1)}> + {parseNumberWithCommas( + chatsLimit.PRO.totalIncluded + + chatsLimit.PRO.increaseStep.amount + )} + + )} + {selectedChatsLimitIndex !== 2 && ( + setSelectedChatsLimitIndex(2)}> + {parseNumberWithCommas( + chatsLimit.PRO.totalIncluded + + chatsLimit.PRO.increaseStep.amount * 2 + )} + + )} + {selectedChatsLimitIndex !== 3 && ( + setSelectedChatsLimitIndex(3)}> + {parseNumberWithCommas( + chatsLimit.PRO.totalIncluded + + chatsLimit.PRO.increaseStep.amount * 3 + )} + + )} + {selectedChatsLimitIndex !== 4 && ( + setSelectedChatsLimitIndex(4)}> + {parseNumberWithCommas( + chatsLimit.PRO.totalIncluded + + chatsLimit.PRO.increaseStep.amount * 4 + )} + + )} + + {' '} + chats/mo + + + A chat is counted whenever a user starts a discussion. It is + independant of the number of messages he sends and receives. + + , + + + + } + size="sm" + isLoading={selectedStorageLimitIndex === undefined} + > + {parseNumberWithCommas( + storageLimit.PRO.totalIncluded + + storageLimit.PRO.increaseStep.amount * + (selectedStorageLimitIndex ?? 0) + )} + + + {selectedStorageLimitIndex !== 0 && ( + setSelectedStorageLimitIndex(0)} + > + {parseNumberWithCommas( + storageLimit.PRO.totalIncluded + )} + + )} + {selectedStorageLimitIndex !== 1 && ( + setSelectedStorageLimitIndex(1)} + > + {parseNumberWithCommas( + storageLimit.PRO.totalIncluded + + storageLimit.PRO.increaseStep.amount + )} + + )} + {selectedStorageLimitIndex !== 2 && ( + setSelectedStorageLimitIndex(2)} + > + {parseNumberWithCommas( + storageLimit.PRO.totalIncluded + + storageLimit.PRO.increaseStep.amount * 2 + )} + + )} + {selectedStorageLimitIndex !== 3 && ( + setSelectedStorageLimitIndex(3)} + > + {parseNumberWithCommas( + storageLimit.PRO.totalIncluded + + storageLimit.PRO.increaseStep.amount * 3 + )} + + )} + {selectedStorageLimitIndex !== 4 && ( + setSelectedStorageLimitIndex(4)} + > + {parseNumberWithCommas( + storageLimit.PRO.totalIncluded + + storageLimit.PRO.increaseStep.amount * 4 + )} + + )} + + {' '} + GB of storage + + + You accumulate storage for every file that your user upload + into your bot. If you delete the result, it will free up the + space. + + , + 'Custom domains', + 'In-depth analytics', + ]} + /> + + + + + ) +} diff --git a/apps/builder/components/shared/ChangePlanForm/StarterPlanContent.tsx b/apps/builder/components/shared/ChangePlanForm/StarterPlanContent.tsx new file mode 100644 index 0000000000..291a4f36d5 --- /dev/null +++ b/apps/builder/components/shared/ChangePlanForm/StarterPlanContent.tsx @@ -0,0 +1,280 @@ +import { + Stack, + Heading, + chakra, + HStack, + Menu, + MenuButton, + Button, + MenuList, + MenuItem, + Text, +} from '@chakra-ui/react' +import { ChevronLeftIcon } from 'assets/icons' +import { useWorkspace } from 'contexts/WorkspaceContext' +import { Plan } from 'db' +import { useEffect, useState } from 'react' +import { + chatsLimit, + getChatsLimit, + getStorageLimit, + storageLimit, + parseNumberWithCommas, +} from 'utils' +import { MoreInfoTooltip } from '../MoreInfoTooltip' +import { FeaturesList } from './components/FeaturesList' +import { computePrice, formatPrice } from './helpers' + +type StarterPlanContentProps = { + initialChatsLimitIndex?: number + initialStorageLimitIndex?: number + onPayClick: (props: { + selectedChatsLimitIndex: number + selectedStorageLimitIndex: number + }) => Promise +} + +export const StarterPlanContent = ({ + initialChatsLimitIndex, + initialStorageLimitIndex, + onPayClick, +}: StarterPlanContentProps) => { + const { workspace } = useWorkspace() + const [selectedChatsLimitIndex, setSelectedChatsLimitIndex] = + useState() + const [selectedStorageLimitIndex, setSelectedStorageLimitIndex] = + useState() + const [isPaying, setIsPaying] = useState(false) + + useEffect(() => { + if ( + selectedChatsLimitIndex === undefined && + initialChatsLimitIndex !== undefined + ) + setSelectedChatsLimitIndex(initialChatsLimitIndex) + if ( + selectedStorageLimitIndex === undefined && + initialStorageLimitIndex !== undefined + ) + setSelectedStorageLimitIndex(initialStorageLimitIndex) + }, [ + initialChatsLimitIndex, + initialStorageLimitIndex, + selectedChatsLimitIndex, + selectedStorageLimitIndex, + ]) + + const workspaceChatsLimit = workspace ? getChatsLimit(workspace) : undefined + const workspaceStorageLimit = workspace + ? getStorageLimit(workspace) + : undefined + + const isCurrentPlan = + chatsLimit[Plan.STARTER].totalIncluded + + chatsLimit[Plan.STARTER].increaseStep.amount * + (selectedChatsLimitIndex ?? 0) === + workspaceChatsLimit && + storageLimit[Plan.STARTER].totalIncluded + + storageLimit[Plan.STARTER].increaseStep.amount * + (selectedStorageLimitIndex ?? 0) === + workspaceStorageLimit + + const getButtonLabel = () => { + if ( + selectedChatsLimitIndex === undefined || + selectedStorageLimitIndex === undefined + ) + return '' + if (workspace?.plan === Plan.PRO) return 'Downgrade' + if (workspace?.plan === Plan.STARTER) { + if (isCurrentPlan) return 'Your current plan' + + if ( + selectedChatsLimitIndex !== initialChatsLimitIndex || + selectedStorageLimitIndex !== initialStorageLimitIndex + ) + return 'Update' + } + return 'Upgrade' + } + + const handlePayClick = async () => { + if ( + selectedChatsLimitIndex === undefined || + selectedStorageLimitIndex === undefined + ) + return + setIsPaying(true) + await onPayClick({ + selectedChatsLimitIndex, + selectedStorageLimitIndex, + }) + setIsPaying(false) + } + + return ( + + + + Upgrade to Starter + + For individuals & small businesses. + + {formatPrice( + computePrice( + Plan.STARTER, + selectedChatsLimitIndex ?? 0, + selectedStorageLimitIndex ?? 0 + ) ?? NaN + )} + / month + + + + + } + size="sm" + isLoading={selectedChatsLimitIndex === undefined} + > + {parseNumberWithCommas( + chatsLimit.STARTER.totalIncluded + + chatsLimit.STARTER.increaseStep.amount * + (selectedChatsLimitIndex ?? 0) + )} + + + {selectedChatsLimitIndex !== 0 && ( + setSelectedChatsLimitIndex(0)}> + {parseNumberWithCommas( + chatsLimit.STARTER.totalIncluded + )} + + )} + {selectedChatsLimitIndex !== 1 && ( + setSelectedChatsLimitIndex(1)}> + {parseNumberWithCommas( + chatsLimit.STARTER.totalIncluded + + chatsLimit.STARTER.increaseStep.amount + )} + + )} + {selectedChatsLimitIndex !== 2 && ( + setSelectedChatsLimitIndex(2)}> + {parseNumberWithCommas( + chatsLimit.STARTER.totalIncluded + + chatsLimit.STARTER.increaseStep.amount * 2 + )} + + )} + {selectedChatsLimitIndex !== 3 && ( + setSelectedChatsLimitIndex(3)}> + {parseNumberWithCommas( + chatsLimit.STARTER.totalIncluded + + chatsLimit.STARTER.increaseStep.amount * 3 + )} + + )} + {selectedChatsLimitIndex !== 4 && ( + setSelectedChatsLimitIndex(4)}> + {parseNumberWithCommas( + chatsLimit.STARTER.totalIncluded + + chatsLimit.STARTER.increaseStep.amount * 4 + )} + + )} + + {' '} + chats/mo + + + A chat is counted whenever a user starts a discussion. It is + independant of the number of messages he sends and receives. + + , + + + + } + size="sm" + isLoading={selectedStorageLimitIndex === undefined} + > + {parseNumberWithCommas( + storageLimit.STARTER.totalIncluded + + storageLimit.STARTER.increaseStep.amount * + (selectedStorageLimitIndex ?? 0) + )} + + + {selectedStorageLimitIndex !== 0 && ( + setSelectedStorageLimitIndex(0)}> + {parseNumberWithCommas( + storageLimit.STARTER.totalIncluded + )} + + )} + {selectedStorageLimitIndex !== 1 && ( + setSelectedStorageLimitIndex(1)}> + {parseNumberWithCommas( + storageLimit.STARTER.totalIncluded + + storageLimit.STARTER.increaseStep.amount + )} + + )} + {selectedStorageLimitIndex !== 2 && ( + setSelectedStorageLimitIndex(2)}> + {parseNumberWithCommas( + storageLimit.STARTER.totalIncluded + + storageLimit.STARTER.increaseStep.amount * 2 + )} + + )} + {selectedStorageLimitIndex !== 3 && ( + setSelectedStorageLimitIndex(3)}> + {parseNumberWithCommas( + storageLimit.STARTER.totalIncluded + + storageLimit.STARTER.increaseStep.amount * 3 + )} + + )} + {selectedStorageLimitIndex !== 4 && ( + setSelectedStorageLimitIndex(4)}> + {parseNumberWithCommas( + storageLimit.STARTER.totalIncluded + + storageLimit.STARTER.increaseStep.amount * 4 + )} + + )} + + {' '} + GB of storage + + + You accumulate storage for every file that your user upload into + your bot. If you delete the result, it will free up the space. + + , + 'Branding removed', + 'File upload input block', + 'Create folders', + ]} + /> + + + + ) +} diff --git a/apps/builder/components/shared/ChangePlanForm/components/FeaturesList.tsx b/apps/builder/components/shared/ChangePlanForm/components/FeaturesList.tsx new file mode 100644 index 0000000000..2ed9ab3b4a --- /dev/null +++ b/apps/builder/components/shared/ChangePlanForm/components/FeaturesList.tsx @@ -0,0 +1,21 @@ +import { + ListProps, + UnorderedList, + Flex, + ListItem, + ListIcon, +} from '@chakra-ui/react' +import { CheckIcon } from 'assets/icons' + +type FeaturesListProps = { features: (string | JSX.Element)[] } & ListProps + +export const FeaturesList = ({ features, ...props }: FeaturesListProps) => ( + + {features.map((feat, idx) => ( + + + {feat} + + ))} + +) diff --git a/apps/builder/components/shared/ChangePlanForm/helpers.ts b/apps/builder/components/shared/ChangePlanForm/helpers.ts new file mode 100644 index 0000000000..a9d05acb7d --- /dev/null +++ b/apps/builder/components/shared/ChangePlanForm/helpers.ts @@ -0,0 +1,86 @@ +import { Plan } from 'db' +import { chatsLimit, prices, storageLimit } from 'utils' + +export const computePrice = ( + plan: Plan, + selectedTotalChatsIndex: number, + selectedTotalStorageIndex: number +) => { + if (plan !== Plan.STARTER && plan !== Plan.PRO) return + const { + increaseStep: { price: chatsPrice }, + } = chatsLimit[plan] + const { + increaseStep: { price: storagePrice }, + } = storageLimit[plan] + return ( + prices[plan] + + selectedTotalChatsIndex * chatsPrice + + selectedTotalStorageIndex * storagePrice + ) +} + +const europeanUnionCountryCodes = [ + 'AT', + 'BE', + 'BG', + 'CY', + 'CZ', + 'DE', + 'DK', + 'EE', + 'ES', + 'FI', + 'FR', + 'GR', + 'HR', + 'HU', + 'IE', + 'IT', + 'LT', + 'LU', + 'LV', + 'MT', + 'NL', + 'PL', + 'PT', + 'RO', + 'SE', + 'SI', + 'SK', +] + +const europeanUnionExclusiveLanguageCodes = [ + 'fr', + 'de', + 'it', + 'el', + 'pl', + 'fi', + 'nl', + 'hr', + 'cs', + 'hu', + 'ro', + 'sl', + 'sv', + 'bg', +] + +export const guessIfUserIsEuropean = () => + navigator.languages.some((language) => { + const [languageCode, countryCode] = language.split('-') + return countryCode + ? europeanUnionCountryCodes.includes(countryCode) + : europeanUnionExclusiveLanguageCodes.includes(languageCode) + }) + +export const formatPrice = (price: number) => { + const isEuropean = guessIfUserIsEuropean() + const formatter = new Intl.NumberFormat(isEuropean ? 'fr-FR' : 'en-US', { + style: 'currency', + currency: isEuropean ? 'EUR' : 'USD', + maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) + }) + return formatter.format(price) +} diff --git a/apps/builder/components/shared/ChangePlanForm/index.ts b/apps/builder/components/shared/ChangePlanForm/index.ts new file mode 100644 index 0000000000..6d5a904477 --- /dev/null +++ b/apps/builder/components/shared/ChangePlanForm/index.ts @@ -0,0 +1 @@ +export { ChangePlanForm } from './ChangePlanForm' diff --git a/apps/builder/services/stripe.ts b/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx similarity index 50% rename from apps/builder/services/stripe.ts rename to apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx index 9fbaf5dba2..8f1e623be4 100644 --- a/apps/builder/services/stripe.ts +++ b/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx @@ -1,55 +1,61 @@ -import { Plan, User } from 'db' import { loadStripe } from '@stripe/stripe-js/pure' +import { Plan, User } from 'db' import { env, isDefined, isEmpty, sendRequest } from 'utils' +import { guessIfUserIsEuropean } from '../helpers' -type Props = { +type UpgradeProps = { user: User - customerId?: string - currency: 'usd' | 'eur' - plan: 'pro' | 'team' + stripeId?: string + plan: Plan workspaceId: string + additionalChats: number + additionalStorage: number } export const pay = async ({ - customerId, + stripeId, ...props -}: Props): Promise<{ newPlan: Plan } | undefined | void> => - isDefined(customerId) - ? updatePlan({ ...props, customerId }) +}: UpgradeProps): Promise<{ newPlan: Plan } | undefined | void> => + isDefined(stripeId) + ? updatePlan({ ...props, stripeId }) : redirectToCheckout(props) -const updatePlan = async ({ - customerId, +export const updatePlan = async ({ + stripeId, plan, workspaceId, - currency, -}: Omit): Promise<{ newPlan: Plan } | undefined> => { + additionalChats, + additionalStorage, +}: Omit): Promise<{ newPlan: Plan } | undefined> => { const { data, error } = await sendRequest<{ message: string }>({ - method: 'POST', - url: '/api/stripe/update-subscription', - body: { workspaceId, plan, customerId, currency }, + method: 'PUT', + url: '/api/stripe/subscription', + body: { workspaceId, plan, stripeId, additionalChats, additionalStorage }, }) if (error || !data) return - return { newPlan: plan === 'team' ? Plan.TEAM : Plan.PRO } + return { newPlan: plan } } -const redirectToCheckout = async ({ +export const redirectToCheckout = async ({ user, - currency, plan, workspaceId, -}: Omit) => { + additionalChats, + additionalStorage, +}: Omit) => { if (isEmpty(env('STRIPE_PUBLIC_KEY'))) throw new Error('NEXT_PUBLIC_STRIPE_PUBLIC_KEY is missing in env') const { data, error } = await sendRequest<{ sessionId: string }>({ method: 'POST', - url: '/api/stripe/checkout', + url: '/api/stripe/subscription', body: { email: user.email, - currency, + currency: guessIfUserIsEuropean() ? 'eur' : 'usd', plan, workspaceId, href: location.origin + location.pathname, + additionalChats, + additionalStorage, }, }) if (error || !data) return diff --git a/apps/builder/components/shared/ChangePlanForm/queries/useCurrentSubscriptionInfo.ts b/apps/builder/components/shared/ChangePlanForm/queries/useCurrentSubscriptionInfo.ts new file mode 100644 index 0000000000..4af314e9cb --- /dev/null +++ b/apps/builder/components/shared/ChangePlanForm/queries/useCurrentSubscriptionInfo.ts @@ -0,0 +1,30 @@ +import { Plan } from 'db' +import { fetcher } from 'services/utils' +import useSWR from 'swr' + +export const useCurrentSubscriptionInfo = ({ + stripeId, + plan, +}: { + stripeId?: string | null + plan?: Plan +}) => { + const { data, mutate } = useSWR< + { + additionalChatsIndex: number + additionalStorageIndex: number + }, + Error + >( + stripeId && (plan === Plan.STARTER || plan === Plan.PRO) + ? `/api/stripe/subscription?stripeId=${stripeId}` + : null, + fetcher + ) + return { + data: !stripeId + ? { additionalChatsIndex: 0, additionalStorageIndex: 0 } + : data, + mutate, + } +} diff --git a/apps/builder/components/shared/Info.tsx b/apps/builder/components/shared/Info.tsx index 69c4e9f175..9e6ad18938 100644 --- a/apps/builder/components/shared/Info.tsx +++ b/apps/builder/components/shared/Info.tsx @@ -7,10 +7,9 @@ import { Text, useDisclosure, } from '@chakra-ui/react' -import { Plan } from 'db' import React from 'react' -import { UpgradeModal } from './modals/UpgradeModal' -import { LimitReached } from './modals/UpgradeModal/UpgradeModal' +import { ChangePlanModal } from './modals/ChangePlanModal' +import { LimitReached } from './modals/ChangePlanModal' export const Info = (props: AlertProps) => ( @@ -27,30 +26,34 @@ export const UnlockPlanInfo = ({ contentLabel, buttonLabel = 'More info', type, - plan = Plan.PRO, + ...props }: { - contentLabel: string + contentLabel: React.ReactNode buttonLabel?: string type?: LimitReached - plan: Plan -}) => { +} & AlertProps) => { const { isOpen, onOpen, onClose } = useDisclosure() return ( {contentLabel} - - + ) } diff --git a/apps/builder/components/shared/MoreInfoTooltip.tsx b/apps/builder/components/shared/MoreInfoTooltip.tsx index d5098b7fb2..d4969e3137 100644 --- a/apps/builder/components/shared/MoreInfoTooltip.tsx +++ b/apps/builder/components/shared/MoreInfoTooltip.tsx @@ -8,7 +8,7 @@ type Props = { export const MoreInfoTooltip = ({ children }: Props) => { return ( - + diff --git a/apps/builder/components/shared/PlanTag.tsx b/apps/builder/components/shared/PlanTag.tsx new file mode 100644 index 0000000000..d56972a66e --- /dev/null +++ b/apps/builder/components/shared/PlanTag.tsx @@ -0,0 +1,30 @@ +import { Tag } from '@chakra-ui/react' +import { Plan } from 'db' + +export const PlanTag = ({ plan }: { plan?: Plan }) => { + switch (plan) { + case Plan.LIFETIME: + case Plan.PRO: { + return ( + + Pro + + ) + } + case Plan.OFFERED: + case Plan.STARTER: { + return ( + + Starter + + ) + } + default: { + return ( + + Free + + ) + } + } +} diff --git a/apps/builder/components/shared/buttons/PublishButton.tsx b/apps/builder/components/shared/buttons/PublishButton.tsx index 16dcdc876a..25da59a849 100644 --- a/apps/builder/components/shared/buttons/PublishButton.tsx +++ b/apps/builder/components/shared/buttons/PublishButton.tsx @@ -15,14 +15,12 @@ import { import { ChevronLeftIcon } from 'assets/icons' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useWorkspace } from 'contexts/WorkspaceContext' -import { Plan } from 'db' import { InputBlockType } from 'models' import { useRouter } from 'next/router' import { timeSince } from 'services/utils' import { isFreePlan } from 'services/workspace' import { isNotDefined } from 'utils' -import { UpgradeModal } from '../modals/UpgradeModal' -import { LimitReached } from '../modals/UpgradeModal/UpgradeModal' +import { LimitReached, ChangePlanModal } from '../modals/ChangePlanModal' export const PublishButton = (props: ButtonProps) => { const { workspace } = useWorkspace() @@ -50,8 +48,7 @@ export const PublishButton = (props: ButtonProps) => { return ( - { +export const UpgradeButton = ({ type, ...props }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure() const { workspace } = useWorkspace() return ( @@ -19,7 +18,7 @@ export const UpgradeButton = ({ type, plan = Plan.PRO, ...props }: Props) => { onClick={onOpen} > {props.children ?? 'Upgrade'} - + ) } diff --git a/apps/builder/components/shared/buttons/UploadButton.tsx b/apps/builder/components/shared/buttons/UploadButton.tsx index d9de00f5a8..8dcbefb512 100644 --- a/apps/builder/components/shared/buttons/UploadButton.tsx +++ b/apps/builder/components/shared/buttons/UploadButton.tsx @@ -29,7 +29,7 @@ export const UploadButton = ({ }, ], }) - if (urls.length) onFileUploaded(urls[0]) + if (urls.length && urls[0]) onFileUploaded(urls[0]) setIsUploading(false) } diff --git a/apps/builder/components/shared/hooks/useToast.tsx b/apps/builder/components/shared/hooks/useToast.tsx index 6e05979f21..c260095a60 100644 --- a/apps/builder/components/shared/hooks/useToast.tsx +++ b/apps/builder/components/shared/hooks/useToast.tsx @@ -7,12 +7,14 @@ export const useToast = () => { title, description, status = 'error', + ...props }: UseToastOptions) => { toast({ position: 'bottom-right', description, title, status, + ...props, }) } diff --git a/apps/builder/components/shared/modals/ChangePlanModal.tsx b/apps/builder/components/shared/modals/ChangePlanModal.tsx new file mode 100644 index 0000000000..0a2e7e523b --- /dev/null +++ b/apps/builder/components/shared/modals/ChangePlanModal.tsx @@ -0,0 +1,53 @@ +import { + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalOverlay, + Stack, + Button, + HStack, +} from '@chakra-ui/react' +import { Info } from 'components/shared/Info' +import { ChangePlanForm } from 'components/shared/ChangePlanForm' + +export enum LimitReached { + BRAND = 'remove branding', + CUSTOM_DOMAIN = 'add custom domain', + FOLDER = 'create folders', + FILE_INPUT = 'use file input blocks', +} + +type ChangePlanModalProps = { + type?: LimitReached + isOpen: boolean + onClose: () => void +} + +export const ChangePlanModal = ({ + onClose, + isOpen, + type, +}: ChangePlanModalProps) => { + return ( + + + + + {type && ( + You need to upgrade your plan in order to {type} + )} + + + + + + + + + + + ) +} diff --git a/apps/builder/components/shared/modals/UpgradeModal/ActionButton.tsx b/apps/builder/components/shared/modals/UpgradeModal/ActionButton.tsx deleted file mode 100644 index 438d10474d..0000000000 --- a/apps/builder/components/shared/modals/UpgradeModal/ActionButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Button, ButtonProps } from '@chakra-ui/react' -import * as React from 'react' - -export const ActionButton = (props: ButtonProps) => ( - - - - - - - ) -} - -const PersonalProPlanContent = ({ - currency, - type, -}: { - currency: 'eur' | 'usd' - type?: LimitReached -}) => { - return ( - - You need to upgrade your plan in order to {type} - - - Upgrade to Personal Pro{' '} - plan - - For solo creators who want to do even more. - - {currency === 'eur' ? '39€' : '$39'} - / month - - Everything in Personal, plus: - - - ) -} - -const TeamPlanContent = ({ - currency, - type, -}: { - currency: 'eur' | 'usd' - type?: LimitReached -}) => { - return ( - - You need to upgrade your plan in order to {type} - - - Upgrade to Team plan - - For teams to build typebots together in one spot. - - {currency === 'eur' ? '99€' : '$99'} - / month - - - - } - hasArrow - placement="top" - > - - Everything in Pro - - - , plus: - - - - ) -} - -const FeatureList = ({ - features, - ...props -}: { features: string[] } & ListProps) => ( - - {features.map((feat) => ( - - - {feat} - - ))} - -) diff --git a/apps/builder/components/shared/modals/UpgradeModal/index.tsx b/apps/builder/components/shared/modals/UpgradeModal/index.tsx deleted file mode 100644 index e8b972d56f..0000000000 --- a/apps/builder/components/shared/modals/UpgradeModal/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { UpgradeModal } from './UpgradeModal' diff --git a/apps/builder/components/shared/modals/index.ts b/apps/builder/components/shared/modals/index.ts new file mode 100644 index 0000000000..a33f48a9c4 --- /dev/null +++ b/apps/builder/components/shared/modals/index.ts @@ -0,0 +1 @@ +export { ChangePlanModal } from './ChangePlanModal' diff --git a/apps/builder/contexts/ResultsProvider.tsx b/apps/builder/contexts/ResultsProvider.tsx index 3c963332cb..683e0917a9 100644 --- a/apps/builder/contexts/ResultsProvider.tsx +++ b/apps/builder/contexts/ResultsProvider.tsx @@ -15,7 +15,6 @@ const resultsContext = createContext<{ hasMore: boolean resultHeader: ResultHeaderCell[] totalResults: number - totalHiddenResults?: number tableData: TableData[] onDeleteResults: (totalResultsDeleted: number) => void fetchMore: () => void @@ -33,14 +32,12 @@ export const ResultsProvider = ({ workspaceId, typebotId, totalResults, - totalHiddenResults, onDeleteResults, }: { children: ReactNode workspaceId: string typebotId: string totalResults: number - totalHiddenResults?: number onDeleteResults: (totalResultsDeleted: number) => void }) => { const { publishedTypebot, linkedTypebots } = useTypebot() @@ -84,7 +81,6 @@ export const ResultsProvider = ({ tableData, resultHeader, totalResults, - totalHiddenResults, onDeleteResults, fetchMore, mutate, diff --git a/apps/builder/libs/theme.ts b/apps/builder/libs/theme.ts index 69b6aca17d..a7bfe83d3c 100644 --- a/apps/builder/libs/theme.ts +++ b/apps/builder/libs/theme.ts @@ -98,6 +98,12 @@ const components = { }, }, }, + Tooltip: { + defaultProps: { + rounded: 'md', + hasArrow: true, + }, + }, } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/builder/package.json b/apps/builder/package.json index 5db0db69f9..b8fdbe6b3f 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -9,7 +9,7 @@ "start": "next start", "lint": "next lint", "test": "pnpm playwright test", - "test:open": "PWDEBUG=1 pnpm playwright test" + "test:open": "NO_RETRIES=1 pnpm playwright test --debug" }, "dependencies": { "@chakra-ui/css-reset": "2.0.7", diff --git a/apps/builder/pages/_document.tsx b/apps/builder/pages/_document.tsx index 486cf9fee8..ca2bcfbad7 100644 --- a/apps/builder/pages/_document.tsx +++ b/apps/builder/pages/_document.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-sync-scripts */ import Document, { Html, Head, @@ -22,7 +23,6 @@ class MyDocument extends Document { rel="stylesheet" /> - {/* eslint-disable-next-line @next/next/no-sync-scripts */}