diff --git a/apps/demo/components/SurveySwitch.tsx b/apps/demo/components/SurveySwitch.tsx deleted file mode 100644 index 752c3ec037..0000000000 --- a/apps/demo/components/SurveySwitch.tsx +++ /dev/null @@ -1,22 +0,0 @@ -interface SurveySwitchProps { - value: "website" | "app"; - formbricks: any; -} - -export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => { - return ( - - ); -}; diff --git a/apps/demo/next.config.mjs b/apps/demo/next.config.mjs index c8a450c888..151cc3ec71 100644 --- a/apps/demo/next.config.mjs +++ b/apps/demo/next.config.mjs @@ -1,15 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, - async redirects() { - return [ - { - source: "/", - destination: "/app", - permanent: false, - }, - ]; - }, images: { remotePatterns: [ { diff --git a/apps/demo/pages/app/index.tsx b/apps/demo/pages/index.tsx similarity index 97% rename from apps/demo/pages/app/index.tsx rename to apps/demo/pages/index.tsx index 3a4fd17a47..421ed20bad 100644 --- a/apps/demo/pages/app/index.tsx +++ b/apps/demo/pages/index.tsx @@ -1,9 +1,8 @@ import Image from "next/image"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import formbricks from "@formbricks/js/app"; -import { SurveySwitch } from "../../components/SurveySwitch"; -import fbsetup from "../../public/fb-setup.png"; +import formbricks from "@formbricks/js"; +import fbsetup from "../public/fb-setup.png"; declare const window: any; @@ -63,7 +62,6 @@ const AppPage = ({}) => {
-

Formbricks In-product Survey Demo App diff --git a/apps/demo/pages/website/index.tsx b/apps/demo/pages/website/index.tsx deleted file mode 100644 index b75abd8afb..0000000000 --- a/apps/demo/pages/website/index.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import Image from "next/image"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import formbricks from "@formbricks/js/website"; -import { SurveySwitch } from "../../components/SurveySwitch"; -import fbsetup from "../../public/fb-setup.png"; - -declare const window: any; - -const AppPage = ({}) => { - const [darkMode, setDarkMode] = useState(false); - const router = useRouter(); - - useEffect(() => { - if (darkMode) { - document.body.classList.add("dark"); - } else { - document.body.classList.remove("dark"); - } - }, [darkMode]); - - useEffect(() => { - // enable Formbricks debug mode by adding formbricksDebug=true GET parameter - const addFormbricksDebugParam = () => { - const urlParams = new URLSearchParams(window.location.search); - if (!urlParams.has("formbricksDebug")) { - urlParams.set("formbricksDebug", "true"); - const newUrl = `${window.location.pathname}?${urlParams.toString()}`; - window.history.replaceState({}, "", newUrl); - } - }; - - addFormbricksDebugParam(); - - if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) { - const defaultAttributes = { - language: "en", - }; - - formbricks.init({ - environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID, - apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST, - attributes: defaultAttributes, - }); - } - - // Connect next.js router to Formbricks - if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) { - const handleRouteChange = formbricks?.registerRouteChange; - router.events.on("routeChangeComplete", handleRouteChange); - - return () => { - router.events.off("routeChangeComplete", handleRouteChange); - }; - } - }); - - return ( -
-
-
- -
-

- Formbricks Website Survey Demo App -

-

- This app helps you test your app surveys. You can create and test user actions, create and - update user attributes, etc. -

-
-
- - -
- -
-
-
-

1. Setup .env

-

- Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env -

- fb setup - -
-

You're connected with env:

-
- - {process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID} - - - - - -
-
-
-
-

2. Widget Logs

-

- Look at the logs to understand how the widget works.{" "} - Open your browser console to see the logs. -

- {/*
- -
*/} -
-
- -
-
-

- Reset person / pull data from Formbricks app -

-

- On formbricks.reset() the local state will be deleted and formbricks gets{" "} - reinitialized. -

- - -

- If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and - try again. -

-
-
-
-
- ); -}; - -export default AppPage; diff --git a/apps/docs/app/app-surveys/actions/page.mdx b/apps/docs/app/app-surveys/actions/page.mdx index 64cba73f6c..8d3bf1b33c 100644 --- a/apps/docs/app/app-surveys/actions/page.mdx +++ b/apps/docs/app/app-surveys/actions/page.mdx @@ -9,8 +9,6 @@ export const metadata = { "Dive deep into how actions in Formbricks help products and organizations to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.", }; -#### App Surveys - # Actions Actions are predefined events within your app that prompt Formbricks to display a survey when triggered. These are detected by the Formbricks widget, which then presents the appropriate survey based on your predefined settings. diff --git a/apps/docs/app/app-surveys/advanced-targeting/page.mdx b/apps/docs/app/app-surveys/advanced-targeting/page.mdx index 70e998b048..d93bd5cb7f 100644 --- a/apps/docs/app/app-surveys/advanced-targeting/page.mdx +++ b/apps/docs/app/app-surveys/advanced-targeting/page.mdx @@ -4,8 +4,6 @@ export const metadata = { "Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, metadata, and other segments. This helps you get more relevant feedback and make data-driven decisions.", }; -#### App Surveys - # Advanced Targeting Advanced Targeting allows you to show surveys to the right group of people. You can target surveys based on user attributes, device type, and more instead of spraying and praying. This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code. diff --git a/apps/docs/app/app-surveys/framework-guides/page.mdx b/apps/docs/app/app-surveys/framework-guides/page.mdx index 915f3ad58f..205e0adc72 100644 --- a/apps/docs/app/app-surveys/framework-guides/page.mdx +++ b/apps/docs/app/app-surveys/framework-guides/page.mdx @@ -42,8 +42,8 @@ All you need to do is copy a ` ``` @@ -58,9 +58,6 @@ All you need to do is copy a ` - -``` - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - - - URL of the hosted Formbricks instance. - - - -Refer to our [Example HTML project](https://github.com/formbricks/examples/tree/main/html) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - ---- - -## ReactJS - -Install the Formbricks SDK using one of the package managers ie `npm`,`pnpm`,`yarn`. Note that zod is required as a peer dependency and must also be installed in your project. - - - -```shell {{ title: 'npm' }} -npm install @formbricks/js zod -``` -```shell {{ title: 'pnpm' }} -pnpm add @formbricks/js zod -``` -```shell {{ title: 'yarn' }} -yarn add @formbricks/js zod -``` - - - -Now, update your App.js/ts file to initialise Formbricks. - - - -```js -// other imports -import formbricks from "@formbricks/js/website"; - -if (typeof window !== "undefined") { - formbricks.init({ - environmentId: "", - apiHost: "", - }); -} - -function App() { - // your own app -} - -export default App; -``` - - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - - - URL of the hosted Formbricks instance. - - - -Refer to our [Example ReactJs project](https://github.com/formbricks/examples/tree/main/reactjs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - ---- - -## NextJS - -NextJs projects typically follow two main conventions: the App Directory and the Pages Directory. -To ensure smooth integration with the Formbricks SDK, which operates solely on the client side, follow the -guidelines for each convention below: - -- App directory: You will have to define a new component in `app/formbricks.tsx` file and call it in your `app/layout.tsx` file. -- Pages directory: You will have to visit your `_app.tsx` and just initialise Formbricks there. - -Code snippets for the integration for both conventions are provided to further assist you. - - - -```shell {{ title: 'npm' }} -npm install @formbricks/js zod -``` -```shell {{ title: 'pnpm' }} -pnpm add @formbricks/js zod -``` -```shell {{ title: 'yarn' }} -yarn add @formbricks/js zod -``` - - - - -### App Directory - - - - -```tsx {{title: 'Typescript'}} -"use client"; - -import { usePathname, useSearchParams } from "next/navigation"; -import { useEffect } from "react"; -import formbricks from "@formbricks/js/website"; - -export default function FormbricksProvider() { - const pathname = usePathname(); - const searchParams = useSearchParams(); - - useEffect(() => { - formbricks.init({ - environmentId: "", - apiHost: "", - }); - }, []); - - useEffect(() => { - formbricks?.registerRouteChange(); - }, [pathname, searchParams]); - - return null; -} -``` - - - - -```tsx {{title: 'Typescript'}} -// other imports -import FormbricksProvider from "./formbricks"; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - ); -} -``` - - - - -Refer to our [Example NextJS App Directory project](https://github.com/formbricks/examples/tree/main/nextjs-app) for more help! - -### Pages Directory - - - - -```tsx {{ title: 'Typescript' }} -// other import -import { useRouter } from "next/router"; -import { useEffect } from "react"; -import formbricks from "@formbricks/js/website"; - -if (typeof window !== "undefined") { - formbricks.init({ - environmentId: "", - apiHost: "", - }); -} - -export default function App({ Component, pageProps }: AppProps) { - const router = useRouter(); - - useEffect(() => { - // Connect next.js router to Formbricks - const handleRouteChange = formbricks?.registerRouteChange; - router.events.on("routeChangeComplete", handleRouteChange); - - return () => { - router.events.off("routeChangeComplete", handleRouteChange); - }; - }, []); - return ; -} -``` - - - -Refer to our [Example NextJS Pages Directory project](https://github.com/formbricks/examples/tree/main/nextjs-pages) for more help! - -### Required customizations to be made - - - - Formbricks Environment ID. - - - - - URL of the hosted Formbricks instance. - - - -First initialize the Formbricks SDK, ensure that it only runs on the client side. -To connect the Next.js router to Formbricks and ensure the SDK can keep track of every page change, we are registering the route change event. - -Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - ---- - -## VueJs - -Integrating the Formbricks SDK with Vue.js is a straightforward process. -We will make sure the SDK is only loaded and used on the client side, as it's not intended for server-side usage. - - - -```shell {{ title: 'npm' }} -npm install @formbricks/js -``` - -```shell {{ title: 'pnpm' }} -pnpm add @formbricks/js -``` - -```shell {{ title: 'yarn' }} -yarn add @formbricks/js -``` - - - - - -```js -import formbricks from "@formbricks/js/website"; - -if (typeof window !== "undefined") { - formbricks.init({ - environmentId: "", - apiHost: "", - }); -} - -export default formbricks; -``` - - - - - -```js -// other imports -import formbricks from "@/formbricks"; - -const app = createApp(App); - -app.use(router); - -app.mount("#app"); - -router.afterEach((to, from) => { - if (typeof formbricks !== "undefined") { - formbricks.registerRouteChange(); - } -}); -``` - - - -### Required customizations to be made - - - - Formbricks Environment ID. - - - - - URL of the hosted Formbricks instance. - - - -Refer to our [Example VueJs project](https://github.com/formbricks/examples/tree/main/vuejs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup! - -## Validate your setup - -Once you have completed the steps above, you can validate your setup by checking the **Setup Checklist** in the Settings. Your widget status indicator should go from this: - - - -To this: - - - -## Debugging Formbricks Integration - -Enabling Formbricks debug mode in your browser is a useful troubleshooting step for identifying and resolving complex issues. This section outlines how to activate debug mode, covers common use cases, and provides insights into specific debug log messages. - -### Activate Debug Mode - -To activate Formbricks debug mode: - -1. **Via URL Parameter:** - - - Enable debug mode mode by adding `?formbricksDebug=true` to your application's URL (e.g. `https://example.com?formbricksDebug=true` or `https://example.com?page=123&formbricksDebug=true`). This parameter will enable debugging for the current page. - -2. **View Debug Logs:** - - - Open your browser's developer tools by pressing `F12` or right-clicking and selecting "Inspect." - - Navigate to the "Console" tab to view Formbricks debugging information. - - **How to Open Browser Console:** - - - **Google Chrome:** Press `F12` or right-click, select "Inspect," and go to the "Console" tab. - - **Firefox:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab. - - **Safari:** Press `Option + Command + C` to open the developer tools and navigate to the "Console" tab. - - **Edge:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab. - -### Common Use Cases - -Debug mode is beneficial for scenarios such as: - -- Verifying Formbricks initialization. -- Identifying survey trigger issues. -- Troubleshooting unexpected behavior. - -### Debug Log Messages - -Debug log messages provide insights into: - -- API calls and responses. -- Event tracking, survey triggers and form interactions. -- Initialization errors. - -**Can’t figure it out? [Join our Discord!](https://formbricks.com/discord)** - ---- diff --git a/apps/docs/app/website-surveys/quickstart/images/I1.webp b/apps/docs/app/website-surveys/quickstart/images/I1.webp deleted file mode 100644 index b607cc0ec8..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I1.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/images/I2.webp b/apps/docs/app/website-surveys/quickstart/images/I2.webp deleted file mode 100644 index 4102110058..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I2.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/images/I3.webp b/apps/docs/app/website-surveys/quickstart/images/I3.webp deleted file mode 100644 index 3e2d7fbedc..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I3.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/images/I4.webp b/apps/docs/app/website-surveys/quickstart/images/I4.webp deleted file mode 100644 index f7bae96938..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I4.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/images/I5.webp b/apps/docs/app/website-surveys/quickstart/images/I5.webp deleted file mode 100644 index 33c217013f..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I5.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/images/I6.webp b/apps/docs/app/website-surveys/quickstart/images/I6.webp deleted file mode 100644 index a24c2af56d..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I6.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/images/I7.webp b/apps/docs/app/website-surveys/quickstart/images/I7.webp deleted file mode 100644 index 4a3a352d2d..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I7.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/images/I8.webp b/apps/docs/app/website-surveys/quickstart/images/I8.webp deleted file mode 100644 index e5c0ab7442..0000000000 Binary files a/apps/docs/app/website-surveys/quickstart/images/I8.webp and /dev/null differ diff --git a/apps/docs/app/website-surveys/quickstart/page.mdx b/apps/docs/app/website-surveys/quickstart/page.mdx deleted file mode 100644 index 7395a19d53..0000000000 --- a/apps/docs/app/website-surveys/quickstart/page.mdx +++ /dev/null @@ -1,115 +0,0 @@ -import { MdxImage } from "@/components/MdxImage"; - -import I1 from "./images/I1.webp"; -import I2 from "./images/I2.webp"; -import I3 from "./images/I3.webp"; -import I4 from "./images/I4.webp"; -import I5 from "./images/I5.webp"; -import I6 from "./images/I6.webp"; -import I7 from "./images/I7.webp"; -import I8 from "./images/I8.webp"; - -export const metadata = { - title: "Formbricks Quickstart Guide: Website Surveys Made Easier & Faster", - description: - "Formbricks is the easiest way to create and manage website surveys. This quickstart guide will show you how to create your first website survey in under 5 minutes.", -}; - -#### Website Surveys - -# Quickstart - -Website Surveys make it easy for your public website visitors to give you feedback. They are a great way to get feedback from your users, without interrupting their workflow. This quickstart guide will show you how to create your first website survey in under 5 minutes. - - - Website Surveys are ideal for **public facing websites**. If you are looking to run surveys in your app where you have user identification & want advanced user targeting, head over to the [App Surveys Quickstart Guide](/app-surveys/quickstart). - - -1. **Create a free Formbricks Cloud account**: While you can [self-host](/self-hosting/deployment) Formbricks, but the quickest and easiest way to get started is with the free Cloud plan. Just [sign up here](https://app.formbricks.com/auth/signup) and you'll be guided to our onboarding like below: - - - Website & App Surveys have the same integration process. The difference will come when we setup our survey. - - - - -2. **Connect your App/Website**: Once you get through a couple of onboarding steps, you’ll be asked to connect your app or website. This is where you’ll find the code snippet for both HTML as well as the npm package which you need to embed in your app: - - - -Paste the code snippet in your app and reload the page. You should now see the Formbricks widget in the lower right corner of your app! The integration is now complete. - - - -Onboarding is complete! Now let’s create our first survey as you should see templates to choose from after clicking on **Next**: - - - -3. **Create your first survey**: To be able to see a survey in your app, you need to create one. We’ll choose one of the templates and head over to the survey settings: - -Pick the Survey Type as **Website Survey**. - - - -1. **Set Trigger for the Survey**: Scroll down to Survey Trigger and click on **+ Add action**, choose **New Session**. This will cause this survey to appear when the Formbricks Widget tracks a new session: - - - -5. **Set Recontact Options for debugging**: In Recontact Options we choose the following settings, so that we can play around with the survey more easily. By default, each survey will be shown only once for each session to prevent survey fatigue: - - - Please change this setting later on after testing your survey to prevent survey fatigue for your users. - - - - -6. **Publish your survey**: Now hit **Publish** and you’ll be forwarded to the Summary Page. This is where you’ll find the responses to this survey. - - - ---- - -- We offer framework guides for various frontend tech, head over to the the [Website Survey Framework Guides](/website-survey/framework-guides) to get started with your public facing website surveys. -- Head over to our Website Survey SDK documentation to get started with the [Website Survey JS SDK](/developer-docs/website-survey-sdk). - -Still struggling or something not working as expected? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you! diff --git a/apps/docs/lib/navigation.ts b/apps/docs/lib/navigation.ts index e201180666..904347457c 100644 --- a/apps/docs/lib/navigation.ts +++ b/apps/docs/lib/navigation.ts @@ -22,58 +22,6 @@ export const navigation: Array = [ }, ], }, - { - title: "App Surveys", - links: [ - { title: "Quickstart", href: "/app-surveys/quickstart" }, - { title: "Framework Guides", href: "/app-surveys/framework-guides" }, - { - title: "Features", - children: [ - { title: "Identify Users", href: "/app-surveys/user-identification" }, - { title: "Actions", href: "/app-surveys/actions" }, - { title: "Advanced Targeting", href: "/app-surveys/advanced-targeting" }, - { title: "Show Survey to % of users", href: "/global/show-survey-to-percent-of-users" }, // app and website - { title: "Recontact Options", href: "/app-surveys/recontact" }, - { title: "Hidden Fields", href: "/global/hidden-fields" }, // global - { title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global - { title: "User Metadata", href: "/global/metadata" }, // global - { title: "Custom Styling", href: "/global/overwrite-styling" }, // global - { title: "Conditional Logic", href: "/global/conditional-logic" }, // global - { title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global - { title: "Limit submissions", href: "/global/limit-submissions" }, // global - { title: "Recall Functionality", href: "/global/recall" }, // global - { title: "Partial Submissions", href: "/global/partial-submissions" }, // global - { title: "Shareable Dashboards", href: "/global/shareable-dashboards" }, - ], - }, - ], - }, - { - title: "Website Surveys", - links: [ - { title: "Quickstart", href: "/website-surveys/quickstart" }, - { title: "Framework Guides", href: "/website-surveys/framework-guides" }, - { - title: "Features", - children: [ - { title: "Actions", href: "/website-surveys/actions" }, - { title: "Show Survey to % of users", href: "/global/show-survey-to-percent-of-users" }, // app and website - { title: "Recontact Options", href: "/app-surveys/recontact" }, - { title: "Hidden Fields", href: "/global/hidden-fields" }, // global - { title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global - { title: "User Metadata", href: "/global/metadata" }, // global - { title: "Custom Styling", href: "/global/overwrite-styling" }, // global - { title: "Conditional Logic", href: "/global/conditional-logic" }, // global - { title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global - { title: "Limit submissions", href: "/global/limit-submissions" }, // global - { title: "Recall Functionality", href: "/global/recall" }, // global - { title: "Partial Submissions", href: "/global/partial-submissions" }, // global - { title: "Shareable Dashboards", href: "/global/shareable-dashboards" }, - ], - }, - ], - }, { title: "Link Surveys", links: [ @@ -104,6 +52,33 @@ export const navigation: Array = [ }, ], }, + { + title: "Website & App Surveys", + links: [ + { title: "Quickstart", href: "/app-surveys/quickstart" }, + { title: "Framework Guides", href: "/app-surveys/framework-guides" }, + { + title: "Features", + children: [ + { title: "Identify Users", href: "/app-surveys/user-identification" }, + { title: "Actions", href: "/app-surveys/actions" }, + { title: "Advanced Targeting", href: "/app-surveys/advanced-targeting" }, + { title: "Show Survey to % of users", href: "/global/show-survey-to-percent-of-users" }, // app and website + { title: "Recontact Options", href: "/app-surveys/recontact" }, + { title: "Hidden Fields", href: "/global/hidden-fields" }, // global + { title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global + { title: "User Metadata", href: "/global/metadata" }, // global + { title: "Custom Styling", href: "/global/overwrite-styling" }, // global + { title: "Conditional Logic", href: "/global/conditional-logic" }, // global + { title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global + { title: "Limit submissions", href: "/global/limit-submissions" }, // global + { title: "Recall Functionality", href: "/global/recall" }, // global + { title: "Partial Submissions", href: "/global/partial-submissions" }, // global + { title: "Shareable Dashboards", href: "/global/shareable-dashboards" }, + ], + }, + ], + }, { title: "Core Features", links: [ @@ -142,8 +117,7 @@ export const navigation: Array = [ { title: "Zapier", href: "/developer-docs/integrations/zapier" }, ], }, - { title: "SDK: Web Apps", href: "/developer-docs/app-survey-sdk" }, - { title: "SDK: Public Websites", href: "/developer-docs/website-survey-sdk" }, + { title: "SDK: Formbricks JS", href: "/developer-docs/js-sdk" }, { title: "SDK: React Native", href: "/developer-docs/react-native-in-app-surveys" }, { title: "SDK: Formbricks API", href: "/developer-docs/api-sdk" }, { title: "REST API", href: "/developer-docs/rest-api" }, diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 71f23d4527..e3ee4cc31c 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -121,6 +121,22 @@ const nextConfig = { destination: "/developer-docs/integrations/:path", permanent: true, }, + + { + source: "/developer-docs/website-survey-sdk", + destination: "/developer-docs/js-sdk", + permanent: true, + }, + { + source: "/website-surveys/quickstart", + destination: "/app-surveys/quickstart", + permanent: true, + }, + { + source: "/website-surveys/framework-guides", + destination: "/app-surveys/framework-guides", + permanent: true, + }, ]; }, }; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx index f462c3c3f2..3bf0eeba7f 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx @@ -34,7 +34,7 @@ export const OnboardingSetupInstructions = ({ var apiHost = "${webAppUrl}"; var environmentId = "${environmentId}"; var userId = "testUser"; - var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/app";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}(); + var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}(); `; @@ -44,13 +44,13 @@ export const OnboardingSetupInstructions = ({ !function(){ var apiHost = "${webAppUrl}"; var environmentId = "${environmentId}"; - var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/website";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost})},500)}(); + var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost})},500)}(); `; const npmSnippetForAppSurveys = ` - import formbricks from "@formbricks/js/app"; + import formbricks from "@formbricks/js"; if (typeof window !== "undefined") { formbricks.init({ @@ -69,7 +69,7 @@ export const OnboardingSetupInstructions = ({ const npmSnippetForWebsiteSurveys = ` // other imports - import formbricks from "@formbricks/js/website"; + import formbricks from "@formbricks/js"; if (typeof window !== "undefined") { formbricks.init({ diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx index 20e4f4f053..8238a83e12 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx @@ -36,9 +36,7 @@ const Page = async ({ params }: ConnectPageProps) => {

diff --git a/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx index d2955096ae..a11630f499 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx @@ -7,7 +7,6 @@ import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; -import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; import { getTagsOnResponsesCount } from "@formbricks/lib/tagOnResponse/service"; import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent"; @@ -21,10 +20,9 @@ const Page = async ({ params }) => { throw new Error("Environment not found"); } - const [tags, environmentTagsCount, product, organization, session] = await Promise.all([ + const [tags, environmentTagsCount, organization, session] = await Promise.all([ getTagsByEnvironmentId(params.environmentId), getTagsOnResponsesCount(params.environmentId), - getProductByEnvironmentId(params.environmentId), getOrganizationByEnvironmentId(params.environmentId), getServerSession(authOptions), ]); @@ -45,7 +43,6 @@ const Page = async ({ params }) => { const isTagSettingDisabled = isViewer; const isMultiLanguageAllowed = await getMultiLanguagePermission(organization); - const currentProductChannel = product?.config.channel ?? null; return !isTagSettingDisabled ? ( @@ -54,7 +51,6 @@ const Page = async ({ params }) => { environmentId={params.environmentId} activeId="tags" isMultiLanguageAllowed={isMultiLanguageAllowed} - productChannel={currentProductChannel} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx index 3406132f65..63b120dd7f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx @@ -5,10 +5,9 @@ import { Button } from "@formbricks/ui/components/Button"; interface TEmptyAppSurveysProps { environment: TEnvironment; - surveyType?: "app" | "website"; } -export const EmptyAppSurveys = ({ environment, surveyType = "app" }: TEmptyAppSurveysProps) => { +export const EmptyAppSurveys = ({ environment }: TEmptyAppSurveysProps) => { return (
@@ -19,10 +18,10 @@ export const EmptyAppSurveys = ({ environment, surveyType = "app" }: TEmptyAppSu

You're not plugged in yet!

- Connect your {surveyType} with Formbricks to run {surveyType} surveys. + Connect your website or app with Formbricks to get started.

- + diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx index 5c1c4f2d99..463e8a5d9a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx @@ -16,9 +16,8 @@ export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) => const searchParams = useSearchParams(); const [confetti, setConfetti] = useState(false); - const isAppSurvey = survey.type === "app" || survey.type === "website"; - const widgetSetupCompleted = - survey.type === "app" ? environment.appSetupCompleted : environment.websiteSetupCompleted; + const isAppSurvey = survey.type === "app"; + const widgetSetupCompleted = environment.appSetupCompleted; useEffect(() => { const newSurveyParam = searchParams?.get("success"); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx index 7be3d929fb..36d67a5071 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx @@ -50,8 +50,6 @@ export const SummaryList = ({ attributeClasses, }: SummaryListProps) => { const { setSelectedFilter, selectedFilter } = useResponseFilter(); - const widgetSetupCompleted = - survey.type === "app" ? environment.appSetupCompleted : environment.websiteSetupCompleted; const setFilter = ( questionId: string, @@ -107,10 +105,8 @@ export const SummaryList = ({ return (
- {(survey.type === "app" || survey.type === "website") && - responseCount === 0 && - !widgetSetupCompleted ? ( - + {survey.type === "app" && responseCount === 0 && !environment.appSetupCompleted ? ( + ) : summary.length === 0 ? ( ) : responseCount === 0 ? ( @@ -119,7 +115,6 @@ export const SummaryList = ({ environment={environment} noWidgetRequired={survey.type === "link"} emptyMessage={totalResponseCount === 0 ? undefined : "No response matches your filter"} - widgetSetupCompleted={widgetSetupCompleted} /> ) : ( summary.map((questionSummary) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx index faf533b4b9..17588754cd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx @@ -30,9 +30,7 @@ export const SurveyAnalysisCTA = ({ const router = useRouter(); const [showShareSurveyModal, setShowShareSurveyModal] = useState(searchParams.get("share") === "true"); - - const widgetSetupCompleted = - survey.type === "app" ? environment.appSetupCompleted : environment.websiteSetupCompleted; + const widgetSetupCompleted = environment.appSetupCompleted; useEffect(() => { if (searchParams.get("share") === "true") { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx index 86f6d0fc38..29778ef409 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx @@ -65,9 +65,7 @@ export const SurveyStatusDropdown = ({
- {(survey.type === "link" || - environment.appSetupCompleted || - environment.websiteSetupCompleted) && ( + {(survey.type === "link" || environment.appSetupCompleted) && ( )} diff --git a/apps/web/app/api/packages/[package]/route.ts b/apps/web/app/api/packages/[package]/route.ts index 0cd496b3c6..6c96e9d837 100644 --- a/apps/web/app/api/packages/[package]/route.ts +++ b/apps/web/app/api/packages/[package]/route.ts @@ -7,11 +7,8 @@ export const GET = async (_: NextRequest, { params }: { params: { package: strin const packageRequested = params.package; switch (packageRequested) { - case "app": - path = `../../packages/js-core/dist/app.umd.cjs`; - break; - case "website": - path = `../../packages/js-core/dist/website.umd.cjs`; + case "js": + path = `../../packages/js-core/dist/index.umd.cjs`; break; case "surveys": path = `../../packages/surveys/dist/index.umd.cjs`; diff --git a/apps/web/app/api/v1/client/[environmentId]/app/environment/lib/environmentState.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts similarity index 92% rename from apps/web/app/api/v1/client/[environmentId]/app/environment/lib/environmentState.ts rename to apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts index fb34f8a81a..ca99392067 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/environment/lib/environmentState.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts @@ -18,7 +18,7 @@ import { productCache } from "@formbricks/lib/product/cache"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { surveyCache } from "@formbricks/lib/survey/cache"; import { getSurveys } from "@formbricks/lib/survey/service"; -import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TJsEnvironmentState } from "@formbricks/types/js"; /** @@ -26,7 +26,6 @@ import { TJsEnvironmentState } from "@formbricks/types/js"; * @param environmentId * @returns The environment state * @throws ResourceNotFoundError if the environment or organization does not exist - * @throws InvalidInputError if the channel is not "app" */ export const getEnvironmentState = async ( environmentId: string @@ -52,10 +51,6 @@ export const getEnvironmentState = async ( throw new ResourceNotFoundError("product", null); } - if (product.config.channel && product.config.channel !== "app") { - throw new InvalidInputError("Invalid channel"); - } - if (!environment.appSetupCompleted) { await Promise.all([ prisma.environment.update({ @@ -117,7 +112,7 @@ export const getEnvironmentState = async ( revalidateEnvironment, }; }, - [`environmentState-app-${environmentId}`], + [`environmentState-${environmentId}`], { ...(IS_FORMBRICKS_CLOUD && { revalidate: 24 * 60 * 60 }), tags: [ diff --git a/apps/web/app/api/v1/client/[environmentId]/app/environment/route.ts b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts similarity index 97% rename from apps/web/app/api/v1/client/[environmentId]/app/environment/route.ts rename to apps/web/app/api/v1/client/[environmentId]/environment/route.ts index 919fc350f9..3ec1823f38 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/environment/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/environment/route.ts @@ -1,4 +1,4 @@ -import { getEnvironmentState } from "@/app/api/v1/client/[environmentId]/app/environment/lib/environmentState"; +import { getEnvironmentState } from "@/app/api/v1/client/[environmentId]/environment/lib/environmentState"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { NextRequest } from "next/server"; diff --git a/apps/web/app/api/v1/client/[environmentId]/app/people/[userId]/lib/personState.ts b/apps/web/app/api/v1/client/[environmentId]/identify/people/[userId]/lib/personState.ts similarity index 97% rename from apps/web/app/api/v1/client/[environmentId]/app/people/[userId]/lib/personState.ts rename to apps/web/app/api/v1/client/[environmentId]/identify/people/[userId]/lib/personState.ts index ae3531a6ac..9ced049337 100644 --- a/apps/web/app/api/v1/client/[environmentId]/app/people/[userId]/lib/personState.ts +++ b/apps/web/app/api/v1/client/[environmentId]/identify/people/[userId]/lib/personState.ts @@ -1,4 +1,3 @@ -import { getPersonSegmentIds } from "@/app/api/v1/client/[environmentId]/app/people/[userId]/lib/segments"; import { prisma } from "@formbricks/database"; import { attributeCache } from "@formbricks/lib/attribute/cache"; import { getAttributesByUserId } from "@formbricks/lib/attribute/service"; @@ -17,6 +16,7 @@ import { getResponsesByUserId } from "@formbricks/lib/response/service"; import { segmentCache } from "@formbricks/lib/segment/cache"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TJsPersonState } from "@formbricks/types/js"; +import { getPersonSegmentIds } from "./segments"; /** * diff --git a/apps/web/app/api/v1/client/[environmentId]/app/people/[userId]/lib/segments.ts b/apps/web/app/api/v1/client/[environmentId]/identify/people/[userId]/lib/segments.ts similarity index 100% rename from apps/web/app/api/v1/client/[environmentId]/app/people/[userId]/lib/segments.ts rename to apps/web/app/api/v1/client/[environmentId]/identify/people/[userId]/lib/segments.ts diff --git a/apps/web/app/api/v1/client/[environmentId]/app/people/[userId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/identify/people/[userId]/route.ts similarity index 100% rename from apps/web/app/api/v1/client/[environmentId]/app/people/[userId]/route.ts rename to apps/web/app/api/v1/client/[environmentId]/identify/people/[userId]/route.ts diff --git a/apps/web/app/api/v1/client/[environmentId]/website/environment/lib/environmentState.ts b/apps/web/app/api/v1/client/[environmentId]/website/environment/lib/environmentState.ts deleted file mode 100644 index 0e1d2d73cf..0000000000 --- a/apps/web/app/api/v1/client/[environmentId]/website/environment/lib/environmentState.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { prisma } from "@formbricks/database"; -import { actionClassCache } from "@formbricks/lib/actionClass/cache"; -import { getActionClasses } from "@formbricks/lib/actionClass/service"; -import { cache } from "@formbricks/lib/cache"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { environmentCache } from "@formbricks/lib/environment/cache"; -import { getEnvironment } from "@formbricks/lib/environment/service"; -import { organizationCache } from "@formbricks/lib/organization/cache"; -import { - getMonthlyOrganizationResponseCount, - getOrganizationByEnvironmentId, -} from "@formbricks/lib/organization/service"; -import { - capturePosthogEnvironmentEvent, - sendPlanLimitsReachedEventToPosthogWeekly, -} from "@formbricks/lib/posthogServer"; -import { productCache } from "@formbricks/lib/product/cache"; -import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { getSurveys } from "@formbricks/lib/survey/service"; -import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; -import { TJsEnvironmentState } from "@formbricks/types/js"; - -/** - * Get the environment state - * @param environmentId - * @returns The environment state - * @throws ResourceNotFoundError if the organization, environment or product is not found - * @throws InvalidInputError if the product channel is not website - */ -export const getEnvironmentState = async ( - environmentId: string -): Promise<{ state: TJsEnvironmentState["data"]; revalidateEnvironment?: boolean }> => - cache( - async () => { - let revalidateEnvironment = false; - const [environment, organization, product] = await Promise.all([ - getEnvironment(environmentId), - getOrganizationByEnvironmentId(environmentId), - getProductByEnvironmentId(environmentId), - ]); - - if (!environment) { - throw new ResourceNotFoundError("environment", environmentId); - } - - if (!organization) { - throw new ResourceNotFoundError("organization", null); - } - - if (!product) { - throw new ResourceNotFoundError("product", null); - } - - if (product.config.channel && product.config.channel !== "website") { - throw new InvalidInputError("Product channel is not website"); - } - - // check if response limit is reached - let isWebsiteSurveyResponseLimitReached = false; - if (IS_FORMBRICKS_CLOUD) { - const currentResponseCount = await getMonthlyOrganizationResponseCount(organization.id); - const monthlyResponseLimit = organization.billing.limits.monthly.responses; - - isWebsiteSurveyResponseLimitReached = - monthlyResponseLimit !== null && currentResponseCount >= monthlyResponseLimit; - - if (isWebsiteSurveyResponseLimitReached) { - try { - await sendPlanLimitsReachedEventToPosthogWeekly(environmentId, { - plan: organization.billing.plan, - limits: { monthly: { responses: monthlyResponseLimit, miu: null } }, - }); - } catch (error) { - console.error(`Error sending plan limits reached event to Posthog: ${error}`); - } - } - } - - if (!environment?.websiteSetupCompleted) { - await Promise.all([ - await prisma.environment.update({ - where: { - id: environmentId, - }, - data: { websiteSetupCompleted: true }, - }), - capturePosthogEnvironmentEvent(environmentId, "website setup completed"), - ]); - - revalidateEnvironment = true; - } - - const [surveys, actionClasses] = await Promise.all([ - getSurveys(environmentId), - getActionClasses(environmentId), - ]); - - // Common filter condition for selecting surveys that are in progress, are of type 'website' and have no active segment filtering. - const filteredSurveys = surveys.filter( - (survey) => survey.status === "inProgress" && survey.type === "website" - ); - - const state: TJsEnvironmentState["data"] = { - surveys: filteredSurveys, - actionClasses, - product, - }; - - return { - state, - revalidateEnvironment, - }; - }, - [`environmentState-website-${environmentId}`], - { - ...(IS_FORMBRICKS_CLOUD && { revalidate: 24 * 60 * 60 }), - tags: [ - environmentCache.tag.byId(environmentId), - organizationCache.tag.byEnvironmentId(environmentId), - productCache.tag.byEnvironmentId(environmentId), - surveyCache.tag.byEnvironmentId(environmentId), - actionClassCache.tag.byEnvironmentId(environmentId), - ], - } - )(); diff --git a/apps/web/app/api/v1/client/[environmentId]/website/environment/route.ts b/apps/web/app/api/v1/client/[environmentId]/website/environment/route.ts deleted file mode 100644 index e5b20a0bf4..0000000000 --- a/apps/web/app/api/v1/client/[environmentId]/website/environment/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { responses } from "@/app/lib/api/response"; -import { transformErrorToDetails } from "@/app/lib/api/validator"; -import { NextRequest } from "next/server"; -import { environmentCache } from "@formbricks/lib/environment/cache"; -import { ResourceNotFoundError } from "@formbricks/types/errors"; -import { ZJsSyncInput } from "@formbricks/types/js"; -import { getEnvironmentState } from "./lib/environmentState"; - -export const OPTIONS = async (): Promise => { - return responses.successResponse({}, true); -}; - -export const GET = async ( - _: NextRequest, - { params }: { params: { environmentId: string } } -): Promise => { - try { - const syncInputValidation = ZJsSyncInput.safeParse({ - environmentId: params.environmentId, - }); - - if (!syncInputValidation.success) { - return responses.badRequestResponse( - "Fields are missing or incorrectly formatted", - transformErrorToDetails(syncInputValidation.error), - true - ); - } - - const { environmentId } = syncInputValidation.data; - - try { - const environmentState = await getEnvironmentState(environmentId); - - if (environmentState.revalidateEnvironment) { - environmentCache.revalidate({ - id: syncInputValidation.data.environmentId, - productId: environmentState.state.product.id, - }); - } - - return responses.successResponse( - environmentState.state, - true, - "public, s-maxage=600, max-age=840, stale-while-revalidate=600, stale-if-error=600" - ); - } catch (err) { - if (err instanceof ResourceNotFoundError) { - return responses.notFoundResponse(err.resourceType, err.resourceId); - } - - console.error(err); - return responses.internalServerErrorResponse(err.message ?? "Unable to complete response", true); - } - } catch (error) { - console.error(error); - return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true); - } -}; diff --git a/apps/web/app/api/v1/management/me/route.ts b/apps/web/app/api/v1/management/me/route.ts index 378d452ec6..13ab9d9433 100644 --- a/apps/web/app/api/v1/management/me/route.ts +++ b/apps/web/app/api/v1/management/me/route.ts @@ -24,7 +24,6 @@ export const GET = async () => { }, }, appSetupCompleted: true, - websiteSetupCompleted: true, }, }, }, diff --git a/apps/web/app/lib/formbricks.ts b/apps/web/app/lib/formbricks.ts index 73927d2dee..c80b553cd9 100644 --- a/apps/web/app/lib/formbricks.ts +++ b/apps/web/app/lib/formbricks.ts @@ -1,4 +1,4 @@ -import formbricks from "@formbricks/js/app"; +import formbricks from "@formbricks/js"; import { env } from "@formbricks/lib/env"; export const formbricksEnabled = diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 5545d63325..d5a90c3550 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -165,6 +165,30 @@ const nextConfig = { }, ]; }, + async rewrites() { + return [ + { + source: "/api/packages/website", + destination: "/api/packages/js", + }, + { + source: "/api/packages/app", + destination: "/api/packages/js", + }, + { + source: "/api/v1/client/:environmentId/website/environment", + destination: "/api/v1/client/:environmentId/environment", + }, + { + source: "/api/v1/client/:environmentId/app/environment", + destination: "/api/v1/client/:environmentId/environment", + }, + { + source: "/api/v1/client/:environmentId/app/people/:userId", + destination: "/api/v1/client/:environmentId/identify/people/:userId", + }, + ]; + }, env: { NEXTAUTH_URL: process.env.WEBAPP_URL, }, diff --git a/apps/web/playwright/js.spec.ts b/apps/web/playwright/js.spec.ts index a23a19135c..4f83234cdd 100644 --- a/apps/web/playwright/js.spec.ts +++ b/apps/web/playwright/js.spec.ts @@ -59,7 +59,7 @@ test.describe("JS Package Test", async () => { // Formbricks In App Sync has happened const syncApi = await page.waitForResponse( (response) => { - return response.url().includes("/app/environment"); + return response.url().includes("/environment"); }, { timeout: 120000, diff --git a/packages/database/data-migrations/20241002123456_migrate_survey_types/data-migration.ts b/packages/database/data-migrations/20241002123456_migrate_survey_types/data-migration.ts new file mode 100644 index 0000000000..8d6800e790 --- /dev/null +++ b/packages/database/data-migrations/20241002123456_migrate_survey_types/data-migration.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-console -- logging is allowed in migration scripts */ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); +const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds + +async function runMigration(): Promise { + const startTime = Date.now(); + console.log("Starting data migration..."); + + await prisma.$transaction( + async (transactionPrisma) => { + const websiteSurveys = await transactionPrisma.survey.findMany({ + where: { type: "website" }, + }); + + const updationPromises = []; + + for (const websiteSurvey of websiteSurveys) { + updationPromises.push( + transactionPrisma.survey.update({ + where: { id: websiteSurvey.id }, + data: { + type: "app", + segment: { + connectOrCreate: { + where: { + environmentId_title: { + environmentId: websiteSurvey.environmentId, + title: websiteSurvey.id, + }, + }, + create: { + title: websiteSurvey.id, + isPrivate: true, + environmentId: websiteSurvey.environmentId, + }, + }, + }, + }, + }) + ); + } + + await Promise.all(updationPromises); + console.log(`Updated ${websiteSurveys.length.toString()} website surveys to app surveys`); + }, + { + timeout: TRANSACTION_TIMEOUT, + } + ); + + const endTime = Date.now(); + console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); +} + +function handleError(error: unknown): void { + console.error("An error occurred during migration:", error); + process.exit(1); +} + +function handleDisconnectError(): void { + console.error("Failed to disconnect Prisma client"); + process.exit(1); +} + +function main(): void { + runMigration() + .catch(handleError) + .finally(() => { + prisma.$disconnect().catch(handleDisconnectError); + }); +} + +main(); diff --git a/packages/database/migrations/20241004070040_removed_website_setup_completed/migration.sql b/packages/database/migrations/20241004070040_removed_website_setup_completed/migration.sql new file mode 100644 index 0000000000..45812adf63 --- /dev/null +++ b/packages/database/migrations/20241004070040_removed_website_setup_completed/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `websiteSetupCompleted` on the `Environment` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Environment" DROP COLUMN "websiteSetupCompleted"; diff --git a/packages/database/package.json b/packages/database/package.json index 75e628477c..59a0105688 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -51,7 +51,8 @@ "data-migration:add-display-id-to-response": "ts-node ./data-migrations/20240905120500_refactor_display_response_relationship/data-migration.ts", "data-migration:address-question": "ts-node ./data-migrations/20240924123456_migrate_address_question/data-migration.ts", "data-migration:advanced-logic": "ts-node ./data-migrations/20240828122408_advanced_logic_editor/data-migration.ts", - "data-migration:segments-actions-cleanup": "ts-node ./data-migrations/20240904091113_removed_actions_table/data-migration.ts" + "data-migration:segments-actions-cleanup": "ts-node ./data-migrations/20240904091113_removed_actions_table/data-migration.ts", + "data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts" }, "dependencies": { "@prisma/client": "^5.18.0", diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index d7ed320930..3342dca9ed 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -386,24 +386,23 @@ model Integration { } model Environment { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @updatedAt @map(name: "updated_at") - type EnvironmentType - product Product @relation(fields: [productId], references: [id], onDelete: Cascade) - productId String - widgetSetupCompleted Boolean @default(false) - appSetupCompleted Boolean @default(false) - websiteSetupCompleted Boolean @default(false) - surveys Survey[] - people Person[] - actionClasses ActionClass[] - attributeClasses AttributeClass[] - apiKeys ApiKey[] - webhooks Webhook[] - tags Tag[] - segments Segment[] - integration Integration[] + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + type EnvironmentType + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + productId String + widgetSetupCompleted Boolean @default(false) + appSetupCompleted Boolean @default(false) + surveys Survey[] + people Person[] + actionClasses ActionClass[] + attributeClasses AttributeClass[] + apiKeys ApiKey[] + webhooks Webhook[] + tags Tag[] + segments Segment[] + integration Integration[] @@index([productId]) } diff --git a/packages/ee/advanced-targeting/components/advanced-targeting-card.tsx b/packages/ee/advanced-targeting/components/advanced-targeting-card.tsx index 839b8d6fff..c09cf5a1d1 100644 --- a/packages/ee/advanced-targeting/components/advanced-targeting-card.tsx +++ b/packages/ee/advanced-targeting/components/advanced-targeting-card.tsx @@ -16,6 +16,7 @@ import type { TSegmentUpdateInput, } from "@formbricks/types/segment"; import type { TSurvey } from "@formbricks/types/surveys/types"; +import { Alert, AlertDescription } from "@formbricks/ui/components/Alert"; import { AlertDialog } from "@formbricks/ui/components/AlertDialog"; import { Button } from "@formbricks/ui/components/Button"; import { LoadSegmentModal } from "@formbricks/ui/components/LoadSegmentModal"; @@ -161,7 +162,7 @@ export function AdvancedTargetingCard({ return (
+ +
+ + + + User targeting is currently only available when{" "} + + identifying users + {" "} + with the Formbricks SDK. + + + +
); diff --git a/packages/js-core/package.json b/packages/js-core/package.json index 73e8436c47..c3e2dbebfb 100644 --- a/packages/js-core/package.json +++ b/packages/js-core/package.json @@ -20,33 +20,20 @@ "dist" ], "exports": { - "./app": { - "import": "./dist/app.js", - "require": "./dist/app.umd.cjs", - "types": "./dist/app.d.ts" - }, - "./website": { - "import": "./dist/website.js", - "require": "./dist/website.umd.cjs", - "types": "./dist/website.d.ts" - }, - "./*": "./dist/*" + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs", + "types": "./dist/index.d.ts" }, "typesVersions": { "*": { - "app": [ - "./dist/app.d.ts" - ], - "website": [ - "./dist/website.d.ts" + "*": [ + "./dist/index.d.ts" ] } }, "scripts": { "dev": "vite build --watch --mode dev", - "build:app": "tsc && vite build --config app.vite.config.ts", - "build:website": "tsc && vite build --config website.vite.config.ts", - "build": "pnpm build:app && pnpm build:website", + "build": "tsc && vite build", "build:dev": "tsc && vite build --mode dev", "go": "vite build --watch --mode dev", "lint": "eslint . --ext .ts,.js,.tsx,.jsx", diff --git a/packages/js-core/src/app/lib/person.ts b/packages/js-core/src/app/lib/person.ts deleted file mode 100644 index b3c9b6b9e4..0000000000 --- a/packages/js-core/src/app/lib/person.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NetworkError, Result, err, okVoid } from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { AppConfig } from "./config"; -import { deinitalize, initialize } from "./initialize"; -import { closeSurvey } from "./widget"; - -const appConfig = AppConfig.getInstance(); -const logger = Logger.getInstance(); - -export const logoutPerson = async (): Promise => { - deinitalize(); - appConfig.resetConfig(); -}; - -export const resetPerson = async (): Promise> => { - logger.debug("Resetting state & getting new state from backend"); - closeSurvey(); - - const userId = appConfig.get().personState.data.userId; - if (!userId) { - return err({ - code: "network_error", - status: 500, - message: "Missing userId", - url: `${appConfig.get().apiHost}/api/v1/client/${appConfig.get().environmentId}/people/${userId}/attributes`, - responseMessage: "Missing userId", - }); - } - - const syncParams = { - environmentId: appConfig.get().environmentId, - apiHost: appConfig.get().apiHost, - userId, - attributes: appConfig.get().personState.data.attributes, - }; - await logoutPerson(); - try { - await initialize(syncParams); - return okVoid(); - } catch (e) { - return err(e as NetworkError); - } -}; diff --git a/packages/js-core/src/app/index.ts b/packages/js-core/src/index.ts similarity index 68% rename from packages/js-core/src/app/index.ts rename to packages/js-core/src/index.ts index 88b4cbabd5..9a7811d774 100644 --- a/packages/js-core/src/app/index.ts +++ b/packages/js-core/src/index.ts @@ -1,11 +1,11 @@ -import { TJsAppConfigInput, TJsTrackProperties } from "@formbricks/types/js"; -import { CommandQueue } from "../shared/commandQueue"; -import { ErrorHandler } from "../shared/errors"; -import { Logger } from "../shared/logger"; +import { TJsConfigInput, TJsTrackProperties } from "@formbricks/types/js"; import { trackCodeAction } from "./lib/actions"; import { getApi } from "./lib/api"; import { setAttributeInApp } from "./lib/attributes"; +import { CommandQueue } from "./lib/commandQueue"; +import { ErrorHandler } from "./lib/errors"; import { initialize } from "./lib/initialize"; +import { Logger } from "./lib/logger"; import { checkPageUrl } from "./lib/noCodeActions"; import { logoutPerson, resetPerson } from "./lib/person"; @@ -14,9 +14,9 @@ const logger = Logger.getInstance(); logger.debug("Create command queue"); const queue = new CommandQueue(); -const init = async (initConfig: TJsAppConfigInput) => { +const init = async (initConfig: TJsConfigInput) => { ErrorHandler.init(initConfig.errorHandler); - queue.add(false, "app", initialize, initConfig); + queue.add(false, initialize, initConfig); await queue.wait(); }; @@ -26,27 +26,27 @@ const setEmail = async (email: string): Promise => { }; const setAttribute = async (key: string, value: any): Promise => { - queue.add(true, "app", setAttributeInApp, key, value); + queue.add(true, setAttributeInApp, key, value); await queue.wait(); }; const logout = async (): Promise => { - queue.add(true, "app", logoutPerson); + queue.add(true, logoutPerson); await queue.wait(); }; const reset = async (): Promise => { - queue.add(true, "app", resetPerson); + queue.add(true, resetPerson); await queue.wait(); }; const track = async (name: string, properties?: TJsTrackProperties): Promise => { - queue.add(true, "app", trackCodeAction, name, properties); + queue.add(true, trackCodeAction, name, properties); await queue.wait(); }; const registerRouteChange = async (): Promise => { - queue.add(true, "app", checkPageUrl); + queue.add(true, checkPageUrl); await queue.wait(); }; diff --git a/packages/js-core/src/app/lib/actions.ts b/packages/js-core/src/lib/actions.ts similarity index 84% rename from packages/js-core/src/app/lib/actions.ts rename to packages/js-core/src/lib/actions.ts index 297196f9b5..1148bf3a3e 100644 --- a/packages/js-core/src/app/lib/actions.ts +++ b/packages/js-core/src/lib/actions.ts @@ -1,11 +1,11 @@ import { TJsTrackProperties } from "@formbricks/types/js"; -import { InvalidCodeError, NetworkError, Result, err, okVoid } from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { AppConfig } from "./config"; +import { Config } from "./config"; +import { InvalidCodeError, NetworkError, Result, err, okVoid } from "./errors"; +import { Logger } from "./logger"; import { triggerSurvey } from "./widget"; const logger = Logger.getInstance(); -const appConfig = AppConfig.getInstance(); +const config = Config.getInstance(); export const trackAction = async ( name: string, @@ -17,7 +17,7 @@ export const trackAction = async ( logger.debug(`Formbricks: Action "${aliasName}" tracked`); // get a list of surveys that are collecting insights - const activeSurveys = appConfig.get().filteredSurveys; + const activeSurveys = config.get().filteredSurveys; if (!!activeSurveys && activeSurveys.length > 0) { for (const survey of activeSurveys) { @@ -38,7 +38,7 @@ export const trackCodeAction = ( code: string, properties?: TJsTrackProperties ): Promise> | Result => { - const actionClasses = appConfig.get().environmentState.data.actionClasses; + const actionClasses = config.get().environmentState.data.actionClasses; const codeActionClasses = actionClasses.filter((action) => action.type === "code"); const action = codeActionClasses.find((action) => action.key === code); diff --git a/packages/js-core/src/app/lib/api.ts b/packages/js-core/src/lib/api.ts similarity index 66% rename from packages/js-core/src/app/lib/api.ts rename to packages/js-core/src/lib/api.ts index 508f939230..e2a325ba65 100644 --- a/packages/js-core/src/app/lib/api.ts +++ b/packages/js-core/src/lib/api.ts @@ -1,9 +1,9 @@ import { FormbricksAPI } from "@formbricks/api"; -import { AppConfig } from "./config"; +import { Config } from "./config"; export const getApi = (): FormbricksAPI => { - const inAppConfig = AppConfig.getInstance(); - const { environmentId, apiHost } = inAppConfig.get(); + const config = Config.getInstance(); + const { environmentId, apiHost } = config.get(); if (!environmentId || !apiHost) { throw new Error("formbricks.init() must be called before getApi()"); diff --git a/packages/js-core/src/app/lib/attributes.ts b/packages/js-core/src/lib/attributes.ts similarity index 81% rename from packages/js-core/src/app/lib/attributes.ts rename to packages/js-core/src/lib/attributes.ts index d7f7660902..aef097b806 100644 --- a/packages/js-core/src/app/lib/attributes.ts +++ b/packages/js-core/src/lib/attributes.ts @@ -1,12 +1,12 @@ import { FormbricksAPI } from "@formbricks/api"; import { TAttributes } from "@formbricks/types/attributes"; -import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { fetchPersonState } from "../../shared/personState"; -import { filterSurveys } from "../../shared/utils"; -import { AppConfig } from "./config"; +import { Config } from "./config"; +import { MissingPersonError, NetworkError, Result, err, ok, okVoid } from "./errors"; +import { Logger } from "./logger"; +import { fetchPersonState } from "./personState"; +import { filterSurveys } from "./utils"; -const appConfig = AppConfig.getInstance(); +const config = Config.getInstance(); const logger = Logger.getInstance(); export const updateAttribute = async ( @@ -21,8 +21,8 @@ export const updateAttribute = async ( Error | NetworkError > > => { - const { apiHost, environmentId } = appConfig.get(); - const userId = appConfig.get().personState.data.userId; + const { apiHost, environmentId } = config.get(); + const userId = config.get().personState.data.userId; if (!userId) { return err({ @@ -58,7 +58,7 @@ export const updateAttribute = async ( // @ts-expect-error status: res.error.status ?? 500, message: res.error.message ?? `Error updating person with userId ${userId}`, - url: `${appConfig.get().apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`, + url: `${config.get().apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`, responseMessage: res.error.message, }); } @@ -93,7 +93,7 @@ export const updateAttributes = async ( const updatedAttributes = { ...attributes }; try { - const existingAttributes = appConfig.get().personState.data.attributes; + const existingAttributes = config.get().personState.data.attributes; if (existingAttributes) { for (const [key, value] of Object.entries(existingAttributes)) { if (updatedAttributes[key] === value) { @@ -140,7 +140,7 @@ export const updateAttributes = async ( }; export const isExistingAttribute = (key: string, value: string): boolean => { - if (appConfig.get().personState.data.attributes[key] === value) { + if (config.get().personState.data.attributes[key] === value) { return true; } @@ -156,14 +156,7 @@ export const setAttributeInApp = async ( return okVoid(); } - const userId = appConfig.get().personState.data.userId; - - if (!userId) { - return err({ - code: "missing_person", - message: "Missing userId", - }); - } + const userId = config.get().personState.data.userId; logger.debug("Setting attribute: " + key + " to value: " + value); // check if attribute already exists with this value @@ -172,23 +165,30 @@ export const setAttributeInApp = async ( return okVoid(); } + if (!userId) { + logger.error( + "UserId not provided, please provide a userId in the init method before setting attributes." + ); + return okVoid(); + } + const result = await updateAttribute(key, value.toString()); if (result.ok) { if (result.value.changed) { const personState = await fetchPersonState( { - apiHost: appConfig.get().apiHost, - environmentId: appConfig.get().environmentId, + apiHost: config.get().apiHost, + environmentId: config.get().environmentId, userId, }, true ); - const filteredSurveys = filterSurveys(appConfig.get().environmentState, personState); + const filteredSurveys = filterSurveys(config.get().environmentState, personState); - appConfig.update({ - ...appConfig.get(), + config.update({ + ...config.get(), personState, filteredSurveys, }); diff --git a/packages/js-core/src/shared/commandQueue.ts b/packages/js-core/src/lib/commandQueue.ts similarity index 80% rename from packages/js-core/src/shared/commandQueue.ts rename to packages/js-core/src/lib/commandQueue.ts index c05369acc1..c372a569e7 100644 --- a/packages/js-core/src/shared/commandQueue.ts +++ b/packages/js-core/src/lib/commandQueue.ts @@ -1,13 +1,10 @@ import { wrapThrowsAsync } from "@formbricks/types/error-handlers"; -import { TJsPackageType } from "@formbricks/types/js"; -import { checkInitialized as checkInitializedInApp } from "../app/lib/initialize"; -import { ErrorHandler, Result } from "../shared/errors"; -import { checkInitialized as checkInitializedWebsite } from "../website/lib/initialize"; +import { ErrorHandler, Result } from "./errors"; +import { checkInitialized } from "./initialize"; export class CommandQueue { private queue: { command: (args: any) => Promise> | Result | Promise; - packageType: TJsPackageType; checkInitialized: boolean; commandArgs: any[any]; }[] = []; @@ -17,11 +14,10 @@ export class CommandQueue { public add( checkInitialized: boolean = true, - packageType: TJsPackageType, command: (...args: A[]) => Promise> | Result | Promise, ...args: A[] ) { - this.queue.push({ command, checkInitialized, commandArgs: args, packageType }); + this.queue.push({ command, checkInitialized, commandArgs: args }); if (!this.running) { this.commandPromise = new Promise((resolve) => { @@ -48,8 +44,7 @@ export class CommandQueue { // make sure formbricks is initialized if (currentItem.checkInitialized) { // call different function based on package type - const initResult = - currentItem.packageType === "website" ? checkInitializedWebsite() : checkInitializedInApp(); + const initResult = checkInitialized(); if (initResult && initResult.ok !== true) { errorHandler.handle(initResult.error); diff --git a/packages/js-core/src/app/lib/config.ts b/packages/js-core/src/lib/config.ts similarity index 75% rename from packages/js-core/src/app/lib/config.ts rename to packages/js-core/src/lib/config.ts index 63c5d4e659..ca9cfe6bfd 100644 --- a/packages/js-core/src/app/lib/config.ts +++ b/packages/js-core/src/lib/config.ts @@ -1,9 +1,9 @@ import { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js"; -import { APP_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants"; -import { Result, err, ok, wrapThrows } from "../../shared/errors"; +import { JS_LOCAL_STORAGE_KEY } from "./constants"; +import { Result, err, ok, wrapThrows } from "./errors"; -export class AppConfig { - private static instance: AppConfig | undefined; +export class Config { + private static instance: Config | undefined; private config: TJsConfig | null = null; private constructor() { @@ -14,11 +14,11 @@ export class AppConfig { } } - static getInstance(): AppConfig { - if (!AppConfig.instance) { - AppConfig.instance = new AppConfig(); + static getInstance(): Config { + if (!Config.instance) { + Config.instance = new Config(); } - return AppConfig.instance; + return Config.instance; } public update(newConfig: TJsConfigUpdateInput): void { @@ -45,7 +45,7 @@ export class AppConfig { public loadFromLocalStorage(): Result { if (typeof window !== "undefined") { - const savedConfig = localStorage.getItem(APP_SURVEYS_LOCAL_STORAGE_KEY); + const savedConfig = localStorage.getItem(JS_LOCAL_STORAGE_KEY); if (savedConfig) { // TODO: validate config // This is a hack to get around the fact that we don't have a proper @@ -69,7 +69,7 @@ export class AppConfig { private async saveToStorage(): Promise, Error>> { return wrapThrows(async () => { - await localStorage.setItem(APP_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(this.config)); + await localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(this.config)); })(); } @@ -79,7 +79,7 @@ export class AppConfig { this.config = null; return wrapThrows(async () => { - localStorage.removeItem(APP_SURVEYS_LOCAL_STORAGE_KEY); + localStorage.removeItem(JS_LOCAL_STORAGE_KEY); })(); } } diff --git a/packages/js-core/src/lib/constants.ts b/packages/js-core/src/lib/constants.ts new file mode 100644 index 0000000000..bf19f961e6 --- /dev/null +++ b/packages/js-core/src/lib/constants.ts @@ -0,0 +1,5 @@ +export const RN_ASYNC_STORAGE_KEY = "formbricks-react-native"; +export const JS_LOCAL_STORAGE_KEY = "formbricks-js"; +export const LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY = "formbricks-js-website"; +export const LEGACY_JS_APP_LOCAL_STORAGE_KEY = "formbricks-js-app"; +export const CONTAINER_ID = "formbricks-app-container"; diff --git a/packages/js-core/src/shared/environmentState.ts b/packages/js-core/src/lib/environmentState.ts similarity index 88% rename from packages/js-core/src/shared/environmentState.ts rename to packages/js-core/src/lib/environmentState.ts index ce881a563a..72c6d85dd1 100644 --- a/packages/js-core/src/shared/environmentState.ts +++ b/packages/js-core/src/lib/environmentState.ts @@ -1,11 +1,11 @@ // shared functions for environment and person state(s) import { TJsEnvironmentState, TJsEnvironmentSyncParams } from "@formbricks/types/js"; -import { AppConfig } from "../app/lib/config"; -import { WebsiteConfig } from "../website/lib/config"; +import { Config } from "./config"; import { err } from "./errors"; import { Logger } from "./logger"; import { filterSurveys, getIsDebug } from "./utils"; +const config = Config.getInstance(); const logger = Logger.getInstance(); let environmentStateSyncIntervalId: number | null = null; @@ -19,7 +19,6 @@ let environmentStateSyncIntervalId: number | null = null; */ export const fetchEnvironmentState = async ( { apiHost, environmentId }: TJsEnvironmentSyncParams, - sdkType: "app" | "website", noCache: boolean = false ): Promise => { let fetchOptions: RequestInit = {}; @@ -29,7 +28,7 @@ export const fetchEnvironmentState = async ( logger.debug("No cache option set for sync"); } - const url = `${apiHost}/api/v1/client/${environmentId}/${sdkType}/environment`; + const url = `${apiHost}/api/v1/client/${environmentId}/environment`; const response = await fetch(url, fetchOptions); @@ -56,10 +55,7 @@ export const fetchEnvironmentState = async ( }; }; -export const addEnvironmentStateExpiryCheckListener = ( - sdkType: "app" | "website", - config: AppConfig | WebsiteConfig -): void => { +export const addEnvironmentStateExpiryCheckListener = (): void => { let updateInterval = 1000 * 60; // every minute if (typeof window !== "undefined" && environmentStateSyncIntervalId === null) { environmentStateSyncIntervalId = window.setInterval(async () => { @@ -79,7 +75,6 @@ export const addEnvironmentStateExpiryCheckListener = ( apiHost: config.get().apiHost, environmentId: config.get().environmentId, }, - sdkType, true ); diff --git a/packages/js-core/src/shared/errors.ts b/packages/js-core/src/lib/errors.ts similarity index 100% rename from packages/js-core/src/shared/errors.ts rename to packages/js-core/src/lib/errors.ts diff --git a/packages/js-core/src/app/lib/eventListeners.ts b/packages/js-core/src/lib/eventListeners.ts similarity index 80% rename from packages/js-core/src/app/lib/eventListeners.ts rename to packages/js-core/src/lib/eventListeners.ts index 0a1532b0b0..4ee436a2f9 100644 --- a/packages/js-core/src/app/lib/eventListeners.ts +++ b/packages/js-core/src/lib/eventListeners.ts @@ -1,11 +1,7 @@ import { addEnvironmentStateExpiryCheckListener, clearEnvironmentStateExpiryCheckListener, -} from "../../shared/environmentState"; -import { - addPersonStateExpiryCheckListener, - clearPersonStateExpiryCheckListener, -} from "../../shared/personState"; +} from "./environmentState"; import { addClickEventListener, addExitIntentListener, @@ -15,14 +11,14 @@ import { removeExitIntentListener, removePageUrlEventListeners, removeScrollDepthListener, -} from "../lib/noCodeActions"; -import { AppConfig } from "./config"; +} from "./noCodeActions"; +import { addPersonStateExpiryCheckListener, clearPersonStateExpiryCheckListener } from "./personState"; let areRemoveEventListenersAdded = false; -export const addEventListeners = (config: AppConfig): void => { - addEnvironmentStateExpiryCheckListener("app", config); - addPersonStateExpiryCheckListener(config); +export const addEventListeners = (): void => { + addEnvironmentStateExpiryCheckListener(); + addPersonStateExpiryCheckListener(); addPageUrlEventListeners(); addClickEventListener(); addExitIntentListener(); diff --git a/packages/js-core/src/app/lib/initialize.ts b/packages/js-core/src/lib/initialize.ts similarity index 63% rename from packages/js-core/src/app/lib/initialize.ts rename to packages/js-core/src/lib/initialize.ts index c6366c1733..61a41de96c 100644 --- a/packages/js-core/src/app/lib/initialize.ts +++ b/packages/js-core/src/lib/initialize.ts @@ -1,7 +1,14 @@ import { TAttributes } from "@formbricks/types/attributes"; -import type { TJsAppConfigInput, TJsConfig } from "@formbricks/types/js"; -import { APP_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants"; -import { fetchEnvironmentState } from "../../shared/environmentState"; +import { type TJsConfig, type TJsConfigInput } from "@formbricks/types/js"; +import { trackNoCodeAction } from "./actions"; +import { updateAttributes } from "./attributes"; +import { Config } from "./config"; +import { + JS_LOCAL_STORAGE_KEY, + LEGACY_JS_APP_LOCAL_STORAGE_KEY, + LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY, +} from "./constants"; +import { fetchEnvironmentState } from "./environmentState"; import { ErrorHandler, MissingFieldError, @@ -12,18 +19,14 @@ import { err, okVoid, wrapThrows, -} from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { fetchPersonState } from "../../shared/personState"; -import { filterSurveys, getIsDebug } from "../../shared/utils"; -import { trackNoCodeAction } from "./actions"; -import { updateAttributes } from "./attributes"; -import { AppConfig } from "./config"; +} from "./errors"; import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners"; +import { Logger } from "./logger"; import { checkPageUrl } from "./noCodeActions"; +import { DEFAULT_PERSON_STATE_NO_USER_ID, fetchPersonState } from "./personState"; +import { filterSurveys, getIsDebug } from "./utils"; import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget"; -const appConfigGlobal = AppConfig.getInstance(); const logger = Logger.getInstance(); let isInitialized = false; @@ -32,38 +35,72 @@ export const setIsInitialized = (value: boolean) => { isInitialized = value; }; -const checkForOlderLocalConfig = (): boolean => { - const oldConfig = localStorage.getItem(APP_SURVEYS_LOCAL_STORAGE_KEY); +const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => { + const oldWebsiteConfig = localStorage.getItem(LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY); + const oldAppConfig = localStorage.getItem(LEGACY_JS_APP_LOCAL_STORAGE_KEY); - if (oldConfig) { - const parsedOldConfig = JSON.parse(oldConfig); - if (parsedOldConfig.state || parsedOldConfig.expiresAt) { - // local config follows old structure - return true; + if (oldWebsiteConfig) { + localStorage.removeItem(LEGACY_JS_WEBSITE_LOCAL_STORAGE_KEY); + const parsedOldConfig = JSON.parse(oldWebsiteConfig) as TJsConfig; + + if ( + parsedOldConfig.environmentId && + parsedOldConfig.apiHost && + parsedOldConfig.environmentState && + parsedOldConfig.personState && + parsedOldConfig.filteredSurveys + ) { + const newLocalStorageConfig = { ...parsedOldConfig }; + + return { + changed: true, + newState: newLocalStorageConfig, + }; } } - return false; + if (oldAppConfig) { + localStorage.removeItem(LEGACY_JS_APP_LOCAL_STORAGE_KEY); + const parsedOldConfig = JSON.parse(oldAppConfig) as TJsConfig; + + if ( + parsedOldConfig.environmentId && + parsedOldConfig.apiHost && + parsedOldConfig.environmentState && + parsedOldConfig.personState && + parsedOldConfig.filteredSurveys + ) { + return { + changed: true, + }; + } + } + + return { + changed: false, + }; }; export const initialize = async ( - configInput: TJsAppConfigInput + configInput: TJsConfigInput ): Promise> => { const isDebug = getIsDebug(); if (isDebug) { logger.configure({ logLevel: "debug" }); } + let config = Config.getInstance(); - const isLocalStorageOld = checkForOlderLocalConfig(); + const { changed, newState } = migrateLocalStorage(); - let appConfig = appConfigGlobal; + if (changed) { + config.resetConfig(); + config = Config.getInstance(); - if (isLocalStorageOld) { - logger.debug("Local config is of an older version"); - logger.debug("Resetting config"); - - appConfig.resetConfig(); - appConfig = AppConfig.getInstance(); + // If the js sdk is being used for non identified users, and we have a new state to update to after migrating, we update the state + // otherwise, we just sync again! + if (!configInput.userId && newState) { + config.update(newState); + } } if (isInitialized) { @@ -73,7 +110,7 @@ export const initialize = async ( let existingConfig: TJsConfig | undefined; try { - existingConfig = appConfigGlobal.get(); + existingConfig = config.get(); logger.debug("Found existing configuration."); } catch (e) { logger.debug("No existing configuration found."); @@ -85,7 +122,7 @@ export const initialize = async ( logger.debug( "Formbricks is in error state, but debug mode is active. Resetting config and continuing." ); - appConfigGlobal.resetConfig(); + config.resetConfig(); return okVoid(); } @@ -122,38 +159,32 @@ export const initialize = async ( }); } - if (!configInput.userId) { - logger.debug("No userId provided"); - - return err({ - code: "missing_field", - field: "userId", - }); - } - logger.debug("Adding widget container to DOM"); addWidgetContainer(); let updatedAttributes: TAttributes | null = null; if (configInput.attributes) { - const res = await updateAttributes( - configInput.apiHost, - configInput.environmentId, - configInput.userId, - configInput.attributes - ); - if (res.ok !== true) { - return err(res.error); + if (configInput.userId) { + const res = await updateAttributes( + configInput.apiHost, + configInput.environmentId, + configInput.userId, + configInput.attributes + ); + if (res.ok !== true) { + return err(res.error); + } + updatedAttributes = res.value; + } else { + updatedAttributes = { ...configInput.attributes }; } - updatedAttributes = res.value; } if ( existingConfig && existingConfig.environmentState && existingConfig.environmentId === configInput.environmentId && - existingConfig.apiHost === configInput.apiHost && - existingConfig.personState?.data?.userId === configInput.userId + existingConfig.apiHost === configInput.apiHost ) { logger.debug("Configuration fits init parameters."); let isEnvironmentStateExpired = false; @@ -164,7 +195,12 @@ export const initialize = async ( isEnvironmentStateExpired = true; } - if (existingConfig.personState.expiresAt && new Date(existingConfig.personState.expiresAt) < new Date()) { + // if the config has a userId and the person state has expired, we need to sync the person state + if ( + configInput.userId && + existingConfig.personState.expiresAt && + new Date(existingConfig.personState.expiresAt) < new Date() + ) { logger.debug("Person state expired. Syncing."); isPersonStateExpired = true; } @@ -172,29 +208,33 @@ export const initialize = async ( try { // fetch the environment state (if expired) const environmentState = isEnvironmentStateExpired - ? await fetchEnvironmentState( - { - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - }, - "app" - ) + ? await fetchEnvironmentState({ + apiHost: configInput.apiHost, + environmentId: configInput.environmentId, + }) : existingConfig.environmentState; // fetch the person state (if expired) - const personState = isPersonStateExpired - ? await fetchPersonState({ + + let { personState } = existingConfig; + + if (isPersonStateExpired) { + if (configInput.userId) { + personState = await fetchPersonState({ apiHost: configInput.apiHost, environmentId: configInput.environmentId, userId: configInput.userId, - }) - : existingConfig.personState; + }); + } else { + personState = DEFAULT_PERSON_STATE_NO_USER_ID; + } + } // filter the environment state wrt the person state const filteredSurveys = filterSurveys(environmentState, personState); // update the appConfig with the new filtered surveys - appConfigGlobal.update({ + config.update({ ...existingConfig, environmentState, personState, @@ -204,13 +244,13 @@ export const initialize = async ( const surveyNames = filteredSurveys.map((s) => s.name); logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", ")); } catch (e) { - putFormbricksInErrorState(appConfig); + putFormbricksInErrorState(config); } } else { logger.debug( "No valid configuration found or it has been expired. Resetting config and creating new one." ); - appConfigGlobal.resetConfig(); + config.resetConfig(); logger.debug("Syncing."); try { @@ -219,21 +259,22 @@ export const initialize = async ( apiHost: configInput.apiHost, environmentId: configInput.environmentId, }, - "app", - false - ); - const personState = await fetchPersonState( - { - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - userId: configInput.userId, - }, false ); + const personState = configInput.userId + ? await fetchPersonState( + { + apiHost: configInput.apiHost, + environmentId: configInput.environmentId, + userId: configInput.userId, + }, + false + ) + : DEFAULT_PERSON_STATE_NO_USER_ID; const filteredSurveys = filterSurveys(environmentState, personState); - appConfigGlobal.update({ + config.update({ apiHost: configInput.apiHost, environmentId: configInput.environmentId, personState, @@ -250,14 +291,14 @@ export const initialize = async ( // update attributes in config if (updatedAttributes && Object.keys(updatedAttributes).length > 0) { - appConfigGlobal.update({ - ...appConfigGlobal.get(), + config.update({ + ...config.get(), personState: { - ...appConfigGlobal.get().personState, + ...config.get().personState, data: { - ...appConfigGlobal.get().personState.data, + ...config.get().personState.data, attributes: { - ...appConfigGlobal.get().personState.data.attributes, + ...config.get().personState.data.attributes, ...updatedAttributes, }, }, @@ -266,7 +307,7 @@ export const initialize = async ( } logger.debug("Adding event listeners"); - addEventListeners(appConfigGlobal); + addEventListeners(); addCleanupEventListeners(); setIsInitialized(true); @@ -293,7 +334,7 @@ export const handleErrorOnFirstInit = () => { }; // can't use config.update here because the config is not yet initialized - wrapThrows(() => localStorage.setItem(APP_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))(); + wrapThrows(() => localStorage.setItem(JS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))(); throw new Error("Could not initialize formbricks"); }; @@ -317,7 +358,7 @@ export const deinitalize = (): void => { setIsInitialized(false); }; -export const putFormbricksInErrorState = (appConfig: AppConfig): void => { +export const putFormbricksInErrorState = (config: Config): void => { if (getIsDebug()) { logger.debug("Not putting formbricks in error state because debug mode is active (no error state)"); return; @@ -325,8 +366,8 @@ export const putFormbricksInErrorState = (appConfig: AppConfig): void => { logger.debug("Putting formbricks in error state"); // change formbricks status to error - appConfig.update({ - ...appConfigGlobal.get(), + config.update({ + ...config.get(), status: { value: "error", expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future diff --git a/packages/js-core/src/shared/logger.ts b/packages/js-core/src/lib/logger.ts similarity index 100% rename from packages/js-core/src/shared/logger.ts rename to packages/js-core/src/lib/logger.ts diff --git a/packages/js-core/src/app/lib/noCodeActions.ts b/packages/js-core/src/lib/noCodeActions.ts similarity index 96% rename from packages/js-core/src/app/lib/noCodeActions.ts rename to packages/js-core/src/lib/noCodeActions.ts index f057e7b739..c1bb675260 100644 --- a/packages/js-core/src/app/lib/noCodeActions.ts +++ b/packages/js-core/src/lib/noCodeActions.ts @@ -1,11 +1,11 @@ import type { TActionClass } from "@formbricks/types/action-classes"; -import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { evaluateNoCodeConfigClick, handleUrlFilters } from "../../shared/utils"; import { trackNoCodeAction } from "./actions"; -import { AppConfig } from "./config"; +import { Config } from "./config"; +import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "./errors"; +import { Logger } from "./logger"; +import { evaluateNoCodeConfigClick, handleUrlFilters } from "./utils"; -const appConfig = AppConfig.getInstance(); +const appConfig = Config.getInstance(); const logger = Logger.getInstance(); const errorHandler = ErrorHandler.getInstance(); diff --git a/packages/js-core/src/lib/person.ts b/packages/js-core/src/lib/person.ts new file mode 100644 index 0000000000..b94905e25f --- /dev/null +++ b/packages/js-core/src/lib/person.ts @@ -0,0 +1,34 @@ +import { Config } from "./config"; +import { NetworkError, Result, err, okVoid } from "./errors"; +import { deinitalize, initialize } from "./initialize"; +import { Logger } from "./logger"; +import { closeSurvey } from "./widget"; + +const config = Config.getInstance(); +const logger = Logger.getInstance(); + +export const logoutPerson = async (): Promise => { + deinitalize(); + config.resetConfig(); +}; + +export const resetPerson = async (): Promise> => { + logger.debug("Resetting state & getting new state from backend"); + closeSurvey(); + + const userId = config.get().personState.data.userId; + + const syncParams = { + environmentId: config.get().environmentId, + apiHost: config.get().apiHost, + ...(userId && { userId }), + attributes: config.get().personState.data.attributes, + }; + await logoutPerson(); + try { + await initialize(syncParams); + return okVoid(); + } catch (e) { + return err(e as NetworkError); + } +}; diff --git a/packages/js-core/src/shared/personState.ts b/packages/js-core/src/lib/personState.ts similarity index 83% rename from packages/js-core/src/shared/personState.ts rename to packages/js-core/src/lib/personState.ts index c575dda87e..7c4231168e 100644 --- a/packages/js-core/src/shared/personState.ts +++ b/packages/js-core/src/lib/personState.ts @@ -1,13 +1,14 @@ import { TJsPersonState, TJsPersonSyncParams } from "@formbricks/types/js"; -import { AppConfig } from "../app/lib/config"; +import { Config } from "./config"; import { err } from "./errors"; import { Logger } from "./logger"; import { getIsDebug } from "./utils"; +const config = Config.getInstance(); const logger = Logger.getInstance(); let personStateSyncIntervalId: number | null = null; -export const DEFAULT_PERSON_STATE_WEBSITE: TJsPersonState = { +export const DEFAULT_PERSON_STATE_NO_USER_ID: TJsPersonState = { expiresAt: null, data: { userId: null, @@ -39,7 +40,7 @@ export const fetchPersonState = async ( logger.debug("No cache option set for sync"); } - const url = `${apiHost}/api/v1/client/${environmentId}/app/people/${userId}`; + const url = `${apiHost}/api/v1/client/${environmentId}/identify/people/${userId}`; const response = await fetch(url, fetchOptions); @@ -60,6 +61,8 @@ export const fetchPersonState = async ( const data = await response.json(); const { data: state } = data; + console.log("Person state fetched", state); + const defaultPersonState: TJsPersonState = { expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes data: { @@ -84,25 +87,24 @@ export const fetchPersonState = async ( /** * Add a listener to check if the person state has expired with a certain interval - * @param appConfig - The app config + * @param config - The configuration for the SDK */ -export const addPersonStateExpiryCheckListener = (appConfig: AppConfig): void => { +export const addPersonStateExpiryCheckListener = (): void => { const updateInterval = 1000 * 60; // every 60 seconds if (typeof window !== "undefined" && personStateSyncIntervalId === null) { personStateSyncIntervalId = window.setInterval(async () => { - const userId = appConfig.get().personState.data.userId; + const userId = config.get().personState.data.userId; if (!userId) { return; } // extend the personState validity by 30 minutes: - - appConfig.update({ - ...appConfig.get(), + config.update({ + ...config.get(), personState: { - ...appConfig.get().personState, + ...config.get().personState, expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes }, }); diff --git a/packages/js-core/src/shared/utils.ts b/packages/js-core/src/lib/utils.ts similarity index 97% rename from packages/js-core/src/shared/utils.ts rename to packages/js-core/src/lib/utils.ts index 4911566edc..de566febdb 100644 --- a/packages/js-core/src/shared/utils.ts +++ b/packages/js-core/src/lib/utils.ts @@ -158,11 +158,10 @@ export const getIsDebug = () => window.location.search.includes("formbricksDebug // takes the environment and person state and returns the filtered surveys export const filterSurveys = ( environmentState: TJsEnvironmentState, - personState: TJsPersonState, - sdkType: "app" | "website" = "app" + personState: TJsPersonState ): TSurvey[] => { const { product, surveys } = environmentState.data; - const { displays, responses, lastDisplayAt, segments } = personState.data; + const { displays, responses, lastDisplayAt, segments, userId } = personState.data; if (!displays) { return []; @@ -220,7 +219,7 @@ export const filterSurveys = ( } }); - if (sdkType === "website") { + if (!userId) { return filteredSurveys; } diff --git a/packages/js-core/src/app/lib/widget.ts b/packages/js-core/src/lib/widget.ts similarity index 79% rename from packages/js-core/src/app/lib/widget.ts rename to packages/js-core/src/lib/widget.ts index 29173029d3..ccc03e3b24 100644 --- a/packages/js-core/src/app/lib/widget.ts +++ b/packages/js-core/src/lib/widget.ts @@ -6,19 +6,18 @@ import { TJsFileUploadParams, TJsPersonState, TJsTrackProperties } from "@formbr import { TResponseHiddenFieldValue, TResponseUpdate } from "@formbricks/types/responses"; import { TUploadFileConfig } from "@formbricks/types/storage"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { Logger } from "../../shared/logger"; +import { Config } from "./config"; +import { CONTAINER_ID } from "./constants"; +import { Logger } from "./logger"; import { filterSurveys, getDefaultLanguageCode, getLanguageCode, handleHiddenFields, shouldDisplayBasedOnPercentage, -} from "../../shared/utils"; -import { AppConfig } from "./config"; +} from "./utils"; -const containerId = "formbricks-app-container"; - -const appConfig = AppConfig.getInstance(); +const config = Config.getInstance(); const logger = Logger.getInstance(); let isSurveyRunning = false; let setIsError = (_: boolean) => {}; @@ -65,8 +64,8 @@ const renderWidget = async ( logger.debug(`Delaying survey "${survey.name}" by ${survey.delay} seconds.`); } - const { product } = appConfig.get().environmentState.data ?? {}; - const { attributes } = appConfig.get().personState.data ?? {}; + const { product } = config.get().environmentState.data ?? {}; + const { attributes } = config.get().personState.data ?? {}; const isMultiLanguageSurvey = survey.languages.length > 1; let languageCode = "default"; @@ -82,12 +81,12 @@ const renderWidget = async ( languageCode = displayLanguage; } - const surveyState = new SurveyState(survey.id, null, null, appConfig.get().personState.data.userId); + const surveyState = new SurveyState(survey.id, null, null, config.get().personState.data.userId); const responseQueue = new ResponseQueue( { - apiHost: appConfig.get().apiHost, - environmentId: appConfig.get().environmentId, + apiHost: config.get().apiHost, + environmentId: config.get().environmentId, retryAttempts: 2, onResponseSendingFailed: () => { setIsError(true); @@ -121,21 +120,16 @@ const renderWidget = async ( setIsResponseSendingFinished = f; }, onDisplay: async () => { - const { userId } = appConfig.get().personState.data; - - if (!userId) { - logger.debug("User ID not found. Skipping."); - return; - } + const { userId } = config.get().personState.data; const api = new FormbricksAPI({ - apiHost: appConfig.get().apiHost, - environmentId: appConfig.get().environmentId, + apiHost: config.get().apiHost, + environmentId: config.get().environmentId, }); const res = await api.client.display.create({ surveyId: survey.id, - userId, + ...(userId && { userId }), }); if (!res.ok) { @@ -147,10 +141,10 @@ const renderWidget = async ( surveyState.updateDisplayId(id); responseQueue.updateSurveyState(surveyState); - const existingDisplays = appConfig.get().personState.data.displays; + const existingDisplays = config.get().personState.data.displays; const newDisplay = { surveyId: survey.id, createdAt: new Date() }; const displays = existingDisplays ? [...existingDisplays, newDisplay] : [newDisplay]; - const previousConfig = appConfig.get(); + const previousConfig = config.get(); const updatedPersonState: TJsPersonState = { ...previousConfig.personState, @@ -163,23 +157,21 @@ const renderWidget = async ( const filteredSurveys = filterSurveys(previousConfig.environmentState, updatedPersonState); - appConfig.update({ + config.update({ ...previousConfig, + environmentState: previousConfig.environmentState, personState: updatedPersonState, filteredSurveys, }); }, onResponse: (responseUpdate: TResponseUpdate) => { - const { userId } = appConfig.get().personState.data; - - if (!userId) { - logger.debug("User ID not found. Skipping."); - return; - } + const { userId } = config.get().personState.data; const isNewResponse = surveyState.responseId === null; - surveyState.updateUserId(userId); + if (userId) { + surveyState.updateUserId(userId); + } responseQueue.updateSurveyState(surveyState); responseQueue.add({ @@ -198,20 +190,20 @@ const renderWidget = async ( }); if (isNewResponse) { - const responses = appConfig.get().personState.data.responses; + const responses = config.get().personState.data.responses; const newPersonState: TJsPersonState = { - ...appConfig.get().personState, + ...config.get().personState, data: { - ...appConfig.get().personState.data, + ...config.get().personState.data, responses: [...responses, surveyState.surveyId], }, }; - const filteredSurveys = filterSurveys(appConfig.get().environmentState, newPersonState); + const filteredSurveys = filterSurveys(config.get().environmentState, newPersonState); - appConfig.update({ - ...appConfig.get(), - environmentState: appConfig.get().environmentState, + config.update({ + ...config.get(), + environmentState: config.get().environmentState, personState: newPersonState, filteredSurveys, }); @@ -220,8 +212,8 @@ const renderWidget = async ( onClose: closeSurvey, onFileUpload: async (file: TJsFileUploadParams["file"], params: TUploadFileConfig) => { const api = new FormbricksAPI({ - apiHost: appConfig.get().apiHost, - environmentId: appConfig.get().environmentId, + apiHost: config.get().apiHost, + environmentId: config.get().environmentId, }); return await api.client.storage.uploadFile( @@ -247,11 +239,11 @@ export const closeSurvey = async (): Promise => { removeWidgetContainer(); addWidgetContainer(); - const { environmentState, personState } = appConfig.get(); + const { environmentState, personState } = config.get(); const filteredSurveys = filterSurveys(environmentState, personState); - appConfig.update({ - ...appConfig.get(), + config.update({ + ...config.get(), environmentState, personState, filteredSurveys, @@ -262,12 +254,12 @@ export const closeSurvey = async (): Promise => { export const addWidgetContainer = (): void => { const containerElement = document.createElement("div"); - containerElement.id = containerId; + containerElement.id = CONTAINER_ID; document.body.appendChild(containerElement); }; export const removeWidgetContainer = (): void => { - document.getElementById(containerId)?.remove(); + document.getElementById(CONTAINER_ID)?.remove(); }; const loadFormbricksSurveysExternally = (): Promise => { @@ -276,7 +268,7 @@ const loadFormbricksSurveysExternally = (): Promise resolve(window.formbricksSurveys); script.onerror = (error) => { diff --git a/packages/js-core/src/shared/constants.ts b/packages/js-core/src/shared/constants.ts deleted file mode 100644 index 99df8d0ffc..0000000000 --- a/packages/js-core/src/shared/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const APP_SURVEYS_LOCAL_STORAGE_KEY = "formbricks-js-app"; -export const RN_ASYNC_STORAGE_KEY = "formbricks-react-native"; -export const WEBSITE_SURVEYS_LOCAL_STORAGE_KEY = "formbricks-js-website"; diff --git a/packages/js-core/src/website/index.ts b/packages/js-core/src/website/index.ts deleted file mode 100644 index 4d88916d66..0000000000 --- a/packages/js-core/src/website/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { TJsTrackProperties, TJsWebsiteConfigInput } from "@formbricks/types/js"; -// Shared imports -import { CommandQueue } from "../shared/commandQueue"; -import { ErrorHandler } from "../shared/errors"; -import { Logger } from "../shared/logger"; -// Website package specific imports -import { trackCodeAction } from "./lib/actions"; -import { resetConfig } from "./lib/common"; -import { initialize } from "./lib/initialize"; -import { checkPageUrl } from "./lib/noCodeActions"; - -const logger = Logger.getInstance(); - -logger.debug("Create command queue"); -const queue = new CommandQueue(); - -const init = async (initConfig: TJsWebsiteConfigInput) => { - ErrorHandler.init(initConfig.errorHandler); - queue.add(false, "website", initialize, initConfig); - await queue.wait(); -}; - -const reset = async (): Promise => { - queue.add(true, "website", resetConfig); - await queue.wait(); -}; - -const track = async (name: string, properties?: TJsTrackProperties): Promise => { - queue.add(true, "website", trackCodeAction, name, properties); - await queue.wait(); -}; - -const registerRouteChange = async (): Promise => { - queue.add(true, "website", checkPageUrl); - await queue.wait(); -}; - -const formbricks = { - init, - track, - reset, - registerRouteChange, -}; - -export type TFormbricksWebsite = typeof formbricks; -export default formbricks as TFormbricksWebsite; diff --git a/packages/js-core/src/website/lib/actions.ts b/packages/js-core/src/website/lib/actions.ts deleted file mode 100644 index fef6d027b7..0000000000 --- a/packages/js-core/src/website/lib/actions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { TJsTrackProperties } from "@formbricks/types/js"; -import { InvalidCodeError, NetworkError, Result, err, okVoid } from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { WebsiteConfig } from "./config"; -import { triggerSurvey } from "./widget"; - -const logger = Logger.getInstance(); -const websiteConfig = WebsiteConfig.getInstance(); - -export const trackAction = async ( - name: string, - alias?: string, - properties?: TJsTrackProperties -): Promise> => { - const aliasName = alias || name; - logger.debug(`Formbricks: Action "${aliasName}" tracked`); - - // get a list of surveys that are collecting insights - const activeSurveys = websiteConfig.get().filteredSurveys; - - if (!!activeSurveys && activeSurveys.length > 0) { - for (const survey of activeSurveys) { - for (const trigger of survey.triggers) { - if (trigger.actionClass.name === name) { - await triggerSurvey(survey, name, properties); - } - } - } - } else { - logger.debug("No active surveys to display"); - } - - return okVoid(); -}; - -export const trackCodeAction = ( - code: string, - properties?: TJsTrackProperties -): Promise> | Result => { - const actionClasses = websiteConfig.get().environmentState.data.actionClasses; - - const codeActionClasses = actionClasses.filter((action) => action.type === "code"); - const action = codeActionClasses.find((action) => action.key === code); - - if (!action) { - return err({ - code: "invalid_code", - message: `${code} action unknown. Please add this action in Formbricks first in order to use it in your code.`, - }); - } - - return trackAction(action.name, code, properties); -}; - -export const trackNoCodeAction = (name: string): Promise> => { - return trackAction(name); -}; diff --git a/packages/js-core/src/website/lib/common.ts b/packages/js-core/src/website/lib/common.ts deleted file mode 100644 index 6dc0776ac8..0000000000 --- a/packages/js-core/src/website/lib/common.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NetworkError, Result, err, okVoid } from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { WebsiteConfig } from "./config"; -import { deinitalize, initialize } from "./initialize"; -import { closeSurvey } from "./widget"; - -const websiteConfig = WebsiteConfig.getInstance(); -const logger = Logger.getInstance(); - -export const resetWebsiteConfig = async (): Promise => { - deinitalize(); - websiteConfig.resetConfig(); -}; - -export const resetConfig = async (): Promise> => { - logger.debug("Resetting state & getting new state from backend"); - closeSurvey(); - - const syncParams = { - environmentId: websiteConfig.get().environmentId, - apiHost: websiteConfig.get().apiHost, - }; - - await resetWebsiteConfig(); - try { - await initialize(syncParams); - return okVoid(); - } catch (e) { - return err(e as NetworkError); - } -}; diff --git a/packages/js-core/src/website/lib/config.ts b/packages/js-core/src/website/lib/config.ts deleted file mode 100644 index 113eab2e2b..0000000000 --- a/packages/js-core/src/website/lib/config.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { TJsConfig, TJsConfigUpdateInput } from "@formbricks/types/js"; -import { WEBSITE_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants"; -import { Result, err, ok, wrapThrows } from "../../shared/errors"; - -export class WebsiteConfig { - private static instance: WebsiteConfig | undefined; - private config: TJsConfig | null = null; - - private constructor() { - const localConfig = this.loadFromLocalStorage(); - - if (localConfig.ok) { - this.config = localConfig.value; - } - } - - static getInstance(): WebsiteConfig { - if (!WebsiteConfig.instance) { - WebsiteConfig.instance = new WebsiteConfig(); - } - return WebsiteConfig.instance; - } - - public update(newConfig: TJsConfigUpdateInput): void { - if (newConfig) { - this.config = { - ...this.config, - ...newConfig, - status: { - value: newConfig.status?.value || "success", - expiresAt: newConfig.status?.expiresAt || null, - }, - }; - - this.saveToLocalStorage(); - } - } - - public get(): TJsConfig { - if (!this.config) { - throw new Error("config is null, maybe the init function was not called?"); - } - return this.config; - } - - public loadFromLocalStorage(): Result { - if (typeof window !== "undefined") { - const savedConfig = localStorage.getItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY); - if (savedConfig) { - // TODO: validate config - // This is a hack to get around the fact that we don't have a proper - // way to validate the config yet. - const parsedConfig = JSON.parse(savedConfig) as TJsConfig; - - // check if the config has expired - - // TODO: Figure out the expiration logic - if ( - parsedConfig.environmentState && - parsedConfig.environmentState.expiresAt && - new Date(parsedConfig.environmentState.expiresAt) <= new Date() - ) { - return err(new Error("Config in local storage has expired")); - } - - return ok(parsedConfig); - } - } - - return err(new Error("No or invalid config in local storage")); - } - - private saveToLocalStorage(): Result { - return wrapThrows(() => - localStorage.setItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(this.config)) - )(); - } - - // reset the config - - public resetConfig(): Result { - this.config = null; - - return wrapThrows(() => localStorage.removeItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY))(); - } -} diff --git a/packages/js-core/src/website/lib/eventListeners.ts b/packages/js-core/src/website/lib/eventListeners.ts deleted file mode 100644 index bfe5b74d66..0000000000 --- a/packages/js-core/src/website/lib/eventListeners.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - addEnvironmentStateExpiryCheckListener, - clearEnvironmentStateExpiryCheckListener, -} from "../../shared/environmentState"; -import { - addClickEventListener, - addExitIntentListener, - addPageUrlEventListeners, - addScrollDepthListener, - removeClickEventListener, - removeExitIntentListener, - removePageUrlEventListeners, - removeScrollDepthListener, -} from "../lib/noCodeActions"; -import { WebsiteConfig } from "./config"; - -let areRemoveEventListenersAdded = false; - -export const addEventListeners = (config: WebsiteConfig): void => { - addEnvironmentStateExpiryCheckListener("website", config); - clearEnvironmentStateExpiryCheckListener(); - addPageUrlEventListeners(); - addClickEventListener(); - addExitIntentListener(); - addScrollDepthListener(); -}; - -export const addCleanupEventListeners = (): void => { - if (areRemoveEventListenersAdded) return; - window.addEventListener("beforeunload", () => { - clearEnvironmentStateExpiryCheckListener(); - removePageUrlEventListeners(); - removeClickEventListener(); - removeExitIntentListener(); - removeScrollDepthListener(); - }); - areRemoveEventListenersAdded = true; -}; - -export const removeCleanupEventListeners = (): void => { - if (!areRemoveEventListenersAdded) return; - window.removeEventListener("beforeunload", () => { - clearEnvironmentStateExpiryCheckListener(); - removePageUrlEventListeners(); - removeClickEventListener(); - removeExitIntentListener(); - removeScrollDepthListener(); - }); - areRemoveEventListenersAdded = false; -}; - -export const removeAllEventListeners = (): void => { - clearEnvironmentStateExpiryCheckListener(); - removePageUrlEventListeners(); - removeClickEventListener(); - removeExitIntentListener(); - removeScrollDepthListener(); - removeCleanupEventListeners(); -}; diff --git a/packages/js-core/src/website/lib/initialize.ts b/packages/js-core/src/website/lib/initialize.ts deleted file mode 100644 index 4adfb713b7..0000000000 --- a/packages/js-core/src/website/lib/initialize.ts +++ /dev/null @@ -1,343 +0,0 @@ -import type { TJsConfig, TJsWebsiteConfigInput, TJsWebsiteState } from "@formbricks/types/js"; -import { WEBSITE_SURVEYS_LOCAL_STORAGE_KEY } from "../../shared/constants"; -import { fetchEnvironmentState } from "../../shared/environmentState"; -import { - ErrorHandler, - MissingFieldError, - MissingPersonError, - NetworkError, - NotInitializedError, - Result, - err, - okVoid, - wrapThrows, -} from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { DEFAULT_PERSON_STATE_WEBSITE } from "../../shared/personState"; -import { getIsDebug } from "../../shared/utils"; -import { filterSurveys as filterPublicSurveys } from "../../shared/utils"; -import { trackNoCodeAction } from "./actions"; -import { WebsiteConfig } from "./config"; -import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners"; -import { checkPageUrl } from "./noCodeActions"; -import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget"; - -const logger = Logger.getInstance(); - -let isInitialized = false; - -export const setIsInitialized = (value: boolean) => { - isInitialized = value; -}; - -const migrateLocalStorage = (): { changed: boolean; newState?: TJsConfig } => { - const oldConfig = localStorage.getItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY); - - let newWebsiteConfig: TJsConfig; - if (oldConfig) { - const parsedOldConfig = JSON.parse(oldConfig); - // if the old config follows the older structure, we need to migrate it - if (parsedOldConfig.state || parsedOldConfig.expiresAt) { - logger.debug("Migrating local storage"); - const { apiHost, environmentId, state, expiresAt } = parsedOldConfig as { - apiHost: string; - environmentId: string; - state: TJsWebsiteState; - expiresAt: Date; - }; - const { displays: displaysState, actionClasses, product, surveys, attributes } = state; - - const responses = displaysState - .filter((display) => display.responded) - .map((display) => display.surveyId); - - const displays = displaysState.map((display) => ({ - surveyId: display.surveyId, - createdAt: display.createdAt, - })); - const lastDisplayAt = displaysState - ? displaysState.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0] - .createdAt - : null; - - newWebsiteConfig = { - apiHost, - environmentId, - environmentState: { - data: { - surveys, - actionClasses, - product, - }, - expiresAt, - }, - personState: { - expiresAt, - data: { - userId: null, - segments: [], - displays, - responses, - attributes: attributes ?? {}, - lastDisplayAt, - }, - }, - filteredSurveys: surveys, - status: { - value: "success", - expiresAt: null, - }, - }; - - logger.debug("Migrated local storage to new format"); - - return { changed: true, newState: newWebsiteConfig }; - } - - return { changed: false }; - } - - return { changed: false }; -}; - -export const initialize = async ( - configInput: TJsWebsiteConfigInput -): Promise> => { - const isDebug = getIsDebug(); - if (isDebug) { - logger.configure({ logLevel: "debug" }); - } - - const { changed, newState } = migrateLocalStorage(); - let websiteConfig = WebsiteConfig.getInstance(); - - // If the state was changed due to migration, reset and reinitialize the configuration - if (changed && newState) { - // The state exists in the local storage, so this should not fail - websiteConfig.resetConfig(); // Reset the configuration - - // Re-fetch a new instance of WebsiteConfig after resetting - websiteConfig = WebsiteConfig.getInstance(); - - // Update the new instance with the migrated state - websiteConfig.update(newState); - } - - if (isInitialized) { - logger.debug("Already initialized, skipping initialization."); - return okVoid(); - } - - let existingConfig: TJsConfig | undefined; - try { - existingConfig = websiteConfig.get(); - logger.debug("Found existing configuration."); - } catch (e) { - logger.debug("No existing configuration found."); - } - - // formbricks is in error state, skip initialization - if (existingConfig?.status?.value === "error") { - if (isDebug) { - logger.debug( - "Formbricks is in error state, but debug mode is active. Resetting config and continuing." - ); - websiteConfig.resetConfig(); - return okVoid(); - } - - logger.debug("Formbricks was set to an error state."); - - if (existingConfig?.status?.expiresAt && new Date(existingConfig?.status?.expiresAt) > new Date()) { - logger.debug("Error state is not expired, skipping initialization"); - return okVoid(); - } else { - logger.debug("Error state is expired. Continue with initialization."); - } - } - - ErrorHandler.getInstance().printStatus(); - - logger.debug("Start initialize"); - - if (!configInput.environmentId) { - logger.debug("No environmentId provided"); - return err({ - code: "missing_field", - field: "environmentId", - }); - } - - if (!configInput.apiHost) { - logger.debug("No apiHost provided"); - - return err({ - code: "missing_field", - field: "apiHost", - }); - } - - logger.debug("Adding widget container to DOM"); - addWidgetContainer(); - - if ( - existingConfig && - existingConfig.environmentId === configInput.environmentId && - existingConfig.apiHost === configInput.apiHost && - existingConfig.environmentState - ) { - logger.debug("Configuration fits init parameters."); - if (existingConfig.environmentState.expiresAt < new Date()) { - logger.debug("Configuration expired."); - - try { - // fetch the environment state - - const environmentState = await fetchEnvironmentState( - { - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - }, - "website" - ); - - // filter the surveys with the default person state - - const filteredSurveys = filterPublicSurveys( - environmentState, - DEFAULT_PERSON_STATE_WEBSITE, - "website" - ); - - websiteConfig.update({ - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - environmentState, - personState: DEFAULT_PERSON_STATE_WEBSITE, - filteredSurveys, - }); - } catch (e) { - putFormbricksInErrorState(websiteConfig); - } - } else { - logger.debug("Configuration not expired. Extending expiration."); - websiteConfig.update(existingConfig); - } - } else { - logger.debug( - "No valid configuration found or it has been expired. Resetting config and creating new one." - ); - websiteConfig.resetConfig(); - logger.debug("Syncing."); - - try { - const environmentState = await fetchEnvironmentState( - { - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - }, - "website" - ); - - const filteredSurveys = filterPublicSurveys(environmentState, DEFAULT_PERSON_STATE_WEBSITE, "website"); - - websiteConfig.update({ - apiHost: configInput.apiHost, - environmentId: configInput.environmentId, - environmentState, - personState: DEFAULT_PERSON_STATE_WEBSITE, - filteredSurveys, - }); - } catch (e) { - handleErrorOnFirstInit(); - } - - if (configInput.attributes) { - const currentWebsiteConfig = websiteConfig.get(); - - websiteConfig.update({ - ...currentWebsiteConfig, - personState: { - ...currentWebsiteConfig.personState, - data: { - ...currentWebsiteConfig.personState.data, - attributes: { ...currentWebsiteConfig.personState.data.attributes, ...configInput.attributes }, - }, - }, - }); - } - - // and track the new session event - await trackNoCodeAction("New Session"); - } - - logger.debug("Adding event listeners"); - addEventListeners(websiteConfig); - addCleanupEventListeners(); - - setIsInitialized(true); - logger.debug("Initialized"); - - // check page url if initialized after page load - - checkPageUrl(); - return okVoid(); -}; - -export const handleErrorOnFirstInit = () => { - if (getIsDebug()) { - logger.debug("Not putting formbricks in error state because debug mode is active (no error state)"); - return; - } - - const initialErrorConfig: Partial = { - status: { - value: "error", - expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future - }, - }; - - // can't use config.update here because the config is not yet initialized - wrapThrows(() => - localStorage.setItem(WEBSITE_SURVEYS_LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)) - )(); - throw new Error("Could not initialize formbricks"); -}; - -export const checkInitialized = (): Result => { - logger.debug("Check if initialized"); - if (!isInitialized || !ErrorHandler.initialized) { - return err({ - code: "not_initialized", - message: "Formbricks not initialized. Call initialize() first.", - }); - } - - return okVoid(); -}; - -export const deinitalize = (): void => { - logger.debug("Deinitializing"); - removeWidgetContainer(); - setIsSurveyRunning(false); - removeAllEventListeners(); - setIsInitialized(false); -}; - -export const putFormbricksInErrorState = (websiteConfig: WebsiteConfig): void => { - if (getIsDebug()) { - logger.debug("Not putting formbricks in error state because debug mode is active (no error state)"); - return; - } - - logger.debug("Putting formbricks in error state"); - // change formbricks status to error - websiteConfig.update({ - ...websiteConfig.get(), - status: { - value: "error", - expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future - }, - }); - deinitalize(); -}; diff --git a/packages/js-core/src/website/lib/noCodeActions.ts b/packages/js-core/src/website/lib/noCodeActions.ts deleted file mode 100644 index 8188f086ed..0000000000 --- a/packages/js-core/src/website/lib/noCodeActions.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type { TActionClass } from "@formbricks/types/action-classes"; -import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "../../shared/errors"; -import { Logger } from "../../shared/logger"; -import { evaluateNoCodeConfigClick, handleUrlFilters } from "../../shared/utils"; -import { trackNoCodeAction } from "./actions"; -import { WebsiteConfig } from "./config"; - -const websiteConfig = WebsiteConfig.getInstance(); -const logger = Logger.getInstance(); -const errorHandler = ErrorHandler.getInstance(); - -// Event types for various listeners -const events = ["hashchange", "popstate", "pushstate", "replacestate", "load"]; - -// Page URL Event Handlers -let arePageUrlEventListenersAdded = false; - -export const checkPageUrl = async (): Promise> => { - logger.debug(`Checking page url: ${window.location.href}`); - const actionClasses = websiteConfig.get().environmentState.data.actionClasses; - - const noCodePageViewActionClasses = actionClasses.filter( - (action) => action.type === "noCode" && action.noCodeConfig?.type === "pageView" - ); - - for (const event of noCodePageViewActionClasses) { - const urlFilters = event.noCodeConfig?.urlFilters ?? []; - const isValidUrl = handleUrlFilters(urlFilters); - - if (!isValidUrl) continue; - - const trackResult = await trackNoCodeAction(event.name); - if (trackResult.ok !== true) return err(trackResult.error); - } - - return okVoid(); -}; - -const checkPageUrlWrapper = () => checkPageUrl(); - -export const addPageUrlEventListeners = (): void => { - if (typeof window === "undefined" || arePageUrlEventListenersAdded) return; - events.forEach((event) => window.addEventListener(event, checkPageUrlWrapper)); - arePageUrlEventListenersAdded = true; -}; - -export const removePageUrlEventListeners = (): void => { - if (typeof window === "undefined" || !arePageUrlEventListenersAdded) return; - events.forEach((event) => window.removeEventListener(event, checkPageUrlWrapper)); - arePageUrlEventListenersAdded = false; -}; - -// Click Event Handlers -let isClickEventListenerAdded = false; - -const checkClickMatch = (event: MouseEvent) => { - const { environmentState } = websiteConfig.get(); - if (!environmentState) return; - - const { actionClasses = [] } = environmentState.data; - const noCodeClickActionClasses = actionClasses.filter( - (action) => action.type === "noCode" && action.noCodeConfig?.type === "click" - ); - - const targetElement = event.target as HTMLElement; - - noCodeClickActionClasses.forEach((action: TActionClass) => { - if (evaluateNoCodeConfigClick(targetElement, action)) { - trackNoCodeAction(action.name).then((res) => { - match( - res, - (_value: unknown) => {}, - (err: any) => errorHandler.handle(err) - ); - }); - } - }); -}; - -const checkClickMatchWrapper = (e: MouseEvent) => checkClickMatch(e); - -export const addClickEventListener = (): void => { - if (typeof window === "undefined" || isClickEventListenerAdded) return; - document.addEventListener("click", checkClickMatchWrapper); - isClickEventListenerAdded = true; -}; - -export const removeClickEventListener = (): void => { - if (!isClickEventListenerAdded) return; - document.removeEventListener("click", checkClickMatchWrapper); - isClickEventListenerAdded = false; -}; - -// Exit Intent Handlers -let isExitIntentListenerAdded = false; - -const checkExitIntent = async (e: MouseEvent) => { - const actionClasses = websiteConfig.get().environmentState.data.actionClasses; - - const noCodeExitIntentActionClasses = actionClasses.filter( - (action) => action.type === "noCode" && action.noCodeConfig?.type === "exitIntent" - ); - - if (e.clientY <= 0 && noCodeExitIntentActionClasses.length > 0) { - for (const event of noCodeExitIntentActionClasses) { - const urlFilters = event.noCodeConfig?.urlFilters ?? []; - const isValidUrl = handleUrlFilters(urlFilters); - - if (!isValidUrl) continue; - - const trackResult = await trackNoCodeAction(event.name); - if (trackResult.ok !== true) return err(trackResult.error); - } - } -}; - -const checkExitIntentWrapper = (e: MouseEvent) => checkExitIntent(e); - -export const addExitIntentListener = (): void => { - if (typeof document !== "undefined" && !isExitIntentListenerAdded) { - document.querySelector("body")!.addEventListener("mouseleave", checkExitIntentWrapper); - isExitIntentListenerAdded = true; - } -}; - -export const removeExitIntentListener = (): void => { - if (isExitIntentListenerAdded) { - document.removeEventListener("mouseleave", checkExitIntentWrapper); - isExitIntentListenerAdded = false; - } -}; - -// Scroll Depth Handlers -let scrollDepthListenerAdded = false; -let scrollDepthTriggered = false; - -const checkScrollDepth = async () => { - const scrollPosition = window.scrollY; - const windowSize = window.innerHeight; - const bodyHeight = document.documentElement.scrollHeight; - - if (scrollPosition === 0) { - scrollDepthTriggered = false; - } - - if (!scrollDepthTriggered && scrollPosition / (bodyHeight - windowSize) >= 0.5) { - scrollDepthTriggered = true; - - const actionClasses = websiteConfig.get().environmentState.data.actionClasses; - - const noCodefiftyPercentScrollActionClasses = actionClasses.filter( - (action) => action.type === "noCode" && action.noCodeConfig?.type === "fiftyPercentScroll" - ); - - for (const event of noCodefiftyPercentScrollActionClasses) { - const urlFilters = event.noCodeConfig?.urlFilters ?? []; - const isValidUrl = handleUrlFilters(urlFilters); - - if (!isValidUrl) continue; - - const trackResult = await trackNoCodeAction(event.name); - if (trackResult.ok !== true) return err(trackResult.error); - } - } - - return okVoid(); -}; - -const checkScrollDepthWrapper = () => checkScrollDepth(); - -export const addScrollDepthListener = (): void => { - if (typeof window !== "undefined" && !scrollDepthListenerAdded) { - if (document.readyState === "complete") { - window.addEventListener("scroll", checkScrollDepthWrapper); - } else { - window.addEventListener("load", () => { - window.addEventListener("scroll", checkScrollDepthWrapper); - }); - } - scrollDepthListenerAdded = true; - } -}; - -export const removeScrollDepthListener = (): void => { - if (scrollDepthListenerAdded) { - window.removeEventListener("scroll", checkScrollDepthWrapper); - scrollDepthListenerAdded = false; - } -}; diff --git a/packages/js-core/src/website/lib/widget.ts b/packages/js-core/src/website/lib/widget.ts deleted file mode 100644 index 1118823ee2..0000000000 --- a/packages/js-core/src/website/lib/widget.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { FormbricksAPI } from "@formbricks/api"; -import { ResponseQueue } from "@formbricks/lib/responseQueue"; -import { SurveyState } from "@formbricks/lib/surveyState"; -import { getStyling } from "@formbricks/lib/utils/styling"; -import { TJsPersonState, TJsTrackProperties } from "@formbricks/types/js"; -import { TResponseHiddenFieldValue, TResponseUpdate } from "@formbricks/types/responses"; -import { TUploadFileConfig } from "@formbricks/types/storage"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { Logger } from "../../shared/logger"; -import { filterSurveys as filterPublicSurveys } from "../../shared/utils"; -import { getDefaultLanguageCode, getLanguageCode, handleHiddenFields } from "../../shared/utils"; -import { WebsiteConfig } from "./config"; - -const containerId = "formbricks-website-container"; - -const websiteConfig = WebsiteConfig.getInstance(); -const logger = Logger.getInstance(); - -let isSurveyRunning = false; -let setIsError = (_: boolean) => {}; -let setIsResponseSendingFinished = (_: boolean) => {}; - -export const setIsSurveyRunning = (value: boolean) => { - isSurveyRunning = value; -}; - -const shouldDisplayBasedOnPercentage = (displayPercentage: number) => { - const randomNum = Math.floor(Math.random() * 10000) / 100; - return randomNum <= displayPercentage; -}; - -export const triggerSurvey = async ( - survey: TSurvey, - action?: string, - properties?: TJsTrackProperties -): Promise => { - // Check if the survey should be displayed based on displayPercentage - if (survey.displayPercentage) { - const shouldDisplaySurvey = shouldDisplayBasedOnPercentage(survey.displayPercentage); - if (!shouldDisplaySurvey) { - logger.debug("Survey display skipped based on displayPercentage."); - return; // skip displaying the survey - } - } - - const hiddenFieldsObject: TResponseHiddenFieldValue = handleHiddenFields( - survey.hiddenFields, - properties?.hiddenFields - ); - - await renderWidget(survey, action, hiddenFieldsObject); -}; - -const renderWidget = async ( - survey: TSurvey, - action?: string, - hiddenFields: TResponseHiddenFieldValue = {} -) => { - if (isSurveyRunning) { - logger.debug("A survey is already running. Skipping."); - return; - } - setIsSurveyRunning(true); - - if (survey.delay) { - logger.debug(`Delaying survey by ${survey.delay} seconds.`); - } - - const product = websiteConfig.get().environmentState.data.product; - const attributes = websiteConfig.get().personState.data.attributes; - - const isMultiLanguageSurvey = survey.languages.length > 1; - let languageCode = "default"; - - if (isMultiLanguageSurvey && attributes) { - const displayLanguage = getLanguageCode(survey, attributes); - //if survey is not available in selected language, survey wont be shown - if (!displayLanguage) { - logger.debug("Survey not available in specified language."); - setIsSurveyRunning(true); - return; - } - languageCode = displayLanguage; - } - - const surveyState = new SurveyState(survey.id, null, null); - - const responseQueue = new ResponseQueue( - { - apiHost: websiteConfig.get().apiHost, - environmentId: websiteConfig.get().environmentId, - retryAttempts: 2, - onResponseSendingFailed: () => { - setIsError(true); - }, - onResponseSendingFinished: () => { - setIsResponseSendingFinished(true); - }, - }, - surveyState - ); - const productOverwrites = survey.productOverwrites ?? {}; - const clickOutside = productOverwrites.clickOutsideClose ?? product.clickOutsideClose; - const darkOverlay = productOverwrites.darkOverlay ?? product.darkOverlay; - const placement = productOverwrites.placement ?? product.placement; - const isBrandingEnabled = product.inAppSurveyBranding; - const formbricksSurveys = await loadFormbricksSurveysExternally(); - - setTimeout(() => { - formbricksSurveys.renderSurveyModal({ - survey, - isBrandingEnabled, - clickOutside, - darkOverlay, - languageCode, - placement, - styling: getStyling(product, survey), - getSetIsError: (f: (value: boolean) => void) => { - setIsError = f; - }, - getSetIsResponseSendingFinished: (f: (value: boolean) => void) => { - setIsResponseSendingFinished = f; - }, - onDisplay: async () => { - const api = new FormbricksAPI({ - apiHost: websiteConfig.get().apiHost, - environmentId: websiteConfig.get().environmentId, - }); - const res = await api.client.display.create({ - surveyId: survey.id, - }); - - if (!res.ok) { - throw new Error("Could not create display"); - } - - const { id } = res.data; - - const existingDisplays = websiteConfig.get().personState.data.displays; - const newDisplay = { surveyId: survey.id, createdAt: new Date() }; - const displays = existingDisplays ? [...existingDisplays, newDisplay] : [newDisplay]; - const previousConfig = websiteConfig.get(); - - const updatedPersonState: TJsPersonState = { - ...previousConfig.personState, - data: { - ...previousConfig.personState.data, - displays, - lastDisplayAt: new Date(), - }, - }; - - const filteredSurveys = filterPublicSurveys( - previousConfig.environmentState, - updatedPersonState, - "website" - ); - - websiteConfig.update({ - ...previousConfig, - environmentState: previousConfig.environmentState, - personState: updatedPersonState, - filteredSurveys, - }); - - surveyState.updateDisplayId(id); - responseQueue.updateSurveyState(surveyState); - }, - onResponse: (responseUpdate: TResponseUpdate) => { - const displays = websiteConfig.get().personState.data.displays; - const lastDisplay = displays && displays[displays.length - 1]; - if (!lastDisplay) { - throw new Error("No lastDisplay found"); - } - - const isNewResponse = surveyState.responseId === null; - - responseQueue.updateSurveyState(surveyState); - - responseQueue.add({ - data: responseUpdate.data, - ttc: responseUpdate.ttc, - finished: responseUpdate.finished, - language: - responseUpdate.language === "default" ? getDefaultLanguageCode(survey) : responseUpdate.language, - meta: { - url: window.location.href, - action, - }, - variables: responseUpdate.variables, - hiddenFields, - displayId: surveyState.displayId, - }); - - if (isNewResponse) { - const responses = websiteConfig.get().personState.data.responses; - const newPersonState: TJsPersonState = { - ...websiteConfig.get().personState, - data: { - ...websiteConfig.get().personState.data, - responses: [...responses, surveyState.surveyId], - }, - }; - - const filteredSurveys = filterPublicSurveys( - websiteConfig.get().environmentState, - newPersonState, - "website" - ); - - websiteConfig.update({ - ...websiteConfig.get(), - environmentState: websiteConfig.get().environmentState, - personState: newPersonState, - filteredSurveys, - }); - } - }, - onClose: closeSurvey, - onFileUpload: async ( - file: { type: string; name: string; base64: string }, - params: TUploadFileConfig - ) => { - const api = new FormbricksAPI({ - apiHost: websiteConfig.get().apiHost, - environmentId: websiteConfig.get().environmentId, - }); - - return await api.client.storage.uploadFile( - { - name: file.name, - type: file.type, - base64: file.base64, - }, - params - ); - }, - onRetry: () => { - setIsError(false); - responseQueue.processQueue(); - }, - hiddenFieldsRecord: hiddenFields, - }); - }, survey.delay * 1000); -}; - -export const closeSurvey = async (): Promise => { - // remove container element from DOM - removeWidgetContainer(); - addWidgetContainer(); - - const { environmentState, personState } = websiteConfig.get(); - const filteredSurveys = filterPublicSurveys(environmentState, personState, "website"); - websiteConfig.update({ - ...websiteConfig.get(), - environmentState, - personState, - filteredSurveys, - }); - setIsSurveyRunning(false); - return; -}; - -export const addWidgetContainer = (): void => { - const containerElement = document.createElement("div"); - containerElement.id = containerId; - document.body.appendChild(containerElement); -}; - -export const removeWidgetContainer = (): void => { - document.getElementById(containerId)?.remove(); -}; - -const loadFormbricksSurveysExternally = (): Promise => { - return new Promise((resolve, reject) => { - if (window.formbricksSurveys) { - resolve(window.formbricksSurveys); - } else { - const script = document.createElement("script"); - script.src = `${websiteConfig.get().apiHost}/api/packages/surveys`; - script.async = true; - script.onload = () => resolve(window.formbricksSurveys); - script.onerror = (error) => { - console.error("Failed to load Formbricks Surveys library:", error); - reject(error); - }; - document.head.appendChild(script); - } - }); -}; diff --git a/packages/js-core/app.vite.config.ts b/packages/js-core/vite.config.ts similarity index 91% rename from packages/js-core/app.vite.config.ts rename to packages/js-core/vite.config.ts index 98c3176dc6..17e4644304 100644 --- a/packages/js-core/app.vite.config.ts +++ b/packages/js-core/vite.config.ts @@ -16,10 +16,10 @@ const config = () => { minify: "terser", sourcemap: true, lib: { - entry: resolve(__dirname, "src/app/index.ts"), + entry: resolve(__dirname, "src/index.ts"), name: "formbricks", formats: ["umd"], - fileName: "app", + fileName: "index", }, }, plugins: [ diff --git a/packages/js-core/website.vite.config.ts b/packages/js-core/website.vite.config.ts deleted file mode 100644 index e08e44b1b5..0000000000 --- a/packages/js-core/website.vite.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { resolve } from "path"; -import { defineConfig } from "vite"; -import dts from "vite-plugin-dts"; -import webPackageJson from "../../apps/web/package.json"; - -const config = () => { - return defineConfig({ - define: { - "import.meta.env.VERSION": JSON.stringify(webPackageJson.version), - }, - build: { - rollupOptions: { - output: { inlineDynamicImports: true }, - }, - emptyOutDir: false, // keep the dist folder to avoid errors with pnpm go when folder is empty during build - minify: "terser", - sourcemap: true, - lib: { - entry: resolve(__dirname, "src/website/index.ts"), - name: "formbricks", - formats: ["umd"], - fileName: "website", - }, - }, - plugins: [ - dts({ - rollupTypes: true, - bundledPackages: ["@formbricks/api", "@formbricks/types"], - }), - ], - }); -}; - -export default config; diff --git a/packages/js/index.html b/packages/js/index.html index fc019e8f2e..e47487373f 100644 --- a/packages/js/index.html +++ b/packages/js/index.html @@ -2,12 +2,12 @@