From 95b7a6fb8c51f9a17681cd1362bdced7034ec8d8 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Thu, 28 Sep 2023 20:40:23 +0530 Subject: [PATCH 1/7] fix: action class creation in frontend fails (#867) --- .../(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx index e8cb2db5c8..ff08847f2e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx @@ -79,6 +79,7 @@ export default function AddNoCodeActionModal({ const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig); const updatedData: TActionClassInput = { ...data, + environmentId, noCodeConfig: filteredNoCodeConfig, type: "noCode", } as TActionClassInput; From 48d8fc6aca4fd69bd4e519430ac426771b24dbe6 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Sat, 30 Sep 2023 14:31:46 +0530 Subject: [PATCH 2/7] feat: add new pricing table to formbricks-com (#869) * init: pricing page * fix: ux changes --- .../components/shared/Header.tsx | 2 +- .../components/shared/OpenSourceInfo.tsx | 35 ++ .../components/shared/PricingCalculator.tsx | 115 +++++ .../components/shared/PricingGetStarted.tsx | 47 ++ .../components/shared/PricingTable.tsx | 103 +++++ .../components/shared/Slider.tsx | 24 + .../components/shared/icons/XCircleIcon.jsx | 13 + apps/formbricks-com/package.json | 2 + apps/formbricks-com/pages/index.tsx | 2 - apps/formbricks-com/pages/pricing.tsx | 147 +++++++ .../ui/components/icons/CrossMarkIcon.tsx | 1 - pnpm-lock.yaml | 414 ++++++++++-------- 12 files changed, 723 insertions(+), 182 deletions(-) create mode 100644 apps/formbricks-com/components/shared/OpenSourceInfo.tsx create mode 100644 apps/formbricks-com/components/shared/PricingCalculator.tsx create mode 100644 apps/formbricks-com/components/shared/PricingGetStarted.tsx create mode 100644 apps/formbricks-com/components/shared/PricingTable.tsx create mode 100644 apps/formbricks-com/components/shared/Slider.tsx create mode 100644 apps/formbricks-com/components/shared/icons/XCircleIcon.jsx create mode 100644 apps/formbricks-com/pages/pricing.tsx diff --git a/apps/formbricks-com/components/shared/Header.tsx b/apps/formbricks-com/components/shared/Header.tsx index 76b57ee726..65ef9c71c9 100644 --- a/apps/formbricks-com/components/shared/Header.tsx +++ b/apps/formbricks-com/components/shared/Header.tsx @@ -250,7 +250,7 @@ export default function Header() { */} Pricing diff --git a/apps/formbricks-com/components/shared/OpenSourceInfo.tsx b/apps/formbricks-com/components/shared/OpenSourceInfo.tsx new file mode 100644 index 0000000000..11df9246c1 --- /dev/null +++ b/apps/formbricks-com/components/shared/OpenSourceInfo.tsx @@ -0,0 +1,35 @@ +import { Button } from "@formbricks/ui"; + +export const OpenSourceInfo = () => { + return ( +
+
+
+

+ Open Source +

+ +

+ Formbricks is an open source project. You can self-host it for free. We provide multiple easy + deployment options as per your customisation needs. We have documented the process of self-hosting + Formbricks on your own server using Docker, Bash Scripting, and Building from Source. +

+
+ + +
+
+
+
+ ); +}; diff --git a/apps/formbricks-com/components/shared/PricingCalculator.tsx b/apps/formbricks-com/components/shared/PricingCalculator.tsx new file mode 100644 index 0000000000..03972ac30f --- /dev/null +++ b/apps/formbricks-com/components/shared/PricingCalculator.tsx @@ -0,0 +1,115 @@ +import { Slider } from "@/components/shared/Slider"; +import { useState } from "react"; + +const ProductItem = ({ label, usersCount, price, onSliderChange }) => ( +
+
+
+ {label} +
+
+ {Math.round(usersCount).toLocaleString()} MTU +
+
+ ${price.toFixed(2)} +
+
+
+ +
+ {[3, 4, 5, 6].map((mark) => ( + + {mark === 3 ? "1K" : mark === 4 ? "10K" : mark === 5 ? "100K" : "1M"} + + ))} +
+
+
+); + +const Headers = () => ( +
+

Product

+

+ Subtotal +

+
+); + +const MonthlyEstimate = ({ price }) => ( +
+ + Monthly estimate: + +
+ + ${price.toFixed(2)} + + + {" "} + / month + +
+
+); + +export const PricingCalculator = () => { + const [inProductSlider, setInProductSlider] = useState(Math.log10(1000)); + const [linkSlider, setLinkSlider] = useState(Math.log10(1000)); + + const transformToLog = (value) => Math.pow(10, value); + + const calculatePrice = (users) => { + if (users <= 5000) { + return 0; + } else { + return users * 0.005; + } + }; + + const usersCountForInProductSlider = transformToLog(inProductSlider); + const productSurveysPrice = calculatePrice(usersCountForInProductSlider); + + return ( +
+

+ Pricing Calculator +

+ +
+
+ + +
+ + setInProductSlider(value[0])} + /> + +
+ + setLinkSlider(value[0])} + /> + +
+ + +
+
+
+ ); +}; diff --git a/apps/formbricks-com/components/shared/PricingGetStarted.tsx b/apps/formbricks-com/components/shared/PricingGetStarted.tsx new file mode 100644 index 0000000000..d7774475d1 --- /dev/null +++ b/apps/formbricks-com/components/shared/PricingGetStarted.tsx @@ -0,0 +1,47 @@ +import { Button } from "@formbricks/ui"; + +export const GetStartedWithPricing = ({ showDetailed }: { showDetailed: boolean }) => { + return ( + <> +
+
+
+

Free

+ + {showDetailed && ( +

+ General free usage on every product. Best for early stage startups and hobbyists +

+ )} + + +
+
+

Paid

+ {showDetailed && ( +

+ Formbricks with the next-generation features, Pay only for the tracked users. +

+ )} + + +
+
+
+ + ); +}; diff --git a/apps/formbricks-com/components/shared/PricingTable.tsx b/apps/formbricks-com/components/shared/PricingTable.tsx new file mode 100644 index 0000000000..54311b1c3a --- /dev/null +++ b/apps/formbricks-com/components/shared/PricingTable.tsx @@ -0,0 +1,103 @@ +import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@formbricks/ui"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; + +export const PricingTable = ({ leadRow, pricing, endRow }) => { + return ( +
+
+
+
+ {leadRow.title} +
+
+ {leadRow.free} +
+ +
+ {leadRow.paid} +
+
+
+ +
+ {pricing.map((feature) => ( +
+
+ {feature.name} + {feature.addOnText && ( + + Addon + + )} +
+
+ {feature.addOnText ? ( + + + + {feature.free} + + +

+ {feature.addOnText} +

+
+
+
+ ) : feature.free ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ {feature.addOnText ? ( + + + + {feature.paid} + + +

+ {feature.addOnText} +

+
+
+
+ ) : feature.paid ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ ))} +
+ +
+
+
+ {endRow.title} +
+
+ {endRow.free} +
+ +
+ {endRow.paid} +
+
+
+
+ ); +}; diff --git a/apps/formbricks-com/components/shared/Slider.tsx b/apps/formbricks-com/components/shared/Slider.tsx new file mode 100644 index 0000000000..07bce61744 --- /dev/null +++ b/apps/formbricks-com/components/shared/Slider.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +import { cn } from "@formbricks/lib/cn"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/formbricks-com/components/shared/icons/XCircleIcon.jsx b/apps/formbricks-com/components/shared/icons/XCircleIcon.jsx new file mode 100644 index 0000000000..d1573c7733 --- /dev/null +++ b/apps/formbricks-com/components/shared/icons/XCircleIcon.jsx @@ -0,0 +1,13 @@ + + +; diff --git a/apps/formbricks-com/package.json b/apps/formbricks-com/package.json index 1101313dfd..36de8e409a 100644 --- a/apps/formbricks-com/package.json +++ b/apps/formbricks-com/package.json @@ -47,6 +47,8 @@ "node-fetch": "^3.3.2", "prism-react-renderer": "^2.1.0", "prismjs": "^1.29.0", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.6", "react": "18.2.0", "react-dom": "18.2.0", "react-highlight-words": "^0.20.0", diff --git a/apps/formbricks-com/pages/index.tsx b/apps/formbricks-com/pages/index.tsx index c21f7950a1..c4b8bc18dd 100644 --- a/apps/formbricks-com/pages/index.tsx +++ b/apps/formbricks-com/pages/index.tsx @@ -4,7 +4,6 @@ import Features from "@/components/home/Features"; import Highlights from "@/components/home/Highlights"; import BreakerCTA from "@/components/shared/BreakerCTA"; import Steps from "@/components/home/Steps"; -import Pricing from "@/components/shared/Pricing"; import GitHubSponsorship from "@/components/home/GitHubSponsorship"; import BestPractices from "@/components/shared/BestPractices"; @@ -42,7 +41,6 @@ const IndexPage = () => ( href="https://app.formbricks.com/auth/signup" inverted /> - ); diff --git a/apps/formbricks-com/pages/pricing.tsx b/apps/formbricks-com/pages/pricing.tsx new file mode 100644 index 0000000000..b4969a6d6d --- /dev/null +++ b/apps/formbricks-com/pages/pricing.tsx @@ -0,0 +1,147 @@ +import HeroTitle from "@/components/shared/HeroTitle"; +import Layout from "@/components/shared/Layout"; +import { PricingTable } from "../components/shared/PricingTable"; +import { PricingCalculator } from "../components/shared/PricingCalculator"; +import { GetStartedWithPricing } from "@/components/shared/PricingGetStarted"; +import { OpenSourceInfo } from "@/components/shared/OpenSourceInfo"; + +const inProductSurveys = { + leadRow: { + title: "In-Product Surveys", + free: ( +
+ 5000 tracked users /mo{" "} +
+ ), + paid: "Unlimited", + }, + features: [ + { name: "Unlimited Surveys", free: true, paid: true }, + { name: "Granular Targeting", free: true, paid: true }, + { name: "30+ Templates", free: true, paid: true }, + { name: "API Access", free: true, paid: true }, + { name: "Third-Party Integrations", free: true, paid: true }, + { name: "Unlimited Team Members", free: true, paid: true }, + { name: "Unlimited Responses per Survey", free: true, paid: true }, + { name: "Advanced User Targeting", free: false, paid: true }, + { name: "Multi Language", free: false, paid: true }, + + { + name: "Custom URL for Link Surveys", + free: "10$/mo", + paid: "10$/mo", + addOnText: "Free if you self-host", + }, + { + name: "Remove Formbricks Branding", + free: "10$/mo", + paid: "10$/mo", + addOnText: "Free if you self-host", + }, + ], + endRow: { + title: "In-Product Surveys Pricing", + free: "Free", + paid: ( +
+ Free up to 5000 tracked users/mo, then + $0.005 + / tracked user +
+ ), + }, +}; + +const linkSurveys = { + leadRow: { + title: "Link Surveys", + free: Unlimited, + paid: "Unlimited", + }, + + features: [ + { name: "Unlimited Surveys", free: true, paid: true }, + { name: "Unlimited Responses", free: true, paid: true }, + { name: "Partial Submissions", free: true, paid: true }, + { name: "⚙️ URL Shortener", free: true, paid: true }, + { name: "⚙️ Recall Information", free: true, paid: true }, + { name: "⚙️ Hidden Field Questions", free: true, paid: true }, + { name: "⚙️ Time to Complete Metadata", free: true, paid: true }, + { name: "⚙️ File Upload", free: true, paid: true }, + { name: "⚙️ Signature Question", free: true, paid: true }, + { name: "⚙️ Question Grouping", free: true, paid: true }, + { name: "⚙️ Add Media to Questions", free: true, paid: true }, + ], + + endRow: { + title: "Link Surveys Pricing", + free: "Free", + paid: "Free", + }, +}; + +const integrations = { + leadRow: { + title: "Integrations", + free: Unlimited, + paid: "Unlimited", + }, + features: [ + { name: "Webhooks", free: true, paid: true }, + { name: "Zapier", free: true, paid: true }, + { name: "Google Sheets", free: true, paid: true }, + { name: "n8n", free: true, paid: true }, + { name: "Make", free: true, paid: true }, + ], + endRow: { + title: "Integrations Pricing", + free: "Free", + paid: "Free", + }, +}; + +const PricingPage = () => { + return ( + + + + + + +
+ + +
+ + + +
+ + + + + + +
+ ); +}; + +export default PricingPage; diff --git a/packages/ui/components/icons/CrossMarkIcon.tsx b/packages/ui/components/icons/CrossMarkIcon.tsx index 62b9606985..570eb4190c 100644 --- a/packages/ui/components/icons/CrossMarkIcon.tsx +++ b/packages/ui/components/icons/CrossMarkIcon.tsx @@ -2,7 +2,6 @@ export const CrossMarkIcon: React.FC> = (props) => return ( - 6.6.0' dependencies: eslint: 8.50.0 - eslint-plugin-turbo: 1.8.8(eslint@8.50.0) + eslint-plugin-turbo: 1.10.3(eslint@8.50.0) dev: true /eslint-import-resolver-node@0.3.9: @@ -10740,8 +10797,8 @@ packages: semver: 6.3.1 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.8.8(eslint@8.50.0): - resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} + /eslint-plugin-turbo@1.10.3(eslint@8.50.0): + resolution: {integrity: sha512-g3Mnnk7el1FqxHfqbE/MayLvCsYjA/vKmAnUj66kV4AlM7p/EZqdt42NMcMSKtDVEm0w+utQkkzWG2Xsa0Pd/g==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -21866,64 +21923,65 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.10.13: - resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==} + /turbo-darwin-64@1.10.3: + resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.13: - resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==} + /turbo-darwin-arm64@1.10.3: + resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.13: - resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==} + /turbo-linux-64@1.10.3: + resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.13: - resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==} + /turbo-linux-arm64@1.10.3: + resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.13: - resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==} + /turbo-windows-64@1.10.3: + resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.13: - resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==} + /turbo-windows-arm64@1.10.3: + resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.13: - resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==} + /turbo@1.10.3: + resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==} hasBin: true + requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.13 - turbo-darwin-arm64: 1.10.13 - turbo-linux-64: 1.10.13 - turbo-linux-arm64: 1.10.13 - turbo-windows-64: 1.10.13 - turbo-windows-arm64: 1.10.13 + turbo-darwin-64: 1.10.3 + turbo-darwin-arm64: 1.10.3 + turbo-linux-64: 1.10.3 + turbo-linux-arm64: 1.10.3 + turbo-windows-64: 1.10.3 + turbo-windows-arm64: 1.10.3 dev: true /tween-functions@1.2.0: From 0225362a92a8adfff8e1eabbd7072a9e69dc97f1 Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:52:42 +0530 Subject: [PATCH 3/7] feat: Add caching to more services (survey, environment, team, profile) (#835) * feat: caching in surveys * fix: fixes unstable_cache date parsing error * fix: fixes survey revalidation in displays and responses * fix: fixes survey cache * fix: adds comments * fix: response cache tag * fix cache validation and tag naming * move TSurveysWithAnalytics to TSurveys * add caching to more services --------- Co-authored-by: Matthias Nannt --- apps/web/app/(app)/FormbricksClient.tsx | 17 +- apps/web/app/(app)/PosthogIdentify.tsx | 3 - .../surveys/SurveyDropDownMenu.tsx | 4 +- .../[environmentId]/surveys/SurveyList.tsx | 18 +- .../app/api/auth/[...nextauth]/authOptions.ts | 32 +- apps/web/app/api/v1/js/surveys.ts | 6 +- apps/web/app/api/v1/js/sync/lib/sync.ts | 4 +- .../shared/WidgetStatusIndicator.tsx | 2 +- packages/lib/services/actionClass.ts | 5 +- packages/lib/services/attributeClass.ts | 3 +- packages/lib/services/displays.ts | 6 + packages/lib/services/environment.ts | 146 ++++--- packages/lib/services/person.ts | 4 +- packages/lib/services/product.ts | 51 ++- packages/lib/services/profile.ts | 105 +++-- packages/lib/services/response.ts | 11 + packages/lib/services/survey.ts | 402 +++++++++++------- packages/lib/services/team.ts | 155 ++++--- 18 files changed, 586 insertions(+), 388 deletions(-) diff --git a/apps/web/app/(app)/FormbricksClient.tsx b/apps/web/app/(app)/FormbricksClient.tsx index ebc694e2da..6289105b52 100644 --- a/apps/web/app/(app)/FormbricksClient.tsx +++ b/apps/web/app/(app)/FormbricksClient.tsx @@ -7,7 +7,6 @@ import { useEffect } from "react"; type UsageAttributesUpdaterProps = { numSurveys: number; - totalSubmissions: number; }; export default function FormbricksClient({ session }) { @@ -19,31 +18,23 @@ export default function FormbricksClient({ session }) { }); formbricks.setUserId(session.user.id); formbricks.setEmail(session.user.email); - if (session.user.teams?.length > 0) { - formbricks.setAttribute("Plan", session.user.teams[0].plan); - formbricks.setAttribute("Name", session?.user?.name); - } } }, [session]); return null; } -const updateUsageAttributes = (numSurveys, totalSubmissions) => { +const updateUsageAttributes = (numSurveys) => { if (!formbricksEnabled) return; if (numSurveys >= 3) { formbricks.setAttribute("HasThreeSurveys", "true"); } - - if (totalSubmissions >= 20) { - formbricks.setAttribute("HasTwentySubmissions", "true"); - } }; -export function UsageAttributesUpdater({ numSurveys, totalSubmissions }: UsageAttributesUpdaterProps) { +export function UsageAttributesUpdater({ numSurveys }: UsageAttributesUpdaterProps) { useEffect(() => { - updateUsageAttributes(numSurveys, totalSubmissions); - }, [numSurveys, totalSubmissions]); + updateUsageAttributes(numSurveys); + }, [numSurveys]); return null; } diff --git a/apps/web/app/(app)/PosthogIdentify.tsx b/apps/web/app/(app)/PosthogIdentify.tsx index b022fb4056..dc055535d5 100644 --- a/apps/web/app/(app)/PosthogIdentify.tsx +++ b/apps/web/app/(app)/PosthogIdentify.tsx @@ -12,9 +12,6 @@ export default function PosthogIdentify({ session }: { session: Session }) { useEffect(() => { if (posthogEnabled && session.user && posthog) { posthog.identify(session.user.id); - if (session.user.teams?.length > 0) { - posthog?.group("team", session.user.teams[0].id); - } } }, [session, posthog]); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx index 0e711af727..3df34eca02 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu.tsx @@ -15,7 +15,7 @@ import { } from "@/components/shared/DropdownMenu"; import LoadingSpinner from "@/components/shared/LoadingSpinner"; import type { TEnvironment } from "@formbricks/types/v1/environment"; -import type { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; +import type { TSurvey } from "@formbricks/types/v1/surveys"; import { ArrowUpOnSquareStackIcon, DocumentDuplicateIcon, @@ -32,7 +32,7 @@ import toast from "react-hot-toast"; interface SurveyDropDownMenuProps { environmentId: string; - survey: TSurveyWithAnalytics; + survey: TSurvey; environment: TEnvironment; otherEnvironment: TEnvironment; surveyBaseUrl: string; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx index 5058fbb62a..5665ec320b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx @@ -2,15 +2,14 @@ import { UsageAttributesUpdater } from "@/app/(app)/FormbricksClient"; import SurveyDropDownMenu from "@/app/(app)/environments/[environmentId]/surveys/SurveyDropDownMenu"; import SurveyStarter from "@/app/(app)/environments/[environmentId]/surveys/SurveyStarter"; import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator"; +import { SURVEY_BASE_URL } from "@formbricks/lib/constants"; import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getSurveysWithAnalytics } from "@formbricks/lib/services/survey"; +import { getSurveys } from "@formbricks/lib/services/survey"; import type { TEnvironment } from "@formbricks/types/v1/environment"; -import type { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Badge } from "@formbricks/ui"; import { ComputerDesktopIcon, LinkIcon, PlusIcon } from "@heroicons/react/24/solid"; import Link from "next/link"; -import { SURVEY_BASE_URL } from "@formbricks/lib/constants"; export default async function SurveysList({ environmentId }: { environmentId: string }) { const product = await getProductByEnvironmentId(environmentId); @@ -22,10 +21,10 @@ export default async function SurveysList({ environmentId }: { environmentId: st if (!environment) { throw new Error("Environment not found"); } - const surveys: TSurveyWithAnalytics[] = await getSurveysWithAnalytics(environmentId); + const surveys = await getSurveys(environmentId); + const environments: TEnvironment[] = await getEnvironments(product.id); const otherEnvironment = environments.find((e) => e.type !== environment.type)!; - const totalSubmissions = surveys.reduce((acc, survey) => acc + (survey.analytics?.numResponses || 0), 0); if (surveys.length === 0) { return ; @@ -45,7 +44,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st {surveys - .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()) + .sort((a, b) => b.updatedAt?.getTime() - a.updatedAt?.getTime()) .map((survey) => (
  • @@ -82,9 +81,6 @@ export default async function SurveysList({ environmentId }: { environmentId: st tooltip environmentId={environmentId} /> -

    - {survey.analytics.numResponses} responses -

    )} {survey.status === "draft" && ( @@ -93,7 +89,7 @@ export default async function SurveysList({ environmentId }: { environmentId: st
    ))} - + ); } diff --git a/apps/web/app/api/auth/[...nextauth]/authOptions.ts b/apps/web/app/api/auth/[...nextauth]/authOptions.ts index 3c61f999d4..0351fb872a 100644 --- a/apps/web/app/api/auth/[...nextauth]/authOptions.ts +++ b/apps/web/app/api/auth/[...nextauth]/authOptions.ts @@ -1,8 +1,9 @@ import { env } from "@/env.mjs"; import { verifyPassword } from "@/lib/auth"; -import { verifyToken } from "@formbricks/lib/jwt"; import { prisma } from "@formbricks/database"; import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { verifyToken } from "@formbricks/lib/jwt"; +import { getProfileByEmail } from "@formbricks/lib/services/profile"; import type { IdentityProvider } from "@prisma/client"; import type { NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; @@ -133,42 +134,16 @@ export const authOptions: NextAuthOptions = { ], callbacks: { async jwt({ token }) { - const existingUser = await prisma.user.findFirst({ - where: { email: token.email! }, - select: { - id: true, - createdAt: true, - onboardingCompleted: true, - memberships: { - select: { - teamId: true, - role: true, - team: { - select: { - plan: true, - }, - }, - }, - }, - name: true, - }, - }); + const existingUser = await getProfileByEmail(token?.email!); if (!existingUser) { return token; } - const teams = existingUser.memberships.map((membership) => ({ - id: membership.teamId, - role: membership.role, - plan: membership.team.plan, - })); - const additionalAttributs = { id: existingUser.id, createdAt: existingUser.createdAt, onboardingCompleted: existingUser.onboardingCompleted, - teams, name: existingUser.name, }; @@ -185,7 +160,6 @@ export const authOptions: NextAuthOptions = { // @ts-ignore session.user.onboardingCompleted = token?.onboardingCompleted; // @ts-ignore - session.user.teams = token?.teams; session.user.name = token.name || ""; return session; diff --git a/apps/web/app/api/v1/js/surveys.ts b/apps/web/app/api/v1/js/surveys.ts index 8b53d212ce..21095d928a 100644 --- a/apps/web/app/api/v1/js/surveys.ts +++ b/apps/web/app/api/v1/js/surveys.ts @@ -5,13 +5,13 @@ import { TSurvey } from "@formbricks/types/v1/surveys"; import { unstable_cache } from "next/cache"; const getSurveysCacheTags = (environmentId: string, personId: string): string[] => [ - `env-${environmentId}-surveys`, - `env-${environmentId}-product`, + `environments-${environmentId}-surveys`, + `environments-${environmentId}-product`, personId, ]; const getSurveysCacheKey = (environmentId: string, personId: string): string[] => [ - `env-${environmentId}-person-${personId}-syncSurveys`, + `environments-${environmentId}-person-${personId}-syncSurveys`, ]; export const getSurveysCached = (environmentId: string, person: TPerson) => diff --git a/apps/web/app/api/v1/js/sync/lib/sync.ts b/apps/web/app/api/v1/js/sync/lib/sync.ts index e1b4385c31..a9ee6f7ec2 100644 --- a/apps/web/app/api/v1/js/sync/lib/sync.ts +++ b/apps/web/app/api/v1/js/sync/lib/sync.ts @@ -1,7 +1,7 @@ import { getSurveysCached } from "@/app/api/v1/js/surveys"; import { MAU_LIMIT } from "@formbricks/lib/constants"; import { getActionClassesCached } from "@formbricks/lib/services/actionClass"; -import { getEnvironmentCached } from "@formbricks/lib/services/environment"; +import { getEnvironment } from "@formbricks/lib/services/environment"; import { createPerson, getMonthlyActivePeopleCount, getPersonCached } from "@formbricks/lib/services/person"; import { getProductByEnvironmentIdCached } from "@formbricks/lib/services/product"; import { createSession, extendSession, getSessionCached } from "@formbricks/lib/services/session"; @@ -26,7 +26,7 @@ export const getUpdatedState = async ( let session: TSession | null; // check if environment exists - environment = await getEnvironmentCached(environmentId); + environment = await getEnvironment(environmentId); if (!environment) { throw new Error("Environment does not exist"); diff --git a/apps/web/components/shared/WidgetStatusIndicator.tsx b/apps/web/components/shared/WidgetStatusIndicator.tsx index 1f2bb68785..4f0fa8eb04 100644 --- a/apps/web/components/shared/WidgetStatusIndicator.tsx +++ b/apps/web/components/shared/WidgetStatusIndicator.tsx @@ -31,7 +31,7 @@ export default function WidgetStatusIndicator({ if (!environment?.widgetSetupCompleted && actions && actions.length > 0) { updateEnvironmentAction(environment.id, { widgetSetupCompleted: true }); } - }, [environment, actions]); + }, [environment, actions, updateEnvironmentAction]); const stati = { notImplemented: { diff --git a/packages/lib/services/actionClass.ts b/packages/lib/services/actionClass.ts index c0595b81a1..f6e02c7db5 100644 --- a/packages/lib/services/actionClass.ts +++ b/packages/lib/services/actionClass.ts @@ -12,12 +12,13 @@ import { validateInputs } from "../utils/validate"; const halfHourInSeconds = 60 * 30; export const getActionClassCacheTag = (name: string, environmentId: string): string => - `env-${environmentId}-actionClass-${name}`; + `environments-${environmentId}-actionClass-${name}`; const getActionClassCacheKey = (name: string, environmentId: string): string[] => [ getActionClassCacheTag(name, environmentId), ]; -const getActionClassesCacheTag = (environmentId: string): string => `env-${environmentId}-actionClasses`; +const getActionClassesCacheTag = (environmentId: string): string => + `environments-${environmentId}-actionClasses`; const getActionClassesCacheKey = (environmentId: string): string[] => [ getActionClassesCacheTag(environmentId), ]; diff --git a/packages/lib/services/attributeClass.ts b/packages/lib/services/attributeClass.ts index 672481b804..28774ceec8 100644 --- a/packages/lib/services/attributeClass.ts +++ b/packages/lib/services/attributeClass.ts @@ -13,7 +13,8 @@ import { DatabaseError } from "@formbricks/types/v1/errors"; import { cache } from "react"; import { revalidateTag, unstable_cache } from "next/cache"; -const attributeClassesCacheTag = (environmentId: string): string => `env-${environmentId}-attributeClasses`; +const attributeClassesCacheTag = (environmentId: string): string => + `environments-${environmentId}-attributeClasses`; const getAttributeClassesCacheKey = (environmentId: string): string[] => [ attributeClassesCacheTag(environmentId), diff --git a/packages/lib/services/displays.ts b/packages/lib/services/displays.ts index db0ff1b74b..e07d0f54a5 100644 --- a/packages/lib/services/displays.ts +++ b/packages/lib/services/displays.ts @@ -39,6 +39,8 @@ const selectDisplay = { status: true, }; +export const getDisplaysCacheTag = (surveyId: string) => `surveys-${surveyId}-displays`; + export const createDisplay = async (displayInput: TDisplayInput): Promise => { validateInputs([displayInput, ZDisplayInput]); try { @@ -71,6 +73,10 @@ export const createDisplay = async (displayInput: TDisplayInput): Promise => { - validateInputs([environmentId, ZId]); - let environmentPrisma; +export const getEnvironmentCacheTag = (environmentId: string) => `environments-${environmentId}`; +export const getEnvironmentsCacheTag = (productId: string) => `products-${productId}-environments`; - try { - environmentPrisma = await prisma.environment.findUnique({ - where: { - id: environmentId, - }, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } - - throw error; - } - - try { - const environment = ZEnvironment.parse(environmentPrisma); - return environment; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); - } - throw new ValidationError("Data validation of environment failed"); - } -}); - -export const getEnvironmentCached = (environmentId: string) => +export const getEnvironment = (environmentId: string) => unstable_cache( async () => { - return await getEnvironment(environmentId); + validateInputs([environmentId, ZId]); + let environmentPrisma; + + try { + environmentPrisma = await prisma.environment.findUnique({ + where: { + id: environmentId, + }, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + + try { + const environment = ZEnvironment.parse(environmentPrisma); + return environment; + } catch (error) { + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); + } + throw new ValidationError("Data validation of environment failed"); + } }, - [environmentId], + [`environments-${environmentId}`], { - tags: [environmentId], + tags: [getEnvironmentCacheTag(environmentId)], revalidate: 30 * 60, // 30 minutes } )(); -export const getEnvironments = cache(async (productId: string): Promise => { - validateInputs([productId, ZId]); - let productPrisma; - try { - productPrisma = await prisma.product.findFirst({ - where: { - id: productId, - }, - include: { - environments: true, - }, - }); +export const getEnvironments = async (productId: string): Promise => + unstable_cache( + async () => { + validateInputs([productId, ZId]); + let productPrisma; + try { + productPrisma = await prisma.product.findFirst({ + where: { + id: productId, + }, + include: { + environments: true, + }, + }); - if (!productPrisma) { - throw new ResourceNotFoundError("Product", productId); - } - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } - throw error; - } + if (!productPrisma) { + throw new ResourceNotFoundError("Product", productId); + } + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + throw error; + } - const environments: TEnvironment[] = []; - for (let environment of productPrisma.environments) { - let targetEnvironment: TEnvironment = ZEnvironment.parse(environment); - environments.push(targetEnvironment); - } + const environments: TEnvironment[] = []; + for (let environment of productPrisma.environments) { + let targetEnvironment: TEnvironment = ZEnvironment.parse(environment); + environments.push(targetEnvironment); + } - try { - return environments; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); + try { + return environments; + } catch (error) { + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); + } + throw new ValidationError("Data validation of environments array failed"); + } + }, + [`products-${productId}-environments`], + { + tags: [getEnvironmentsCacheTag(productId)], + revalidate: 30 * 60, // 30 minutes } - throw new ValidationError("Data validation of environments array failed"); - } -}); + )(); export const updateEnvironment = async ( environmentId: string, @@ -104,6 +110,10 @@ export const updateEnvironment = async ( }, data: newData, }); + + revalidateTag(getEnvironmentsCacheTag(updatedEnvironment.productId)); + revalidateTag(getEnvironmentCacheTag(environmentId)); + return updatedEnvironment; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { diff --git a/packages/lib/services/person.ts b/packages/lib/services/person.ts index 61dd47a489..c8e80598ae 100644 --- a/packages/lib/services/person.ts +++ b/packages/lib/services/person.ts @@ -308,9 +308,9 @@ export const getMonthlyActivePeopleCount = async (environmentId: string): Promis return aggregations._count.id; }, - [`env-${environmentId}-mau`], + [`environments-${environmentId}-mau`], { - tags: [`env-${environmentId}-mau`], + tags: [`environments-${environmentId}-mau`], revalidate: 60 * 60 * 6, // 6 hours } )(); diff --git a/packages/lib/services/product.ts b/packages/lib/services/product.ts index daeb672076..f7179928dd 100644 --- a/packages/lib/services/product.ts +++ b/packages/lib/services/product.ts @@ -9,8 +9,10 @@ import { cache } from "react"; import "server-only"; import { z } from "zod"; import { validateInputs } from "../utils/validate"; +import { getEnvironmentCacheTag, getEnvironmentsCacheTag } from "./environment"; -const getProductCacheTag = (environmentId: string): string => `env-${environmentId}-product`; +export const getProductsCacheTag = (teamId: string): string => `teams-${teamId}-products`; +const getProductCacheTag = (environmentId: string): string => `environments-${environmentId}-product`; const getProductCacheKey = (environmentId: string): string[] => [getProductCacheTag(environmentId)]; const selectProduct = { @@ -29,25 +31,33 @@ const selectProduct = { environments: true, }; -export const getProducts = cache(async (teamId: string): Promise => { - validateInputs([teamId, ZId]); - try { - const products = await prisma.product.findMany({ - where: { - teamId, - }, - select: selectProduct, - }); +export const getProducts = async (teamId: string): Promise => + unstable_cache( + async () => { + validateInputs([teamId, ZId]); + try { + const products = await prisma.product.findMany({ + where: { + teamId, + }, + select: selectProduct, + }); - return products; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); + return products; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + }, + [`teams-${teamId}-products`], + { + tags: [getProductsCacheTag(teamId)], + revalidate: 30 * 60, // 30 minutes } - - throw error; - } -}); + )(); export const getProductByEnvironmentId = cache(async (environmentId: string): Promise => { if (!environmentId) { @@ -113,6 +123,7 @@ export const updateProduct = async ( try { const product = ZProduct.parse(updatedProduct); + revalidateTag(getProductsCacheTag(product.teamId)); product.environments.forEach((environment) => { // revalidate environment cache revalidateTag(getProductCacheTag(environment.id)); @@ -155,10 +166,12 @@ export const deleteProduct = cache(async (productId: string): Promise }); if (product) { + revalidateTag(getProductsCacheTag(product.teamId)); + revalidateTag(getEnvironmentsCacheTag(product.id)); product.environments.forEach((environment) => { // revalidate product cache revalidateTag(getProductCacheTag(environment.id)); - revalidateTag(environment.id); + revalidateTag(getEnvironmentCacheTag(environment.id)); }); } diff --git a/packages/lib/services/profile.ts b/packages/lib/services/profile.ts index 8380ae3b74..f11cbc4efe 100644 --- a/packages/lib/services/profile.ts +++ b/packages/lib/services/profile.ts @@ -4,9 +4,10 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/error import { TMembership, TMembershipRole, ZMembershipRole } from "@formbricks/types/v1/memberships"; import { TProfile, TProfileUpdateInput, ZProfileUpdateInput } from "@formbricks/types/v1/profile"; import { MembershipRole, Prisma } from "@prisma/client"; -import { cache } from "react"; +import { unstable_cache, revalidateTag } from "next/cache"; import { validateInputs } from "../utils/validate"; import { deleteTeam } from "./team"; +import { z } from "zod"; const responseSelection = { id: true, @@ -17,30 +18,73 @@ const responseSelection = { onboardingCompleted: true, }; +export const getProfileCacheTag = (userId: string): string => `profiles-${userId}`; +export const getProfileByEmailCacheTag = (email: string): string => `profiles-${email}`; + // function to retrive basic information about a user's profile -export const getProfile = cache(async (userId: string): Promise => { - validateInputs([userId, ZId]); - try { - const profile = await prisma.user.findUnique({ - where: { - id: userId, - }, - select: responseSelection, - }); +export const getProfile = async (userId: string): Promise => + unstable_cache( + async () => { + validateInputs([userId, ZId]); + try { + const profile = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: responseSelection, + }); - if (!profile) { - return null; + if (!profile) { + return null; + } + + return profile; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + }, + [`profiles-${userId}`], + { + tags: [getProfileByEmailCacheTag(userId)], + revalidate: 30 * 60, // 30 minutes } + )(); - return profile; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); +export const getProfileByEmail = async (email: string): Promise => + unstable_cache( + async () => { + validateInputs([email, z.string().email()]); + try { + const profile = await prisma.user.findFirst({ + where: { + email, + }, + select: responseSelection, + }); + + if (!profile) { + return null; + } + + return profile; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + }, + [`profiles-${email}`], + { + tags: [getProfileCacheTag(email)], + revalidate: 30 * 60, // 30 minutes } - - throw error; - } -}); + )(); const updateUserMembership = async (teamId: string, userId: string, role: TMembershipRole) => { validateInputs([teamId, ZId], [userId, ZId], [role, ZMembershipRole]); @@ -74,6 +118,9 @@ export const updateProfile = async ( data: data, }); + revalidateTag(getProfileByEmailCacheTag(updatedProfile.email)); + revalidateTag(getProfileCacheTag(personId)); + return updatedProfile; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") { @@ -83,22 +130,27 @@ export const updateProfile = async ( } } }; -const deleteUser = async (userId: string) => { + +const deleteUser = async (userId: string): Promise => { validateInputs([userId, ZId]); - await prisma.user.delete({ + const profile = await prisma.user.delete({ where: { id: userId, }, }); + revalidateTag(getProfileByEmailCacheTag(profile.email)); + revalidateTag(getProfileCacheTag(userId)); + + return profile; }; // function to delete a user's profile including teams -export const deleteProfile = async (personId: string): Promise => { - validateInputs([personId, ZId]); +export const deleteProfile = async (userId: string): Promise => { + validateInputs([userId, ZId]); try { const currentUserMemberships = await prisma.membership.findMany({ where: { - userId: personId, + userId: userId, }, include: { team: { @@ -131,7 +183,8 @@ export const deleteProfile = async (personId: string): Promise => { } } - await deleteUser(personId); + revalidateTag(getProfileCacheTag(userId)); + await deleteUser(userId); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError("Database operation failed"); diff --git a/packages/lib/services/response.ts b/packages/lib/services/response.ts index 4267dcd98f..008a7e3e32 100644 --- a/packages/lib/services/response.ts +++ b/packages/lib/services/response.ts @@ -16,6 +16,7 @@ import { getPerson, transformPrismaPerson } from "./person"; import { captureTelemetry } from "../telemetry"; import { validateInputs } from "../utils/validate"; import { ZId } from "@formbricks/types/v1/environment"; +import { revalidateTag } from "next/cache"; const responseSelection = { id: true, @@ -75,6 +76,8 @@ const responseSelection = { }, }; +export const getResponsesCacheTag = (surveyId: string) => `surveys-${surveyId}-responses`; + export const getResponsesByPersonId = async (personId: string): Promise | null> => { validateInputs([personId, ZId]); try { @@ -147,6 +150,10 @@ export const createResponse = async (responseInput: Partial): Pr tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), }; + if (response.surveyId) { + revalidateTag(getResponsesCacheTag(response.surveyId)); + } + return response; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { @@ -296,6 +303,10 @@ export const updateResponse = async ( tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag), }; + if (response.surveyId) { + revalidateTag(getResponsesCacheTag(response.surveyId)); + } + return response; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { diff --git a/packages/lib/services/survey.ts b/packages/lib/services/survey.ts index 495799b675..93cec78f89 100644 --- a/packages/lib/services/survey.ts +++ b/packages/lib/services/survey.ts @@ -9,14 +9,26 @@ import { ZSurveyWithAnalytics, } from "@formbricks/types/v1/surveys"; import { Prisma } from "@prisma/client"; -import { revalidateTag } from "next/cache"; -import { cache } from "react"; +import { revalidateTag, unstable_cache } from "next/cache"; import "server-only"; import { z } from "zod"; import { captureTelemetry } from "../telemetry"; import { validateInputs } from "../utils/validate"; +import { getDisplaysCacheTag } from "./displays"; +import { getResponsesCacheTag } from "./response"; -const getSurveysCacheTag = (environmentId: string): string => `env-${environmentId}-surveys`; +// surveys cache key and tags +const getSurveysCacheKey = (environmentId: string): string => `environments-${environmentId}-surveys`; +const getSurveysCacheTag = (environmentId: string): string => `environments-${environmentId}-surveys`; + +// survey cache key and tags +export const getSurveyCacheKey = (surveyId: string): string => `surveys-${surveyId}`; +export const getSurveyCacheTag = (surveyId: string): string => `surveys-${surveyId}`; + +// survey with analytics cache key +const getSurveysWithAnalyticsCacheKey = (environmentId: string): string => + `environments-${environmentId}-surveysWithAnalytics`; +const getSurveyWithAnalyticsCacheKey = (surveyId: string): string => `surveyWithAnalytics-${surveyId}`; export const selectSurvey = { id: true, @@ -78,187 +90,255 @@ export const selectSurveyWithAnalytics = { }, }; -export const preloadSurveyWithAnalytics = (surveyId: string) => { - validateInputs([surveyId, ZId]); - void getSurveyWithAnalytics(surveyId); -}; +export const getSurveyWithAnalytics = async (surveyId: string): Promise => { + const survey = await unstable_cache( + async () => { + validateInputs([surveyId, ZId]); + let surveyPrisma; + try { + surveyPrisma = await prisma.survey.findUnique({ + where: { + id: surveyId, + }, + select: selectSurveyWithAnalytics, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } -export const getSurveyWithAnalytics = cache( - async (surveyId: string): Promise => { - validateInputs([surveyId, ZId]); - let surveyPrisma; - try { - surveyPrisma = await prisma.survey.findUnique({ - where: { - id: surveyId, + throw error; + } + + if (!surveyPrisma) { + throw new ResourceNotFoundError("Survey", surveyId); + } + + let { _count, displays, ...surveyPrismaFields } = surveyPrisma; + + const numDisplays = displays.length; + const numDisplaysResponded = displays.filter((item) => item.status === "responded").length; + const numResponses = _count.responses; + // responseRate, rounded to 2 decimal places + const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0; + + const transformedSurvey = { + ...surveyPrismaFields, + triggers: surveyPrismaFields.triggers.map((trigger) => trigger.eventClass), + analytics: { + numDisplays, + responseRate, + numResponses, }, - select: selectSurveyWithAnalytics, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); + }; + + try { + const survey = ZSurveyWithAnalytics.parse(transformedSurvey); + return survey; + } catch (error) { + console.log(error); + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information + } + throw new ValidationError("Data validation of survey failed"); } - - throw error; + }, + [getSurveyWithAnalyticsCacheKey(surveyId)], + { + tags: [getSurveyCacheTag(surveyId), getDisplaysCacheTag(surveyId), getResponsesCacheTag(surveyId)], + revalidate: 60 * 30, } + )(); - if (!surveyPrisma) { - throw new ResourceNotFoundError("Survey", surveyId); - } - - let { _count, displays, ...surveyPrismaFields } = surveyPrisma; - - const numDisplays = displays.length; - const numDisplaysResponded = displays.filter((item) => item.status === "responded").length; - const numResponses = _count.responses; - // responseRate, rounded to 2 decimal places - const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0; - - const transformedSurvey = { - ...surveyPrismaFields, - triggers: surveyPrismaFields.triggers.map((trigger) => trigger.eventClass), - analytics: { - numDisplays, - responseRate, - numResponses, - }, - }; - - try { - const survey = ZSurveyWithAnalytics.parse(transformedSurvey); - return survey; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information - } - throw new ValidationError("Data validation of survey failed"); - } - } -); - -export const getSurvey = cache(async (surveyId: string): Promise => { - validateInputs([surveyId, ZId]); - let surveyPrisma; - try { - surveyPrisma = await prisma.survey.findUnique({ - where: { - id: surveyId, - }, - select: selectSurvey, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } - - throw error; - } - - if (!surveyPrisma) { + if (!survey) { return null; } - const transformedSurvey = { - ...surveyPrisma, - triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass), + // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them + // https://github.com/vercel/next.js/issues/51613 + return { + ...survey, + createdAt: new Date(survey.createdAt), + updatedAt: new Date(survey.updatedAt), }; +}; - try { - const survey = ZSurvey.parse(transformedSurvey); - return survey; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information - } - throw new ValidationError("Data validation of survey failed"); - } -}); +export const getSurvey = async (surveyId: string): Promise => { + const survey = await unstable_cache( + async () => { + validateInputs([surveyId, ZId]); + let surveyPrisma; + try { + surveyPrisma = await prisma.survey.findUnique({ + where: { + id: surveyId, + }, + select: selectSurvey, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } -export const getSurveys = cache(async (environmentId: string): Promise => { - validateInputs([environmentId, ZId]); - let surveysPrisma; - try { - surveysPrisma = await prisma.survey.findMany({ - where: { - environmentId, - }, - select: selectSurvey, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } + throw error; + } - throw error; - } + if (!surveyPrisma) { + return null; + } - const surveys: TSurvey[] = []; - - try { - for (const surveyPrisma of surveysPrisma) { const transformedSurvey = { ...surveyPrisma, triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass), }; - const survey = ZSurvey.parse(transformedSurvey); - surveys.push(survey); - } - return surveys; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information - } - throw new ValidationError("Data validation of survey failed"); - } -}); -export const getSurveysWithAnalytics = cache( - async (environmentId: string): Promise => { - validateInputs([environmentId, ZId]); - let surveysPrisma; - try { - surveysPrisma = await prisma.survey.findMany({ - where: { - environmentId, - }, - select: selectSurveyWithAnalytics, - }); - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); + try { + const survey = ZSurvey.parse(transformedSurvey); + return survey; + } catch (error) { + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information + } + throw new ValidationError("Data validation of survey failed"); } - - throw error; + }, + [getSurveyCacheKey(surveyId)], + { + tags: [getSurveyCacheTag(surveyId)], + revalidate: 60 * 30, } + )(); - try { - const surveys: TSurveyWithAnalytics[] = []; - for (const { _count, displays, ...surveyPrisma } of surveysPrisma) { - const numDisplays = displays.length; - const numDisplaysResponded = displays.filter((item) => item.status === "responded").length; - const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0; + if (!survey) { + return null; + } - const transformedSurvey = { - ...surveyPrisma, - triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass), - analytics: { - numDisplays, - responseRate, - numResponses: _count.responses, + // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them + // https://github.com/vercel/next.js/issues/51613 + return { + ...survey, + createdAt: new Date(survey.createdAt), + updatedAt: new Date(survey.updatedAt), + }; +}; + +export const getSurveys = async (environmentId: string): Promise => { + const surveys = await unstable_cache( + async () => { + validateInputs([environmentId, ZId]); + let surveysPrisma; + try { + surveysPrisma = await prisma.survey.findMany({ + where: { + environmentId, }, - }; - const survey = ZSurveyWithAnalytics.parse(transformedSurvey); - surveys.push(survey); + select: selectSurvey, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; } - return surveys; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information + + const surveys: TSurvey[] = []; + + try { + for (const surveyPrisma of surveysPrisma) { + const transformedSurvey = { + ...surveyPrisma, + triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass), + }; + const survey = ZSurvey.parse(transformedSurvey); + surveys.push(survey); + } + return surveys; + } catch (error) { + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information + } + throw new ValidationError("Data validation of survey failed"); } - throw new ValidationError("Data validation of survey failed"); + }, + [getSurveysCacheKey(environmentId)], + { + tags: [getSurveysCacheTag(environmentId)], + revalidate: 60 * 30, } - } -); + )(); + + // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them + // https://github.com/vercel/next.js/issues/51613 + return surveys.map((survey) => ({ + ...survey, + createdAt: new Date(survey.createdAt), + updatedAt: new Date(survey.updatedAt), + })); +}; + +// TODO: Cache doesn't work for updated displays & responses +export const getSurveysWithAnalytics = async (environmentId: string): Promise => { + const surveysWithAnalytics = await unstable_cache( + async () => { + validateInputs([environmentId, ZId]); + let surveysPrisma; + try { + surveysPrisma = await prisma.survey.findMany({ + where: { + environmentId, + }, + select: selectSurveyWithAnalytics, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + + try { + const surveys: TSurveyWithAnalytics[] = []; + for (const { _count, displays, ...surveyPrisma } of surveysPrisma) { + const numDisplays = displays.length; + const numDisplaysResponded = displays.filter((item) => item.status === "responded").length; + const responseRate = numDisplays ? Math.round((numDisplaysResponded / numDisplays) * 100) / 100 : 0; + + const transformedSurvey = { + ...surveyPrisma, + triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass), + analytics: { + numDisplays, + responseRate, + numResponses: _count.responses, + }, + }; + const survey = ZSurveyWithAnalytics.parse(transformedSurvey); + surveys.push(survey); + } + return surveys; + } catch (error) { + if (error instanceof z.ZodError) { + console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information + } + throw new ValidationError("Data validation of survey failed"); + } + }, + [getSurveysWithAnalyticsCacheKey(environmentId)], + { + tags: [getSurveysCacheTag(environmentId)], // TODO: add tags for displays and responses + } + )(); + + // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them + // https://github.com/vercel/next.js/issues/51613 + return surveysWithAnalytics.map((survey) => ({ + ...survey, + createdAt: new Date(survey.createdAt), + updatedAt: new Date(survey.updatedAt), + })); +}; export async function updateSurvey(updatedSurvey: Partial): Promise { const surveyId = updatedSurvey.id; @@ -432,6 +512,7 @@ export async function updateSurvey(updatedSurvey: Partial): Promise => { - try { - const teams = await prisma.team.findMany({ - where: { - memberships: { - some: { - userId, - }, - }, - }, - select, - }); +export const getTeamsByUserIdCacheTag = (userId: string) => `users-${userId}-teams`; +export const getTeamByEnvironmentIdCacheTag = (environmentId: string) => `environments-${environmentId}-team`; - return teams; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } - - throw error; - } -}); - -export const getTeamByEnvironmentId = cache(async (environmentId: string): Promise => { - validateInputs([environmentId, ZId]); - try { - const team = await prisma.team.findFirst({ - where: { - products: { - some: { - environments: { +export const getTeamsByUserId = async (userId: string): Promise => + unstable_cache( + async () => { + try { + const teams = await prisma.team.findMany({ + where: { + memberships: { some: { - id: environmentId, + userId, }, }, }, - }, - }, - select, - }); + select, + }); - return team; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); + return teams; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + }, + [`users-${userId}-teams`], + { + tags: [getTeamsByUserIdCacheTag(userId)], + revalidate: 30 * 60, // 30 minutes } + )(); - throw error; - } -}); +export const getTeamByEnvironmentId = async (environmentId: string): Promise => + unstable_cache( + async () => { + validateInputs([environmentId, ZId]); + try { + const team = await prisma.team.findFirst({ + where: { + products: { + some: { + environments: { + some: { + id: environmentId, + }, + }, + }, + }, + }, + select: { ...select, memberships: true }, // include memberships + }); -export const updateTeam = async (teamId: string, data: TTeamUpdateInput) => { + return team; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } + }, + [`environments-${environmentId}-team`], + { + tags: [getTeamByEnvironmentIdCacheTag(environmentId)], + revalidate: 30 * 60, // 30 minutes + } + )(); + +export const updateTeam = async (teamId: string, data: TTeamUpdateInput): Promise => { try { const updatedTeam = await prisma.team.update({ where: { id: teamId, }, data, + select: { ...select, memberships: true, products: { select: { environments: true } } }, // include memberships & environments }); - return updatedTeam; + // revalidate cache for members + updatedTeam?.memberships.forEach((membership) => { + revalidateTag(getTeamsByUserIdCacheTag(membership.userId)); + }); + + // revalidate cache for environments + updatedTeam?.products.forEach((product) => { + product.environments.forEach((environment) => { + revalidateTag(getTeamByEnvironmentIdCacheTag(environment.id)); + }); + }); + + const team = { + ...updatedTeam, + memberships: undefined, + products: undefined, + }; + + return team; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") { throw new ResourceNotFoundError("Team", teamId); @@ -107,11 +145,32 @@ export const updateTeam = async (teamId: string, data: TTeamUpdateInput) => { export const deleteTeam = async (teamId: string) => { validateInputs([teamId, ZId]); try { - await prisma.team.delete({ + const deletedTeam = await prisma.team.delete({ where: { id: teamId, }, + select: { ...select, memberships: true, products: { select: { environments: true } } }, // include memberships & environments }); + + // revalidate cache for members + deletedTeam?.memberships.forEach((membership) => { + revalidateTag(getTeamsByUserIdCacheTag(membership.userId)); + }); + + // revalidate cache for environments + deletedTeam?.products.forEach((product) => { + product.environments.forEach((environment) => { + revalidateTag(getTeamByEnvironmentIdCacheTag(environment.id)); + }); + }); + + const team = { + ...deletedTeam, + memberships: undefined, + products: undefined, + }; + + return team; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError("Database operation failed"); @@ -121,7 +180,7 @@ export const deleteTeam = async (teamId: string) => { } }; -export const createDemoProduct = cache(async (teamId: string) => { +export const createDemoProduct = async (teamId: string) => { validateInputs([teamId, ZId]); const productWithEnvironment = Prisma.validator()({ include: { @@ -300,4 +359,4 @@ export const createDemoProduct = cache(async (teamId: string) => { ); return demoProduct; -}); +}; From 3c2087452cfedec0ec4afa023982ae3c12533da8 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sun, 1 Oct 2023 01:01:14 +0900 Subject: [PATCH 4/7] fix: typo in comment in find-first.ts (#874) enviroment -> environment --- apps/web/pages/api/v1/environments/find-first.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/pages/api/v1/environments/find-first.ts b/apps/web/pages/api/v1/environments/find-first.ts index f5fd31cf3f..df62fa556b 100644 --- a/apps/web/pages/api/v1/environments/find-first.ts +++ b/apps/web/pages/api/v1/environments/find-first.ts @@ -13,7 +13,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) // GET if (req.method === "GET") { - // find first production enviroment of the user + // find first production environment of the user const firstMembership = await prisma.membership.findFirst({ where: { userId: user.id, From 13afba76150555b13f00028e5e02c03f5b5cb2be Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Sun, 1 Oct 2023 01:10:59 +0530 Subject: [PATCH 5/7] feat: move env vars from build-time to run-time to better support self-hosting environments (#789) * feat: privacy, imprint, and terms URL env vars now do not need rebuilding * feat: disable_singup env var now do not need rebuilding * feat: password_reset_disabled env var now do not need rebuilding * feat: email_verification_disabled env var now do not need rebuilding * feat: github_oauth & google_oauth env var now do not need rebuilding * feat: move logic of env vars to serverside and send boolean client-side * feat: invite_disabled env var now do not need rebuilding * feat: rename vars logically * feat: migration guide * feat: update docker-compose as per v1.1 * deprecate: unused NEXT_PUBLIC_VERCEL_URL & VERCEL_URL * deprecate: unused RAILWAY_STATIC_URL * deprecate: unused RENDER_EXTERNAL_URL * deprecate: unused HEROKU_APP_NAME * fix: define WEBAPP_URL & replace NEXT_WEBAPP_URL with it * migrate: NEXT_PUBLIC_IS_FORMBRICKS_CLOUD to IS_FORMBRICKS_CLOUD * chore: move all env parsing to a constants.ts from page files * feat: migrate client side envs to server side * redo: isFormbricksCloud to navbar serverside page * fix: constants is now a server only file * fix: removal of use swr underway * fix: move 1 tag away from swr to service * feat: move away from tags swr * feat: move away from surveys swr * feat: move away from eventClass swr * feat: move away from event swr * fix: make constants server-only * remove comments from .env.example, use constants in MetaInformation * clean up services * rename tag function * fix build error * fix smaller bugs, fix Response % not working in summary --------- Co-authored-by: Matthias Nannt --- .env.docker | 20 +- .env.example | 24 ++- .gitpod.yml | 2 +- .../docs/self-hosting/from-source/page.mdx | 107 +++++------ .../self-hosting/migrating-to-1.1/page.mdx | 138 +++++++++++++ .../components/docs/Navigation.tsx | 1 + .../ActionsAttributesTabs.tsx | 2 +- .../actions/ActionActivityTab.tsx | 88 +++++++-- .../actions/ActionDetailModal.tsx | 2 +- .../(actionsAndAttributes)/actions/actions.ts | 31 +++ .../attributes/AttributeActivityTab.tsx | 50 +++-- .../attributes/AttributeClassesTable.tsx | 3 - .../attributes/AttributeDetailModal.tsx | 14 +- .../attributes/actions.ts | 14 ++ .../attributes/page.tsx | 3 +- .../[environmentId]/AddProductModal.tsx | 6 +- .../[environmentId]/EnvironmentsNavbar.tsx | 4 +- .../[environmentId]/Navigation.tsx | 7 +- .../environments/[environmentId]/actions.ts | 8 + .../google-sheets/AddIntegrationModal.tsx | 4 +- .../integrations/google-sheets/Connect.tsx | 6 +- .../google-sheets/GoogleSheetWrapper.tsx | 15 +- .../integrations/google-sheets/Home.tsx | 7 +- .../integrations/google-sheets/page.tsx | 31 +-- .../integrations/webhooks/WebhookTable.tsx | 13 +- .../integrations/webhooks/page.tsx | 14 +- .../environments/[environmentId]/layout.tsx | 7 +- .../(activitySection)/ActivityFeed.tsx | 7 +- .../(activitySection)/ActivitySection.tsx | 11 +- .../(activitySection)/ActivityTimeline.tsx | 7 +- .../(responseSection)/ResponseSection.tsx | 9 +- .../(responseSection)/ResponseTimeline.tsx | 7 +- .../(responseSection)/ResponsesFeed.tsx | 11 +- .../people/[personId]/page.tsx | 7 +- .../[environmentId]/people/page.tsx | 11 +- .../settings/SettingsNavbar.tsx | 29 +-- .../[environmentId]/settings/billing/page.tsx | 3 +- .../[environmentId]/settings/layout.tsx | 22 ++- .../lookandfeel/EditHighlightBorder.tsx | 8 +- .../settings/lookandfeel/page.tsx | 3 +- .../members/EditMemberships/TeamActions.tsx | 12 +- .../settings/members/actions.ts | 4 +- .../[environmentId]/settings/members/page.tsx | 2 + .../settings/product/DeleteProduct.tsx | 13 +- .../settings/setup/SetupInstructions.tsx | 16 +- .../[environmentId]/settings/setup/page.tsx | 7 +- .../settings/tags/EditTagsWrapper.tsx | 121 ++++++------ .../[environmentId]/settings/tags/actions.ts | 15 ++ .../[environmentId]/settings/tags/page.tsx | 18 +- .../surveys/SurveyDropDownMenu.tsx | 2 +- .../[environmentId]/surveys/SurveyList.tsx | 6 +- .../surveys/[surveyId]/(analysis)/data.ts | 3 +- .../(analysis)/responses/actions.ts | 19 ++ .../responses/components/ResponsePage.tsx | 27 ++- .../components/ResponseTagsWrapper.tsx | 117 +++++------ .../responses/components/ResponseTimeline.tsx | 17 +- .../responses/components/SingleResponse.tsx | 17 +- .../[surveyId]/(analysis)/responses/page.tsx | 27 ++- .../summary/components/LinkSurveyModal.tsx | 2 +- .../summary/components/StatusDropdown.tsx | 32 ---- .../summary/components/SuccessMessage.tsx | 46 +++-- .../summary/components/SummaryList.tsx | 13 +- .../summary/components/SummaryPage.tsx | 27 ++- .../[surveyId]/(analysis)/summary/page.tsx | 21 +- .../surveys/[surveyId]/CustomFilter.tsx | 7 +- .../surveys/[surveyId]/ResponseFilter.tsx | 22 +-- .../surveys/[surveyId]/SummaryHeader.tsx | 36 ++-- .../[surveyId]/edit/AddQuestionButton.tsx | 13 +- .../surveys/[surveyId]/edit/QuestionsView.tsx | 11 +- .../[surveyId]/edit/ResponseOptionsCard.tsx | 2 +- .../surveys/[surveyId]/edit/SurveyEditor.tsx | 4 +- .../surveys/[surveyId]/edit/SurveyMenuBar.tsx | 4 +- .../[surveyId]/edit/WhenToSendCard.tsx | 51 ++--- apps/web/app/(auth)/auth/login/page.tsx | 13 +- apps/web/app/(auth)/auth/signup/page.tsx | 38 ++-- apps/web/app/(auth)/invite/page.tsx | 2 +- .../app/api/auth/[...nextauth]/authOptions.ts | 4 +- apps/web/app/api/cron/close_surveys/route.ts | 4 +- .../app/api/google-sheet/callback/route.ts | 14 +- apps/web/app/api/google-sheet/route.ts | 12 +- apps/web/app/api/v1/users/route.ts | 13 +- apps/web/app/page.tsx | 22 +-- apps/web/app/s/[surveyId]/LegalFooter.tsx | 14 +- apps/web/app/s/[surveyId]/LinkSurvey.tsx | 22 ++- apps/web/app/s/[surveyId]/page.tsx | 3 +- apps/web/components/auth/SigninForm.tsx | 21 +- apps/web/components/auth/SignupForm.tsx | 60 +++--- .../components/environments/SecondNavBar.tsx | 20 +- .../components/shared/EmptySpaceFiller.tsx | 20 +- .../components/shared/SurveyNavBarName.tsx | 26 +-- .../shared/SurveyStatusDropdown.tsx | 38 ++-- .../shared/SurveyStatusIndicator.tsx | 15 +- apps/web/env.mjs | 73 +++---- apps/web/lib/apiKeys.ts | 39 ---- .../lib/attributeClasses/attributeClasses.ts | 38 ---- .../mutateAttributeClasses.ts | 14 -- apps/web/lib/email.ts | 25 ++- apps/web/lib/environments/environments.ts | 17 -- .../lib/environments/mutateEnvironments.ts | 11 -- apps/web/lib/eventClasses/eventClasses.ts | 66 ------- .../lib/eventClasses/mutateEventClasses.ts | 14 -- apps/web/lib/events/events.ts | 17 -- apps/web/lib/members.ts | 18 -- apps/web/lib/memberships.ts | 14 -- apps/web/lib/people/people.ts | 55 ------ apps/web/lib/products/mutateProducts.ts | 14 -- apps/web/lib/products/products.ts | 37 ---- apps/web/lib/profile/mutateProfile.ts | 11 -- apps/web/lib/profile/profile.ts | 14 -- apps/web/lib/responses/responses.ts | 27 --- apps/web/lib/surveys/mutateSurveys.ts | 14 -- apps/web/lib/surveys/surveys.ts | 135 ------------- apps/web/lib/tags/mutateTags.ts | 154 --------------- apps/web/lib/tags/tags.ts | 30 --- apps/web/lib/teams/mutateTeams.ts | 11 -- apps/web/lib/teams/teams.ts | 24 --- apps/web/next.config.mjs | 4 +- .../surveys/[surveyId]/responses/index.ts | 5 +- .../pages/api/v1/environments/find-first.ts | 105 ---------- .../api/v1/teams/[teamId]/invite/index.ts | 4 +- docker/docker-compose.yml | 32 ++++ packages/js/src/lib/widget.ts | 8 +- packages/lib/constants.ts | 54 ++++-- packages/lib/responseQueue.ts | 11 +- packages/lib/services/actions.ts | 48 +++++ packages/lib/services/attributeClass.ts | 29 ++- packages/lib/services/googleSheet.ts | 11 +- packages/lib/services/product.ts | 71 +++++++ packages/lib/services/response.ts | 15 +- packages/lib/services/survey.ts | 68 ++++++- packages/lib/services/tag.ts | 181 ++++++++++++++++++ packages/lib/services/tagOnResponse.ts | 48 +++++ packages/lib/surveyState.ts | 9 + packages/types/v1/auth.ts | 22 +-- packages/types/v1/surveys.ts | 12 ++ packages/types/v1/tags.ts | 4 +- turbo.json | 27 ++- 137 files changed, 1764 insertions(+), 1727 deletions(-) create mode 100644 apps/formbricks-com/app/docs/self-hosting/migrating-to-1.1/page.mdx create mode 100644 apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts create mode 100644 apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/tags/actions.ts delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/StatusDropdown.tsx delete mode 100644 apps/web/lib/apiKeys.ts delete mode 100644 apps/web/lib/attributeClasses/attributeClasses.ts delete mode 100644 apps/web/lib/attributeClasses/mutateAttributeClasses.ts delete mode 100644 apps/web/lib/environments/environments.ts delete mode 100644 apps/web/lib/environments/mutateEnvironments.ts delete mode 100644 apps/web/lib/eventClasses/eventClasses.ts delete mode 100644 apps/web/lib/eventClasses/mutateEventClasses.ts delete mode 100644 apps/web/lib/events/events.ts delete mode 100644 apps/web/lib/memberships.ts delete mode 100644 apps/web/lib/people/people.ts delete mode 100644 apps/web/lib/products/mutateProducts.ts delete mode 100644 apps/web/lib/products/products.ts delete mode 100644 apps/web/lib/profile/mutateProfile.ts delete mode 100644 apps/web/lib/profile/profile.ts delete mode 100644 apps/web/lib/responses/responses.ts delete mode 100644 apps/web/lib/surveys/mutateSurveys.ts delete mode 100644 apps/web/lib/tags/mutateTags.ts delete mode 100644 apps/web/lib/tags/tags.ts delete mode 100644 apps/web/lib/teams/mutateTeams.ts delete mode 100644 apps/web/lib/teams/teams.ts delete mode 100644 apps/web/pages/api/v1/environments/find-first.ts create mode 100644 packages/lib/services/tag.ts create mode 100644 packages/lib/services/tagOnResponse.ts diff --git a/.env.docker b/.env.docker index d0c378e4d3..106955bcfb 100644 --- a/.env.docker +++ b/.env.docker @@ -7,7 +7,7 @@ # BASICS # ############ -NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000 +WEBAPP_URL=http://localhost:3000 ############## # DATABASE # @@ -63,25 +63,25 @@ NEXTAUTH_URL=http://localhost:3000 ##################### # Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too. -NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1 +EMAIL_VERIFICATION_DISABLED=1 # Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too. -NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1 +PASSWORD_RESET_DISABLED=1 # Signup. Disable the ability for new users to create an account. -# NEXT_PUBLIC_SIGNUP_DISABLED=1 +# SIGNUP_DISABLED=1 # Team Invite. Disable the ability for invited users to create an account. -# NEXT_PUBLIC_INVITE_DISABLED=1 +# INVITE_DISABLED=1 ########## # Other # ########## # Display privacy policy, imprint and terms of service links in the footer of signup & public pages. -NEXT_PUBLIC_PRIVACY_URL= -NEXT_PUBLIC_TERMS_URL= -NEXT_PUBLIC_IMPRINT_URL= +PRIVACY_URL= +TERMS_URL= +IMPRINT_URL= # Disable Sentry warning SENTRY_IGNORE_API_RESOLUTION_ERROR=1 @@ -90,12 +90,12 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=1 NEXT_PUBLIC_SENTRY_DSN= # Configure Github Login -NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0 +GITHUB_AUTH_ENABLED=0 GITHUB_ID= GITHUB_SECRET= # Configure Google Login -NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0 +GOOGLE_AUTH_ENABLED=0 GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= diff --git a/.env.example b/.env.example index 7b3c52feec..11c21391d3 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ # BASICS # ############ -NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000 +WEBAPP_URL=http://localhost:3000 ############## # DATABASE # @@ -20,6 +20,7 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu # Cold boots will be faster and you'll be able to scale your DB independently of your app. # @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy # PRISMA_GENERATE_DATAPROXY=true +# i dont think we use it so ask Matti and remove it PRISMA_GENERATE_DATAPROXY= ############### @@ -34,9 +35,6 @@ NEXTAUTH_SECRET=RANDOM_STRING # You do not need the NEXTAUTH_URL environment variable in Vercel. NEXTAUTH_URL=http://localhost:3000 -# If you encounter NEXT_AUTH URL problems this should always be localhost:3000 (or whatever port your app is running on) -# NEXTAUTH_URL_INTERNAL=http://localhost:3000 - ################ # MAIL SETUP # ################ @@ -64,33 +62,33 @@ SMTP_PASSWORD=smtpPassword ##################### # Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too. -# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1 +# EMAIL_VERIFICATION_DISABLED=1 # Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too. -# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1 +# PASSWORD_RESET_DISABLED=1 # Signup. Disable the ability for new users to create an account. -# NEXT_PUBLIC_SIGNUP_DISABLED=1 +# SIGNUP_DISABLED=1 # Team Invite. Disable the ability for invited users to create an account. -# NEXT_PUBLIC_INVITE_DISABLED=1 +# INVITE_DISABLED=1 ########## # Other # ########## # Display privacy policy, imprint and terms of service links in the footer of signup & public pages. -NEXT_PUBLIC_PRIVACY_URL= -NEXT_PUBLIC_TERMS_URL= -NEXT_PUBLIC_IMPRINT_URL= +PRIVACY_URL= +TERMS_URL= +IMPRINT_URL= # Configure Github Login -NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0 +GITHUB_AUTH_ENABLED=0 GITHUB_ID= GITHUB_SECRET= # Configure Google Login -NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0 +GOOGLE_AUTH_ENABLED=0 GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= diff --git a/.gitpod.yml b/.gitpod.yml index 54f05d33a1..3c5941863b 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -32,7 +32,7 @@ tasks: command: | gp sync-await init && cp .env.example .env && - sed -i -r "s#^(NEXT_PUBLIC_WEBAPP_URL=).*#\1 $(gp url 3000)#" .env && + sed -i -r "s#^(WEBAPP_URL=).*#\1 $(gp url 3000)#" .env && sed -i -r "s#^(NEXTAUTH_URL=).*#\1 $(gp url 3000)#" .env && turbo --filter "@formbricks/web" go diff --git a/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx b/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx index 89e3cbe2f0..137e14b120 100644 --- a/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx +++ b/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx @@ -29,7 +29,7 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are -2. **Modify the `.env.docker` file as required by your setup.**
    This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`) +2. **Modify the `.env.docker` file as required by your setup.**
    This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# PASSWORD_RESET_DISABLED=1`) ## Editing a NEXT_PUBLIC_* variable? @@ -50,67 +50,64 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are You can now access the app on [http://localhost:3000](http://localhost:3000). You will be automatically redirected to the login. To use your local installation of Formbricks, create a new account. -### Important Run-time Variables +## Important Run-time Variables These variables must also be provided at runtime. -| Variable | Description | Required | Default | -| ------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- | --- | -| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` | -| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` | -| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | | -| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) | -| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` | -| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | | -| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | | -| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | | -| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | | -| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | | -| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | | -| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | | -| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | | -| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | | -| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | | -| CRON_SECRET | API Secret for running cron jobs. | optional | | -| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | -| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | -| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | | -| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | | -| INTERNAL_SECRET | Internal Secret (Currently we do not use this anywhere). | optional | | -| RENDER_EXTERNAL_URL | External URL for rendering (used instead of WEBAPP_URL). | optional | | -| HEROKU_APP_NAME | Heroku app name (used instead of WEBAPP_URL). | optional | | -| RAILWAY_STATIC_URL | Railway static URL (used instead of WEBAPP_URL). | optional | | | +| Variable | Description | Required | Default | +| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- | +| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` | +| SURVEY_BASE_URL | Base URL of the link surveys. | required | `http://localhost:3000/s/` | +| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` | +| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | | +| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) | +| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` | +| PRIVACY_URL | URL for privacy policy. | optional | | +| TERMS_URL | URL for terms of service. | optional | | +| IMPRINT_URL | URL for imprint. | optional | | +| SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | | +| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | | +| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | | +| INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | | +| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | | +| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | | +| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | | +| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | | +| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | | +| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | | +| GITHUB_AUTH_ENABLED | Enables GitHub login if set to `1`. | optional | | +| GOOGLE_AUTH_ENABLED | Enables Google login if set to `1`. | optional | | +| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | | +| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | | +| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | | +| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | | +| CRON_SECRET | API Secret for running cron jobs. | optional | | +| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | +| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | +| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | | +| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | | +| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | | +| IS_FORMBRICKS_CLOUD | Uses Formbricks Cloud if set to `1` | optional | | +| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` | -### Build-time Variables +## Build-time Variables These variables must be provided at the time of the docker build and can be provided by updating the `.env` file. -| Variable | Description | Required | Default | -| -------------------------------------------------- | -------------------------------------------------------------------------- | -------- | ----------------------- | -| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files. | required | `http://localhost:3000` | -| NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | | -| NEXT_PUBLIC_PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | | -| NEXT_PUBLIC_SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | | -| NEXT_PUBLIC_INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | | -| NEXT_PUBLIC_PRIVACY_URL | URL for privacy policy. | optional | | -| NEXT_PUBLIC_TERMS_URL | URL for terms of service. | optional | | -| NEXT_PUBLIC_IMPRINT_URL | URL for imprint. | optional | | -| NEXT_PUBLIC_GITHUB_AUTH_ENABLED | Enables GitHub login if set to `1`. | optional | | -| NEXT_PUBLIC_GOOGLE_AUTH_ENABLED | Enables Google login if set to `1`. | optional | | -| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API. | optional | | -| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | If you already have a preset environment ID of the environment. | optional | | -| NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID | If you already have an onboarding survey ID to show new users. | optional | | -| NEXT_PUBLIC_IS_FORMBRICKS_CLOUD | Uses Formbricks Cloud if set to `1` | optional | | -| NEXT_PUBLIC_POSTHOG_API_KEY | API key for PostHog analytics. | optional | | -| NEXT_PUBLIC_POSTHOG_API_HOST | Host for PostHog analytics. | optional | | -| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking. | optional | | -| NEXT_PUBLIC_DOCSEARCH_APP_ID | ID of the DocSearch application. | optional | | -| NEXT_PUBLIC_DOCSEARCH_API_KEY | API key of the DocSearch application. | optional | | -| NEXT_PUBLIC_DOCSEARCH_INDEX_NAME | Name of the DocSearch index. | optional | | -| NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID | Environment ID for Formbricks (if you already have one) | optional | | -| NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID | Survey ID for the feedback survey on the docs site. | optional | | -| NEXT_PUBLIC_FORMBRICKS_COM_API_HOST | Host for the Formbricks API. | optional | | -| NEXT_PUBLIC_VERCEL_URL | Vercel URL (used instead of WEBAPP_URL). | optional | +| Variable | Description | Required | Default | +| -------------------------------------------------- | --------------------------------------------------------------- | -------- | ------- | +| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API. | optional | | +| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | If you already have a preset environment ID of the environment. | optional | | +| NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID | If you already have an onboarding survey ID to show new users. | optional | | +| NEXT_PUBLIC_POSTHOG_API_KEY | API key for PostHog analytics. | optional | | +| NEXT_PUBLIC_POSTHOG_API_HOST | Host for PostHog analytics. | optional | | +| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking. | optional | | +| NEXT_PUBLIC_DOCSEARCH_APP_ID | ID of the DocSearch application. | optional | | +| NEXT_PUBLIC_DOCSEARCH_API_KEY | API key of the DocSearch application. | optional | | +| NEXT_PUBLIC_DOCSEARCH_INDEX_NAME | Name of the DocSearch index. | optional | | +| NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID | Environment ID for Formbricks (if you already have one) | optional | | +| NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID | Survey ID for the feedback survey on the docs site. | optional | | +| NEXT_PUBLIC_FORMBRICKS_COM_API_HOST | Host for the Formbricks API. | optional | | ## Debugging diff --git a/apps/formbricks-com/app/docs/self-hosting/migrating-to-1.1/page.mdx b/apps/formbricks-com/app/docs/self-hosting/migrating-to-1.1/page.mdx new file mode 100644 index 0000000000..7d3ab4f1b2 --- /dev/null +++ b/apps/formbricks-com/app/docs/self-hosting/migrating-to-1.1/page.mdx @@ -0,0 +1,138 @@ +export const meta = { + title: "Migrating Formbricks to v1.1", + description: + "Formbricks v1.1 comes with an amazing set of features including the ability to define most environment variables at runtime itself! No need to build the image again! This guide will help you migrate your existing Formbricks instance to v1.1", +}; + +#### Self-Hosting + +# Migrating to v1.1 + +Formbricks v1.1 includes a lot of new features and improvements. However, it also comes with a few breaking changes specifically with the environment variables. This guide will help you migrate your existing Formbricks instance to v1.1 without losing any data. + +## Changes in .env + +### Renamed Environment Variables +This was introduced because we got a lot of requests from our users for the ability to define some common environment variables at runtime itself i.e. without having to rebuild the image for the changes to take effect. +This is now possible with v1.1. However, due to Next.JS best practices, we had to deprecate the prefix **NEXT_PUBLIC_** in the following environment variables: + +| till v1.0 | v1.1 | +| ------------------------------------------- | --------------------------- | +| **NEXT_PUBLIC_**EMAIL_VERIFICATION_DISABLED | EMAIL_VERIFICATION_DISABLED | +| **NEXT_PUBLIC_**PASSWORD_RESET_DISABLED | PASSWORD_RESET_DISABLED | +| **NEXT_PUBLIC_**SIGNUP_DISABLED | SIGNUP_DISABLED | +| **NEXT_PUBLIC_**INVITE_DISABLED | INVITE_DISABLED | +| **NEXT_PUBLIC_**PRIVACY_URL | PRIVACY_URL | +| **NEXT_PUBLIC_**TERMS_URL | TERMS_URL | +| **NEXT_PUBLIC_**IMPRINT_URL | IMPRINT_URL | +| **NEXT_PUBLIC_**GITHUB_AUTH_ENABLED | GITHUB_AUTH_ENABLED | +| **NEXT_PUBLIC_**GOOGLE_AUTH_ENABLED | GOOGLE_AUTH_ENABLED | +| **NEXT_PUBLIC_**WEBAPP_URL | WEBAPP_URL | +| **NEXT_PUBLIC_**IS_FORMBRICKS_CLOUD | IS_FORMBRICKS_CLOUD | +| **NEXT_PUBLIC_**SURVEY_BASE_URL | SURVEY_BASE_URL | + + +Please note that their values and the logic remains exactly the same. Only the prefix has been deprecated. The other environment variables remain the same as well. + + +### Deprecated Environment Variables + +- **NEXT_PUBLIC_VERCEL_URL**: Was used as Vercel URL (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable to your Vercel URL. +- **RAILWAY_STATIC_URL**: Was used as Railway Static URL (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable. +- **RENDER_EXTERNAL_URL**: Was used as an external URL to Render (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable. +- **HEROKU_APP_NAME**: Was used to build the App name on a Heroku hosted webapp, but from v1.1, you can just set the WEBAPP_URL environment variable. +- **NEXT_PUBLIC_WEBAPP_URL**: Was used for the same purpose as WEBAPP_URL, but from v1.1, you can just set the WEBAPP_URL environment variable. +- **PRISMA_GENERATE_DATAPROXY**: Was used to tell Prisma that it should generate the runtime for Dataproxy usage. But its officially deprecated now. + +## Helper Shell Script +For a seamless migration, below is a shell script for your self-hosted instance that will automatically update your environment variables to be compliant with the new naming conventions. + +### Building From Source +The below script will: +1. Create a backup of your existing .env file as `.env.old` +2. Update the .env file to be compliant with the new naming conventions + + +```shell {{ title: '.env file' }} +for var in NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED NEXT_PUBLIC_PASSWORD_RESET_DISABLED NEXT_PUBLIC_SIGNUP_DISABLED NEXT_PUBLIC_INVITE_DISABLED NEXT_PUBLIC_PRIVACY_URL NEXT_PUBLIC_TERMS_URL NEXT_PUBLIC_IMPRINT_URL NEXT_PUBLIC_GITHUB_AUTH_ENABLED NEXT_PUBLIC_GOOGLE_AUTH_ENABLED NEXT_PUBLIC_WEBAPP_URL NEXT_PUBLIC_IS_FORMBRICKS_CLOUD NEXT_PUBLIC_SURVEY_BASE_URL; do sed -i.old "s/^$var=/$(echo $var | sed 's/NEXT_PUBLIC_//')=/" .env; done; echo "Formbricks environment variables have been migrated as per v1.1! You are good to go." +``` + + + +### Docker & Single Script Setup + +Now that these variables can be defined at runtime, you can append them inside your `x-environment` in the `docker-compose.yml` itself. +For a more detailed guide on these environment variables, please refer to the [Important Runtime Variables](/docs/self-hosting/from-source#important-run-time-variables) section. + + +```yaml {{ title: 'docker-compose.yml' }} +version: "3.3" +x-environment: &environment + environment: + # The url of your Formbricks instance used in the admin panel + WEBAPP_URL: + + # PostgreSQL DB for Formbricks to connect to + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" + + # Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy + # Cold boots will be faster and you'll be able to scale your DB independently of your app. + # @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy + # PRISMA_GENERATE_DATAPROXY=true + PRISMA_GENERATE_DATAPROXY: + + # NextJS Auth + # @see: https://next-auth.js.org/configuration/options#nextauth_secret + # You can use: `openssl rand -base64 32` to generate one + NEXTAUTH_SECRET: + + # Set this to your public-facing URL, e.g., https://example.com + # You do not need the NEXTAUTH_URL environment variable in Vercel. + NEXTAUTH_URL: http://localhost:3000 + + # PostgreSQL password + POSTGRES_PASSWORD: postgres + + # Email Configuration + MAIL_FROM: + SMTP_HOST: + SMTP_PORT: + SMTP_SECURE_ENABLED: + SMTP_USER: + SMTP_PASSWORD: + + # Uncomment the below and set it to 1 to disable Email Verification for new signups + # EMAIL_VERIFICATION_DISABLED: + + # Uncomment the below and set it to 1 to disable Password Reset + # PASSWORD_RESET_DISABLED: + + # Uncomment the below and set it to 1 to disable Signups + # SIGNUP_DISABLED: + + # Uncomment the below and set it to 1 to disable Invites + # INVITE_DISABLED: + + # Uncomment the below and set a value to have your own Privacy Page URL on the signup & login page + # PRIVACY_URL: + + # Uncomment the below and set a value to have your own Terms Page URL on the auth and the surveys page + # TERMS_URL: + + # Uncomment the below and set a value to have your own Imprint Page URL on the auth and the surveys page + # IMPRINT_URL: + + # Uncomment the below and set to 1 if you want to enable GitHub OAuth + # GITHUB_AUTH_ENABLED: + # GITHUB_ID: + # GITHUB_SECRET: + + # Uncomment the below and set to 1 if you want to enable Google OAuth + # GOOGLE_AUTH_ENABLED: + # GOOGLE_CLIENT_ID: + # GOOGLE_CLIENT_SECRET: + +``` + + +Did we miss something? Are you still facing issues migrating your app? [Join our Discord!](https://formbricks.com/discord) We'd be happy to help! \ No newline at end of file diff --git a/apps/formbricks-com/components/docs/Navigation.tsx b/apps/formbricks-com/components/docs/Navigation.tsx index 0608c66539..c6aa78fe74 100644 --- a/apps/formbricks-com/components/docs/Navigation.tsx +++ b/apps/formbricks-com/components/docs/Navigation.tsx @@ -246,6 +246,7 @@ export const navigation: Array = [ { title: "Production", href: "/docs/self-hosting/production" }, { title: "Docker", href: "/docs/self-hosting/docker" }, { title: "From Source", href: "/docs/self-hosting/from-source" }, + { title: "Migration to v1.1", href: "/docs/self-hosting/migrating-to-1.1" }, ], }, { diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx index 7c21c62163..104a0f59dc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx @@ -22,5 +22,5 @@ export default function ActionsAttributesTabs({ activeId, environmentId }: Actio }, ]; - return ; + return ; } diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx index c6beed9eaf..54e13b363a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx @@ -1,21 +1,67 @@ +"use client"; + import LoadingSpinner from "@/components/shared/LoadingSpinner"; import { ErrorComponent } from "@formbricks/ui"; import { Label } from "@formbricks/ui"; -import { useEventClass } from "@/lib/eventClasses/eventClasses"; import { convertDateTimeStringShort } from "@formbricks/lib/time"; import { capitalizeFirstLetter } from "@/lib/utils"; import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid"; - +import { TActionClass } from "@formbricks/types/v1/actionClasses"; +import { useEffect, useState } from "react"; +import { + getActionCountInLastHourAction, + getActionCountInLast24HoursAction, + getActionCountInLast7DaysAction, + GetActiveInactiveSurveysAction, +} from "./actions"; interface ActivityTabProps { - environmentId: string; - actionClassId: string; + actionClass: TActionClass; } -export default function EventActivityTab({ environmentId, actionClassId }: ActivityTabProps) { - const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClassId); +export default function EventActivityTab({ actionClass }: ActivityTabProps) { + // const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClass.id); - if (isLoadingEventClass) return ; - if (isErrorEventClass) return ; + const [numEventsLastHour, setNumEventsLastHour] = useState(); + const [numEventsLast24Hours, setNumEventsLast24Hours] = useState(); + const [numEventsLast7Days, setNumEventsLast7Days] = useState(); + const [activeSurveys, setActiveSurveys] = useState(); + const [inactiveSurveys, setInactiveSurveys] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setLoading(true); + updateState(); + + async function updateState() { + try { + setLoading(true); + const [ + numEventsLastHourData, + numEventsLast24HoursData, + numEventsLast7DaysData, + activeInactiveSurveys, + ] = await Promise.all([ + getActionCountInLastHourAction(actionClass.id), + getActionCountInLast24HoursAction(actionClass.id), + getActionCountInLast7DaysAction(actionClass.id), + GetActiveInactiveSurveysAction(actionClass.id), + ]); + setNumEventsLastHour(numEventsLastHourData); + setNumEventsLast24Hours(numEventsLast24HoursData); + setNumEventsLast7Days(numEventsLast7DaysData); + setActiveSurveys(activeInactiveSurveys.activeSurveys); + setInactiveSurveys(activeInactiveSurveys.inactiveSurveys); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + } + }, [actionClass.id]); + + if (loading) return ; + if (error) return ; return (
    @@ -24,15 +70,15 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
    -

    {eventClass.numEventsLastHour}

    +

    {numEventsLastHour}

    last hour

    -

    {eventClass.numEventsLast24Hours}

    +

    {numEventsLast24Hours}

    last 24 hours

    -

    {eventClass.numEventsLast7Days}

    +

    {numEventsLast7Days}

    last week

    @@ -40,8 +86,8 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
    - {eventClass.activeSurveys.length === 0 &&

    -

    } - {eventClass.activeSurveys.map((surveyName) => ( + {activeSurveys?.length === 0 &&

    -

    } + {activeSurveys?.map((surveyName) => (

    {surveyName}

    @@ -49,8 +95,8 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
    - {eventClass.inactiveSurveys.length === 0 &&

    -

    } - {eventClass.inactiveSurveys.map((surveyName) => ( + {inactiveSurveys?.length === 0 &&

    -

    } + {inactiveSurveys?.map((surveyName) => (

    {surveyName}

    @@ -61,28 +107,28 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ

    - {convertDateTimeStringShort(eventClass.createdAt?.toString())} + {convertDateTimeStringShort(actionClass.createdAt?.toString())}

    {" "}

    - {convertDateTimeStringShort(eventClass.updatedAt?.toString())} + {convertDateTimeStringShort(actionClass.updatedAt?.toString())}

    - {eventClass.type === "code" ? ( + {actionClass.type === "code" ? ( - ) : eventClass.type === "noCode" ? ( + ) : actionClass.type === "noCode" ? ( - ) : eventClass.type === "automatic" ? ( + ) : actionClass.type === "automatic" ? ( ) : null}
    -

    {capitalizeFirstLetter(eventClass.type)}

    +

    {capitalizeFirstLetter(actionClass.type)}

    diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx index 1017545e98..f0362ac33c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx @@ -20,7 +20,7 @@ export default function ActionDetailModal({ const tabs = [ { title: "Activity", - children: , + children: , }, { title: "Settings", diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts new file mode 100644 index 0000000000..844156de4e --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts @@ -0,0 +1,31 @@ +"use server"; + +import { + getActionCountInLast24Hours, + getActionCountInLast7Days, + getActionCountInLastHour, +} from "@formbricks/lib/services/actions"; +import { getSurveysByActionClassId } from "@formbricks/lib/services/survey"; + +export const getActionCountInLastHourAction = async (actionClassId: string) => { + return await getActionCountInLastHour(actionClassId); +}; + +export const getActionCountInLast24HoursAction = async (actionClassId: string) => { + return await getActionCountInLast24Hours(actionClassId); +}; + +export const getActionCountInLast7DaysAction = async (actionClassId: string) => { + return await getActionCountInLast7Days(actionClassId); +}; + +export const GetActiveInactiveSurveysAction = async ( + actionClassId: string +): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => { + const surveys = await getSurveysByActionClassId(actionClassId); + const response = { + activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name), + inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name), + }; + return response; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx index 0b66b3d820..54eb7d067c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx @@ -1,31 +1,53 @@ +"use client"; + +import { GetActiveInactiveSurveysAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions"; import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useAttributeClass } from "@/lib/attributeClasses/attributeClasses"; import { capitalizeFirstLetter } from "@/lib/utils"; import { convertDateTimeStringShort } from "@formbricks/lib/time"; +import { TAttributeClass } from "@formbricks/types/v1/attributeClasses"; import { ErrorComponent, Label } from "@formbricks/ui"; import { TagIcon } from "@heroicons/react/24/solid"; +import { useEffect, useState } from "react"; interface EventActivityTabProps { - attributeClassId: string; - environmentId: string; + attributeClass: TAttributeClass; } -export default function AttributeActivityTab({ environmentId, attributeClassId }: EventActivityTabProps) { - const { attributeClass, isLoadingAttributeClass, isErrorAttributeClass } = useAttributeClass( - environmentId, - attributeClassId - ); +export default function AttributeActivityTab({ attributeClass }: EventActivityTabProps) { + const [activeSurveys, setActiveSurveys] = useState(); + const [inactiveSurveys, setInactiveSurveys] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - if (isLoadingAttributeClass) return ; - if (isErrorAttributeClass) return ; + useEffect(() => { + setLoading(true); + + getSurveys(); + + async function getSurveys() { + try { + setLoading(true); + const activeInactive = await GetActiveInactiveSurveysAction(attributeClass.id); + setActiveSurveys(activeInactive.activeSurveys); + setInactiveSurveys(activeInactive.inactiveSurveys); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + } + }, [attributeClass.id]); + + if (loading) return ; + if (error) return ; return (
    - {attributeClass.activeSurveys.length === 0 &&

    -

    } - {attributeClass.activeSurveys.map((surveyName) => ( + {activeSurveys?.length === 0 &&

    -

    } + {activeSurveys?.map((surveyName) => (

    {surveyName}

    @@ -33,8 +55,8 @@ export default function AttributeActivityTab({ environmentId, attributeClassId }
    - {attributeClass.inactiveSurveys.length === 0 &&

    -

    } - {attributeClass.inactiveSurveys.map((surveyName) => ( + {inactiveSurveys?.length === 0 &&

    -

    } + {inactiveSurveys?.map((surveyName) => (

    {surveyName}

    diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx index cfce41c9d9..f76eecf980 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx @@ -8,11 +8,9 @@ import { useMemo } from "react"; import { TAttributeClass } from "@formbricks/types/v1/attributeClasses"; export default function AttributeClassesTable({ - environmentId, attributeClasses, children: [TableHeading, howToAddAttributeButton, attributeRows], }: { - environmentId: string; attributeClasses: TAttributeClass[]; children: [JSX.Element, JSX.Element, JSX.Element[]]; }) { @@ -69,7 +67,6 @@ export default function AttributeClassesTable({ ))}
    void; - attributeClass: AttributeClass; + attributeClass: TAttributeClass; } -export default function AttributeDetailModal({ - environmentId, - open, - setOpen, - attributeClass, -}: AttributeDetailModalProps) { +export default function AttributeDetailModal({ open, setOpen, attributeClass }: AttributeDetailModalProps) { const tabs = [ { title: "Activity", - children: , + children: , }, { title: "Settings", diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts new file mode 100644 index 0000000000..55f9b34315 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts @@ -0,0 +1,14 @@ +"use server"; + +import { getSurveysByAttributeClassId } from "@formbricks/lib/services/survey"; + +export const GetActiveInactiveSurveysAction = async ( + attributeClassId: string +): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => { + const surveys = await getSurveysByAttributeClassId(attributeClassId); + const response = { + activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name), + inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name), + }; + return response; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx index 37cea27263..6a37ff53bf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx @@ -14,9 +14,10 @@ export const metadata: Metadata = { export default async function AttributesPage({ params }) { let attributeClasses = await getAttributeClasses(params.environmentId); + return ( <> - + diff --git a/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx b/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx index 2548948f04..2a0dab6026 100644 --- a/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx @@ -1,7 +1,7 @@ "use client"; +import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions"; import Modal from "@/components/shared/Modal"; -import { createProduct } from "@/lib/products/products"; import { Button, Input, Label } from "@formbricks/ui"; import { PlusCircleIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; @@ -19,9 +19,9 @@ export default function AddProductModal({ environmentId, open, setOpen }: AddPro const [loading, setLoading] = useState(false); const { register, handleSubmit } = useForm(); - const submitProduct = async (data) => { + const submitProduct = async (data: { name: string }) => { setLoading(true); - const newEnv = await createProduct(environmentId, data); + const newEnv = await createProductAction(environmentId, data.name); router.push(`/environments/${newEnv.id}/`); setOpen(false); setLoading(false); diff --git a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx index 701c29c1de..8e5296e288 100644 --- a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx @@ -1,7 +1,7 @@ export const revalidate = REVALIDATION_INTERVAL; import Navigation from "@/app/(app)/environments/[environmentId]/Navigation"; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment"; import { getProducts } from "@formbricks/lib/services/product"; import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/services/team"; @@ -11,6 +11,7 @@ import type { Session } from "next-auth"; interface EnvironmentsNavbarProps { environmentId: string; session: Session; + isFormbricksCloud: boolean; } export default async function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) { @@ -41,6 +42,7 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env products={products} environments={environments} session={session} + isFormbricksCloud={IS_FORMBRICKS_CLOUD} /> ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx index c2790c8e35..00a054f784 100644 --- a/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx @@ -21,7 +21,6 @@ import { formbricksLogout } from "@/lib/formbricks"; import { capitalizeFirstLetter, truncate } from "@/lib/utils"; import formbricks from "@formbricks/js"; import { cn } from "@formbricks/lib/cn"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { TEnvironment } from "@formbricks/types/v1/environment"; import { TProduct } from "@formbricks/types/v1/product"; import { TTeam } from "@formbricks/types/v1/teams"; @@ -71,6 +70,7 @@ interface NavigationProps { team: TTeam; products: TProduct[]; environments: TEnvironment[]; + isFormbricksCloud: boolean; } export default function Navigation({ @@ -80,6 +80,7 @@ export default function Navigation({ session, products, environments, + isFormbricksCloud, }: NavigationProps) { const router = useRouter(); const pathname = usePathname(); @@ -171,7 +172,7 @@ export default function Navigation({ icon: CreditCardIcon, label: "Billing & Plan", href: `/environments/${environment.id}/settings/billing`, - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, ], }, @@ -453,7 +454,7 @@ export default function Navigation({ ))} - {IS_FORMBRICKS_CLOUD && ( + {isFormbricksCloud && (
    - +
    ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx index 9914eb783d..243a5bb899 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx @@ -3,20 +3,21 @@ import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator"; import { TResponseWithSurvey } from "@formbricks/types/v1/responses"; import Link from "next/link"; +import { TEnvironment } from "@formbricks/types/v1/environment"; export default function ResponseFeed({ responses, sortByDate, - environmentId, + environment, }: { responses: TResponseWithSurvey[]; sortByDate: boolean; - environmentId: string; + environment: TEnvironment; }) { return ( <> {responses.length === 0 ? ( - + ) : (
    {responses @@ -53,12 +54,12 @@ export default function ResponseFeed({
    + href={`/environments/${environment.id}/surveys/${response.survey.id}/summary`}> {response.survey.name}
    diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx index 02fd8bc4db..9bb2fbf8fd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx @@ -5,8 +5,13 @@ import AttributesSection from "@/app/(app)/environments/[environmentId]/people/[ import ResponseSection from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection"; import HeadingSection from "@/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection"; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getEnvironment } from "@formbricks/lib/services/environment"; export default async function PersonPage({ params }) { + const environment = await getEnvironment(params.environmentId); + if (!environment) { + throw new Error("Environment not found"); + } return (
    @@ -15,7 +20,7 @@ export default async function PersonPage({ params }) {
    - +
    diff --git a/apps/web/app/(app)/environments/[environmentId]/people/page.tsx b/apps/web/app/(app)/environments/[environmentId]/people/page.tsx index bcd734c513..18e234457d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/page.tsx @@ -3,6 +3,7 @@ export const revalidate = REVALIDATION_INTERVAL; import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; import { truncateMiddle } from "@/lib/utils"; import { PEOPLE_PER_PAGE, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getEnvironment } from "@formbricks/lib/services/environment"; import { getPeople, getPeopleCount } from "@formbricks/lib/services/person"; import { TPerson } from "@formbricks/types/v1/people"; import { Pagination, PersonAvatar } from "@formbricks/ui"; @@ -19,7 +20,13 @@ export default async function PeoplePage({ searchParams: { [key: string]: string | string[] | undefined }; }) { const pageNumber = searchParams.page ? parseInt(searchParams.page as string) : 1; - const totalPeople = await getPeopleCount(params.environmentId); + const [environment, totalPeople] = await Promise.all([ + getEnvironment(params.environmentId), + getPeopleCount(params.environmentId), + ]); + if (!environment) { + throw new Error("Environment not found"); + } const maxPageNumber = Math.ceil(totalPeople / PEOPLE_PER_PAGE); let hidePagination = false; @@ -37,7 +44,7 @@ export default async function PeoplePage({ {people.length === 0 ? ( ) : ( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/SettingsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/SettingsNavbar.tsx index 82fd341905..de2e6975ca 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/SettingsNavbar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/SettingsNavbar.tsx @@ -1,9 +1,8 @@ "use client"; -import { useProduct } from "@/lib/products/products"; -import { useTeam } from "@/lib/teams/teams"; import { truncate } from "@/lib/utils"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { TProduct } from "@formbricks/types/v1/product"; +import { TTeam } from "@formbricks/types/v1/teams"; import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui"; import { ChevronDownIcon } from "@heroicons/react/20/solid"; import { @@ -25,10 +24,18 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useMemo, useState } from "react"; -export default function SettingsNavbar({ environmentId }: { environmentId: string }) { +export default function SettingsNavbar({ + environmentId, + isFormbricksCloud, + team, + product, +}: { + environmentId: string; + isFormbricksCloud: boolean; + team: TTeam; + product: TProduct; +}) { const pathname = usePathname(); - const { team } = useTeam(environmentId); - const { product } = useProduct(environmentId); const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false); interface NavigationLink { @@ -113,7 +120,7 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin name: "Billing & Plan", href: `/environments/${environmentId}/settings/billing`, icon: CreditCardIcon, - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, current: pathname?.includes("/billing"), }, ], @@ -152,21 +159,21 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin href: "https://formbricks.com/gdpr", icon: LinkIcon, target: "_blank", - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, { name: "Privacy", href: "https://formbricks.com/privacy", icon: LinkIcon, target: "_blank", - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, { name: "Terms", href: "https://formbricks.com/terms", icon: LinkIcon, target: "_blank", - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, { name: "License", @@ -178,7 +185,7 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin ], }, ], - [environmentId, pathname] + [environmentId, isFormbricksCloud, pathname] ); if (!navigation) return null; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx index d2d21c1c9e..d71594bf91 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx @@ -1,6 +1,7 @@ export const revalidate = REVALIDATION_INTERVAL; -import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx index d990621ed0..08561f7430 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx @@ -1,15 +1,33 @@ import { Metadata } from "next"; import SettingsNavbar from "./SettingsNavbar"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; +import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; export const metadata: Metadata = { title: "Settings", }; -export default function SettingsLayout({ children, params }) { +export default async function SettingsLayout({ children, params }) { + const [team, product] = await Promise.all([ + getTeamByEnvironmentId(params.environmentId), + getProductByEnvironmentId(params.environmentId), + ]); + if (!team) { + throw new Error("Team not found"); + } + if (!product) { + throw new Error("Product not found"); + } return ( <>
    - +
    {children}
    diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx index 093d950c81..71d68b838c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx @@ -1,7 +1,6 @@ "use client"; import { cn } from "@formbricks/lib/cn"; -import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants"; import { TProduct } from "@formbricks/types/v1/product"; import { Button, ColorPicker, Label, Switch } from "@formbricks/ui"; import { useState } from "react"; @@ -10,11 +9,12 @@ import { updateProductAction } from "./actions"; interface EditHighlightBorderProps { product: TProduct; + defaultBrandColor: string; } -export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => { +export const EditHighlightBorder = ({ product, defaultBrandColor }: EditHighlightBorderProps) => { const [showHighlightBorder, setShowHighlightBorder] = useState(product.highlightBorderColor ? true : false); - const [color, setColor] = useState(product.highlightBorderColor || DEFAULT_BRAND_COLOR); + const [color, setColor] = useState(product.highlightBorderColor || defaultBrandColor); const [updatingBorder, setUpdatingBorder] = useState(false); const handleUpdateHighlightBorder = async () => { @@ -32,7 +32,7 @@ export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => { const handleSwitch = (checked: boolean) => { if (checked) { if (!color) { - setColor(DEFAULT_BRAND_COLOR); + setColor(defaultBrandColor); setShowHighlightBorder(true); } else { setShowHighlightBorder(true); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx index f1ce4391ef..8c725ae937 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx @@ -8,6 +8,7 @@ import { EditFormbricksSignature } from "./EditSignature"; import { EditBrandColor } from "./EditBrandColor"; import { EditPlacement } from "./EditPlacement"; import { EditHighlightBorder } from "./EditHighlightBorder"; +import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants"; export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) { const product = await getProductByEnvironmentId(params.environmentId); @@ -29,7 +30,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro noPadding title="Highlight Border" description="Make sure your users notice the survey you display"> - + Create New Team - {env.NEXT_PUBLIC_INVITE_DISABLED !== "1" && isAdminOrOwner && ( + {!isInviteDisabled && isAdminOrOwner && ( @@ -94,7 +83,7 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa disabled={isStatusChangeDisabled} style={isStatusChangeDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}}>
    - + {survey.status === "inProgress" && "In-progress"} {survey.status === "paused" && "Paused"} @@ -107,7 +96,8 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa { - triggerSurveyMutate({ status: value }) + const castedValue = value as "draft" | "inProgress" | "paused" | "completed"; + surveyMutateAction({ ...survey, status: castedValue }) .then(() => { toast.success( value === "inProgress" @@ -152,14 +142,14 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa variant="darkCTA" size="sm" className="flex h-full w-full justify-center px-3 lg:px-6" - href={`/environments/${environmentId}/surveys/${surveyId}/edit`}> + href={`/environments/${environment.id}/surveys/${surveyId}/edit`}> Edit
    - +
    ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx index 8911641ef4..1271d3a195 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx @@ -1,10 +1,8 @@ "use client"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useProduct } from "@/lib/products/products"; import { getQuestionDefaults, questionTypes, universalQuestionPresets } from "@/lib/questions"; import { cn } from "@formbricks/lib/cn"; -import { ErrorComponent } from "@formbricks/ui"; +import { TProduct } from "@formbricks/types/v1/product"; import { PlusIcon } from "@heroicons/react/24/solid"; import { createId } from "@paralleldrive/cuid2"; import * as Collapsible from "@radix-ui/react-collapsible"; @@ -12,17 +10,12 @@ import { useState } from "react"; interface AddQuestionButtonProps { addQuestion: (question: any) => void; - environmentId: string; + product: TProduct; } -export default function AddQuestionButton({ addQuestion, environmentId }: AddQuestionButtonProps) { - const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); - +export default function AddQuestionButton({ addQuestion, product }: AddQuestionButtonProps) { const [open, setOpen] = useState(false); - if (isLoadingProduct) return ; - if (isErrorProduct) return ; - return ( void; activeQuestionId: string | null; setActiveQuestionId: (questionId: string | null) => void; - environmentId: string; + product: TProduct; invalidQuestions: String[] | null; setInvalidQuestions: (invalidQuestions: String[] | null) => void; } @@ -28,7 +29,7 @@ export default function QuestionsView({ setActiveQuestionId, localSurvey, setLocalSurvey, - environmentId, + product, invalidQuestions, setInvalidQuestions, }: QuestionsViewProps) { @@ -58,7 +59,7 @@ export default function QuestionsView({ }; // function to validate individual questions - const validateSurvey = (question: Question) => { + const validateSurvey = (question: TSurveyQuestion) => { // prevent this function to execute further if user hasnt still tried to save the survey if (invalidQuestions === null) { return; @@ -212,7 +213,7 @@ export default function QuestionsView({
    - +
    { if (autoComplete) { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx index ead7a41912..6f3d1f29d9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx @@ -46,7 +46,7 @@ export default function SurveyEditor({ // when the survey type changes, we need to reset the active question id to the first question useEffect(() => { - if (localSurvey && localSurvey.questions?.length > 0) { + if (localSurvey?.questions?.length && localSurvey.questions.length > 0) { setActiveQuestionId(localSurvey.questions[0].id); } }, [localSurvey?.type]); @@ -77,7 +77,7 @@ export default function SurveyEditor({ setLocalSurvey={setLocalSurvey} activeQuestionId={activeQuestionId} setActiveQuestionId={setActiveQuestionId} - environmentId={environment.id} + product={product} invalidQuestions={invalidQuestions} setInvalidQuestions={setInvalidQuestions} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx index 822ff1c9fe..7c455a50dc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx @@ -205,8 +205,8 @@ export default function SurveyMenuBar({
    diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx index 5b46da1f36..b9e9be88ac 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx @@ -16,7 +16,7 @@ import { } from "@formbricks/ui"; import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { TActionClass } from "@formbricks/types/v1/actionClasses"; @@ -40,30 +40,36 @@ export default function WhenToSendCard({ const autoClose = localSurvey.autoClose !== null; - let newTrigger = { - id: "", // Set the appropriate value for the id - createdAt: new Date(), - updatedAt: new Date(), - name: "", - type: "code" as const, // Set the appropriate value for the type - environmentId: "", - description: null, - noCodeConfig: null, - }; + let newTrigger = useMemo( + () => ({ + id: "", // Set the appropriate value for the id + createdAt: new Date(), + updatedAt: new Date(), + name: "", + type: "code" as const, // Set the appropriate value for the type + environmentId: "", + description: null, + noCodeConfig: null, + }), + [] + ); - const addTriggerEvent = () => { + const addTriggerEvent = useCallback(() => { const updatedSurvey = { ...localSurvey }; updatedSurvey.triggers = [...localSurvey.triggers, newTrigger]; setLocalSurvey(updatedSurvey); - }; + }, [newTrigger, localSurvey, setLocalSurvey]); - const setTriggerEvent = (idx: number, actionClassId: string) => { - const updatedSurvey = { ...localSurvey }; - updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => { - return actionClass.id === actionClassId; - })!; - setLocalSurvey(updatedSurvey); - }; + const setTriggerEvent = useCallback( + (idx: number, actionClassId: string) => { + const updatedSurvey = { ...localSurvey }; + updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => { + return actionClass.id === actionClassId; + })!; + setLocalSurvey(updatedSurvey); + }, + [actionClassArray, localSurvey, setLocalSurvey] + ); const removeTriggerEvent = (idx: number) => { const updatedSurvey = { ...localSurvey }; @@ -95,11 +101,12 @@ export default function WhenToSendCard({ const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, delay: value }; setLocalSurvey(updatedSurvey); }; + useEffect(() => { if (activeIndex !== null) { setTriggerEvent(activeIndex, actionClassArray[actionClassArray.length - 1].id); } - }, [actionClassArray]); + }, [actionClassArray, activeIndex, setTriggerEvent]); useEffect(() => { if (localSurvey.type === "link") { @@ -112,7 +119,7 @@ export default function WhenToSendCard({ if (localSurvey.triggers.length === 0) { addTriggerEvent(); } - }, []); + }, [addTriggerEvent, localSurvey.triggers.length]); return ( <> diff --git a/apps/web/app/(auth)/auth/login/page.tsx b/apps/web/app/(auth)/auth/login/page.tsx index f3521ccb2b..e778c9d5ec 100644 --- a/apps/web/app/(auth)/auth/login/page.tsx +++ b/apps/web/app/(auth)/auth/login/page.tsx @@ -2,6 +2,12 @@ import { SigninForm } from "@/components/auth/SigninForm"; import Testimonial from "@/components/auth/Testimonial"; import FormWrapper from "@/components/auth/FormWrapper"; import { Metadata } from "next"; +import { + GITHUB_OAUTH_ENABLED, + GOOGLE_OAUTH_ENABLED, + PASSWORD_RESET_DISABLED, + SIGNUP_ENABLED, +} from "@formbricks/lib/constants"; export const metadata: Metadata = { title: "Login", @@ -16,7 +22,12 @@ export default function SignInPage() {
    - +
    diff --git a/apps/web/app/(auth)/auth/signup/page.tsx b/apps/web/app/(auth)/auth/signup/page.tsx index a5f519967d..76f3b02d47 100644 --- a/apps/web/app/(auth)/auth/signup/page.tsx +++ b/apps/web/app/(auth)/auth/signup/page.tsx @@ -1,15 +1,25 @@ -"use client"; - import Link from "next/link"; -import { useSearchParams } from "next/navigation"; import { SignupForm } from "@/components/auth/SignupForm"; import FormWrapper from "@/components/auth/FormWrapper"; import Testimonial from "@/components/auth/Testimonial"; -import { env } from "@/env.mjs"; +import { + EMAIL_VERIFICATION_DISABLED, + GITHUB_OAUTH_ENABLED, + GOOGLE_OAUTH_ENABLED, + INVITE_DISABLED, + PASSWORD_RESET_DISABLED, + PRIVACY_URL, + SIGNUP_ENABLED, + TERMS_URL, + WEBAPP_URL, +} from "@formbricks/lib/constants"; -export default function SignUpPage() { - const searchParams = useSearchParams(); - const inviteToken = searchParams?.get("inviteToken"); +export default function SignUpPage({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { + const inviteToken = searchParams["inviteToken"] ?? null; return (
    @@ -18,9 +28,7 @@ export default function SignUpPage() {
    - {( - inviteToken ? env.NEXT_PUBLIC_INVITE_DISABLED === "1" : env.NEXT_PUBLIC_SIGNUP_DISABLED === "1" - ) ? ( + {(inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED) ? ( <>

    Sign up disabled

    @@ -35,7 +43,15 @@ export default function SignUpPage() { ) : ( - + )}

    diff --git a/apps/web/app/(auth)/invite/page.tsx b/apps/web/app/(auth)/invite/page.tsx index 67a8a4dc81..473168ce96 100644 --- a/apps/web/app/(auth)/invite/page.tsx +++ b/apps/web/app/(auth)/invite/page.tsx @@ -2,7 +2,6 @@ import { sendInviteAcceptedEmail } from "@/lib/email"; import { verifyInviteToken } from "@formbricks/lib/jwt"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { getServerSession } from "next-auth"; -import { env } from "process"; import { prisma } from "@formbricks/database"; import { NotLoggedInContent, @@ -11,6 +10,7 @@ import { UsedContent, RightAccountContent, } from "./InviteContentComponents"; +import { env } from "@/env.mjs"; export default async function JoinTeam({ searchParams }) { const currentUser = await getServerSession(authOptions); diff --git a/apps/web/app/api/auth/[...nextauth]/authOptions.ts b/apps/web/app/api/auth/[...nextauth]/authOptions.ts index 0351fb872a..5dbdefac8b 100644 --- a/apps/web/app/api/auth/[...nextauth]/authOptions.ts +++ b/apps/web/app/api/auth/[...nextauth]/authOptions.ts @@ -1,7 +1,7 @@ import { env } from "@/env.mjs"; import { verifyPassword } from "@/lib/auth"; import { prisma } from "@formbricks/database"; -import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { EMAIL_VERIFICATION_DISABLED, INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; import { verifyToken } from "@formbricks/lib/jwt"; import { getProfileByEmail } from "@formbricks/lib/services/profile"; import type { IdentityProvider } from "@prisma/client"; @@ -166,7 +166,7 @@ export const authOptions: NextAuthOptions = { }, async signIn({ user, account }: any) { if (account.provider === "credentials" || account.provider === "token") { - if (!user.emailVerified && env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") { + if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`; } return true; diff --git a/apps/web/app/api/cron/close_surveys/route.ts b/apps/web/app/api/cron/close_surveys/route.ts index 320571e0bf..41b06ddfb3 100644 --- a/apps/web/app/api/cron/close_surveys/route.ts +++ b/apps/web/app/api/cron/close_surveys/route.ts @@ -1,13 +1,13 @@ -import { env } from "@/env.mjs"; import { responses } from "@/lib/api/response"; import { prisma } from "@formbricks/database"; +import { CRON_SECRET } from "@formbricks/lib/constants"; import { headers } from "next/headers"; export async function POST() { const headersList = headers(); const apiKey = headersList.get("x-api-key"); - if (!apiKey || apiKey !== env.CRON_SECRET) { + if (!apiKey || apiKey !== CRON_SECRET) { return responses.notAuthenticatedResponse(); } diff --git a/apps/web/app/api/google-sheet/callback/route.ts b/apps/web/app/api/google-sheet/callback/route.ts index 8644ce18d8..e2c4540adc 100644 --- a/apps/web/app/api/google-sheet/callback/route.ts +++ b/apps/web/app/api/google-sheet/callback/route.ts @@ -1,6 +1,10 @@ -import { env } from "@/env.mjs"; import { prisma } from "@formbricks/database"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; +import { + GOOGLE_SHEETS_CLIENT_ID, + WEBAPP_URL, + GOOGLE_SHEETS_CLIENT_SECRET, + GOOGLE_SHEETS_REDIRECT_URL, +} from "@formbricks/lib/constants"; import { google } from "googleapis"; import { NextRequest, NextResponse } from "next/server"; @@ -18,9 +22,9 @@ export async function GET(req: NextRequest) { return NextResponse.json({ message: "`code` must be a string" }, { status: 400 }); } - const client_id = env.GOOGLE_SHEETS_CLIENT_ID; - const client_secret = env.GOOGLE_SHEETS_CLIENT_SECRET; - const redirect_uri = env.GOOGLE_SHEETS_REDIRECT_URL; + const client_id = GOOGLE_SHEETS_CLIENT_ID; + const client_secret = GOOGLE_SHEETS_CLIENT_SECRET; + const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL; if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 }); if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 }); if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 }); diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index 63bd97d0a2..475bc8a268 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -1,5 +1,9 @@ import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; -import { env } from "@/env.mjs"; +import { + GOOGLE_SHEETS_CLIENT_ID, + GOOGLE_SHEETS_CLIENT_SECRET, + GOOGLE_SHEETS_REDIRECT_URL, +} from "@formbricks/lib/constants"; import { google } from "googleapis"; import { NextRequest, NextResponse } from "next/server"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; @@ -24,9 +28,9 @@ export async function GET(req: NextRequest) { return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 }); } - const client_id = env.GOOGLE_SHEETS_CLIENT_ID; - const client_secret = env.GOOGLE_SHEETS_CLIENT_SECRET; - const redirect_uri = env.GOOGLE_SHEETS_REDIRECT_URL; + const client_id = GOOGLE_SHEETS_CLIENT_ID; + const client_secret = GOOGLE_SHEETS_CLIENT_SECRET; + const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL; if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 }); if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 }); if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 }); diff --git a/apps/web/app/api/v1/users/route.ts b/apps/web/app/api/v1/users/route.ts index 148df1729e..16fefd3f2c 100644 --- a/apps/web/app/api/v1/users/route.ts +++ b/apps/web/app/api/v1/users/route.ts @@ -3,13 +3,18 @@ import { verifyInviteToken } from "@formbricks/lib/jwt"; import { populateEnvironment } from "@/lib/populate"; import { prisma } from "@formbricks/database"; import { NextResponse } from "next/server"; -import { env } from "@/env.mjs"; import { Prisma } from "@prisma/client"; -import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { + EMAIL_VERIFICATION_DISABLED, + INTERNAL_SECRET, + INVITE_DISABLED, + SIGNUP_ENABLED, + WEBAPP_URL, +} from "@formbricks/lib/constants"; export async function POST(request: Request) { let { inviteToken, ...user } = await request.json(); - if (inviteToken ? env.NEXT_PUBLIC_INVITE_DISABLED === "1" : env.NEXT_PUBLIC_SIGNUP_DISABLED === "1") { + if (inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED) { return NextResponse.json({ error: "Signup disabled" }, { status: 403 }); } user = { ...user, ...{ email: user.email.toLowerCase() } }; @@ -117,7 +122,7 @@ export async function POST(request: Request) { await prisma.invite.delete({ where: { id: inviteId } }); } - if (env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") { + if (!EMAIL_VERIFICATION_DISABLED) { await sendVerificationEmail(userData); } return NextResponse.json(userData); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 8c0a36a8b7..30ac126219 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,28 +1,10 @@ import ClientLogout from "@/app/ClientLogout"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; +import { getEnvironmentByUser } from "@formbricks/lib/services/environment"; import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; -import { headers } from "next/headers"; import { redirect } from "next/navigation"; -async function getEnvironment() { - const cookie = headers().get("cookie") || ""; - const res = await fetch(`${WEBAPP_URL}/api/v1/environments/find-first`, { - headers: { - cookie, - }, - }); - - if (!res.ok) { - const error = await res.json(); - console.error(error); - throw new Error("Failed to fetch data"); - } - - return res.json(); -} - export default async function Home() { const session: Session | null = await getServerSession(authOptions); @@ -36,7 +18,7 @@ export default async function Home() { let environment; try { - environment = await getEnvironment(); + environment = await getEnvironmentByUser(session?.user); } catch (error) { console.error("error getting environment", error); } diff --git a/apps/web/app/s/[surveyId]/LegalFooter.tsx b/apps/web/app/s/[surveyId]/LegalFooter.tsx index 583d4582ef..dbe33b4815 100644 --- a/apps/web/app/s/[surveyId]/LegalFooter.tsx +++ b/apps/web/app/s/[surveyId]/LegalFooter.tsx @@ -1,19 +1,19 @@ -import { env } from "@/env.mjs"; +import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; import Link from "next/link"; export default function LegalFooter() { - if (!env.NEXT_PUBLIC_IMPRINT_URL && !env.NEXT_PUBLIC_PRIVACY_URL) return null; + if (!IMPRINT_URL && !PRIVACY_URL) return null; return (
    - {env.NEXT_PUBLIC_IMPRINT_URL && ( - + {IMPRINT_URL && ( + Imprint )} - {env.NEXT_PUBLIC_IMPRINT_URL && env.NEXT_PUBLIC_PRIVACY_URL && | } - {env.NEXT_PUBLIC_PRIVACY_URL && ( - + {IMPRINT_URL && PRIVACY_URL && | } + {PRIVACY_URL && ( + Privacy Policy )} diff --git a/apps/web/app/s/[surveyId]/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/LinkSurvey.tsx index 626e34c769..53387bf789 100644 --- a/apps/web/app/s/[surveyId]/LinkSurvey.tsx +++ b/apps/web/app/s/[surveyId]/LinkSurvey.tsx @@ -3,7 +3,6 @@ import ContentWrapper from "@/components/shared/ContentWrapper"; import { SurveyInline } from "@/components/shared/Survey"; import { createDisplay } from "@formbricks/lib/client/display"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; import { ResponseQueue } from "@formbricks/lib/responseQueue"; import { SurveyState } from "@formbricks/lib/surveyState"; import { TProduct } from "@formbricks/types/v1/product"; @@ -21,6 +20,7 @@ interface LinkSurveyProps { personId?: string; emailVerificationStatus?: string; prefillAnswer?: string; + webAppUrl: string; } export default function LinkSurvey({ @@ -29,6 +29,7 @@ export default function LinkSurvey({ personId, emailVerificationStatus, prefillAnswer, + webAppUrl, }: LinkSurveyProps) { const searchParams = useSearchParams(); const isPreview = searchParams?.get("preview") === "true"; @@ -42,7 +43,7 @@ export default function LinkSurvey({ () => new ResponseQueue( { - apiHost: WEBAPP_URL, + apiHost: webAppUrl, retryAttempts: 2, onResponseSendingFailed: (response) => { alert(`Failed to send response: ${JSON.stringify(response, null, 2)}`); @@ -52,7 +53,7 @@ export default function LinkSurvey({ }, surveyState ), - [] + [personId, webAppUrl] ); const [autoFocus, setAutofocus] = useState(false); @@ -63,6 +64,10 @@ export default function LinkSurvey({ } }, []); + useEffect(() => { + responseQueue.updateSurveyState(surveyState); + }, [responseQueue, surveyState]); + if (emailVerificationStatus && emailVerificationStatus !== "verified") { if (emailVerificationStatus === "fishy") { return ; @@ -89,9 +94,16 @@ export default function LinkSurvey({ survey={survey} brandColor={product.brandColor} formbricksSignature={product.formbricksSignature} - onDisplay={() => createDisplay({ surveyId: survey.id }, window?.location?.origin)} + onDisplay={async () => { + if (!isPreview) { + const { id } = await createDisplay({ surveyId: survey.id }, window?.location?.origin); + const newSurveyState = surveyState.copy(); + newSurveyState.updateDisplayId(id); + setSurveyState(newSurveyState); + } + }} onResponse={(responseUpdate) => { - responseQueue.add(responseUpdate); + !isPreview && responseQueue.add(responseUpdate); }} onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)} activeQuestionId={activeQuestionId} diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx index 5a427d9936..c9fba5ec42 100644 --- a/apps/web/app/s/[surveyId]/page.tsx +++ b/apps/web/app/s/[surveyId]/page.tsx @@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL; import LinkSurvey from "@/app/s/[surveyId]/LinkSurvey"; import SurveyInactive from "@/app/s/[surveyId]/SurveyInactive"; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants"; import { getOrCreatePersonByUserId } from "@formbricks/lib/services/person"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; import { getSurvey } from "@formbricks/lib/services/survey"; @@ -59,6 +59,7 @@ export default async function LinkSurveyPage({ params, searchParams }) { personId={person?.id} emailVerificationStatus={emailVerificationStatus} prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null} + webAppUrl={WEBAPP_URL} /> ); } diff --git a/apps/web/components/auth/SigninForm.tsx b/apps/web/components/auth/SigninForm.tsx index 6f0c15fe61..8f205693dd 100644 --- a/apps/web/components/auth/SigninForm.tsx +++ b/apps/web/components/auth/SigninForm.tsx @@ -1,7 +1,6 @@ "use client"; import { GoogleButton } from "@/components/auth/GoogleButton"; -import { env } from "@/env.mjs"; import { Button, PasswordInput } from "@formbricks/ui"; import { XCircleIcon } from "@heroicons/react/24/solid"; import { signIn } from "next-auth/react"; @@ -10,7 +9,17 @@ import { useSearchParams } from "next/navigation"; import { useRef, useState } from "react"; import { GithubButton } from "./GithubButton"; -export const SigninForm = () => { +export const SigninForm = ({ + publicSignUpEnabled, + passwordResetEnabled, + googleOAuthEnabled, + githubOAuthEnabled, +}: { + publicSignUpEnabled: boolean; + passwordResetEnabled: boolean; + googleOAuthEnabled: boolean; + githubOAuthEnabled: boolean; +}) => { const searchParams = useSearchParams(); const emailRef = useRef(null); @@ -79,7 +88,7 @@ export const SigninForm = () => { className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" />
    - {env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && ( + {passwordResetEnabled && isPasswordFocused && (
    { - {env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && ( + {googleOAuthEnabled && ( <> )} - {env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && ( + {githubOAuthEnabled && ( <> )}
    - {env.NEXT_PUBLIC_SIGNUP_DISABLED !== "1" && ( + {publicSignUpEnabled && (
    New to Formbricks?
    diff --git a/apps/web/components/auth/SignupForm.tsx b/apps/web/components/auth/SignupForm.tsx index 4e69aced10..9ec41de945 100644 --- a/apps/web/components/auth/SignupForm.tsx +++ b/apps/web/components/auth/SignupForm.tsx @@ -2,7 +2,6 @@ import { GoogleButton } from "@/components/auth/GoogleButton"; import IsPasswordValid from "@/components/auth/IsPasswordValid"; -import { env } from "@/env.mjs"; import { createUser } from "@/lib/users/users"; import { Button, PasswordInput } from "@formbricks/ui"; import { XCircleIcon } from "@heroicons/react/24/solid"; @@ -11,7 +10,23 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useMemo, useRef, useState } from "react"; import { GithubButton } from "./GithubButton"; -export const SignupForm = () => { +export const SignupForm = ({ + webAppUrl, + privacyUrl, + termsUrl, + passwordResetEnabled, + emailVerificationDisabled, + googleOAuthEnabled, + githubOAuthEnabled, +}: { + webAppUrl: string; + privacyUrl: string | undefined; + termsUrl: string | undefined; + passwordResetEnabled: boolean; + emailVerificationDisabled: boolean; + googleOAuthEnabled: boolean; + githubOAuthEnabled: boolean; +}) => { const searchParams = useSearchParams(); const router = useRouter(); const [error, setError] = useState(""); @@ -19,15 +34,13 @@ export const SignupForm = () => { const nameRef = useRef(null); const inviteToken = searchParams?.get("inviteToken"); - // const callbackUrl = env.NEXT_PUBLIC_WEBAPP_URL + "/invite?token=" + inviteToken; - const callbackUrl = useMemo(() => { if (inviteToken) { - return env.NEXT_PUBLIC_WEBAPP_URL + "/invite?token=" + inviteToken; + return webAppUrl + "/invite?token=" + inviteToken; } else { - return env.NEXT_PUBLIC_WEBAPP_URL; + return webAppUrl; } - }, [inviteToken]); + }, [inviteToken, webAppUrl]); const handleSubmit = async (e: any) => { e.preventDefault(); @@ -45,10 +58,9 @@ export const SignupForm = () => { e.target.elements.password.value, inviteToken ); - const url = - env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED === "1" - ? `/auth/signup-without-verification-success` - : `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`; + const url = emailVerificationDisabled + ? `/auth/signup-without-verification-success` + : `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`; router.push(url); } catch (e: any) { @@ -144,7 +156,7 @@ export const SignupForm = () => { className="focus:border-brand focus:ring-brand block w-full rounded-md shadow-sm sm:text-sm" />
    - {env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && ( + {passwordResetEnabled && isPasswordFocused && (
    { - {env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && ( + {googleOAuthEnabled && ( <> )} - {env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && ( + {githubOAuthEnabled && ( <> )}
    - {(env.NEXT_PUBLIC_TERMS_URL || env.NEXT_PUBLIC_PRIVACY_URL) && ( + {(termsUrl || privacyUrl) && (
    By signing up, you agree to our
    - {env.NEXT_PUBLIC_TERMS_URL && ( - + {termsUrl && ( + Terms of Service )} - {env.NEXT_PUBLIC_TERMS_URL && env.NEXT_PUBLIC_PRIVACY_URL && and } - {env.NEXT_PUBLIC_PRIVACY_URL && ( - + {termsUrl && privacyUrl && and } + {privacyUrl && ( + Privacy Policy. )} diff --git a/apps/web/components/environments/SecondNavBar.tsx b/apps/web/components/environments/SecondNavBar.tsx index 96b3b32e8c..ec553fcf10 100644 --- a/apps/web/components/environments/SecondNavBar.tsx +++ b/apps/web/components/environments/SecondNavBar.tsx @@ -1,27 +1,39 @@ import { cn } from "@formbricks/lib/cn"; import SurveyNavBarName from "@/components/shared/SurveyNavBarName"; import Link from "next/link"; +import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; +import { getSurvey } from "@formbricks/lib/services/survey"; interface SecondNavbarProps { tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[]; activeId: string; surveyId?: string; - environmentId?: string; + environmentId: string; } -export default function SecondNavbar({ +export default async function SecondNavbar({ tabs, activeId, surveyId, environmentId, ...props }: SecondNavbarProps) { + const product = await getProductByEnvironmentId(environmentId!); + if (!product) { + throw new Error("Product not found"); + } + + let survey; + if (surveyId) { + survey = await getSurvey(surveyId); + } + return (
    - {surveyId && environmentId && ( - + {survey && environmentId && ( + )}
    {" "}