Compare commits

..

1 Commits

Author SHA1 Message Date
ShubhamPalriwala
b8a8897ca7 fix: duplicate team dropdown in navbar 2024-04-19 12:00:23 +05:30
243 changed files with 8788 additions and 8709 deletions

View File

@@ -173,9 +173,6 @@ ENTERPRISE_LICENSE_KEY=
# OpenTelemetry URL for tracing
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces
# Unsplash API Key
UNSPLASH_ACCESS_KEY=
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
# REDIS_URL:

View File

@@ -1,12 +1,11 @@
name: Cron - Report usage to Stripe
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
# schedule:
# This will run the job at 20:00 UTC every day of every month.
# - cron: "0 20 * * *"
schedule:
# This will run the job at 20:00 UTC every day of every month.
- cron: "0 20 * * *"
jobs:
cron-reportUsageToStripe:
env:

View File

@@ -1,12 +1,11 @@
name: Cron - Survey status update
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
# schedule:
# Runs “At 00:00.” (see https://crontab.guru)
# - cron: "0 0 * * *"
schedule:
# Runs “At 00:00.” (see https://crontab.guru)
- cron: "0 0 * * *"
jobs:
cron-weeklySummary:
env:

View File

@@ -1,12 +1,11 @@
name: Cron - Weekly summary
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
# schedule:
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
# - cron: "0 8 * * 1"
schedule:
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
- cron: "0 8 * * 1"
jobs:
cron-weeklySummary:
env:

View File

@@ -81,7 +81,6 @@ jobs:
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
REDIS_URL: ${{ secrets.REDIS_URL }}
steps:
- name: Checkout code

View File

@@ -78,7 +78,6 @@ jobs:
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
REDIS_URL: ${{ secrets.REDIS_URL }}
steps:
- name: Checkout code

View File

@@ -1,3 +1,4 @@
import { classNames } from "@/lib/utils";
import {
ClockIcon,
CogIcon,
@@ -10,8 +11,6 @@ import {
UsersIcon,
} from "lucide-react";
import { classNames } from "../lib/utils";
const navigation = [
{ name: "Home", href: "#", icon: HomeIcon, current: true },
{ name: "History", href: "#", icon: ClockIcon, current: false },

View File

@@ -1,25 +0,0 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
interface SurveySwitchProps {
value: "website" | "app";
formbricks: any;
}
export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
return (
<Select
value={value}
onValueChange={(v) => {
formbricks.logout();
window.location.href = `/${v}`;
}}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="website">Website Surveys</SelectItem>
<SelectItem value="app">App Surveys</SelectItem>
</SelectContent>
</Select>
);
};

View File

@@ -1,7 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@formbricks/ui"],
async redirects() {
return [
{

View File

@@ -12,14 +12,12 @@
},
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "^0.368.0",
"next": "14.2.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"eslint-config-formbricks": "workspace:*",
"@formbricks/tsconfig": "workspace:*"
"eslint-config-formbricks": "workspace:*"
}
}

View File

@@ -2,9 +2,8 @@ import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricksApp from "@formbricks/js/app";
import formbricks from "@formbricks/js";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
declare const window: any;
@@ -31,24 +30,28 @@ export default function AppPage({}) {
window.history.replaceState({}, "", newUrl);
}
};
addFormbricksDebugParam();
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
const userInitAttributes = { language: "de", "Init Attribute 1": "eight", "Init Attribute 2": "two" };
const isUserId = window.location.href.includes("userId=true");
const defaultAttributes = {
language: "gu",
};
const userInitAttributes = { "Init Attribute 1": "eight", "Init Attribute 2": "two" };
formbricksApp.init({
const attributes = isUserId ? { ...defaultAttributes, ...userInitAttributes } : defaultAttributes;
const userId = isUserId ? "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" : undefined;
formbricks.init({
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
userId,
attributes: userInitAttributes,
attributes,
});
}
// Connect next.js router to Formbricks
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const handleRouteChange = formbricksApp?.registerRouteChange;
const handleRouteChange = formbricks?.registerRouteChange;
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
@@ -60,19 +63,15 @@ export default function AppPage({}) {
return (
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col justify-between md:flex-row">
<div className="flex items-center gap-2">
<SurveySwitch value="app" formbricks={formbricksApp} />
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks In-product Survey Demo App
</h1>
<p className="text-slate-700 dark:text-slate-300">
This app helps you test your app surveys. You can create and test user actions, create and
update user attributes, etc.
</p>
</div>
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks In-product Survey Demo App
</h1>
<p className="text-slate-700 dark:text-slate-300">
This app helps you test your in-app surveys. You can create and test user actions, create and
update user attributes, etc.
</p>
</div>
<button
className="mt-2 rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
onClick={() => setDarkMode(!darkMode)}>
@@ -126,7 +125,7 @@ export default function AppPage({}) {
<button
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricksApp.reset();
formbricks.reset();
}}>
Reset
</button>
@@ -141,7 +140,7 @@ export default function AppPage({}) {
<button
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricksApp.track("Code Action");
formbricks.track("Code Action");
}}>
Code Action
</button>
@@ -185,7 +184,7 @@ export default function AppPage({}) {
<div>
<button
onClick={() => {
formbricksApp.setAttribute("Plan", "Free");
formbricks.setAttribute("Plan", "Free");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Free&apos;
@@ -208,7 +207,7 @@ export default function AppPage({}) {
<div>
<button
onClick={() => {
formbricksApp.setAttribute("Plan", "Paid");
formbricks.setAttribute("Plan", "Paid");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Paid&apos;
@@ -231,7 +230,7 @@ export default function AppPage({}) {
<div>
<button
onClick={() => {
formbricksApp.setEmail("test@web.com");
formbricks.setEmail("test@web.com");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Email
@@ -250,6 +249,41 @@ export default function AppPage({}) {
</p>
</div>
</div>
<div className="p-6">
{router.query.userId === "true" ? (
<div>
<button
onClick={() => {
window.location.href = "/app";
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Deactivate User Identification
</button>
</div>
) : (
<div>
<button
onClick={() => {
window.location.href = "/app?userId=true";
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Activate User Identification
</button>
</div>
)}
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button activates/deactivates{" "}
<a
href="https://formbricks.com/docs/attributes/identify-users"
target="_blank"
className="underline dark:text-blue-500">
user identification
</a>{" "}
with the userId &apos;THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING&apos;
</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,197 +0,0 @@
import { MonitorIcon } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricksWebsite from "@formbricks/js/website";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
declare const window: any;
export default function 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: "de",
};
formbricksWebsite.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 = formbricksWebsite?.registerRouteChange;
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
}
});
return (
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col items-center justify-between md:flex-row">
<div className="flex items-center gap-2">
<SurveySwitch value="website" formbricks={formbricksWebsite} />
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks Website Survey Demo App
</h1>
<p className="text-slate-700 dark:text-slate-300">
This app helps you test your app surveys. You can create and test user actions, create and
update user attributes, etc.
</p>
</div>
</div>
<button
className="mt-2 rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
onClick={() => setDarkMode(!darkMode)}>
{darkMode ? "Toggle Light Mode" : "Toggle Dark Mode"}
</button>
</div>
<div className="my-4 grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">1. Setup .env</h3>
<p className="text-slate-700 dark:text-slate-300">
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
</p>
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
<div className="mt-4 flex-col items-start text-sm text-slate-700 sm:flex sm:items-center sm:text-base dark:text-slate-300">
<p className="mb-1 sm:mb-0 sm:mr-2">You&apos;re connected with env:</p>
<div className="flex items-center">
<strong className="w-32 truncate sm:w-auto">
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
</strong>
<span className="relative ml-2 flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
</span>
</div>
</div>
</div>
<div className="mt-4 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">2. Widget Logs</h3>
<p className="text-slate-700 dark:text-slate-300">
Look at the logs to understand how the widget works.{" "}
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
</p>
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
<LogsContainer />
</div> */}
</div>
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
<p className="text-slate-700 dark:text-slate-300">
On formbricks.reset() the local state will <strong>be deleted</strong> and formbricks gets{" "}
<strong>reinitialized</strong>.
</p>
<button
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricksWebsite.reset();
}}>
Reset
</button>
<p className="text-xs text-slate-700 dark:text-slate-300">
If you made a change in Formbricks app and it does not seem to work, hit &apos;Reset&apos; and
try again.
</p>
</div>
<div className="pt-6">
<div>
<button
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricksWebsite.track("New Session");
}}>
Track New Session
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends an Action to the Formbricks API called &apos;New Session&apos;. You will
find it in the Actions Tab.
</p>
</div>
</div>
<div className="pt-6">
<div>
<button
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricksWebsite.track("Exit Intent");
}}>
Track Exit Intent
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends an Action to the Formbricks API called &apos;Exit Intent&apos;. You can also
move your mouse to the top of the browser to trigger the exit intent.
</p>
</div>
</div>
<div className="pt-6">
<div>
<button
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricksWebsite.track("50% Scroll");
}}>
Track 50% Scroll
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends an Action to the Formbricks API called &apos;50% Scroll&apos;. You can also
scroll down to trigger the 50% scroll.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,23 @@
{
"extends": "@formbricks/tsconfig/nextjs.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -1,5 +0,0 @@
export async function GET(_: Request, { params }: { params: { surveyId: string } }) {
const surveyId = params.surveyId;
// redirect to Formbricks Cloud
return Response.redirect(`https://app.formbricks.com/s/${surveyId}`, 301);
}

View File

@@ -1,137 +0,0 @@
import { MdxImage } from "@/components/shared/MdxImage";
import AddApiKey from "./add-api-key.webp";
import ApiKeySecret from "./api-key-secret.webp";
export const metadata = {
title: "Formbricks API Overview: Public Client & Management API Breakdown",
description:
"Formbricks provides a powerful API to manage your surveys, responses, users, displays, actions, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
};
#### API
# API Overview
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
View our [API Documentation](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh) in more than 30 frameworks and languages. Or directly try out our APIs in Postman by clicking the button below:
<div className="max-w-full sm:max-w-3xl">
<a target="_blank" href="https://formbricks.postman.co/collection/11026000-927c954f-85a9-4f8f-b0ec-14191b903737?source=rip_html">
<img alt="Run in Postman" src="https://run.pstmn.io/button.svg"/>
</a>
</div>
## Public Client API
The [Public Client API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#5c981d9e-5e7d-455d-9795-b9c45bc2f930) is designed for our SDKs and **does not require authentication**. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
We currently have the following Client API methods exposed and below is their documentation attached in Postman:
- [Actions API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#b8f3a10e-1642-4d82-a629-fef0a8c6c86c) - Create actions for a Person
- [Displays API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#949272bf-daec-4d72-9b52-47af3d74a62c) - Mark Survey as Displayed or Update an existing Display by linking it with a Response for a Person
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#ee3d2188-4253-4bca-9238-6b76455805a9) - Create & Update a Person (e.g. attributes, email, userId, etc)
- [Responses API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#8c773032-536c-483c-a237-c7697347946e) - Create & Update a Response for a Survey
## Management API
The [Management API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#98fce5a1-1365-4125-8de1-acdb28206766) provides access to all data and settings that your account has access to in the Formbricks app. This API **requires a personal API Key** for authentication, which can be generated in the Settings section of the Formbricks app. Checkout the [API Key Setup](#how-to-generate-an-api-key) below to generate & manage API Keys.
We currently have the following Management API methods exposed and below is their documentation attached in Postman:
- [Action Class API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#81947f69-99fc-41c9-a184-f3260e02be48) - Create, List, and Delete Action Classes
- [Attribute Class API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#31089010-d468-4a7c-943e-8ebe71b9a36e) - Create, List, and Delete Attribute Classes
- [Me API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#79e08365-641d-4b2d-aea2-9a855e0438ec) - Retrieve Account Information
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#cffc27a6-dafb-428f-8ea7-5165bedb911e) - List and Delete People
- [Response API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#e544ec0d-8b30-4e33-8d35-2441cb40d676) - List, List by Survey, Update, and Delete Responses
- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, and Delete Surveys
- [Webhook API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c) - List, Create, and Delete Webhooks
## How to Generate an API key
The API requests are authorized with a personal API key. This API key gives you the same rights as if you were logged in at formbricks.com - **don't share it around!**
1. Go to your settings on [app.formbricks.com](https://app.formbricks.com).
2. Go to page “API keys”
<MdxImage src={AddApiKey} alt="Add API Key" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
3. Create a key for the development or production environment.
4. Copy the key immediately. You wont be able to see it again.
<MdxImage
src={ApiKeySecret}
alt="API Key Secret"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>
### Store API key safely!
Anyone who has your API key has full control over your account. For security reasons, you cannot view the API key again.
</Note>
### Test your API Key
Hit the below request to verify that you are authenticated with your API Key and the server is responding.
## Get My Profile {{ tag: 'GET', label: '/api/v1/me' }}
<Row>
<Col>
Get the product details and environment type of your account.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Delete a personal API key
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/).
2. Go to page “API keys”.
3. Find the key you wish to revoke and select “Delete”.
4. Your API key will stop working immediately.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/me">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/me' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"id": "cll2m30r70004mx0huqkitgqv",
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"product": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Product"
},
"widgetSetupCompleted": false
}
```
```json {{ title: '401 Not Authenticated' }}
Not authenticated
```
</CodeGroup>
</Col>
</Row>
Cant figure it out? Join our [Discord](https://discord.com/invite/3YFcABF2Ts) and we'd be glad to assist you!
---

View File

@@ -0,0 +1,85 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Actions API Documentation - Manage Your Survey Data Seamlessly",
description:
"Unlock the full potential of Formbricks' Client Actions API. Create Actions right from the API.",
};
#### Client API
# Actions API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This API can be used to:
- [Add Action for User](#add-action-for-user)
---
## Add Action for User {{ tag: 'POST', label: '/api/v1/client/<environment-id>/actions' }}
Adds an Actions for a given User by their User ID
<Row>
<Col>
### Mandatory Body Fields
<Properties>
<Property name="userId" type="string">
The id of the user for whom the action is being created.
</Property>
<Property name="name" type="string">
The name of the Action being created.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/actions">
```bash {{ title: 'cURL' }}
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/actions' \
--data-raw '{
"userId": "1",
"name": "new_action_v2"
}'
```
```json {{ title: 'Example Request Body' }}
{
"userId": "1",
"name": "new_action_v3"
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"name": "Required"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,145 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Public Client API Guide: Manage Survey Displays & Responses",
description:
"Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on how to mark create and update survey displays for users.",
};
#### Client API
# Displays API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This set of API can be used to
- [Create Display](#create-display)
- [Update Display](#update-display)
---
## Create Display {{ tag: 'POST', label: '/api/v1/client/<environment-id>/diplays' }}
<Row>
<Col>
Create Display of survey for a user
### Mandatory Request Body JSON Keys
<Properties>
<Property name="surveyId" type="string">
Survey ID to mark as viewed for a person
</Property>
</Properties>
### Optional Request Body JSON Keys
<Properties>
<Property name="userId" type="string">
Already existing user's ID to mark as viewed for a survey
</Property>
<Property name="responseId" type="string">
Already existing response's ID to link with this new Display
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/displays">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/client/displays' \
-H 'Content-Type: application/json' \
-d '{
"surveyId":"<survey-id>",
"userId":"<user-id>"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clphzz6oo00083zdmc7e0nwzi"
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"surveyId": "Required"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Display {{ tag: 'PUT', label: '/api/v1/client/<environment-id>/diplays/<display-id>' }}
<Row>
<Col>
Update a display by it's ID
### Optional Request Body JSON Keys
<Properties>
<Property name="userId" type="string">
Already existing user's ID to mark as viewed for a survey
</Property>
<Property name="responseId" type="string">
Already existing response's ID to link with this new Display
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="PUT" label="/api/v1/client/<environment-id>/displays/<display-id>">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/client/<environment-id>/displays/<display-id>' \
-H 'Content-Type: application/json' \
-d '{
"userId":"<user-id>"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"surveyId": "Required"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,47 @@
export const metadata = {
title: "Formbricks API Overview: Public Client & Management API Breakdown",
description:
"Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
};
#### API
# API Overview
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
Checkout the [API Key Setup](/docs/api/management/api-key-setup) - to generate, store, or delete API Keys.
## Public Client API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
- [Actions API](/docs/api/client/actions) - Create actions for a person
- [Displays API](/docs/api/client/displays) - Mark Survey as Displayed or Responded for a Person
- [People API](/docs/api/client/people) - Create & update people (e.g. attributes)
- [Responses API](/docs/api/client/responses) - Create & update responses for a survey
## Management API
The Management API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the Management API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed.
**Auth:** Personal API Key
API requests made to the Management API are authorized using a personal API key. This key grants the same rights and access as if you were logged in at formbricks.com. It's essential to keep your API key secure and not share it with others.
To generate, store, or delete an API key, follow the instructions provided on the following page [API Key](/docs/api/management/api-key-setup).
- [Action Class API](/docs/api/management/action-classes) - Create, Update, and Delete Action Classes
- [Attribute Class API](/docs/api/management/attribute-classes) - Create, Update, and Delete Attribute Classes
- [Me API](/docs/api/management/me) - Retrieve Account Information
- [People API](/docs/api/management/people) - Create, Update, and Delete People
- [Responses API](/docs/api/management/responses) - Create, Update, and Delete Responses
- [Surveys API](/docs/api/management/surveys) - Create, Update, and Delete Surveys
- [Webhook API](/docs/api/management/webhooks) - Create, Update, and Delete Webhooks
<Note>
By understanding the differences between these two APIs, you can choose the appropriate one for your needs,
ensuring a secure and efficient integration with the Formbricks platform.
</Note>
---

View File

@@ -0,0 +1,130 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Public Client API Guide: Manage Users",
description:
"Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on creating and updating users to help in user identification.",
};
#### Client API
# People API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This set of API can be used to
- [Create Person](#create-person)
- [Update Person](#update-person)
---
## Create Person {{ tag: 'POST', label: '/api/v1/client/<environment-id>/people' }}
<Row>
<Col>
Create User with your own User ID
### Mandatory Request Body JSON Keys
<Properties>
<Property name="userId" type="string">
User ID which you would like to identify the person with
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/people">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/client/<environment-id>/people' \
-H 'Content-Type: application/json' \
-d '{
"userId":"docs_user"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"userId": "docs_user"
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"surveyId": "Required"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Person {{ tag: 'POST', label: '/api/v1/client/<environment-id>/people/<user-id>' }}
<Row>
<Col>
Update Person by their User ID
### Mandatory Request Body JSON Keys
<Properties>
<Property name="attributes" type="JSON">
Key Value pairs of attributes to add to the user
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/people/<user-id>">
```bash {{ title: 'cURL' }}
curl -X POST \
--location \
'https://app.formbricks.com/api/v1/client/<environment-id>/people/<user-id>'
-H 'Content-Type: application/json' \
-d '{
"attributes":{
"welcome_to":"formbricks"
}
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {}
}
```
```json {{ title: '500 Internal Server Error' }}
{
"code": "internal_server_error",
"message": "Database operation failed",
"details": {}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,200 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Responses API Documentation - Manage Your Survey Data Seamlessly",
description:
"Unlock the full potential of Formbricks' Responses API. From fetching to updating survey responses, our comprehensive guide helps you integrate and manage survey data efficiently without compromising security. Ideal for client-side interactions.",
};
#### Client API
# Responses API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This set of API can be used to
- [Create Response](#create-response)
- [Update Response](#update-response)
---
## Create Response {{ tag: 'POST', label: '/api/v1/client/<environment-id>/responses' }}
Add a new response to a survey.
<Row>
<Col>
### Mandatory Body Fields
<Properties>
<Property name="surveyId" type="string">
The id of the survey the response belongs to.
</Property>
<Property name="finished" type="boolean">
Marks whether the response is complete or not.
</Property>
<Property name="data" type="string">
The data of the response as JSON object (key: questionId, value: answer).
</Property>
</Properties>
### Optional Body Fields
<Properties>
<Property name="userId" type="string" required>
Pre-existing User ID to identify the user sending the response
</Property>
</Properties>
### Parameters Explained
| field name | required | default | description |
| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| data | yes | - | The response data object (answers to the survey). In this object the key is the questionId, the value the answer of the user to this question. |
| userId | no | - | The person this response is connected to. |
| surveyId | yes | - | The survey this response is connected to. |
| finished | yes | false | Mark a response as complete to be able to filter accordingly. |
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/responses">
```bash {{ title: 'cURL' }}
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/responses' \
--data-raw '{
"surveyId":"cloqzeuu70000z8khcirufo60",
"userId": "1",
"finished": true,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}'
```
```json {{ title: 'Example Request Body' }}
{
"userId": "1",
"surveyId": "cloqzeuu70000z8khcirufo60",
"finished": true,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "clp84xdld0002px36fkgue5ka",
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "surveyId was not provided.",
"details": {
"surveyId": "This field is required."
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Response {{ tag: 'PUT', label: '/api/v1/client/<environment-id>/responses/<response-id>' }}
Update an existing response in a survey.
<Row>
<Col>
### Mandatory Body Fields
<Properties>
<Property name="finished" type="boolean">
Marks whether the response is complete or not.
</Property>
<Property name="data" type="string">
The data of the response as JSON object (key: questionId, value: answer).
</Property>
</Properties>
### Parameters Explained
| field name | required | default | description |
| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| data | yes | - | The response data object (answers to the survey). In this object the key is the questionId, the value the answer of the user to this question. |
| finished | yes | false | Mark a response as complete to be able to filter accordingly. |
</Col>
<Col sticky>
<CodeGroup title="Request" tag="PUT" label="/api/v1/client/<environment-id>/responses/<response-id>">
```bash {{ title: 'cURL' }}
curl --location --request PUT 'https://app.formbricks.com/api/v1/client/<environment-id>/responses/<response-id>' \
--data-raw '{
"finished":false,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}'
```
```json {{ title: 'Example Request Body' }}
{
"finished":false,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "data was not provided.",
"details": {
"data": "This field is required."
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Response not found"
}
```
</CodeGroup>
</Col>
</Row>

View File

@@ -0,0 +1,297 @@
import { Fence } from "@/components/shared/Fence";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Action Class",["Fetch","Create","Delete"])
#### Management API
# Action Classes API
This set of API can be used to
- [List Actions](#get-all-action-classes)
- [Get Action](#get-action-class-by-id)
- [Create Actions](#create-action-class)
- [Delete Actions](#delete-action-class)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## Get all Action Classes {{ tag: 'GET', label: '/api/v1/management/action-classes' }}
<Row>
<Col>
Get all the existing action classes in your environment.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/action-classes">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/action-classes' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "cln8k0t47000gz87nw4ibwv35",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:13:19.207Z",
"name": "New Session",
"description": "Gets fired when a new session is created",
"type": "automatic",
"noCodeConfig": null,
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
{
"id": "cln8k0t55000uz87noerwdooj",
"createdAt": "2023-10-02T07:13:19.241Z",
"updatedAt": "2023-10-02T07:13:19.241Z",
"name": "Invited Team Member",
"description": "Person invited a team member",
"type": "noCode",
"noCodeConfig": {
"type": "innerHtml",
"innerHtml": {
"value": "Add Team Member"
}
},
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
]
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Action Class by ID {{ tag: 'GET', label: '/api/v1/management/action-classes/<action-class-id>' }}
<Row>
<Col>
Fetch an action class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/action-classes/<action-class-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/action-classes/<action-class-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0t55000uz87noerwdooj",
"createdAt": "2023-10-02T07:13:19.241Z",
"updatedAt": "2023-10-02T07:13:19.241Z",
"name": "Invited Team Member",
"description": "Person invited a team member",
"type": "noCode",
"noCodeConfig": {
"type": "innerHtml",
"innerHtml": {
"value": "Add Team Member"
}
},
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Action Class {{ tag: 'POST', label: '/api/v1/management/action-classes/' }}
<Row>
<Col>
Create an action class.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"environmentId": "cln8k0t47000fz87njmmu2bck",
"name": "My Action from API",
"type": "code"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/management/action-classes/">
```bash {{ title: 'cURL' }}
curl -X POST https://app.formbricks.com/api/v1/management/action-classes/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Action from API", "type": "code"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln9w1cno0008z8zu79nk5w0c",
"createdAt": "2023-10-03T05:37:26.100Z",
"updatedAt": "2023-10-03T05:37:26.100Z",
"name": "My Action from API",
"description": null,
"type": "code",
"noCodeConfig": null,
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Action Class {{ tag: 'DELETE', label: '/api/v1/management/action-classes/<action-class-id>' }}
<Row>
<Col>
Delete an action class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/action-classes/<action-class-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/action-classes/<action-class-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln9w1cno0008z8zu79nk5w0c",
"createdAt": "2023-10-03T05:37:26.100Z",
"updatedAt": "2023-10-03T05:37:26.100Z",
"name": "My Action from API",
"description": null,
"type": "code",
"noCodeConfig": null,
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,102 @@
import { MdxImage } from "@/components/shared/MdxImage";
import AddApiKey from "./add-api-key.webp";
import ApiKeySecret from "./api-key-secret.webp";
export const metadata = {
title: "Formbricks API Key: Setup and Testing",
description:
"This guide provides step-by-step instructions to generate, store, and delete API keys, ensuring safe and authenticated access to your Formbricks account.",
};
#### API
# API Key Setup
## Auth: Personal API key
The API requests are authorized with a personal API key. This API key gives you the same rights as if you were logged in at formbricks.com - **don't share it around!**
### How to generate an API key
1. Go to your settings on [app.formbricks.com](https://app.formbricks.com).
2. Go to page “API keys”
<MdxImage src={AddApiKey} alt="Add API Key" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
3. Create a key for the development or production environment.
4. Copy the key immediately. You wont be able to see it again.
<MdxImage
src={ApiKeySecret}
alt="API Key Secret"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>
### Store API key safely Anyone who has your API key has full control over your account. For security
reasons, you cannot view the API key again.
</Note>
### Test your API Key
Hit the below request to verify that you are authenticated with your API Key and the server is responding.
## Get My Profile {{ tag: 'GET', label: '/api/v1/me' }}
<Row>
<Col>
Get the product details and environment type of your account.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/me">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/me' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"id": "cll2m30r70004mx0huqkitgqv",
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"product": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Product"
},
"widgetSetupCompleted": false
}
```
```json {{ title: '401 Not Authenticated' }}
Not authenticated
```
</CodeGroup>
</Col>
</Row>
---
### Delete a personal API key
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/).
2. Go to page “API keys”.
3. Find the key you wish to revoke and select “Delete”.
4. Your API key will stop working immediately.

View File

@@ -0,0 +1,289 @@
import { Fence } from "@/components/shared/Fence";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Attribute Class",["Fetch","Create","Delete"])
#### Management API
# Attribute Classes API
This set of API can be used to
- [List Attributes](#get-all-attribute-classes)
- [Get Attributes](#get-attribute-class-by-id)
- [Create Attributes](#create-attribute-class)
- [Delete Attributes](#delete-attribute-class)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## Get all Attribute Classes {{ tag: 'GET', label: '/api/v1/management/attribute-classes' }}
<Row>
<Col>
Get all the existing attribute classes in your environment.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/attribute-classes">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/attribute-classes' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "cln8k0t47000kz87n3lh23zf0",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:13:19.207Z",
"name": "email",
"description": "The email of the person",
"archived": false,
"type": "automatic",
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
{
"id": "cln8k0t55000xz87nrtwbo7sf",
"createdAt": "2023-10-02T07:13:19.241Z",
"updatedAt": "2023-10-02T07:13:19.241Z",
"name": "Name",
"description": "Full Name of the Person",
"archived": false,
"type": "code",
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
]
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Attribute Class by ID {{ tag: 'GET', label: '/api/v1/management/attribute-classes/<attribute-class-id>' }}
<Row>
<Col>
Fetch an Attribute class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/attribute-classes/<attribute-class-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/attribute-classes/<attribute-class-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0t47000jz87nfwcey6mh",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:13:19.207Z",
"name": "userId",
"description": "The internal ID of the person",
"archived": false,
"type": "automatic",
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Attribute Class {{ tag: 'POST', label: '/api/v1/management/attribute-classes/' }}
<Row>
<Col>
Create an Attribute class.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"environmentId": "clmlmwdqq0003196ufewo6ibg",
"name": "My Attribute from API",
"type": "code",
"description": "My description"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/management/attribute-classes/">
```bash {{ title: 'cURL' }}
curl -X POST https://app.formbricks.com/api/v1/management/attribute-classes/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"environmentId": "clmlmwdqq0003196ufewo6ibg", "name": "My Attribute from API", "type": "code", "description":"My description"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clna0hd7z0009z8zue2z3a7wy",
"createdAt": "2023-10-03T07:41:51.792Z",
"updatedAt": "2023-10-03T07:41:51.792Z",
"name": "My Attribute from API",
"description": null,
"archived": false,
"type": "code",
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Attribute Class {{ tag: 'DELETE', label: '/api/v1/management/attribute-classes/<attribute-class-id>' }}
<Row>
<Col>
Delete an Attribute class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/attribute-classes/<attribute-class-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/attribute-classes/<attribute-class-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clna0hd7z0009z8zue2z3a7wy",
"createdAt": "2023-10-03T07:41:51.792Z",
"updatedAt": "2023-10-03T07:41:51.792Z",
"name": "My Attribute from API",
"description": null,
"archived": false,
"type": "code",
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,78 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Me API: Fetch your environment details",
description:
"Dive into Formbricks' Me API within the Public Client API suite. Seamlessly fetch your own current environment details.",
};
#### Management API
# Me API
This API can be used to get your own current environment details.
<Note>You will need an API Key to interact with these APIs.</Note>
---
## Get Environment {{ tag: 'GET', label: '/api/v1/management/me' }}
<Row>
<Col>
Get your current environment details.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/me">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/me' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"id": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:14:14.162Z",
"type": "production",
"product": {
"id": "cln8k0t47000ez87n57aqywvz",
"name": "Demo Product"
},
"widgetSetupCompleted": true
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,232 @@
import { Fence } from "@/components/shared/Fence";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("People",["Fetch","Delete"])
#### Management API
# People API
This set of API can be used to
- [List People](#list-people)
- [Get Person](#get-person)
- [Delete Person](#delete-person)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List People {{ tag: 'GET', label: '/api/v1/management/people' }}
<Row>
<Col>
List People
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/people">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/people' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "b4wgrzl363dn3zb6yy5gf265",
"attributes": {
"userId": "CYO618",
"email": "sophia@amazon.com",
"Name": "Sophia Johnson",
"Role": "Designer",
"Company": "Amazon",
"Experience": "7 years",
"Usage Frequency": "Yearly",
"Company Size": "1628 employees",
"Product Satisfaction Score": "62",
"Recommendation Likelihood": "9"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
{
"id": "jrb5iyzqvnkg9322ckhde3j4",
"attributes": {
"userId": "CYO511",
"email": "antonio@ibm.com",
"Name": "Antonio García",
"Role": "Designer",
"Company": "IBM",
"Experience": "1 years",
"Usage Frequency": "Weekly",
"Company Size": "4023 employees",
"Product Satisfaction Score": "77",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
]
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"userId": ""
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Person {{ tag: 'GET', label: '/api/v1/management/people/<person-id>' }}
<Row>
<Col>
Get Person by ID
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/people/<person-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/people/<person-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "jrb5iyzqvnkg9322ckhde3j4",
"attributes": {
"userId": "CYO511",
"email": "antonio@ibm.com",
"Name": "Antonio García",
"Role": "Designer",
"Company": "IBM",
"Experience": "1 years",
"Usage Frequency": "Weekly",
"Company Size": "4023 employees",
"Product Satisfaction Score": "77",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Person not found",
"details": {
"resource_id": "clmlmykc2000019vz5o3jglsa",
"resource_type": "Person"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Person {{ tag: 'DELETE', label: '/api/v1/management/people/<person-id>' }}
<Row>
<Col>
Delete Person by ID
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/people/<person-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/people/<person-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"success": "Person deleted successfully"
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Person not found",
"details": {
"resource_id": "clmlmykc2000019vz5o3jglsa",
"resource_type": "Person"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,379 @@
import { Fence } from "@/components/shared/Fence";
import { generateManagementApiMetadata } from "@/lib/utils";
export const metadata = generateManagementApiMetadata("Responses", ["Fetch", "Delete"]);
#### Management API
# Responses API
This set of API can be used to
- [List Responses](#list-all-responses)
- [List all Responses by surveyId](#list-all-responses-by-survey-id)
- [Get Response](#get-response-by-id)
- [Delete Response](#delete-a-response)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List all Responses {{ tag: 'GET', label: '/api/v1/management/responses' }}
<Row>
<Col>
Retrieve all the responses you have received in your environment.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/responses' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data":[
{
"id": "cln8k0tqv00pcz87no4qrw333",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "MacOS",
"browser": "Chrome"
}
},
"personAttributes": null,
"person": {
"id": "e0x4i5tvsp8puxfztyrwykvn",
"attributes": {
"userId": "CYO675",
"email": "ravi@netflix.com",
"Name": "Ravi Kumar",
"Role": "Manager",
"Company": "Netflix",
"Experience": "6 years",
"Usage Frequency": "Monthly",
"Company Size": "4610 employees",
"Product Satisfaction Score": "43",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
},
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## List all Responses by surveyId {{ tag: 'GET', label: '/api/v1/management/responses?surveyId=<survey-Id>' }}
<Row>
<Col>
Retrieve all the responses received in your survey.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses?surveyId=<survey-Id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/responses?surveyId=<survey-Id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data":[
{
"id": "cln8k0tqv00pcz87no4qrw333",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "MacOS",
"browser": "Chrome"
}
},
"personAttributes": null,
"person": {
"id": "e0x4i5tvsp8puxfztyrwykvn",
"attributes": {
"userId": "CYO675",
"email": "ravi@netflix.com",
"Name": "Ravi Kumar",
"Role": "Manager",
"Company": "Netflix",
"Experience": "6 years",
"Usage Frequency": "Monthly",
"Company Size": "4610 employees",
"Product Satisfaction Score": "43",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
},
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Response by ID {{ tag: 'GET', label: '/api/v1/management/responses/<response-id>' }}
<Row>
<Col>
Retrieve a response by its ID.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses/<response-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/responses/<response-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data":
{
"id": "cln8k0tqv00pbz87nwo5lr72b",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "Windows",
"browser": "Edge"
}
},
"personAttributes": null,
"person": {
"id": "hsx38f15v50ua8383uadagq5",
"attributes": {
"userId": "CYO278",
"email": "jorge@facebook.com",
"Name": "Jorge Sanchez",
"Role": "Product Manager",
"Company": "Facebook",
"Experience": "10 years",
"Usage Frequency": "Daily",
"Company Size": "1685 employees",
"Product Satisfaction Score": "84",
"Recommendation Likelihood": "6"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete a response {{ tag: 'DELETE', label: '/api/v1/client/responses/<response-id>' }}
<Row>
<Col>
Delete Response by ID
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/client/responses/<response-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/responses/<response-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cln8k0tqv00pbz87nwo5lr72b",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "Windows",
"browser": "Edge"
}
},
"personAttributes": null,
"person": {
"id": "hsx38f15v50ua8383uadagq5",
"attributes": {
"userId": "CYO278",
"email": "jorge@facebook.com",
"Name": "Jorge Sanchez",
"Role": "Product Manager",
"Company": "Facebook",
"Experience": "10 years",
"Usage Frequency": "Daily",
"Company Size": "1685 employees",
"Product Satisfaction Score": "84",
"Recommendation Likelihood": "6"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "surveyId was not provided.",
"details": {
"surveyId": "This field is required."
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,789 @@
import { Fence } from "@/components/shared/Fence";
import { generateManagementApiMetadata } from "@/lib/utils";
export const metadata = generateManagementApiMetadata("Surveys", ["Fetch", "Create", "Update", "Delete"]);
#### Management API
# Surveys API
This set of API can be used to
- [List All Surveys](#list-all-surveys)
- [Get Survey](#get-survey-by-id)
- [Create Survey](#create-survey)
- [Update Survey](#update-survey-by-id)
- [Delete Survey](#delete-survey-by-id)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }}
<Row>
<Col>
Retrieve all the surveys you have for the environment with pagination.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name="offset" type="number">
The number of surveys to skip before returning the results.
</Property>
<Property name="limit" type="number">
The number of surveys to return.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/surveys?offset=20&limit=10' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "cllnfy2780fromy0hy7uoxvtn",
"createdAt": "2023-08-23T07:56:20.516Z",
"updatedAt": "2023-08-23T07:56:26.947Z",
"name": "Product Market Fit (Superhuman)",
"type": "link",
"environmentId": "cll2m30r70004mx0huqkitgqv",
"status": "inProgress",
"attributeFilters": [],
"displayOption": "displayOnce",
"autoClose": null,
"triggers": [],
"redirectUrl": null,
"recontactDays": null,
"questions": [
{
"id": "gml6mgy71efgtq8np3s9je5p",
"type": "cta",
"headline": "You are one of our power users! Do you have 5 minutes?",
"required": false,
"buttonLabel": "Happy to help!",
"logic": [
{
"condition": "skipped",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We would love to understand your user experience better. Sharing your insight helps a lot.</span></p>",
"buttonExternal": false,
"dismissButtonLabel": "No, thanks."
},
{
"id": "kp62fbqe8cfzmvy8qwpr81b2",
"type": "multipleChoiceSingle",
"headline": "How disappointed would you be if you could no longer use My Product?",
"subheader": "Please select one of the following options:",
"required": true,
"choices": [
{
"id": "bdgy1hnwd7uwmfxk1ljqp1n5",
"label": "Not at all disappointed"
},
{
"id": "poabnvgtwenp8rb2v70gj4hj",
"label": "Somewhat disappointed"
},
{
"id": "opfiqyqz8wrqn0i0f7t24d3n",
"label": "Very disappointed"
}
],
"shuffleOption": "none"
},
{
"id": "klvpwd4x08x8quesihvw5l92",
"type": "multipleChoiceSingle",
"headline": "What is your role?",
"subheader": "Please select one of the following options:",
"required": true,
"choices": [
{
"id": "c8nerw6l9gpsxcmqkn10f9hy",
"label": "Founder"
},
{
"id": "ebjqezei6a2axtuq86cleetn",
"label": "Executive"
},
{
"id": "ctiijjblyhlp22snypfamqt1",
"label": "Product Manager"
},
{
"id": "ibalyr0mhemfkkr82vypmg40",
"label": "Product Owner"
},
{
"id": "fipk606aegslbd0e7yhc0xjx",
"label": "Software Engineer"
}
],
"shuffleOption": "none"
},
{
"id": "ryo75306flyg72iaeditbv51",
"type": "openText",
"headline": "What type of people do you think would most benefit from My Product?",
"required": true
},
{
"id": "lkjaxb73ulydzeumhd51sx9g",
"type": "openText",
"headline": "What is the main benefit you receive from My Product?",
"required": true
},
{
"id": "ec7agikkr58j8uonhioinkyk",
"type": "openText",
"headline": "How can we improve My Product for you?",
"subheader": "Please be as specific as possible.",
"required": true
}
],
"thankYouCard": {
"enabled": true,
"headline": "Thank you!",
"subheader": "We appreciate your feedback."
},
"delay": 0,
"autoComplete": null,
"closeOnDate": null
}
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Survey by ID {{ tag: 'GET', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>
<Col>
Get a specific survey by its ID.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys/<survey-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/surveys/<survey-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0tjz00n5z87nwq527h3z",
"createdAt": "2023-10-02T07:13:19.775Z",
"updatedAt": "2023-10-02T07:13:19.775Z",
"name": "Churn Survey",
"type": "link",
"environmentId": "cln8k0t47000fz87njmmu2bck",
"status": "inProgress",
"attributeFilters": [],
"displayOption": "displayOnce",
"autoClose": null,
"triggers": [],
"redirectUrl": null,
"recontactDays": null,
"questions": [
{
"id": "churn-reason",
"type": "multipleChoiceSingle",
"headline": "Why did you cancel your subscription?",
"subheader": "We're sorry to see you leave. Help us do better:",
"required": true,
"logic": [
{
"condition": "equals",
"value": "Difficult to use",
"destination": "easier-to-use"
},
{
"condition": "equals",
"value": "It's too expensive",
"destination": "30-off"
},
{
"condition": "equals",
"value": "I am missing features",
"destination": "missing-features"
},
{
"condition": "equals",
"value": "Poor customer service",
"destination": "poor-service"
},
{
"condition": "equals",
"value": "I just didn't need it anymore",
"destination": "end"
}
],
"choices": [
{
"id": "isud2xethsw63dlwl89kr4kj",
"label": "Difficult to use"
},
{
"id": "opuu4ba3dlele3n0gjkuh27c",
"label": "It's too expensive"
},
{
"id": "gnypapo0rhvkt8pwosrphvbl",
"label": "I am missing features"
},
{
"id": "wkgsrsrazd9kfunqhzjezx6t",
"label": "Poor customer service"
},
{
"id": "pykmgyyw74vg0gaeryj6bo4c",
"label": "I just didn't need it anymore"
}
]
},
{
"id": "easier-to-use",
"type": "openText",
"headline": "What would have made {{productName}} easier to use?",
"subheader": "",
"required": true,
"buttonLabel": "Send",
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "30-off",
"type": "cta",
"headline": "Get 30% off for the next year!",
"required": true,
"buttonLabel": "Get 30% off",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
"buttonUrl": "https://formbricks.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
},
{
"id": "missing-features",
"type": "openText",
"headline": "What features are you missing?",
"subheader": "",
"required": true,
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "poor-service",
"type": "cta",
"headline": "So sorry to hear 😔 Talk to our CEO directly!",
"required": true,
"buttonLabel": "Send email to CEO",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
"buttonUrl": "mailto:ceo@company.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
}
],
"thankYouCard": {
"enabled": false
},
"delay": 0,
"autoComplete": null,
"closeOnDate": null,
"surveyClosedMessage": null,
"verifyEmail": null
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Survey {{ tag: 'POST', label: '/api/v1/management/surveys' }}
<Row>
<Col>
Create a survey
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"environmentId": "clmlmwdqq0003196ufewo6ibg",
"type": "link",
"name": "My new Survey"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/management/surveys">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/management/surveys' \
--header \
'x-api-key: <your-api-key>'
```
```bash {{ title: 'cURL' }}
curl -X POST https://app.formbricks.com/api/v1/management/surveys/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Survey from API", "type": "link"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clna6bqnz000az8zubq3e757t",
"createdAt": "2023-10-03T10:25:26.975Z",
"updatedAt": "2023-10-03T10:25:26.975Z",
"name": "My new Survey",
"redirectUrl": null,
"type": "link",
"environmentId": "cln8k0t47000fz87njmmu2bck",
"status": "draft",
"questions": [],
"thankYouCard": {
"enabled": false
},
"displayOption": "displayOnce",
"recontactDays": null,
"autoClose": null,
"delay": 0,
"autoComplete": null,
"closeOnDate": null,
"surveyClosedMessage": null,
"verifyEmail": null
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Survey by ID {{ tag: 'PUT', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>
<Col>
Update a survey by its ID
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"name": "My renamed Survey",
"redirectUrl":"https://formbricks.com",
"type":"web"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="PUT" label="/api/v1/management/surveys/<survey-id>">
```bash {{ title: 'cURL' }}
curl -X PUT https://app.formbricks.com/api/v1/management/surveys/<survey-id> \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"name": "My renamed Survey"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cloqzeuu70000z8khcirufo60",
"createdAt": "2023-11-09T09:23:42.367Z",
"updatedAt": "2023-11-09T09:23:42.367Z",
"name": "My renamed Survey",
"redirectUrl": null,
"type": "link",
"environmentId": "clonzr6vc0009z8md7y06hipl",
"status": "inProgress",
"welcomeCard": {
"html": "Thanks for providing your feedback - let's go!",
"enabled": false,
"headline": "Welcome!",
"timeToFinish": false
},
"questions": [
{
"id": "l9rwn5nbk48y44tvnyyjcvca",
"type": "openText",
"headline": "Why did you leave the platform?",
"required": true,
"inputType": "text"
}
],
"thankYouCard": {
"enabled": true,
"headline": "Thank you!",
"subheader": "We appreciate your feedback."
},
"hiddenFields": {
"enabled": true,
"fieldIds": []
},
"displayOption": "displayOnce",
"recontactDays": null,
"autoClose": null,
"delay": 0,
"autoComplete": 50,
"closeOnDate": null,
"surveyClosedMessage": null,
"productOverwrites": null,
"singleUse": {
"enabled": false,
"isEncrypted": true
},
"verifyEmail": null,
"pin": null,
"triggers": [],
"attributeFilters": []
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>
<Col>
Delete a survey by its ID.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/surveys/<survey-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE \
'https://app.formbricks.com/api/v1/management/surveys/<survey-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0tjz00n5z87nwq527h3z",
"createdAt": "2023-10-02T07:13:19.775Z",
"updatedAt": "2023-10-02T07:13:19.775Z",
"name": "Churn Survey",
"type": "link",
"environmentId": "cln8k0t47000fz87njmmu2bck",
"status": "inProgress",
"attributeFilters": [],
"displayOption": "displayOnce",
"autoClose": null,
"triggers": [],
"redirectUrl": null,
"recontactDays": null,
"questions": [
{
"id": "churn-reason",
"type": "multipleChoiceSingle",
"headline": "Why did you cancel your subscription?",
"subheader": "We're sorry to see you leave. Help us do better:",
"required": true,
"logic": [
{
"condition": "equals",
"value": "Difficult to use",
"destination": "easier-to-use"
},
{
"condition": "equals",
"value": "It's too expensive",
"destination": "30-off"
},
{
"condition": "equals",
"value": "I am missing features",
"destination": "missing-features"
},
{
"condition": "equals",
"value": "Poor customer service",
"destination": "poor-service"
},
{
"condition": "equals",
"value": "I just didn't need it anymore",
"destination": "end"
}
],
"choices": [
{
"id": "isud2xethsw63dlwl89kr4kj",
"label": "Difficult to use"
},
{
"id": "opuu4ba3dlele3n0gjkuh27c",
"label": "It's too expensive"
},
{
"id": "gnypapo0rhvkt8pwosrphvbl",
"label": "I am missing features"
},
{
"id": "wkgsrsrazd9kfunqhzjezx6t",
"label": "Poor customer service"
},
{
"id": "pykmgyyw74vg0gaeryj6bo4c",
"label": "I just didn't need it anymore"
}
]
},
{
"id": "easier-to-use",
"type": "openText",
"headline": "What would have made {{productName}} easier to use?",
"subheader": "",
"required": true,
"buttonLabel": "Send",
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "30-off",
"type": "cta",
"headline": "Get 30% off for the next year!",
"required": true,
"buttonLabel": "Get 30% off",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
"buttonUrl": "https://formbricks.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
},
{
"id": "missing-features",
"type": "openText",
"headline": "What features are you missing?",
"subheader": "",
"required": true,
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "poor-service",
"type": "cta",
"headline": "So sorry to hear 😔 Talk to our CEO directly!",
"required": true,
"buttonLabel": "Send email to CEO",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
"buttonUrl": "mailto:ceo@company.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
}
],
"thankYouCard": {
"enabled": false
},
"delay": 0,
"autoComplete": null,
"closeOnDate": null,
"surveyClosedMessage": null,
"verifyEmail": null
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,398 @@
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Webhook",["Fetch","Create","Delete"])
#### Management API
# Webhook API
Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment.
The behavior of the webhooks is determined by their trigger settings. The trigger determines which updates the webhook sends. Current available triggers include "responseCreated", "responseUpdated", and "responseFinished". This allows you to customize your webhooks to only send notifications for the events that are relevant to your application.
Webhooks are tied to a specific Formbricks environment. Once set, a webhook will receive updates from all surveys within this environment. This makes it easy to manage your data flow and ensure that all relevant updates are caught by the webhook.
This set of API can be used to
- [List All Webhooks](#list-webhooks)
- [Get Webhook](#retrieve-webhook-by-id)
- [Create Webhook](#create-webhook)
- [Delete Webhook](#delete-webhook-by-id)
And the detailed Webhook Payload is elaborated [here](#webhook-payload).
These APIs are designed to facilitate seamless integration of Formbricks with third-party systems. By making use of our webhook API, you can automate the process of sending data to these systems whenever significant events occur within your Formbricks environment.
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List Webhooks {{ tag: 'GET', label: '/api/v1/webhooks' }}
<Row>
<Col>
Learn how to retrieve a list of all webhooks via API.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/webhooks">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/webhooks' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": [
{
"id": "cliu1kdza000219zftad4ip6c",
"createdAt": "2023-06-13T08:49:04.198Z",
"updatedAt": "2023-06-13T08:49:04.198Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": [
"responseFinished"
]
}
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Retrieve Webhook by ID {{ tag: 'GET', label: '/api/v1/webhooks/<webhook-id>' }}
<Row>
<Col>
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/webhooks/<webhook-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/webhooks/<webhook-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cliu167rk000019zfhbo68bar",
"createdAt": "2023-06-13T08:38:02.960Z",
"updatedAt": "2023-06-13T08:38:02.960Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": [
"responseFinished"
]
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Webhook {{ tag: 'POST', label: '/api/v1/webhooks' }}
Add a webhook to your product.
<Row>
<Col>
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Request Body Parameters
<Properties>
<Property name="url" type="string" required>
The URL where the webhook will send data to.
</Property>
<Property name="triggers" type="string[]" required>
List of events that will trigger the webhook.
</Property>
<Property name="surveyIds" type="string[]">
List of survey IDs that will trigger the webhook. If not provided, the webhook will be triggered for all surveys.
</Property>
</Properties>
| field name | required | default | description |
| ---------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| url | yes | - | The endpoint that the webhook will send data to |
| trigger | yes | - | The event that will trigger the webhook ("responseCreated" or "responseUpdated" or "responseFinished") |
| surveyIds | no | - | List of survey IDs that will trigger the webhook. If not provided, the webhook will be triggered for all surveys. |
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/webhooks">
```bash {{ title: 'cURL' }}
curl --location --request POST 'https://app.formbricks.com/api/v1/webhooks' \
--header 'x-api-key: <your-api-key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"url": "https://mysystem.com/myendpoint",
"triggers": ["responseFinished"]
}'
```
```json {{ title: 'Example Request Body' }}
{
"url": "https://mysystem.com/myendpoint",
"triggers": ["responseFinished"]
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cliu1kdza000219zftad4ip6c",
"createdAt": "2023-06-13T08:49:04.198Z",
"updatedAt": "2023-06-13T08:49:04.198Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": ["responseFinished"],
"surveyIds": ["clisypjy4000319t4imm289uo"]
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Missing trigger",
"details": {
"missing_field": "trigger"
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Webhook by ID {{ tag: 'DELETE', label: '/api/v1/webhooks/<webhook-id>' }}
<Row>
<Col>
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/webhooks/<webhook-id>">
```bash {{ title: 'cURL' }}
curl --location --request DELETE 'https://app.formbricks.com/api/v1/webhooks/<webhook-id>' \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cliu167rk000019zfhbo68bar",
"createdAt": "2023-06-13T08:38:02.960Z",
"updatedAt": "2023-06-13T08:38:02.960Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": ["responseFinished"]
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Webhook not found.",
"details": {
"webhookId": "The requested webhook does not exist."
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Webhook Payload
This documentation helps understand the payload structure that will be received when the webhook is triggered in Formbricks.
<Row>
<Col sticky>
| Variable | Type | Description |
| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| webhookId | String | Webhook's Id |
| event | String | The name of the trigger event [responseCreated, responseUpdated, responseFinished] |
| data | Object | Contains the details of the newly created response. |
| data.id | String | Formbricks Response ID. |
| data.createdAt | String | The timestamp when the response was created. |
| data.updatedAt | String | The timestamp when the response was last updated. |
| data.surveyId | String | The identifier of the survey associated with this response. |
| data.finished | Boolean | A boolean value indicating whether the survey response is marked as finished. |
| data.data | Object | An object containing the response data, where keys are question identifiers, and values are the corresponding answers given by the respondent. |
| data.meta | Object | Additional metadata related to the response, such as the user's operating system and browser information. |
| data.personAttributes | Object | An object with attributes related to the respondent, such as their email and a user ID (if available). |
| data.person | Object | Information about the respondent, including their unique id, attributes, and creation/update timestamps. |
| data.notes | Array | An array of notes associated with the response (if any). |
| data.tags | Array | An array of tags assigned to the response (if any). |
</Col>
<Col>
### An example webhook payload
<CodeGroup title="Payload">
```json
{
"webhookId": "cljwxvjos0003qhnvj2jg4k5i",
"event": "responseCreated",
"data": {
"id": "cljwy2m8r0001qhclco1godnu",
"createdAt": "2023-07-10T14:14:17.115Z",
"updatedAt": "2023-07-10T14:14:17.115Z",
"surveyId": "cljsf3d7a000019cv9apt2t27",
"finished": false,
"data": {
"qumbk3fkr6cky8850bvvq5z1": "Executive"
},
"meta": {
"userAgent": {
"os": "Mac OS",
"browser": "Chrome"
}
},
"personAttributes": {
"email": "test@web.com",
"userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING"
},
"person": {
"id": "cljold01t0000qh8ewzigzmjk",
"attributes": {
"email": "test@web.com",
"userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING"
},
"createdAt": "2023-07-04T17:56:17.154Z",
"updatedAt": "2023-07-04T17:56:17.154Z"
},
"notes": [],
"tags": []
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -95,7 +95,7 @@ Click "Create a webhook":
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Enter the Formbricks API key. Learn how to get one from the [API Key tutorial](/docs/additional-features/api#how-to-generate-an-api-key).
Enter the Formbricks API key. Learn how to get one from the [API Key tutorial](/docs/api/management/api-key-setup).
<MdxImage
src={EnterApiKey}

View File

@@ -90,7 +90,7 @@ Click on Create New Credentail button to add your host and API Key
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Now you need an API key. Please refer to the [API Key Setup](/docs/additional-features/api#how-to-generate-an-api-key) page to learn how to create one.
Now you need an API key. Please refer to the [API Key Setup](/docs/api/management/api-key-setup) page to learn how to create one.
Once you copied it in the API Key field, hit Save button to test the connection and save the credentials.

View File

@@ -93,7 +93,7 @@ Now, you have to connect Zapier with Formbricks via an API Key:
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Now you need an API key. Please refer to the [API Key Setup](/docs/additional-features/api#how-to-generate-an-api-key) page to learn how to create one.
Now you need an API key. Please refer to the [API Key Setup](/docs/api/management/api-key-setup) page to learn how to create one.
Once you copied it in the newly opened Zapier window, you will be connected:

View File

@@ -1,81 +0,0 @@
import { TellaVideo } from "@/components/docs/TellaVideo";
export const metadata = {
title: "Embed Surveys in Your Web Page",
description: "Embed Formbricks surveys seamlessly into your website or web application using an iframe.",
};
#### Embed Surveys
# Embed Surveys in Your Web Page
Embedding Formbricks surveys directly into your web pages allows you to integrate interactive surveys without redirecting users to a separate survey site. This method ensures a seamless integration and maintains the aesthetic continuity of your website or application.
## How to Use it?
<TellaVideo tellaVideoIdentifier="clvavyy2f00000fjr0mple922"/>
1. Create and publish a link survey.
2. Open survey summary page and click on **share** button on the top right.
3. In the survey share modal, click on **Embed survey** button.
4. Navigate to **Embed in a Web Page** tab and click on Copy code
5. Paste the copied iframe code into the HTML of your web page where you want the survey to appear.
### Example of Embedding a Survey
<Col>
<CodeGroup title="Example Embedding Code">
```html
<div style="position: relative; height:100vh; max-height:100vh; overflow:auto;">
<iframe
src="https://app.formbricks.com/s/<your-surveyId>"
frameborder="0"
style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">
</iframe>
</div>
```
</CodeGroup>
</Col>
## Iframe Events
The iframe fires a **formbricksSurveyCompleted** event when a user finishes a survey within the embedded iframe. This event can be captured through a message listener in your webpage's JavaScript
### How to Use it?
1. Embed the Formbricks survey on your webpage using the iframe method as described above.
2. Add an event listener to your webpages JavaScript that listens for `message` events from the iframe.
3. Check if the received message indicates that the survey is completed by comparing the `event.data` with the value `formbricksSurveyCompleted`.
<Note>
It is important to verify the origin of the message to ensure it comes from the iframe containing your
survey, enhancing the security of your event handling.
</Note>
4. Implement your custom actions within the callback function based on the survey completion.
### Example of Handling Survey Completion Events
<Col>
<CodeGroup title="Example Code for Event Listener">
```javascript
window.addEventListener("message", (event) => {
// Replace 'https://app.formbricks.com' with the actual web app url
if (event.origin === "https://app.formbricks.com" && event.data === "formbricksSurveyCompleted") {
console.log("Survey completed!");
// Implement your custom actions here
}
});
```
</CodeGroup>
</Col>

View File

@@ -210,15 +210,11 @@ export const navigation: Array<NavGroup> = [
{ title: "Source Tracking", href: "/docs/link-surveys/source-tracking" },
{ title: "Hidden Fields", href: "/docs/link-surveys/hidden-fields" },
{ title: "Start At Question", href: "/docs/link-surveys/start-at-question" },
{ title: "Embed Surveys", href: "/docs/link-surveys/embed-surveys" },
],
},
{
title: "Additional Features",
links: [
{ title: "Multi Language Surveys", href: "/docs/additional-features/multi-language-surveys" },
{ title: "API", href: "/docs/additional-features/api" },
],
links: [{ title: "Multi Language Surveys", href: "/docs/additional-features/multi-language-surveys" }],
},
{
title: "Best Practices",
@@ -270,6 +266,29 @@ export const navigation: Array<NavGroup> = [
{ title: "FAQ", href: "/docs/faq" },
],
},
{
title: "Client API",
links: [
{ title: "Overview", href: "/docs/api/client/overview" },
{ title: "Actions", href: "/docs/api/client/actions" },
{ title: "Displays", href: "/docs/api/client/displays" },
{ title: "People", href: "/docs/api/client/people" },
{ title: "Responses", href: "/docs/api/client/responses" },
],
},
{
title: "Management API",
links: [
{ title: "API Key Setup", href: "/docs/api/management/api-key-setup" },
{ title: "Action Classes", href: "/docs/api/management/action-classes" },
{ title: "Attribute Classes", href: "/docs/api/management/attribute-classes" },
{ title: "Me", href: "/docs/api/management/me" },
{ title: "People", href: "/docs/api/management/people" },
{ title: "Responses", href: "/docs/api/management/responses" },
{ title: "Surveys", href: "/docs/api/management/surveys" },
{ title: "Webhooks", href: "/docs/api/management/webhooks" },
],
},
];
export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {

View File

@@ -1,18 +0,0 @@
import React from "react";
export function TellaVideo({ tellaVideoIdentifier }: { tellaVideoIdentifier: string }) {
return (
<div>
<iframe
className="aspect-video"
style={{
width: "100%",
height: "100%",
border: 0,
}}
src={`https://www.tella.tv/video/${tellaVideoIdentifier}/embed?b=0&title=0&a=1&loop=0&autoPlay=true&t=0&muted=1&wt=0`}
allowFullScreen={true}
title="Tella Video Help"></iframe>
</div>
);
}

View File

@@ -2,7 +2,6 @@ import CalLogoLight from "@/images/clients/cal-logo-light.svg";
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
import GumtreeLogo from "@/images/clients/gumtree.png";
import LelyLogo from "@/images/clients/lely-logo.webp";
import NILogoDark from "@/images/clients/niLogoDark.svg";
import OpinodoLogo from "@/images/clients/opinodo.png";
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
@@ -43,11 +42,10 @@ export const Hero: React.FC = ({}) => {
know what your customers need.
</span>
</p>
<div className="mx-auto mt-5 max-w-xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0 lg:max-w-5xl">
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-3 lg:grid-cols-9">
<div className="mx-auto mt-5 max-w-xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0 lg:max-w-4xl">
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-3 lg:grid-cols-8">
<Image src={FlixbusLogo} alt="Flixbus Flix Flixtrain Logo" className="rounded-md" width={200} />
<Image src={GumtreeLogo} alt="Gumtree Logo" width={200} />
<Image src={LelyLogo} alt="Lely Logo" width={200} />
<Image src={CalLogoLight} alt="Cal Logo" width={170} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" width={200} />
<Image src={OpinodoLogo} alt="Opinodo.com Logo" width={200} />

View File

@@ -19,7 +19,7 @@ export const SurveyTypeSelection: React.FC = () => {
subheading="Follow individual feedback trails or zoom out for the big picture. All in one place."
/>
<div className="space-y-6 text-center md:flex md:space-x-8 md:space-y-0">
<div className="flex space-x-8 text-center">
<OptionCard
size="lg"
title="On your website"
@@ -32,7 +32,7 @@ export const SurveyTypeSelection: React.FC = () => {
<OptionCard
size="lg"
title="In emails"
description="Embed branded surveys in your emails."
description="Create on brand surveys, embedded in your emails."
onSelect={() => {
router.push("/open-source-form-builder");
}}>

View File

@@ -2,7 +2,6 @@ import CalLogoLight from "@/images/clients/cal-logo-light.svg";
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
import GumtreeLogo from "@/images/clients/gumtree.png";
import LelyLogo from "@/images/clients/lely-logo.webp";
import NILogoDark from "@/images/clients/niLogoDark.svg";
import OpinodoLogo from "@/images/clients/opinodo.png";
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
@@ -16,16 +15,35 @@ export default function LogoBar() {
10,000+ teams at the worlds best companies trust Formbricks
</p>
<div className="mt-5 items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-3 md:gap-10 lg:grid-cols-9">
<Image src={FlixbusLogo} alt="Flixbus Flix Flixtrain Logo" width={200} />
<Image src={GumtreeLogo} alt="Gumtree Logo" width={200} />
<Image src={LelyLogo} alt="Lely Logo" width={200} />
<Image src={CalLogoLight} alt="Cal Logo" width={200} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" width={200} />
<Image src={OpinodoLogo} alt="Crowd.dev Logo" width={200} />
<Image src={CrowdLogoLight} alt="Crowd.dev Logo" width={200} />
<Image src={OptimoleLogo} alt="Optimole Logo" width={200} />
<Image src={NILogoDark} alt="Neverinstall Logo" width={200} />
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-2 md:gap-10 lg:grid-cols-8">
<Image
src={FlixbusLogo}
alt="Flixbus Flix Flixtrain Logo"
className="rounded-lg pb-1 "
width={200}
/>
<Image
src={GumtreeLogo}
alt="Flixbus Flix Flixtrain Logo"
className="rounded-lg pb-1 "
width={200}
/>
<Image src={CalLogoLight} alt="Cal Logo" className="block dark:hidden" width={170} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" className="pb-1" width={200} />
<Image
src={OpinodoLogo}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 dark:hidden"
width={200}
/>
<Image
src={CrowdLogoLight}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 dark:hidden"
width={200}
/>
<Image src={OptimoleLogo} alt="Optimole Logo" className="pb-1" width={200} />
<Image src={NILogoDark} alt="Neverinstall Logo" className="block pb-1 dark:hidden" width={200} />
</div>
</div>
</div>

View File

@@ -1,11 +1,7 @@
import CCPALogo from "@/images/ccpa.svg";
import GPDRLogo from "@/images/gdpr.svg";
import Image from "next/image";
import Link from "next/link";
import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6";
import { FooterLogo } from "./Logo";
import SourceForgeBadge from "./SourceForgeBadge";
const navigation = {
products: [
@@ -93,11 +89,6 @@ export default function Footer() {
</Link>
))}
</div>
<div className="flex space-x-4">
<SourceForgeBadge />
<Image src={GPDRLogo} alt="GDPR Logo" width={50} />
<Image src={CCPALogo} alt="CCPA Logo" width={50} />
</div>
</div>
<div className="grid grid-cols-2 gap-8 lg:col-span-2 lg:grid-cols-4">
<div>

View File

@@ -1,34 +0,0 @@
import React, { useEffect } from "react";
const SourceForgeBadge: React.FC = () => {
useEffect(() => {
const script = document.createElement("script");
script.async = true;
script.src = "https://b.sf-syn.com/badge_js?sf_id=3747607&variant_id=sf";
const firstScript = document.getElementsByTagName("script")[0];
firstScript.parentNode?.insertBefore(script, firstScript);
return () => {
// Clean up the script when the component unmounts
firstScript.parentNode?.removeChild(script);
};
}, []);
return (
<div
data-id="3747607"
data-badge="heart-badge-white"
data-variant-id="sf"
style={{ width: "75px" }}
className="p-0.5">
<a
href="https://sourceforge.net/software/product/Formbricks/"
target="_blank"
rel="noopener noreferrer">
Formbricks Reviews
</a>
</div>
);
};
export default SourceForgeBadge;

View File

@@ -1,11 +0,0 @@
<svg width="32" height="50" viewBox="0 0 32 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.59947 48.1181C9.09296 48.1181 9.38736 47.8022 9.47458 47.4725L9.17472 47.3736C9.10928 47.5934 8.92393 47.8022 8.59947 47.8022C8.26962 47.8022 7.95884 47.5605 7.95884 47.1044C7.95884 46.6318 8.27784 46.3983 8.59677 46.3983C8.92117 46.3983 9.10112 46.5879 9.15834 46.8241L9.46642 46.7198C9.37921 46.3791 9.0875 46.0879 8.59677 46.0879C8.09519 46.0879 7.62354 46.4698 7.62354 47.1044C7.62354 47.739 8.07881 48.1181 8.59947 48.1181ZM10.0062 47.1016C10.0062 46.6319 10.3279 46.3983 10.6523 46.3983C10.9795 46.3983 11.3012 46.6319 11.3012 47.1016C11.3012 47.5715 10.9795 47.805 10.6523 47.805C10.3279 47.805 10.0062 47.5715 10.0062 47.1016ZM9.67091 47.1016C9.67091 47.7418 10.1453 48.1181 10.6523 48.1181C11.1593 48.1181 11.6365 47.7418 11.6365 47.1016C11.6365 46.4643 11.1593 46.0879 10.6523 46.0879C10.1453 46.0879 9.67091 46.4643 9.67091 47.1016ZM14.1244 48.0769V46.1291H13.68L13.0612 47.6154L12.4341 46.1291H11.9979V48.0769H12.3169V46.6483L12.914 48.0769H13.1974L13.7999 46.6428V48.0769H14.1244ZM14.9367 47.0247V46.4203H15.2775C15.4847 46.4203 15.6101 46.5385 15.6101 46.7253C15.6101 46.9093 15.4847 47.0247 15.2775 47.0247H14.9367ZM15.3266 47.316C15.6973 47.316 15.9427 47.066 15.9427 46.7225C15.9427 46.3819 15.6973 46.1291 15.3266 46.1291H14.6096V48.0769H14.9367V47.316H15.3266ZM17.4735 48.0769V47.7637H16.5874V46.1291H16.2603V48.0769H17.4735ZM18.0944 48.0769V46.1291H17.7619V48.0769H18.0944ZM19.8987 48.0769H20.2558L19.5033 46.1291H19.1244L18.372 48.0769H18.7182L18.8981 47.5879H19.716L19.8987 48.0769ZM19.307 46.4863L19.6015 47.283H19.0126L19.307 46.4863ZM22.1554 48.0769V46.1291H21.8282V47.511L20.9504 46.1291H20.5333V48.0769H20.8604V46.6016L21.8146 48.0769H22.1554ZM24.0293 46.4396V46.1291H22.4454V46.4396H23.0724V48.0769H23.3996V46.4396H24.0293Z" fill="#64748b"/>
<path d="M7.4822 43.2638C9.40139 43.2638 10.2215 41.9451 10.4221 41.0836L9.10479 40.7055C8.98269 41.1539 8.55521 41.8748 7.4822 41.8748C6.55747 41.8748 5.70255 41.1978 5.70255 40.0287C5.70255 38.7187 6.63596 38.121 7.46476 38.121C8.55521 38.121 8.9478 38.8506 9.04374 39.2814L10.3436 38.8682C10.1429 37.9715 9.32289 36.7671 7.46476 36.7671C5.73744 36.7671 4.27185 38.0858 4.27185 40.0287C4.27185 41.9715 5.70254 43.2638 7.4822 43.2638ZM13.8911 43.2638C15.8104 43.2638 16.6304 41.9451 16.831 41.0836L15.5138 40.7055C15.3916 41.1539 14.9641 41.8748 13.8911 41.8748C12.9664 41.8748 12.1115 41.1978 12.1115 40.0287C12.1115 38.7187 13.0449 38.121 13.8737 38.121C14.9641 38.121 15.3567 38.8506 15.4527 39.2814L16.7525 38.8682C16.5519 37.9715 15.7319 36.7671 13.8737 36.7671C12.1464 36.7671 10.6808 38.0858 10.6808 40.0287C10.6808 41.9715 12.1115 43.2638 13.8911 43.2638ZM18.8345 39.6946V38.1033H19.6371C20.1605 38.1033 20.5182 38.4023 20.5182 38.9033C20.5182 39.3868 20.1605 39.6946 19.6371 39.6946H18.8345ZM19.7767 40.8902C21.0329 40.8902 21.8965 40.0726 21.8965 38.8945C21.8965 37.7341 21.0329 36.899 19.7767 36.899H17.4474V43.1319H18.8258V40.8902H19.7767ZM26.1414 43.1319H27.6419L25.3388 36.899H23.7424L21.4132 43.1319H22.8613L23.3062 41.866H25.6965L26.1414 43.1319ZM24.5188 38.4462L25.2603 40.6H23.7599L24.5188 38.4462Z" fill="#64748b"/>
<path d="M14.393 7.2895V0.507324H6.22569V2.47134L5.29443 5.16282L6.22569 6.10842V8.80019L8.08849 10.6914V11.5645L9.73642 14.2559V15.4197L12.4588 18.9114V21.8213H15.3962L19.838 24.3672V26.3315L26.2859 25.3131V20.8757L21.7725 16.0466H12.3824V7.2895H14.393Z" fill="#64748b"/>
<path d="M15.1082 23.3091V22.4849H12.3821V23.3091H15.1082Z" fill="#64748b"/>
<path d="M15.3815 25.5071V24.4082H16.1993V25.5071H15.3815Z" fill="#64748b"/>
<path d="M16.1989 24.4082V23.584H17.0167V24.4082H16.1989Z" fill="#64748b"/>
<path d="M13.7455 9.20666C13.7455 8.90341 13.9895 8.65723 14.2907 8.65723H21.3787C21.68 8.65723 21.924 8.90341 21.924 9.20666V14.1284C21.924 14.432 21.68 14.6779 21.3787 14.6779H14.2907C13.9895 14.6779 13.7455 14.432 13.7455 14.1284V9.20666Z" fill="#64748b"/>
<path d="M17.0931 12.9479L16.017 11.9471C15.896 11.8345 15.896 11.6528 16.017 11.5402C16.1373 11.4284 16.3327 11.4278 16.4538 11.5386C16.4541 11.5391 16.4546 11.5394 16.4551 11.5399L17.3501 12.3699L19.4863 10.3833C19.6063 10.2718 19.8015 10.2713 19.9223 10.382C19.9231 10.3825 19.9239 10.3833 19.9247 10.3842C20.046 10.4977 20.0458 10.68 19.9239 10.7932L17.6072 12.9479C17.4663 13.079 17.237 13.0795 17.0953 12.9495C17.0944 12.949 17.0939 12.9485 17.0931 12.9479Z" fill="#131212"/>
<path d="M19.7595 8.79228V7.27003C19.7595 6.26615 18.9517 5.45215 17.9553 5.45215C16.9589 5.45215 16.1511 6.26615 16.1511 7.27003V8.79228" stroke="#64748b" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,19 +0,0 @@
<svg width="34" height="50" viewBox="0 0 34 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.1891 49.1803H12.5685L11.7691 47.2431H11.3665L10.567 49.1803H10.9349L11.126 48.694H11.995L12.1891 49.1803ZM11.5606 47.5983L11.8734 48.3907H11.2477L11.5606 47.5983ZM14.1523 49.1803V48.8688H13.2109V47.2431H12.8633V49.1803H14.1523ZM14.8121 49.1803V47.2431H14.4588V49.1803H14.8121ZM17.1461 49.1803V48.1995H16.141V48.4863H16.8188C16.8014 48.653 16.645 48.9208 16.2308 48.9208C15.8774 48.9208 15.553 48.683 15.553 48.2104C15.553 47.7295 15.9006 47.5055 16.2336 47.5055C16.561 47.5055 16.7638 47.6913 16.8332 47.9098L17.1606 47.7923C17.0505 47.4726 16.7319 47.2021 16.2336 47.2021C15.7007 47.2021 15.1997 47.5738 15.1997 48.2104C15.1997 48.8497 15.6746 49.2213 16.2105 49.2213C16.5378 49.2213 16.7492 49.0765 16.839 48.9344L16.8651 49.1803H17.1461ZM19.3023 49.1803V47.2431H18.9547V48.6175L18.022 47.2431H17.5788V49.1803H17.9264V47.7131L18.9402 49.1803H19.3023ZM21.0877 49.1803V48.8743H20.1666V48.3606H21.0008V48.0628H20.1666V47.5491H21.0877V47.2431H19.819V49.1803H21.0877ZM21.8469 48.8798V47.5437H22.2148C22.571 47.5437 22.8636 47.7623 22.8636 48.2158C22.8636 48.6639 22.5681 48.8798 22.2118 48.8798H21.8469ZM22.2234 49.1803C22.7796 49.1803 23.2256 48.8361 23.2256 48.2158C23.2256 47.5929 22.7854 47.2431 22.2263 47.2431H21.4993V49.1803H22.2234Z" fill="#64748b"/>
<path d="M11.1576 44.2621V40.9485H7.81151V42.1201H9.78584C9.70237 42.4698 9.24824 43.1343 8.16377 43.1343C7.09784 43.1343 6.18945 42.4611 6.18945 41.1671C6.18945 39.7856 7.23684 39.2261 8.12664 39.2261C9.22037 39.2261 9.65604 39.9256 9.75804 40.3103L11.1669 39.8469C10.8796 38.9813 10.0082 37.9321 8.12664 37.9321C6.27287 37.9321 4.70642 39.2173 4.70642 41.1671C4.70642 43.1256 6.20799 44.3933 8.03397 44.3933C8.97017 44.3933 9.59117 44.0261 9.87851 43.6326L9.97117 44.2621H11.1576ZM13.5866 42.9594V39.366H14.4394C15.4497 39.366 16.2746 39.9256 16.2746 41.1671C16.2746 42.4086 15.4497 42.9594 14.4394 42.9594H13.5866ZM14.495 44.2621C16.4786 44.2621 17.804 43.0818 17.804 41.1671C17.804 39.2523 16.4786 38.0633 14.5043 38.0633H12.1222V44.2621H14.495ZM20.1248 40.8436V39.2611H20.9775C21.5336 39.2611 21.9137 39.5583 21.9137 40.0567C21.9137 40.5376 21.5336 40.8436 20.9775 40.8436H20.1248ZM21.1258 42.0326C22.4606 42.0326 23.3782 41.2195 23.3782 40.0479C23.3782 38.8938 22.4606 38.0633 21.1258 38.0633H18.651V44.2621H20.1155V42.0326H21.1258ZM27.2181 44.2621H28.8309L27.4962 41.7529C28.3026 41.4818 28.7938 40.8436 28.7938 40.0043C28.7938 38.9026 27.9596 38.0633 26.662 38.0633H24.0574V44.2621H25.5219V41.9539H26.041L27.2181 44.2621ZM25.5219 40.7649V39.2611H26.3839C26.9864 39.2611 27.3108 39.5496 27.3108 40.0129C27.3108 40.4501 26.9864 40.7649 26.3839 40.7649H25.5219Z" fill="#64748b"/>
<path d="M16.8006 1.87061L17.3072 3.38312H18.9472L17.6206 4.31787L18.1272 5.83039L16.8006 4.89506L15.4739 5.83039L15.9808 4.31787L14.6536 3.38312H16.2936L16.8006 1.87061Z" fill="#64748b"/>
<path d="M16.8006 26.7607L17.3072 28.2736H18.9472L17.6206 29.2083L18.1272 30.7206L16.8006 29.7859L15.4739 30.7206L15.9808 29.2083L14.6536 28.2736H16.2936L16.8006 26.7607Z" fill="#64748b"/>
<path d="M23.5467 4.13281L24.053 5.6453H25.6933L24.3664 6.58044L24.8733 8.09257L23.5467 7.15744L22.2195 8.09257L22.7264 6.58044L21.3998 5.6453H23.0401L23.5467 4.13281Z" fill="#64748b"/>
<path d="M29.0669 8.42285L29.5731 9.93523H31.2135L29.8866 10.8702L30.3935 12.3827L29.0669 11.448L27.7399 12.3827L28.2468 10.8702L26.9199 9.93523H28.5597L29.0669 8.42285Z" fill="#64748b"/>
<path d="M29.0669 20.5381L29.5735 22.0512H31.2135L29.8866 22.9859L30.3937 24.498L29.0669 23.5635L27.7399 24.498L28.2468 22.9859L26.9199 22.0512H28.5599L29.0669 20.5381Z" fill="#64748b"/>
<path d="M4.53114 8.42285L5.03772 9.93523H6.67777L5.35101 10.8702L5.85758 12.3827L4.53114 11.448L3.20412 12.3827L3.71092 10.8702L2.38416 9.93523H4.02459L4.53114 8.42285Z" fill="#64748b"/>
<path d="M30.294 14.3154L30.8006 15.8279H32.4406L31.1138 16.7626L31.6206 18.2751L30.294 17.341L28.9671 18.2751L29.4743 16.7626L28.1471 15.8279H29.7868L30.294 14.3154Z" fill="#64748b"/>
<path d="M3.30594 14.3154L3.81249 15.8279H5.45246L4.12549 16.7626L4.63265 18.2751L3.30594 17.341L1.97889 18.2751L2.48578 16.7626L1.15881 15.8279H2.79905L3.30594 14.3154Z" fill="#64748b"/>
<path d="M4.53128 20.5381L5.0378 22.0512H6.67777L5.35118 22.9859L5.85764 24.498L4.53128 23.5635L3.20432 24.498L3.71112 22.9859L2.38416 22.0512H4.02448L4.53128 20.5381Z" fill="#64748b"/>
<path d="M24.7739 25.6294L25.2805 27.1417H26.9206L25.5934 28.077L26.1006 29.5893L24.7739 28.6546L23.4467 29.5893L23.9539 28.077L22.627 27.1417H24.2673L24.7739 25.6294Z" fill="#64748b"/>
<path d="M10.0512 4.13281L10.5576 5.6453H12.198L10.871 6.58044L11.3782 8.09257L10.0512 7.15744L8.7245 8.09257L9.2313 6.58044L7.9043 5.6453H9.5443L10.0512 4.13281Z" fill="#64748b"/>
<path d="M8.82574 25.6294L9.33248 27.1425H10.9727L9.64581 28.0767L10.1526 29.5893L8.82574 28.6551L7.49914 29.5893L8.00588 28.0767L6.67908 27.1425H8.31888L8.82574 25.6294Z" fill="#64748b"/>
<path d="M12.4551 15.3887C12.4551 15.0869 12.7145 14.8423 13.0344 14.8423H20.5654C20.8855 14.8423 21.1447 15.0869 21.1447 15.3887V20.2604C21.1447 20.5623 20.8855 20.8068 20.5654 20.8068H13.0344C12.7145 20.8068 12.4551 20.5623 12.4551 20.2604V15.3887Z" fill="#64748b"/>
<path d="M16.0118 19.1917L14.8686 18.2004C14.7399 18.0887 14.7399 17.9087 14.8686 17.7972C14.9962 17.6865 15.2039 17.6857 15.3325 17.7956C15.333 17.7961 15.3334 17.7964 15.334 17.7969L16.285 18.6193L18.5547 16.6513C18.6822 16.5406 18.8896 16.5401 19.0179 16.6496C19.0188 16.6504 19.0196 16.651 19.0205 16.6518C19.1494 16.7644 19.1491 16.9453 19.0196 17.0573L16.5581 19.1917C16.4084 19.3215 16.1648 19.3223 16.0142 19.1934C16.0133 19.1928 16.0127 19.1923 16.0118 19.1917Z" fill="#64748b"/>
<path d="M18.8449 14.9775V13.4766C18.8449 12.478 17.9867 11.6685 16.928 11.6685C15.8693 11.6685 15.0111 12.478 15.0111 13.4766V14.9775" stroke="#64748b" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -210,13 +210,20 @@ const nextConfig = {
destination: "/blog",
permanent: true,
},
{
source: '/docs/api/:slug*',
destination: '/docs/additional-features/api',
permanent: false,
}
];
},
async rewrites() {
return {
fallback: [
// These rewrites are checked after both pages/public files
// and dynamic routes are checked
{
source: "/:path*",
destination: `https://app.formbricks.com/s/:path*`,
},
],
};
},
};
export default withPlausibleProxy({ customDomain: "https://plausible.formbricks.com" })(

View File

@@ -0,0 +1,252 @@
import AuthorBox from "@/components/shared/AuthorBox";
import LayoutMdx from "@/components/shared/LayoutMdx";
import Image from "next/image";
import Formbricks from "./formbricks-best-open-source-hotjar-alternative.webp";
import FullStory from "./fullstory-comprehensive-analytics-tool.webp";
import Smartlook from "./g2-crowd-award-winner-Smartlook.webp";
import Header from "./header-best-hotjar-alternatives-2024-incl-open-source-solutions.webp";
import LuckyOrange from "./lucky-orange-best-analytics-tool-2024.webp";
import MouseFlow from "./mouseflow-best-hotjar-alternatives-2024.webp";
export const meta = {
title: "Best HotJar Alternatives 2024 incl. Open Source",
description:
"Looking for HotJar alternatives? We curated a list of the best HotJar alternatives going into 2024 for you.",
date: "2023-12-29",
publishedTime: "2023-12-29T12:00:00",
authors: ["Johannes Dancker"],
section: "Feedback Apps",
tags: ["Feedback Apps", "Formbricks", "Smartlook", "Lucky Orange", "Fullstory", "Mouseflow"],
ogImage: "/blog/hotjar.jpg"
};
<Image src={Header} alt="Get the best HotJar features with these 5 tools." className="w-full rounded-lg" />
<AuthorBox
name="Johannes"
title="Co-Founder and CEO"
date="December 29th, 2023"
duration="10"
author={"Johannes"}
/>
HotJar is a popular product experience insights platform that provides you with valuable data and insights into how users interact with your websites. It offers features such as heatmaps and recordings, surveys, and funnels to help you get these insights.
But while its a staple in user behavior analytics, this article introduces a wide range of alternatives just waiting to be discovered. These options are just for you, whether you're a budget-conscious blogger or an enterprise giant.
As we discuss these options, the next section will guide you through the essential criteria to consider when comparing them to HotJar. These criteria will empower you to pinpoint the perfect fit for your specific requirements.
## How we compare HotJar alternatives 👇
We'll categorize the criteria into three main factors to improve your choice.
1. **Feature depth**
- **Surveys & Forms**: Can you gather users voices through polls and surveys to understand your audience better?
- **Heatmaps & Recordings**: Do you want basic click maps or detailed session replays with visitor insights?
- **Integrations**: Does it work well with your existing third-party analytics tools?
2. **Pricing**:
- **Freemium Plans**
- **Premium Plans**
3. **Privacy**: Is it fully GDPR, CCPA, or HIPAA-compliant?
4. **Extensibility**: How extensible and customizable are each of the tools?
Now, we will explore these options based on the factors mentioned above.
## 5 Free HotJar Alternatives in 2024
Let's have a look at the best HotJar alternatives in 2024, including open source options - all of which start free!
### Formbricks - The Open Source HotJar Ask Alternative
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in-app micro surveys. Ask any user segment at any point in the user journey."
className="w-full rounded-lg"
/>
[Formbricks](https://formbricks.com/) is an open source micro-survey solution designed to gather specific user feedback at the perfect moment in the journey. It allows you to create and deploy **targeted surveys within your app or on public websites** without disrupting the user experience.
It's super good at one thing: making your forms and survey experiences awesome. It shows you why people abandon your forms, which questions cause churn, and how to fix them to get more people to finish. No heatmaps or fancy recordings, just laser focus on surveys.
Formbricks compares to HotJar based on the aforementioned factors:
**Feature depth**:
- **Surveys & Forms**: Formbricks specializes in in-product micro-surveys for SaaS and digital products. With Formbricks, you're better equipped to understand user behavior, improve your product, and make data-driven decisions. You can seamlessly integrate surveys into web, mobile, and desktop applications. If forms and surveys are your primary concern, this is the best tool for you.
- **Heatmaps & Recordings**: Formbricks focuses on forms and surveys. No fancy website heatmaps here for now, but you get detailed insights into individual form fields and how users interact with them. If youre looking for open source heatmaps, [OpenReplay](https://openreplay.com/) might be worth checking out.
- **Integrations:** HotJar plays well with lots of other tools, while Formbricks is still young in this aspect. But it works with the most popular ones, like Zapier, Make.com, Airtable, Notion, Slack, etc. The Formbricks team and [open source community](/community) are working on adding more all the time.
**Pricing**: Both tools have free plans, but HotJar's paid plans can get expensive. Formbricks is generally cheaper, especially if you only care about targeted surveys. Formbricks has a very generous free plan to get started easily. Paid plans begin at $30 per month for link surveys and $0.15 per submission for web and in-app surveys, **after your survey submission exceeds 250 submissions.** If you self-host Formbricks, [its completely free.](/pricing)
**Privacy:** Formbricks Cloud is hosted in Germany and has full GDPR as well as CCPA compliance. Since Formbricks is easily self-hostable, keeping full control over your data is smooth.
**Extensibility:** Unlike HotJar, Formbricks is an open source solution. It provides APIs that allow you to build anything on top, below, and around it as per your customization needs - your imagination is the limit.
Lets see how Formbricks compares side-by-side with HotJar.
| Factors | Formbricks | HotJar |
| ---------------------- | ---------- | ------ |
| Surveys & Forms | 🟢 | 🟢 |
| Heatmaps & Recordings | 🔴 | 🟢 |
| Integrations | 🟡🟢 | 🟢 |
| Pricing | 🟢 | 🟡🟢 |
| Privacy and compliance | 🟢 | 🟡 |
| Extensibility | 🟢 | 🟡 |
### Smartlook - G2 Crowd Award Winner
<Image
src={Smartlook}
alt="Smartlook is a comprehensive product analytics and visual user insights tool designed to help businesses gain deep insights into user behavior on their websites or mobile applications."
className="w-full rounded-lg"
/>
Smartlook, which was recently acquired by Cisco, is a comprehensive product analytics and visual user insights tool designed to help businesses gain deep insights into user behavior on their websites or mobile applications. It offers a range of features that enable organizations to understand, analyze, and optimize the user experience.
Smartlook has also received numerous awards and recognition, including the G2 Crowd Awards for **Top 100 Software Products** and **Best Products for Marketers**, as well as being named on Deloittes Technology Fast 50 in Central Europe.
Below is an overview of how Smartlook compares with HotJar.
**Features**:
- **Surveys**: Both platforms include survey features, but Smartlook does not offer standalone surveys, unlike HotJar. Instead, you can create surveys through its integration with Survicate (for a deeper look into Survicate, go [here](https://formbricks.com/blog/best-feedback-app-and-how-to-use-them)).
- **Heatmaps & Recordings**: They both offer heatmap and recording features. Although Smartlook provides a more comprehensive insight into recordings by combining them with funnel analysis, this will help you pinpoint the exact recordings you need.
- **Integrations:** Both platforms work well with many external tools. However, if you're a big enterprise seeking a broader selection of tools, HotJar is the better choice.
**Pricing**: Compared to HotJar, Smartlook is relatively more expensive. Its pro plan provides only 30 heatmaps a month and three months of storage. HotJars equivalent business plan offers unlimited heatmaps with 12 months of data storage.
**Privacy and Compliance:** Smartlook stores all data on EU servers. If you collect personal data with Smartlook, GDPR applies. Smartlook is also fully CCPA-compliant.
**Extensibility:** Smartlook, like HotJar, offers practical methods for programmatically accessing information on different resources. The API empowers you to analyze visitor data more comprehensively and delve deeper into the values captured by Smartlook. However, it may not offer the precise level of control and flexibility needed for intricate integrations or custom workflows.
| Factors | Smartlook | HotJar |
| ---------------------- | --------- | ------ |
| Surveys & Forms | 🟡🟢 | 🟢 |
| Heatmaps & Recordings | 🟢 | 🟢 |
| Integrations | 🟢 | 🟢 |
| Pricing | 🔴 | 🟢 |
| Privacy and compliance | 🟢 | 🟢 |
| Extensibility | 🟡 | 🟡 |
### Lucky Orange
<Image
src={LuckyOrange}
alt="Lucky Orange is a tool for web analytics and conversion optimization. It helps businesses understand how users behave on their websites."
className="w-full rounded-lg"
/>
Lucky Orange is a tool for web analytics and conversion optimization. It helps businesses understand how users behave on their websites. Its features include **surveys**, **session recordings**, **live view,** and **conversion funnels**. These features move beyond vanity metrics, aiming to uncover the reasons behind visitors' actions on your website.
**Feature depth**:
- **Surveys:** Like HotJar, Lucky Orange offers survey features, but in a more limited fashion. You can choose from four survey types that suit your needs. They include **multiple-choice**, **like-or-dislike**, **rating**, and **open-ended** surveys. You can also customize how your survey is triggered based on your users location on your website and their devices, or if you want a delay before your survey is triggered.
- **Heatmaps & Recordings:** Both Lucky Orange and HotJar offer session recordings; however, while this feature is on par with HotJars features like filtering, some users still report that the [session viewer crashes when watching a desktop session on the mobile screen](https://www.g2.com/products/lucky-orange/reviews/lucky-orange-review-7862805).
- **Integrations**: Lucky Orange offers a smaller but growing selection of integrations compared to HotJar, focusing on essentials like Google Analytics, CMS platforms, and marketing automation tools.
**Pricing**: Lucky Orange offers pricing plans suitable for businesses of different sizes, including a 7-day free trial; as of the time of writing this article, they begin at $32 per month. Each of these plans is based on sessions but only has 60-day data storage, unlike Hojar, which provides data storage for 365 days.
**Privacy & Compliance:** Lucky Orange tools, like HotJar, are fully CCPA and GDPR-compliant. This means that it does not store sensitive information, ensuring a secure and trustworthy environment for user data.
**Extensibility:** Lucky Orange currently works with fewer outside tools than HotJar. This might be a drawback for large companies. However, they're planning to add support for a public API in the future. This means you'll be able to build on top of it.
| Factors | Lucky Orange | HotJar |
| ---------------------- | ------------ | ------ |
| Surveys & Forms | 🟢 | 🟢 |
| Heatmaps & Recordings | 🟡🟢 | 🟢 |
| Integrations | 🟡 | 🟢 |
| Pricing | 🟡 | 🟢 |
| Privacy and compliance | 🟢 | 🟢 |
| Extensibility | 🟡 | 🟢 |
### FullStory
<Image
src={FullStory}
alt="FullStory is a comprehensive user experience analytics platform that enables businesses to gain detailed insights into user interactions with their websites and applications."
className="w-full rounded-lg"
/>
FullStory is a comprehensive user experience analytics platform that enables businesses to gain detailed insights into user interactions with their websites and applications. Through features such as session recordings, dynamic heatmaps, and advanced analytics, FullStory provides a nuanced understanding of user behavior.
Lets see how FullStory compares to HotJar in terms of the factors we mentioned earlier:
**Features:**
- **Surveys:** Both FullStory and HotJar offer survey features, with FullStory utilizing integration with third-party tools like Survicate and SurveyMonkey for versatility and in-depth feedback.
FullStory wins here if you are looking for a versatile tool to integrate with other existing third-party survey tools. However, if you want an all-in-one solution, HotJar is your go-to solution.
- **Heatmaps & Recordings:** FullStory's interactive heatmaps and detailed visuals help you better understand how users interact with your website, improving the analysis of page activities. This feature is similar to what HotJar offers.
However, a notable difference is that in FullStory, you can't save a session to watch later. So, if you find a recording interesting and want to see it again, you'll need to search for it manually when you want to revisit it.
- **Integrations:** Like HotJar, FullStory integrates seamlessly with various third-party tools, enhancing its versatility and allowing users to integrate it into their existing tech stack.
**Pricing:** FullStory does not have a free plan, and the price for paid plans is available upon request from the sales team. Although their pricing page states that you get a 14-day free trial for their business plan.
**Privacy & Compliance:** You are in full control of what data FullStory captures and saves. FullStory is not just GDPR and CCPA-compliant but also holds a SOC 2 Type II attestation and a SOC 3 report.
**Extensibility:** FullStory also provides several APIs, like HotJar, including a Webhooks API that enables developers to build on top of its functionality and integrate it into their workflows. However, they might not provide the level of control and flexibility required for more complex integrations or custom workflows.
Heres how FullStory and HotJar compare side by side:
| Factors | FullStory | HotJar |
| ---------------------- | --------- | ------ |
| Surveys & Forms | 🟢 | 🟢 |
| Heatmaps & Recordings | 🟢 | 🟢 |
| Integrations | 🟢 | 🟢 |
| Pricing | 🟡 | 🟢 |
| Privacy and compliance | 🟢 | 🟢 |
| Extensibility | 🟡 | 🟡 |
### Mouseflow
<Image
src={MouseFlow}
alt="Mouseflow is a web analytics tool designed to provide insights into user behavior on websites."
className="w-full rounded-lg"
/>
Mouseflow is a web analytics tool designed to provide insights into user behavior on websites. It offers features such as session recordings, heatmaps, surveys, and funnel analysis to help businesses optimize user experiences and conversions.
**Feature depth**
- **Surveys**: Mouseflow provides you with a funnel-like analysis for in-depth form analytics, which is not available on HotJar. With Mouseflow, you can replay sessions from visitors who dropped out or succeeded in completing the form. It also helps you analyze how users interact with each of your form fields.
- **Heatmaps & Recordings:** Mouseflow, like Hojar, provides heatmaps and session recordings to visualize user interactions and behaviors, aiding in the analysis of website engagement.
However, HotJar samples the data you collect daily. That is, you are allowed to review just a small fraction of the whole set of data you receive daily. For example, if you have 3000 recordings per month, you are allowed to record just 100 daily sessions on standard plans.
- **Integrations:** Mouseflow, like HotJar, integrates with about 58 third-party tools, including other analytics, eCommerce, CMS, and marketing platforms in your stack.
**Pricing**: Mouseflow's pricing is tiered based on usage and additional features. It offers a range of plans to accommodate businesses of different sizes.
Its free plan comes with 500 recordings per month, unlimited page views, and a month of storage, all for one website. Paid plans begin at $31/month, however, if you are an enterprise, you can contact their sales team to create a customized plan.
**Privacy & Compliance:** Mouseflow is committed to data protection. Its compliant with GDPR, CCPA, CPRA, and VCDPA.
**Extensibility:** Like HotJar, Mouseflow's API and Webhooks enable developers to build custom integrations, connecting them to virtually any platform or tool imaginable. However, they might not provide the level of control and flexibility required for more complex integrations or custom workflows.
| Factors | Mouseflow | HotJar |
| ---------------------- | --------- | ------ |
| Surveys & Forms | 🟢 | 🟡🟢 |
| Heatmaps & Recordings | 🟢 | 🟡🟢 |
| Integrations | 🟢 | 🟢 |
| Pricing | 🟢 | 🟢 |
| Privacy and compliance | 🟢 | 🟢 |
| Extensibility | 🟡 | 🟡 |
## So, which option is the better fit for you?
If you're seeking a comprehensive solution encompassing heatmaps, recordings, surveys, and a strong focus on privacy, MouseFlow emerges as a prime choice.
On the other hand, if your primary emphasis is on highly targeted surveys, [Formbricks](http://www.formbricks.com/) stands out as the optimal solution.
What's even more noteworthy is that it is the sole open-source solution for website surveys available. This translates to not just being completely free to use if you self-host, but also offering the freedom for modification due to its extensibility and the liberty to seamlessly integrate with your preferred tools.
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;

View File

@@ -1,345 +0,0 @@
import AuthorBox from "@/components/shared/AuthorBox";
import LayoutMdx from "@/components/shared/LayoutMdx";
import Image from "next/image";
import Formbricks from "./formbricks-best-open-source-hotjar-alternative.webp";
import FullStory from "./fullstory-comprehensive-analytics-tool.webp";
import Smartlook from "./g2-crowd-award-winner-Smartlook.webp";
import Header from "./header-best-hotjar-alternatives-2024-incl-open-source-solutions.webp";
import LuckyOrange from "./lucky-orange-best-analytics-tool-2024.webp";
import MouseFlow from "./mouseflow-best-hotjar-alternatives-2024.webp";
import SurveyMonkey from "./surveyMonkey-the-worlds-most-popular-free-online-survey-tool.webp";
export const meta = {
title: "Best Hotjar Alternatives 2024",
description:
"Discover 2024's best Hotjar alternatives with advanced website surveys and user behavior tools. Elevate your website insights and user experience today!",
date: "2023-04-22",
publishedTime: "2024-04-22T12:00:00",
authors: ["Johannes"],
section: "Feedback Apps",
tags: ["Feedback Apps", "Formbricks", "SurveyMonkey", "Smartlook", "Lucky Orange", "Fullstory", "Mouseflow"],
ogImage: "/blog/hotjar.jpg"
};
<Image src={Header} alt="Get the best HotJar features with these 5 tools." className="w-full rounded-lg" />
<AuthorBox name="Johannes" title="Co-Founder" date="December 29th, 2023" duration="4" author={"Johannes"} />
Hotjar is a popular product experience insights platform that provides valuable data and insights into how users interact with your websites. It offers features such as heatmaps and recordings, surveys, and funnels to help you get these insights.
But while its a staple in user behavior analytics, this article introduces 6 of the best Hotjar alternatives just waiting to be discovered. Whether you're a budget-conscious blogger or an enterprise giant, there is an option in this list perfect for your needs 🤓
To have a framework to compare the different options, the following section will guide you through the essential criteria to consider when comparing them to Hotjar. These criteria will empower you to pinpoint the perfect fit for your requirements.
## How We Compare & Group Hotjar Alternatives
There are many reasons why users seek alternatives to Hotjar. To make choosingt the best one for you easy, we'll categorize the criteria into three main factors:
1. **Feature depth**
- **Website Surveys & Forms**: Does the tool help you gather users voices through polls and surveys to understand your audience better?
- **Heatmaps & Recordings**: Do you want a tool for click maps and session replays with visitor insights?
- **Integrations**: Does the tool work well with your current tech stack?
2. **Privacy**: Is the tool fully GDPR, CCPA, or HIPAA compliant? What about data ownership?
3. **Extensibility**: Do they provide API services for extensibility and customization?
4. **Pricing: Freemium and Premium Plans**
Moving forward, well group these Hotjar alternatives into two different categories: first, for **website** **survey tools**, and second, for **behavioral analysis tools**.
### Two Hotjar Ask Alternatives For Website Surveys
1. Formbricks
2. SurveyMonkey
### Four Hotjar Alternatives For User Behavior Analysis
1. Smartlook
2. Lucky Orange
3. FullStory
4. MouseFlow
Enough setting up, let's dive right in! 🤿
## Hotjar Ask Alternatives For Targeted Website Surveys
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in-app micro surveys. Ask any user segment at any point in the user journey."
className="w-full rounded-lg"
/>
### Formbricks - The Open Source Hotjar Alternative
[Formbricks](http://formbricks.com/) is open-source survey software designed to gather specific feedback at the perfect moment in the user or customer journey. Formbricks allows you to create and deploy targeted surveys within your app, website, or links without disrupting your user experience.
Formbricks is super good at one thing: making your surveys and survey experiences awesome. We show you why people abandon your surveys, which questions cause churn, and how to fix them to get more people to finish. No heatmaps or fancy recordings, just laser focus on surveys.
### Formbricks vs Hotjar
**TL;DR**: Lets see how Formbricks compares side-by-side to Hotjar.
| Factors | Formbricks | Hotjar |
| --- | --- | --- |
| Surveys & Forms | 🟢 | 🟡🟢 |
| Heatmaps & Recordings | 🔴 | 🟢 |
| Integrations | 🟡🟢 | 🟡🟢 |
| Pricing | 🟢 | 🟡🟢 |
| Privacy and Compliance | 🟢 | 🟡 |
| Extensibility | 🟢 | 🟡 |
Now, well go into the details.
**Feature depth**
- **Surveys & Forms**: Formbricks offers a diverse range of survey templates, including options like [Product-Market fit](https://formbricks.com/docs/best-practices/pmf-survey), [Improving Newsletter Content](https://formbricks.com/docs/best-practices/improve-email-content), and NPS surveys. These surveys can be seamlessly to your website using the Formbricks SDK.
To learn more about setting up the Formbricks widget, visit our guide on [How to set up the Formbricks widget](https://formbricks.com/docs/getting-started/quickstart-in-app-survey).
Furthermore, Formbricks provides a comprehensive analytics dashboard that assists in optimizing survey conversion rates. This dashboard helps gather user data, analyze drop-offs, and track the number of users who viewed your surveys. The level of analytics provided by Formbricks is comparable to Hotjar's offerings.
- **Heatmaps & Recordings**: Formbricks focuses on surveys. No fancy website heatmaps here for now, but you get detailed insights into individual form fields and how users interact with them. If youre looking for open source heatmaps, OpenReplay might be worth checking out.
- **Integrations:** Formbricks integrates with popular tools like Zapier, Make.com, Airtable, Notion, and Slack, and these integrations are all available **on its Free Plan**. The Formbricks team and open-source community are continuously adding more integrations.
Hotjar, on the other hand, offers integrations with many tools for their surveys, but these are only available on their Business Plan. The Basic plan offers just one integration (HubSpot).
**Privacy:** Formbricks takes data privacy very seriously. Hence, we take on considerable additional effort to collect as little data as necessary and handle it safely and securely.
Formbricks is easily self-hostable and if you do so you have full control over the data you collect. This removes a huge chunk of privacy compliance and security reviews because the data never leaves your servers. Our privacy policy does not apply here, since no data is ever processed by Formbricks (the company).
We also offer Formbricks as a managed service in the Cloud. Formbricks Cloud is hosted by a German entity (GmbH) in Germany and comes with full GDPR and CCPA compliance. Learn more about how we handle private data on our Cloud in the [Privacy Policy](https://formbricks.com/privacy-policy). We also provide a [guide on how to create a GDPR compliant](https://formbricks.com/gdpr-guide) survey as well as [GDPR FAQs](https://formbricks.com/gdpr).
**Extensibility:** Formbricks stands out for its open-source nature and extensive API access, **available on all plans**. This unique feature allows users to customize and enhance their experience without limitations. In contrast, Hotjar's API access is restricted to its Scale plan, which comes at a higher price point of **$128/month** with as little as 500 sessions / day.
**Pricing**: Both tools have free plans, but Formbricks gives you more value on a free plan than Hotjar. Ultimately, Formbricks offers a lot more value for your money, especially if youre mostly interested in running surveys.
If you decide to [self-host](https://formbricks.com/docs/self-hosting/deployment) Formbricks on your servers, the community edition **is completely free including Branding Removal.** If you require the Enterprise Edition because you need Team Roles, Advanced Targeting or Multi-language Surveys, [please reach out.](mailto:hola@formbricks.com)
### SurveyMonkey
<Image
src={SurveyMonkey}
alt="SurveyMonkey is the world's most popular online survey tool. It is one the top alternatives to Hotjar."
className="w-full rounded-lg"
/>
SurveyMonkey prides itself on being the global leader in online surveys and forms, and rightfully so, as it has been around for over 20 years. It is an online survey software that helps you create and run professional online surveys.
According to their website, SurveyMonkey provides answers to more than 20 million questions every day, helping organizations of all sizes build products people love, create winning marketing strategies, delight their customers, and cultivate an engaged and happy workforce.
### SurveyMonkey vs Hotjar
**TL:DR**: How SurveyMonkey compares side-by-side to Hotjar.
| Factors | SurveyMonkey | Hotjar |
| --- | --- | --- |
| Surveys & Forms | 🟡🟢 | 🟡🟢 |
| Heatmaps & Recordings | 🔴 | 🟢 |
| Integrations | 🟡🟢 | 🟡🟢 |
| Privacy and Compliance | 🟢 | 🟢 |
| Extensibility | 🟡🟢 | 🟡🟢 |
| Pricing | 🟢 | 🟡 |
Now, lets dive into the details.
**Feature depth:**
- **Surveys & Forms:** SurveyMonkey provides over 150 survey templates cutting across different categories ranging from customer feedback, human resources, and events, among many others. However, most of its survey templates and features, like adding a custom logo, logic skips, and analysis features, are only available on paid plans.
Another feature that gives SurveyMonkey an edge over competitors is their recently rolled out Build with AI feature; if you dont know where or how to start building your survey, all you have to do is enter a prompt into the text area stating what you need a survey for.
- **Heatmaps & Recordings:** SurveyMonkey does not offer heatmaps or recording features, unlike Hotjar. However, if you need feedback on an image, they provide a click map feature, which is available on paid plans.
- **Integrations:** SurveyMonkey integrates with popular apps like Office 365, Google Drive, and Slack, but these integrations are only available on the **Team plans**.
**Privacy:** Like Hotjar, SurveyMonkey complies with GDPR and CCPA regulations, although its HIPAA-compliant features are only available on its enterprise plans.
**Extensibility:** SurveyMonkey provides an API to integrate survey data into your mobile and web applications. But its only **available on its Enterprise Plan**.
**Pricing:** SurveyMonkeys paid plans begin at $25/month whereas paid plans for Hotjar surveys begin at **$47.2/month**. However, dont forget that SurveyMonkey is a full-fledged survey software, so youll get more value for your money if you are concerned about only surveys.
## 4 Hotjar Alternatives For User Behavior Analysis
### Smartlook - G2 Crowd Award Winner
<Image
src={Smartlook}
alt="Smartlook is a comprehensive product analytics and visual user insights tool designed to help businesses gain deep insights into user behavior on their websites or mobile applications."
className="w-full rounded-lg"
/>
[Smartlook](https://smartlook.com/), which was recently acquired by [Cisco](https://cisco.com/), is a comprehensive product analytics and visual user insights tool designed to help businesses gain deep insights into user behavior on their websites or mobile applications. Like Hotjar, it offers a range of features, including heatmaps, surveys, and session recordings, that enable organizations to understand, analyze, and optimize the user experience.
It has received numerous awards and recognition, including the G2 Crowd Awards for **Top 100 Software Products** and **Best Products for Marketers**, as well as being named on Deloittes Technology Fast 50 in Central Europe.
### Smartlook vs Hotjar
**TL;DR**: How Smartlook compares side-by-side to Hotjar.
| Factors | Smartlook | Hotjar |
| --- | --- | --- |
| Surveys & Forms | 🟡🟢 | 🟢 |
| Heatmaps & Recordings | 🟢 | 🟢 |
| Integrations | 🟢 | 🟢 |
| Privacy and Compliance | 🟢 | 🟢 |
| Extensibility | 🟡🟢 | 🟡 |
| Pricing | 🔴 | 🟢 |
Next, well into the details.
**Feature depth**:
- **Surveys**: Smartlook also offers survey features in addition to user behavior analysis tools. However, it does not offer standalone surveys, unlike Hotjar. Instead, you can create surveys through its integration with Survicate.
For a deeper look into Survicate, check out our article on best feedback apps.
- **Heatmaps & Recordings**: Similar to Hotjar, Smartlook provides comprehensive heatmap and recording features. Your recordings are displayed on a dashboard with versatile filtering capabilities and customizable user information. You can also save recordings to a vault session in case the amount of time designated for them to remain in your account expires due to your purchase plan.
Smartlook provides three types of heatmap overlays for heatmaps: click and move, and scroll overlays to help you see how your users use your website.
- **Integrations:** Both platforms work well with many external tools. However, if you're a big enterprise seeking a broader selection of tools, Hotjar is the better choice, but it's worth noting that with Hotjar, only Hubspot and Microsoft Teams integrations are available on free plans.
**Privacy and Compliance**: Smartlook is privacy compliant just like Hotjar. Smartlook stores all data on EU servers. If you collect personal data with Smartlook, GDPR applies. Smartlook is also fully CCPA compliant.
**Extensibility:** Smartlook, like Hotjar, provides a REST API you can build on top of, and its available across all plans; however, there are rate limits across the plans. Its free plan accepts only twenty (20) requests per hour; its paid plan accepts one hundred (100) requests per hour, and its REST API add-on accepts one thousand (1,000) requests per hour.
On the contrary, Hotjars API integration for heatmaps and recordings is only available on its scale plan at **$170.4/month**.
**Pricing**: Compared to Hotjar, Smartlook is relatively more expensive. Its pro plan provides only 30 heatmaps a month and three months of storage. Hotjars equivalent business plan offers unlimited heatmaps with 12 months of data storage
### Lucky Orange
<Image
src={LuckyOrange}
alt="Lucky Orange is a tool for web analytics and conversion optimization. It helps businesses understand how users behave on their websites."
className="w-full rounded-lg"
/>
Lucky Orange is a tool for web analytics and conversion optimization. It helps businesses understand how users behave on their websites. Its features include surveys, session recordings, live viewing, and conversion funnels. These features move beyond vanity metrics, aiming to uncover the reasons behind visitors' actions on your website.
### LuckyOrange vs Hotjar
**TL:DR**: Lets see how they compare side-by-side.
| Factors | Lucky Orange | Hotjar |
| --- | --- | --- |
| Surveys & Forms | 🟢 | 🟢 |
| Heatmaps & Recordings | 🟡🟢 | 🟢 |
| Integrations | 🟡 | 🟢 |
| Privacy and Compliance | 🟢 | 🟢 |
| Extensibility | 🟡 | 🟢 |
| Pricing | 🟡 | 🟢 |
Next, well take a look at the details.
**Feature depth**:
- **Surveys:** Like Hotjar, Lucky Orange offers survey features. You can choose from four survey types that suit your needs. They include Multiple Choice, Like or Dislike, rating, and open-ended surveys.
You can also customize how your survey is triggered based on your users location on your website and their devices, or if you want a delay before your survey is triggered. There are also advanced settings with which you can customize your survey.
- **Heatmaps & Recordings:** Both Lucky Orange and Hotjar offer session recordings; however, while this feature is on par with Hotjars features like filtering, some users still report that the [session viewer crashes when watching a desktop session on the mobile screen](https://www.g2.com/products/lucky-orange/reviews/lucky-orange-review-7862805).
- **Integrations**: Lucky Orange offers a smaller but growing selection of integrations compared to Hotjar, focusing on essentials like Google Analytics, CMS platforms, and marketing automation tools.
**Privacy & Compliance**: Lucky Orange tools, like Hotjar, are fully CCPA and GDPR-compliant. This means that it does not store sensitive information, ensuring a secure and trustworthy environment for user data.
**Extensibility:** Lucky Orange currently works with fewer third-party tools than Hotjar. This might be a drawback for large companies. However, they're planning to add support for a public API in the future. This means you'll be able to build on top of it.
**Pricing**: Lucky Orange offers pricing plans suitable for businesses of different sizes, including a 7-day free trial; as of the time of writing this article, they begin at $32 per month. Each of these plans is based on sessions but only has 60-day data storage, unlike Hojar, which provides data storage for 365 days.
### FullStory
<Image
src={FullStory}
alt="FullStory is a comprehensive user experience analytics platform that enables businesses to gain detailed insights into user interactions with their websites and applications."
className="w-full rounded-lg"
/>
FullStory is a comprehensive user experience analytics platform that enables businesses to gain detailed insights into user interactions with their websites and applications. Through features such as session recordings, dynamic heatmaps, and advanced analytics, FullStory provides a nuanced understanding of user behavior.
### FullStory vs Hotjar
**TL;DR**: FullStory vs Hotjar side by side.
| Factors | FullStory | Hotjar |
| --- | --- | --- |
| Surveys & Forms | 🟢 | 🟢 |
| Heatmaps & Recordings | 🟢 | 🟢 |
| Integrations | 🟢 | 🟢 |
| Pricing | 🟡 | 🟢 |
| Privacy and Compliance | 🟢 | 🟢 |
| Extensibility | 🟡 | 🟡 |
**Features depth**:
- **Surveys:** Both FullStory and Hotjar offer survey features, with FullStory utilizing integration with third-party tools like Survicate and SurveyMonkey for versatility and in-depth feedback.
FullStory wins here if you are looking for a versatile tool to integrate with other existing third-party survey tools. However, if you want an all-in-one solution, Hotjar is your go-to solution.>
- **Heatmaps & Recordings:** FullStory's interactive heatmaps and detailed visuals help you better understand how users interact with your website, improving the analysis of page activities. This feature is similar to what Hotjar offers.
However, a notable difference is that in FullStory, you can't save a session to watch later. So, if you find a recording interesting and want to see it again, you'll need to search for it manually when you want to revisit it.
- **Integrations:** Like Hotjar, FullStory integrates seamlessly with various third-party tools, enhancing its versatility and allowing users to integrate it into their existing tech stack.
**Privacy & Compliance:** You are in full control of what data FullStory captures and saves. FullStory is not just GDPR and CCPA-compliant but also holds a SOC 2 Type II attestation and a SOC 3 report.
**Extensibility:** FullStory also provides several APIs, like Hotjar, including a Webhooks API that enables developers to build on top of its functionality and integrate it into their workflows. However, its worth noting that this is available on paid plans.
**Pricing:** FullStory does not have a free plan, and the price for paid plans is available upon request from the sales team. Although their pricing page states that you get a 14-day free trial for their business plan.
### Mouseflow
<Image
src={MouseFlow}
alt="Mouseflow is a web analytics tool designed to provide insights into user behavior on websites."
className="w-full rounded-lg"
/>
Mouseflow is a web analytics tool designed to provide insights into user behavior on websites. It offers features such as session recordings, heatmaps, surveys, and funnel analysis to help businesses optimize user experiences and conversions.
### MouseFlow vs Hotjar
**TL;DR**: MouseFlow side-by-side with Hotjar.
| Factors | Mouseflow | Hotjar |
| --- | --- | --- |
| Surveys & Forms | 🟢 | 🟡🟢 |
| Heatmaps & Recordings | 🟢 | 🟡🟢 |
| Integrations | 🟢 | 🟢 |
| Pricing | 🟢 | 🟢 |
| Privacy and Compliance | 🟢 | 🟢 |
| Extensibility | 🟡 | 🟡 |
Feature depth
- **Surveys**: Mouseflow provides you with a funnel-like analysis for in-depth form analytics, which is not available on Hotjar. With Mouseflow, you can replay sessions from visitors who dropped out or succeeded in completing the form. It also helps you analyze how users interact with each of your form fields.
- **Heatmaps & Recordings:** Mouseflow, like Hojar, provides heatmaps and session recordings to visualize user interactions and behaviors, aiding in the analysis of website engagement.
However, Hotjar samples the data you collect daily. That is, you are allowed to review just a small fraction of the whole set of data you receive daily. For example, if you have 3000 recordings per month, you are allowed to record just 100 daily sessions on standard plans.
- **Integrations:** Mouseflow, like Hotjar, integrates with about 58 third-party tools, including other analytics, eCommerce, CMS, and marketing platforms in your stack.
**Privacy & Compliance:** Mouseflow is committed to data protection. Its compliant with GDPR, CCPA, CPRA, and VCDPA.
**Extensibility**: Like Hotjar, Mouseflow's open REST API and Webhooks enable developers to build custom integrations, connecting them to virtually any platform or tool imaginable. However, the API is only available on its Growth plan, starting at $109/month.
**Pricing:** Mouseflow's pricing is tiered based on usage and additional features. It offers a range of plans to accommodate businesses of different sizes.
Its free plan comes with 500 recordings per month, unlimited page views, and a month of storage, all for one website. Paid plans begin at $31/month, however, if you are an enterprise, you can contact their sales team to create a customized plan.
## So, which option is the better fit for you?
If you're seeking a comprehensive solution encompassing heatmaps, recordings, surveys, and a strong focus on privacy, MouseFlow emerges as a prime choice.
On the other hand, if your primary emphasis is on forms and surveys, [Formbricks](http://www.formbricks.com/) stands out as the optimal Hotjar alternative. What's even more noteworthy is that it is the sole open-source solution for website surveys available. This translates to not just being completely free to use if you self-host, but also offering the freedom for modification due to its extensibility and the liberty to seamlessly integrate with your preferred tools.
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;

View File

@@ -4,7 +4,7 @@ import { formbricksEnabled } from "@/app/lib/formbricks";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import formbricks from "@formbricks/js/app";
import formbricks from "@formbricks/js";
import { env } from "@formbricks/lib/env";
type UsageAttributesUpdaterProps = {

View File

@@ -20,14 +20,9 @@ import {
interface ActivityTabProps {
actionClass: TActionClass;
environmentId: string;
isUserTargetingEnabled: boolean;
}
export default function EventActivityTab({
actionClass,
environmentId,
isUserTargetingEnabled,
}: ActivityTabProps) {
export default function EventActivityTab({ actionClass, environmentId }: ActivityTabProps) {
// const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClass.id);
const [numEventsLastHour, setNumEventsLastHour] = useState<number | undefined>();
@@ -51,9 +46,9 @@ export default function EventActivityTab({
numEventsLast7DaysData,
activeInactiveSurveys,
] = await Promise.all([
isUserTargetingEnabled ? getActionCountInLastHourAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast24HoursAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast7DaysAction(actionClass.id, environmentId) : 0,
getActionCountInLastHourAction(actionClass.id, environmentId),
getActionCountInLast24HoursAction(actionClass.id, environmentId),
getActionCountInLast7DaysAction(actionClass.id, environmentId),
getActiveInactiveSurveysAction(actionClass.id, environmentId),
]);
setNumEventsLastHour(numEventsLastHourData);
@@ -67,7 +62,7 @@ export default function EventActivityTab({
setLoading(false);
}
}
}, [actionClass.id, environmentId, isUserTargetingEnabled]);
}, [actionClass.id, environmentId]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorComponent />;
@@ -75,25 +70,23 @@ export default function EventActivityTab({
return (
<div className="grid grid-cols-3 pb-2">
<div className="col-span-2 space-y-4 pr-6">
{isUserTargetingEnabled && (
<div>
<Label className="text-slate-500">Ocurrances</Label>
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLastHour}</p>
<p className="text-xs text-slate-500">last hour</p>
</div>
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast24Hours}</p>
<p className="text-xs text-slate-500">last 24 hours</p>
</div>
<div className="px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast7Days}</p>
<p className="text-xs text-slate-500">last week</p>
</div>
<div>
<Label className="text-slate-500">Ocurrances</Label>
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLastHour}</p>
<p className="text-xs text-slate-500">last hour</p>
</div>
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast24Hours}</p>
<p className="text-xs text-slate-500">last 24 hours</p>
</div>
<div className="px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast7Days}</p>
<p className="text-xs text-slate-500">last week</p>
</div>
</div>
)}
</div>
<div>
<Label className="text-slate-500">Active surveys</Label>

View File

@@ -12,19 +12,15 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import ActionDetailModal from "./ActionDetailModal";
import AddNoCodeActionModal from "./AddActionModal";
interface ActionClassesTableProps {
environmentId: string;
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
isUserTargetingEnabled: boolean;
}
export default function ActionClassesTable({
environmentId,
actionClasses,
children: [TableHeading, actionRows],
isUserTargetingEnabled,
}: ActionClassesTableProps) {
}: {
environmentId: string;
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
}) {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
const { membershipRole, isLoading, error } = useMembershipRole(environmentId);
@@ -87,7 +83,6 @@ export default function ActionClassesTable({
setOpen={setActionDetailModalOpen}
actionClass={activeActionClass}
membershipRole={membershipRole}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
<AddNoCodeActionModal
environmentId={environmentId}

View File

@@ -13,7 +13,6 @@ interface ActionDetailModalProps {
setOpen: (v: boolean) => void;
actionClass: TActionClass;
membershipRole?: TMembershipRole;
isUserTargetingEnabled: boolean;
}
export default function ActionDetailModal({
@@ -22,18 +21,11 @@ export default function ActionDetailModal({
setOpen,
actionClass,
membershipRole,
isUserTargetingEnabled,
}: ActionDetailModalProps) {
const tabs = [
{
title: "Activity",
children: (
<EventActivityTab
actionClass={actionClass}
environmentId={environmentId}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
),
children: <EventActivityTab actionClass={actionClass} environmentId={environmentId} />,
},
{
title: "Settings",

View File

@@ -4,34 +4,16 @@ import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(action
import { Metadata } from "next";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export const metadata: Metadata = {
title: "Actions",
};
export default async function ActionClassesComponent({ params }) {
const [actionClasses, team] = await Promise.all([
getActionClasses(params.environmentId),
getTeamByEnvironmentId(params.environmentId),
]);
if (!team) {
throw new Error("Team not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
: true;
let actionClasses = await getActionClasses(params.environmentId);
return (
<>
<ActionClassesTable
environmentId={params.environmentId}
actionClasses={actionClasses}
isUserTargetingEnabled={isUserTargetingEnabled}>
<ActionClassesTable environmentId={params.environmentId} actionClasses={actionClasses}>
<ActionTableHeading />
{actionClasses.map((actionClass) => (
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} />

View File

@@ -1,9 +1,7 @@
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityTimeline";
import { getActionsByPersonId } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export default async function ActivitySection({
environmentId,
@@ -12,20 +10,9 @@ export default async function ActivitySection({
environmentId: string;
personId: string;
}) {
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
: true;
const [environment, actions] = await Promise.all([
getEnvironment(environmentId),
isUserTargetingEnabled ? getActionsByPersonId(personId, 1) : [],
getActionsByPersonId(personId, 1),
]);
if (!environment) {
@@ -34,11 +21,7 @@ export default async function ActivitySection({
return (
<div className="md:col-span-1">
<ActivityTimeline
environment={environment}
actions={actions.slice(0, 10)}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
<ActivityTimeline environment={environment} actions={actions.slice(0, 10)} />
</div>
);
}

View File

@@ -1,71 +1,58 @@
import { TAction } from "@formbricks/types/actions";
import { TEnvironment } from "@formbricks/types/environment";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
import { ActivityItemContent, ActivityItemIcon, ActivityItemPopover } from "./ActivityItemComponents";
interface IActivityTimelineProps {
environment: TEnvironment;
actions: TAction[];
isUserTargetingEnabled: boolean;
}
export default function ActivityTimeline({
environment,
actions,
isUserTargetingEnabled,
}: IActivityTimelineProps) {
}: {
environment: TEnvironment;
actions: TAction[];
}) {
return (
<>
<div className="flex items-center justify-between pb-6">
<h2 className="text-lg font-bold text-slate-700">Actions Timeline</h2>
</div>
{!isUserTargetingEnabled ? (
<UpgradePlanNotice
message="Upgrade to the User Targeting plan to store action history."
textForUrl="Upgrade now."
url={`/environments/${environment.id}/settings/billing`}
/>
) : (
<div className="relative">
{actions.length === 0 ? (
<EmptySpaceFiller type={"event"} environment={environment} />
) : (
<div>
{actions.map(
(actionItem, index) =>
actionItem && (
<li key={actionItem.id} className="list-none">
<div className="relative pb-12">
{index !== actions.length - 1 && (
<span
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
aria-hidden="true"
/>
)}
<div className="relative">
<ActivityItemPopover actionItem={actionItem}>
<div className="flex space-x-3 text-left">
<ActivityItemIcon actionItem={actionItem} />
<ActivityItemContent actionItem={actionItem} />
</div>
</ActivityItemPopover>
</div>
<div className="relative">
{actions.length === 0 ? (
<EmptySpaceFiller type={"event"} environment={environment} />
) : (
<div>
{actions.map(
(actionItem, index) =>
actionItem && (
<li key={actionItem.id} className="list-none">
<div className="relative pb-12">
{index !== actions.length - 1 && (
<span
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
aria-hidden="true"
/>
)}
<div className="relative">
<ActivityItemPopover actionItem={actionItem}>
<div className="flex space-x-3 text-left">
<ActivityItemIcon actionItem={actionItem} />
<ActivityItemContent actionItem={actionItem} />
</div>
</ActivityItemPopover>
</div>
</li>
)
</div>
</li>
)
)}
<div className="relative">
{actions.length === 10 && (
<div className="absolute bottom-0 flex h-56 w-full items-end justify-center bg-gradient-to-t from-slate-50 to-transparent"></div>
)}
<div className="relative">
{actions.length === 10 && (
<div className="absolute bottom-0 flex h-56 w-full items-end justify-center bg-gradient-to-t from-slate-50 to-transparent"></div>
)}
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
</>
);
}

View File

@@ -8,19 +8,15 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
export default async function PersonPage({ params }) {
const [environment, environmentTags, product] = await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);
const environment = await getEnvironment(params.environmentId);
const environmentTags = await getTagsByEnvironmentId(params.environmentId);
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error("Product not found");
}
if (!environment) {
throw new Error("Environment not found");
}
return (
<div>
<main className="mx-auto px-4 sm:px-6 lg:px-8">

View File

@@ -28,7 +28,7 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import formbricks from "@formbricks/js/app";
import formbricks from "@formbricks/js";
import { cn } from "@formbricks/lib/cn";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { capitalizeFirstLetter, truncate } from "@formbricks/lib/strings";
@@ -152,7 +152,7 @@ export default function Navigation({
},
{
name: "Settings",
href: `/environments/${environment.id}/settings/product`,
href: `/environments/${environment.id}/settings/profile`,
icon: SettingsIcon,
current: pathname?.includes("/settings"),
hidden: false,
@@ -362,31 +362,6 @@ export default function Navigation({
<DropdownMenuSeparator />
{/* Environment Switch */}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<div>
<p>{capitalizeFirstLetter(environment?.type)}</p>
<p className=" block text-xs text-slate-500">Environment</p>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={environment?.type}
onValueChange={(v) => handleEnvironmentChange(v as "production" | "development")}>
<DropdownMenuRadioItem value="production" className="cursor-pointer">
Production
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="development" className="cursor-pointer">
Development
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{/* Product Switch */}
<DropdownMenuSub>
@@ -465,6 +440,31 @@ export default function Navigation({
</DropdownMenuPortal>
</DropdownMenuSub>
{/* Environment Switch */}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<div>
<p>{capitalizeFirstLetter(environment?.type)}</p>
<p className=" block text-xs text-slate-500">Environment</p>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={environment?.type}
onValueChange={(v) => handleEnvironmentChange(v as "production" | "development")}>
<DropdownMenuRadioItem value="production" className="cursor-pointer">
Production
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="development" className="cursor-pointer">
Development
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{dropdownNavigation.map((item) => (
<DropdownMenuGroup key={item.title}>
<DropdownMenuSeparator />

View File

@@ -11,7 +11,7 @@ import toast from "react-hot-toast";
import { COLOR_DEFAULTS, PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import { Switch } from "@formbricks/ui/Switch";
@@ -24,13 +24,12 @@ type ThemeStylingProps = {
product: TProduct;
environmentId: string;
colors: string[];
isUnsplashConfigured: boolean;
};
export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigured }: ThemeStylingProps) => {
export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingProps) => {
const router = useRouter();
const [localProduct, setLocalProduct] = useState(product);
const [previewSurveyType, setPreviewSurveyType] = useState<TSurveyType>("link");
const [previewSurveyType, setPreviewSurveyType] = useState<"link" | "web">("link");
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
const [styling, setStyling] = useState(product.styling);
@@ -212,7 +211,6 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
colors={colors}
key={styling.background?.bg}
hideCheckmark
isUnsplashConfigured={isUnsplashConfigured}
/>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import { Variants, motion } from "framer-motion";
import { useRef, useState } from "react";
import type { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import { ClientLogo } from "@formbricks/ui/ClientLogo";
import { ResetProgressButton } from "@formbricks/ui/ResetProgressButton";
import { SurveyInline } from "@formbricks/ui/Survey";
@@ -15,8 +15,8 @@ interface ThemeStylingPreviewSurveyProps {
survey: TSurvey;
setQuestionId: (_: string) => void;
product: TProduct;
previewType: TSurveyType;
setPreviewType: (type: TSurveyType) => void;
previewType: "link" | "web";
setPreviewType: (type: "link" | "web") => void;
}
const previewParentContainerVariant: Variants = {
@@ -111,8 +111,6 @@ export const ThemeStylingPreviewSurvey = ({
const onFileUpload = async (file: File) => file.name;
const isAppSurvey = previewType === "app" || previewType === "website";
return (
<div className="flex h-full w-full flex-col items-center justify-items-center">
<motion.div
@@ -139,7 +137,7 @@ export const ThemeStylingPreviewSurvey = ({
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
</div>
<div className="ml-4 flex w-full justify-between font-mono text-sm text-slate-400">
<p>{isAppSurvey ? "Your web app" : "Preview"}</p>
<p>{previewType === "web" ? "Your web app" : "Preview"}</p>
<div className="flex items-center">
<ResetProgressButton onClick={resetQuestionProgress} />
@@ -147,7 +145,7 @@ export const ThemeStylingPreviewSurvey = ({
</div>
</div>
{isAppSurvey ? (
{previewType === "web" ? (
<Modal
isOpen
placement={placement}
@@ -206,9 +204,9 @@ export const ThemeStylingPreviewSurvey = ({
</div>
<div
className={`${isAppSurvey ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1 text-sm`}
onClick={() => setPreviewType("app")}>
App survey
className={`${previewType === "web" ? "rounded-full bg-slate-200" : ""} cursor-pointer px-3 py-1 text-sm`}
onClick={() => setPreviewType("web")}>
In-App survey
</div>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import {
getRemoveLinkBrandingPermission,
} from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { SURVEY_BG_COLORS } from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
@@ -53,12 +53,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
title="Theme"
className="max-w-7xl"
description="Create a style theme for all surveys. You can enable custom styling for each survey.">
<ThemeStyling
environmentId={params.environmentId}
product={product}
colors={SURVEY_BG_COLORS}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
/>
<ThemeStyling environmentId={params.environmentId} product={product} colors={SURVEY_BG_COLORS} />
</SettingsCard>{" "}
<SettingsCard title="Logo" description="Upload your company logo to brand surveys and link previews.">
<EditLogo product={product} environmentId={params.environmentId} isViewer={isViewer} />

View File

@@ -52,9 +52,6 @@ export function EditAvatar({ session, environmentId }: { session: Session; envir
toast.error("Avatar update failed. Please try again.");
} finally {
setIsLoading(false);
if (inputRef.current) {
inputRef.current.value = "";
}
}
};

View File

@@ -4,12 +4,11 @@ import Link from "next/link";
import { TEnvironment } from "@formbricks/types/environment";
import { Button } from "@formbricks/ui/Button";
interface TEmptyAppSurveysProps {
interface TEmptyInAppSurveysProps {
environment: TEnvironment;
surveyType?: "app" | "website";
}
export const EmptyAppSurveys = ({ environment, surveyType = "app" }: TEmptyAppSurveysProps) => {
export const EmptyInAppSurveys = ({ environment }: TEmptyInAppSurveysProps) => {
return (
<div className="flex w-full items-center justify-center gap-8 bg-slate-100 py-12">
<div className="flex h-20 w-20 items-center justify-center rounded-full border border-slate-200 bg-white">
@@ -19,9 +18,7 @@ export const EmptyAppSurveys = ({ environment, surveyType = "app" }: TEmptyAppSu
<div className="flex flex-col">
<h1 className="text-xl font-semibold text-slate-900">You&apos;re not plugged in yet!</h1>
<p className="mt-2 text-sm text-slate-600">
Connect your {surveyType} with Formbricks to run {surveyType} surveys.
</p>
<p className="mt-2 text-sm text-slate-600">Connect your app with Formbricks to run in-app surveys.</p>
<Link className="mt-2" href={`/environments/${environment.id}/settings/setup`}>
<Button variant="darkCTA" size="sm" className="flex w-[120px] justify-center">

View File

@@ -1,6 +1,6 @@
"use client";
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
import { EmptyInAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
import { useEffect, useRef, useState } from "react";
import { getMembershipByUserIdTeamIdAction } from "@formbricks/lib/membership/hooks/actions";
@@ -85,10 +85,8 @@ export default function ResponseTimeline({
return (
<div className="space-y-4">
{(survey.type === "app" || survey.type === "website") &&
responses.length === 0 &&
!environment.widgetSetupCompleted ? (
<EmptyAppSurveys environment={environment} surveyType={survey.type} />
{survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : isFetchingFirstPage ? (
<SkeletonLoader type="response" />
) : responseCount === 0 ? (

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { TSurveyQuestionSummaryMultipleChoice, TSurveyType } from "@formbricks/types/surveys";
import { TSurveyQuestionSummaryMultipleChoice } from "@formbricks/types/surveys";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import { Button } from "@formbricks/ui/Button";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
@@ -13,7 +13,7 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface MultipleChoiceSummaryProps {
questionSummary: TSurveyQuestionSummaryMultipleChoice;
environmentId: string;
surveyType: TSurveyType;
surveyType: string;
}
export const MultipleChoiceSummary = ({
@@ -69,7 +69,7 @@ export const MultipleChoiceSummary = ({
<div className="mt-4 rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-1 pl-6 ">Other values found</div>
<div className="col-span-1 pl-6 ">{surveyType === "app" && "User"}</div>
<div className="col-span-1 pl-6 ">{surveyType === "web" && "User"}</div>
</div>
{result.others
.filter((otherValue) => otherValue.value !== "")
@@ -83,7 +83,7 @@ export const MultipleChoiceSummary = ({
<span>{otherValue.value}</span>
</div>
)}
{surveyType === "app" && otherValue.person && (
{surveyType === "web" && otherValue.person && (
<Link
href={
otherValue.person.id

View File

@@ -23,18 +23,16 @@ export const SuccessMessage = ({ environment, survey, webAppUrl, user }: Summary
const [showLinkModal, setShowLinkModal] = useState(false);
const [confetti, setConfetti] = useState(false);
const isAppSurvey = survey.type === "app" || survey.type === "website";
useEffect(() => {
const newSurveyParam = searchParams?.get("success");
if (newSurveyParam && survey && environment) {
setConfetti(true);
toast.success(
isAppSurvey && !environment.widgetSetupCompleted
survey.type === "web" && !environment.widgetSetupCompleted
? "Almost there! Install widget to start receiving responses."
: "Congrats! Your survey is live.",
{
icon: isAppSurvey && !environment.widgetSetupCompleted ? "🤏" : "🎉",
icon: survey.type === "web" && !environment.widgetSetupCompleted ? "🤏" : "🎉",
duration: 5000,
position: "bottom-right",
}
@@ -47,7 +45,7 @@ export const SuccessMessage = ({ environment, survey, webAppUrl, user }: Summary
url.searchParams.delete("success");
window.history.replaceState({}, "", url.toString());
}
}, [environment, isAppSurvey, searchParams, survey]);
}, [environment, searchParams, survey]);
return (
<>

View File

@@ -1,4 +1,4 @@
import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
import { EmptyInAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
import { CTASummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary";
import { CalSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
import { ConsentSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";
@@ -40,10 +40,8 @@ export const SummaryList = ({
}: SummaryListProps) => {
return (
<div className="mt-10 space-y-8">
{(survey.type === "app" || survey.type === "website") &&
responseCount === 0 &&
!environment.widgetSetupCompleted ? (
<EmptyAppSurveys environment={environment} surveyType={survey.type} />
{survey.type === "web" && responseCount === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : fetchingSummary ? (
<SkeletonLoader type="summary" />
) : responseCount === 0 ? (

View File

@@ -95,6 +95,10 @@ const SummaryPage = ({
updatedResponseCount = await getResponseCountAction(surveyId, filters);
}
setResponseCount(updatedResponseCount);
if (updatedResponseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
}
let updatedSurveySummary;
if (isSharingPage) {

View File

@@ -3,7 +3,6 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
import { getProduct } from "@formbricks/lib/product/service";
@@ -200,61 +199,3 @@ export const resetBasicSegmentFiltersAction = async (surveyId: string) => {
return await resetSegmentInSurvey(surveyId);
};
export async function getImagesFromUnsplashAction(searchQuery: string, page: number = 1) {
const baseUrl = "https://api.unsplash.com/search/photos";
const params = new URLSearchParams({
query: searchQuery,
client_id: UNSPLASH_ACCESS_KEY,
orientation: "landscape",
per_page: "9",
page: page.toString(),
});
try {
const response = await fetch(`${baseUrl}?${params}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to fetch images from Unsplash");
}
const { results } = await response.json();
return results.map((result) => {
const authorName = encodeURIComponent(result.user.first_name + " " + result.user.last_name);
const authorLink = encodeURIComponent(result.user.links.html);
return {
id: result.id,
alt_description: result.alt_description,
urls: {
regularWithAttribution: `${result.urls.regular}&dpr=2&authorLink=${authorLink}&authorName=${authorName}&utm_source=formbricks&utm_medium=referral`,
download: result.links.download_location,
},
};
});
} catch (error) {
throw new Error("Error getting images from Unsplash");
}
}
export async function triggerDownloadUnsplashImageAction(downloadUrl: string) {
try {
const response = await fetch(`${downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to download image from Unsplash");
}
return;
} catch (error) {
throw new Error("Error downloading image from Unsplash");
}
}

View File

@@ -20,7 +20,6 @@ interface BackgroundStylingCardProps {
hideCheckmark?: boolean;
disabled?: boolean;
environmentId: string;
isUnsplashConfigured: boolean;
}
export default function BackgroundStylingCard({
@@ -32,7 +31,6 @@ export default function BackgroundStylingCard({
hideCheckmark,
disabled,
environmentId,
isUnsplashConfigured,
}: BackgroundStylingCardProps) {
const { bgType, brightness } = styling?.background ?? {};
@@ -115,7 +113,6 @@ export default function BackgroundStylingCard({
colors={colors}
bgType={bgType}
environmentId={environmentId}
isUnsplashConfigured={isUnsplashConfigured}
/>
</div>

View File

@@ -36,7 +36,6 @@ const CardStylingSettings = ({
localProduct,
setOpen,
}: CardStylingSettingsProps) => {
const isAppSurvey = surveyType === "app" || surveyType === "website";
const cardBgColor = styling?.cardBackgroundColor?.light || COLOR_DEFAULTS.cardBackgroundColor;
const isLogoHidden = styling?.isLogoHidden ?? false;
@@ -232,14 +231,14 @@ const CardStylingSettings = ({
</div>
)}
{(!surveyType || isAppSurvey) && (
{(!surveyType || surveyType === "web") && (
<div className="flex max-w-xs flex-col gap-4">
<div className="flex items-center gap-2">
<Switch checked={isHighlightBorderAllowed} onCheckedChange={setIsHighlightBorderAllowed} />
<div className="flex flex-col">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-slate-700">Add highlight border</h3>
<Badge text="In-App and Website Surveys" type="gray" size="normal" />
<Badge text="In-App Surveys" type="gray" size="normal" />
</div>
<p className="text-xs text-slate-500">Add an outer border to your survey card.</p>
</div>

View File

@@ -109,7 +109,6 @@ export default function EditThankYouCard({
<form>
<QuestionFormInput
id="headline"
label="Headline"
value={localSurvey?.thankYouCard?.headline}
localSurvey={localSurvey}
questionIdx={localSurvey.questions.length}

View File

@@ -1,13 +1,12 @@
"use client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { AlertCircleIcon, CheckIcon, EarthIcon, LinkIcon, MonitorIcon, SmartphoneIcon } from "lucide-react";
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon, SmartphoneIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys";
import { Badge } from "@formbricks/ui/Badge";
import { Label } from "@formbricks/ui/Label";
@@ -15,7 +14,7 @@ import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
interface HowToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void;
setLocalSurvey: (survey: TSurvey | ((TSurvey) => TSurvey)) => void;
environment: TEnvironment;
}
@@ -40,48 +39,12 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
enabled: type === "link" ? true : prevSurvey.thankYouCard.enabled,
},
}));
// if the type is "app" and the local survey does not already have a segment, we create a new temporary segment
if (type === "app" && !localSurvey.segment) {
const tempSegment: TSegment = {
id: "temp",
isPrivate: true,
title: localSurvey.id,
environmentId: environment.id,
surveys: [localSurvey.id],
filters: [],
createdAt: new Date(),
updatedAt: new Date(),
description: "",
};
setLocalSurvey((prevSurvey) => ({
...prevSurvey,
segment: tempSegment,
}));
}
// if the type is anything other than "app" and the local survey has a temporary segment, we remove it
if (type !== "app" && localSurvey.segment?.id === "temp") {
setLocalSurvey((prevSurvey) => ({
...prevSurvey,
segment: null,
}));
}
};
const options = [
{
id: "website",
name: "Website Survey",
icon: EarthIcon,
description: "Run targeted surveys on public websites.",
comingSoon: false,
alert: !widgetSetupCompleted,
},
{
id: "app",
name: "App Survey",
id: "web",
name: "In-App Survey",
icon: MonitorIcon,
description: "Embed a survey in your web app to collect responses.",
comingSoon: false,
@@ -134,7 +97,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
<hr className="py-1 text-slate-600" />
<div className="p-3">
<RadioGroup
defaultValue="app"
defaultValue="web"
value={localSurvey.type}
onValueChange={setSurveyType}
className="flex flex-col space-y-3">

View File

@@ -1,16 +1,12 @@
import { FileInput } from "@formbricks/ui/FileInput";
interface UploadImageSurveyBgProps {
interface ImageSurveyBgProps {
environmentId: string;
handleBgChange: (url: string, bgType: string) => void;
background: string;
}
export const UploadImageSurveyBg = ({
environmentId,
handleBgChange,
background,
}: UploadImageSurveyBgProps) => {
export const ImageSurveyBg = ({ environmentId, handleBgChange, background }: ImageSurveyBgProps) => {
return (
<div className="mt-2 w-full">
<div className="flex w-full items-center justify-center">
@@ -20,9 +16,9 @@ export const UploadImageSurveyBg = ({
environmentId={environmentId}
onFileUpload={(url: string[]) => {
if (url.length > 0) {
handleBgChange(url[0], "upload");
handleBgChange(url[0], "image");
} else {
handleBgChange("", "upload");
handleBgChange("", "image");
}
}}
fileUrl={background}

View File

@@ -39,13 +39,11 @@ export const SettingsView = ({
isUserTargetingAllowed = false,
isFormbricksCloud,
}: SettingsViewProps) => {
const isWebSurvey = localSurvey.type === "website" || localSurvey.type === "app";
return (
<div className="mt-12 space-y-3 p-5">
<HowToSendCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} environment={environment} />
{localSurvey.type === "app" ? (
{localSurvey.type === "web" ? (
!isUserTargetingAllowed ? (
<TargetingCard
key={localSurvey.segment?.id}
@@ -91,7 +89,7 @@ export const SettingsView = ({
environmentId={environment.id}
/>
{isWebSurvey && (
{localSurvey.type === "web" && (
<SurveyPlacementCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}

View File

@@ -23,7 +23,6 @@ type StylingViewProps = {
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | null>>;
localStylingChanges: TSurveyStyling | null;
setLocalStylingChanges: React.Dispatch<React.SetStateAction<TSurveyStyling | null>>;
isUnsplashConfigured: boolean;
};
export const StylingView = ({
@@ -36,7 +35,6 @@ export const StylingView = ({
styling,
localStylingChanges,
setLocalStylingChanges,
isUnsplashConfigured,
}: StylingViewProps) => {
const [overwriteThemeStyling, setOverwriteThemeStyling] = useState(
localSurvey?.styling?.overwriteThemeStyling ?? false
@@ -164,7 +162,6 @@ export const StylingView = ({
environmentId={environment.id}
colors={colors}
disabled={!overwriteThemeStyling}
isUnsplashConfigured={isUnsplashConfigured}
/>
)}

View File

@@ -6,8 +6,7 @@ import { TabBar } from "@formbricks/ui/TabBar";
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
import { ColorSurveyBg } from "./ColorSurveyBg";
import { UploadImageSurveyBg } from "./ImageSurveyBg";
import { ImageFromUnsplashSurveyBg } from "./UnsplashImages";
import { ImageSurveyBg } from "./ImageSurveyBg";
interface SurveyBgSelectorTabProps {
handleBgChange: (bg: string, bgType: string) => void;
@@ -15,13 +14,11 @@ interface SurveyBgSelectorTabProps {
bgType: string | null | undefined;
environmentId: string;
styling: TSurveyStyling | TProductStyling | null;
isUnsplashConfigured: boolean;
}
const tabs = [
{ id: "color", label: "Color" },
{ id: "animation", label: "Animation" },
{ id: "upload", label: "Upload" },
{ id: "image", label: "Image" },
];
@@ -31,59 +28,52 @@ export default function SurveyBgSelectorTab({
colors,
bgType,
environmentId,
isUnsplashConfigured,
}: SurveyBgSelectorTabProps) {
const [activeTab, setActiveTab] = useState(bgType || "color");
const bgUrl = styling?.background?.bg || "";
const { background } = styling ?? {};
const [colorBackground, setColorBackground] = useState(bgUrl);
const [animationBackground, setAnimationBackground] = useState(bgUrl);
const [uploadBackground, setUploadBackground] = useState(bgUrl);
const [colorBackground, setColorBackground] = useState(background?.bg);
const [animationBackground, setAnimationBackground] = useState(background?.bg);
const [imageBackground, setImageBackground] = useState(background?.bg);
useEffect(() => {
const bgType = background?.bgType;
if (bgType === "color") {
setColorBackground(bgUrl);
setColorBackground(background?.bg);
setAnimationBackground("");
setUploadBackground("");
setImageBackground("");
}
if (bgType === "animation") {
setAnimationBackground(bgUrl);
setAnimationBackground(background?.bg);
setColorBackground("");
setUploadBackground("");
setImageBackground("");
}
if (isUnsplashConfigured && bgType === "image") {
setColorBackground("");
setAnimationBackground("");
setUploadBackground("");
}
if (bgType === "upload") {
setUploadBackground(bgUrl);
if (bgType === "image") {
setImageBackground(background?.bg);
setColorBackground("");
setAnimationBackground("");
}
}, [bgUrl, bgType, isUnsplashConfigured]);
}, [background?.bg, background?.bgType]);
const renderContent = () => {
switch (activeTab) {
case "color":
return <ColorSurveyBg handleBgChange={handleBgChange} colors={colors} background={colorBackground} />;
case "animation":
return <AnimatedSurveyBg handleBgChange={handleBgChange} background={animationBackground} />;
case "upload":
return (
<UploadImageSurveyBg
<ColorSurveyBg handleBgChange={handleBgChange} colors={colors} background={colorBackground ?? ""} />
);
case "animation":
return <AnimatedSurveyBg handleBgChange={handleBgChange} background={animationBackground ?? ""} />;
case "image":
return (
<ImageSurveyBg
environmentId={environmentId}
handleBgChange={handleBgChange}
background={uploadBackground}
background={imageBackground ?? ""}
/>
);
case "image":
if (isUnsplashConfigured) {
return <ImageFromUnsplashSurveyBg handleBgChange={handleBgChange} />;
}
default:
return null;
}
@@ -92,7 +82,7 @@ export default function SurveyBgSelectorTab({
return (
<div className="mt-4 flex flex-col items-center justify-center rounded-lg ">
<TabBar
tabs={tabs.filter((tab) => tab.id !== "image" || isUnsplashConfigured)}
tabs={tabs}
activeId={activeTab}
setActiveId={setActiveTab}
tabStyle="button"

View File

@@ -10,6 +10,7 @@ import { SurveyMenuBar } from "@/app/(app)/environments/[environmentId]/surveys/
import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
import { useCallback, useEffect, useRef, useState } from "react";
import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/actions";
import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { useDocumentVisibility } from "@formbricks/lib/useDocumentVisibility";
@@ -34,7 +35,6 @@ interface SurveyEditorProps {
isUserTargetingAllowed?: boolean;
isMultiLanguageAllowed?: boolean;
isFormbricksCloud: boolean;
isUnsplashConfigured: boolean;
}
export default function SurveyEditor({
@@ -50,7 +50,6 @@ export default function SurveyEditor({
isMultiLanguageAllowed,
isUserTargetingAllowed = false,
isFormbricksCloud,
isUnsplashConfigured,
}: SurveyEditorProps): JSX.Element {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -63,6 +62,8 @@ export default function SurveyEditor({
const [styling, setStyling] = useState(localSurvey?.styling);
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
const createdSegmentRef = useRef(false);
const fetchLatestProduct = useCallback(async () => {
const latestProduct = await refetchProduct(localProduct.id);
if (latestProduct) {
@@ -113,6 +114,39 @@ export default function SurveyEditor({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localSurvey?.type, survey?.questions]);
const handleCreateSegment = async () => {
if (!localSurvey) return;
try {
const createdSegment = await createSegmentAction({
title: localSurvey.id,
description: "",
environmentId: environment.id,
surveyId: localSurvey.id,
filters: [],
isPrivate: true,
});
const localSurveyClone = structuredClone(localSurvey);
localSurveyClone.segment = createdSegment;
setLocalSurvey(localSurveyClone);
} catch (err) {
// set the ref to false to retry during the next render
createdSegmentRef.current = false;
}
};
useEffect(() => {
if (!localSurvey || localSurvey.type !== "web" || !!localSurvey.segment || createdSegmentRef.current) {
return;
}
createdSegmentRef.current = true;
handleCreateSegment();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localSurvey]);
useEffect(() => {
if (!localSurvey?.languages) return;
const enabledLanguageCodes = extractLanguageCodes(getEnabledLanguages(localSurvey.languages ?? []));
@@ -176,7 +210,6 @@ export default function SurveyEditor({
setStyling={setStyling}
localStylingChanges={localStylingChanges}
setLocalStylingChanges={setLocalStylingChanges}
isUnsplashConfigured={isUnsplashConfigured}
/>
)}
@@ -202,9 +235,7 @@ export default function SurveyEditor({
questionId={activeQuestionId}
product={localProduct}
environment={environment}
previewType={
localSurvey.type === "app" || localSurvey.type === "website" ? "modal" : "fullwidth"
}
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
languageCode={selectedLanguageCode}
onFileUpload={async (file) => file.name}
/>

View File

@@ -1,17 +1,29 @@
"use client";
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { isSurveyValid } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import {
isCardValid,
isSurveyLogicCyclic,
validateQuestion,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import { isEqual } from "lodash";
import { AlertTriangleIcon, ArrowLeftIcon, SettingsIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/actions";
import { checkForEmptyFallBackValue } from "@formbricks/lib/utils/recall";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyEditorTabs } from "@formbricks/types/surveys";
import { ZSegmentFilters } from "@formbricks/types/segment";
import {
TI18nString,
TSurvey,
TSurveyEditorTabs,
TSurveyQuestionType,
ZSurveyInlineTriggers,
surveyHasBothTriggers,
} from "@formbricks/types/surveys";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
@@ -53,7 +65,7 @@ export const SurveyMenuBar = ({
const [isSurveySaving, setIsSurveySaving] = useState(false);
const cautionText = "This survey received responses, make changes with caution.";
const faultyQuestions: string[] = [];
let faultyQuestions: string[] = [];
useEffect(() => {
if (audiencePrompt && activeId === "settings") {
@@ -77,7 +89,7 @@ export const SurveyMenuBar = ({
}, [localSurvey, survey]);
const containsEmptyTriggers = useMemo(() => {
if (localSurvey.type === "link") return false;
if (localSurvey.type !== "web") return false;
const noTriggers = !localSurvey.triggers || localSurvey.triggers.length === 0 || !localSurvey.triggers[0];
const noInlineTriggers =
@@ -116,46 +128,240 @@ export const SurveyMenuBar = ({
}
};
const handleSegmentWithIdTemp = async () => {
if (localSurvey.segment && localSurvey.type === "app" && localSurvey.segment?.id === "temp") {
const { filters } = localSurvey.segment;
// create a new private segment
const newSegment = await createSegmentAction({
environmentId: localSurvey.environmentId,
filters,
isPrivate: true,
surveyId: localSurvey.id,
title: localSurvey.id,
});
return newSegment;
const validateSurvey = (survey: TSurvey) => {
const existingQuestionIds = new Set();
faultyQuestions = [];
if (survey.questions.length === 0) {
toast.error("Please add at least one question");
return;
}
if (survey.welcomeCard.enabled) {
if (!isCardValid(survey.welcomeCard, "start", survey.languages)) {
faultyQuestions.push("start");
}
}
if (survey.thankYouCard.enabled) {
if (!isCardValid(survey.thankYouCard, "end", survey.languages)) {
faultyQuestions.push("end");
}
}
let pin = survey?.pin;
if (pin !== null && pin!.toString().length !== 4) {
toast.error("PIN must be a four digit number.");
return;
}
for (let index = 0; index < survey.questions.length; index++) {
const question = survey.questions[index];
const isFirstQuestion = index === 0;
const isValid = validateQuestion(question, survey.languages, isFirstQuestion);
if (!isValid) {
faultyQuestions.push(question.id);
}
}
// if there are any faulty questions, the user won't be allowed to save the survey
if (faultyQuestions.length > 0) {
setInvalidQuestions(faultyQuestions);
setSelectedLanguageCode("default");
toast.error("Please fill all required fields.");
return false;
}
for (const question of survey.questions) {
const existingLogicConditions = new Set();
if (existingQuestionIds.has(question.id)) {
toast.error("There are 2 identical question IDs. Please update one.");
return false;
}
existingQuestionIds.add(question.id);
if (
question.type === TSurveyQuestionType.MultipleChoiceSingle ||
question.type === TSurveyQuestionType.MultipleChoiceMulti
) {
const haveSameChoices =
question.choices.some((element) => element.label[selectedLanguageCode]?.trim() === "") ||
question.choices.some((element, index) =>
question.choices
.slice(index + 1)
.some(
(nextElement) =>
nextElement.label[selectedLanguageCode]?.trim() ===
element.label[selectedLanguageCode].trim()
)
);
if (haveSameChoices) {
toast.error("You have empty or duplicate choices.");
return false;
}
}
if (question.type === TSurveyQuestionType.Matrix) {
const hasDuplicates = (labels: TI18nString[]) => {
const flattenedLabels = labels
.map((label) => Object.keys(label).map((lang) => `${lang}:${label[lang].trim().toLowerCase()}`))
.flat();
return new Set(flattenedLabels).size !== flattenedLabels.length;
};
// Function to check for empty labels in each language
const hasEmptyLabels = (labels: TI18nString[]) => {
return labels.some((label) => Object.values(label).some((value) => value.trim() === ""));
};
if (hasEmptyLabels(question.rows) || hasEmptyLabels(question.columns)) {
toast.error("Empty row or column labels in one or more languages");
setInvalidQuestions([question.id]);
return false;
}
if (hasDuplicates(question.rows)) {
toast.error("You have duplicate row labels.");
return false;
}
if (hasDuplicates(question.columns)) {
toast.error("You have duplicate column labels.");
return false;
}
}
for (const logic of question.logic || []) {
const validFields = ["condition", "destination", "value"].filter(
(field) => logic[field] !== undefined
).length;
if (validFields < 2) {
setInvalidQuestions([question.id]);
toast.error("Incomplete logic jumps detected: Fill or remove them in the Questions tab.");
return false;
}
if (question.required && logic.condition === "skipped") {
toast.error("A logic condition is missing: Please update or delete it in the Questions tab.");
return false;
}
const thisLogic = `${logic.condition}-${logic.value}`;
if (existingLogicConditions.has(thisLogic)) {
setInvalidQuestions([question.id]);
toast.error(
"There are two competing logic conditons: Please update or delete one in the Questions tab."
);
return false;
}
existingLogicConditions.add(thisLogic);
}
}
if (
survey.redirectUrl &&
!survey.redirectUrl.includes("https://") &&
!survey.redirectUrl.includes("http://")
) {
toast.error("Please enter a valid URL for redirecting respondents.");
return false;
}
return true;
};
const handleSurveySave = async (shouldNavigateBack = false) => {
const saveSurveyAction = async (shouldNavigateBack = false) => {
if (localSurvey.questions.length === 0) {
toast.error("Please add at least one question.");
return;
}
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
if (questionWithEmptyFallback) {
toast.error("Fallback missing");
return;
}
if (isSurveyLogicCyclic(localSurvey.questions)) {
toast.error("Cyclic logic detected. Please fix it before saving.");
return;
}
setIsSurveySaving(true);
try {
if (
!isSurveyValid(
localSurvey,
faultyQuestions,
setInvalidQuestions,
selectedLanguageCode,
setSelectedLanguageCode
)
) {
setIsSurveySaving(false);
return;
}
localSurvey.triggers = localSurvey.triggers.filter((trigger) => Boolean(trigger));
localSurvey.questions = localSurvey.questions.map((question) => {
// Create a copy of localSurvey with isDraft removed from every question
const strippedSurvey: TSurvey = {
...localSurvey,
questions: localSurvey.questions.map((question) => {
const { isDraft, ...rest } = question;
return rest;
});
const segment = (await handleSegmentWithIdTemp()) ?? null;
await updateSurveyAction({ ...localSurvey, segment });
}),
};
if (!validateSurvey(localSurvey)) {
setIsSurveySaving(false);
setLocalSurvey(localSurvey);
return;
}
// validate the user segment filters
const localSurveySegment = {
id: strippedSurvey.segment?.id,
filters: strippedSurvey.segment?.filters,
title: strippedSurvey.segment?.title,
description: strippedSurvey.segment?.description,
};
const surveySegment = {
id: survey.segment?.id,
filters: survey.segment?.filters,
title: survey.segment?.title,
description: survey.segment?.description,
};
// if the non-private segment in the survey and the strippedSurvey are different, don't save
if (!strippedSurvey.segment?.isPrivate && !isEqual(localSurveySegment, surveySegment)) {
toast.error("Please save the audience filters before saving the survey");
setIsSurveySaving(false);
return;
}
if (!!strippedSurvey.segment?.filters?.length) {
const parsedFilters = ZSegmentFilters.safeParse(strippedSurvey.segment.filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message ||
"Invalid targeting: Please check your audience filters";
setIsSurveySaving(false);
toast.error(errMsg);
return;
}
}
// if inlineTriggers are present validate with zod
if (!!strippedSurvey.inlineTriggers) {
const parsedInlineTriggers = ZSurveyInlineTriggers.safeParse(strippedSurvey.inlineTriggers);
if (!parsedInlineTriggers.success) {
toast.error("Invalid Custom Actions: Please check your custom actions");
return;
}
}
// validate that both triggers and inlineTriggers are not present
if (surveyHasBothTriggers(strippedSurvey)) {
setIsSurveySaving(false);
toast.error("Survey cannot have both custom and saved actions, please remove one.");
return;
}
strippedSurvey.triggers = strippedSurvey.triggers.filter((trigger) => Boolean(trigger));
try {
await updateSurveyAction({ ...strippedSurvey });
setIsSurveySaving(false);
setLocalSurvey(strippedSurvey);
toast.success("Changes saved.");
if (shouldNavigateBack) {
router.back();
@@ -169,28 +375,21 @@ export const SurveyMenuBar = ({
};
const handleSurveyPublish = async () => {
setIsSurveyPublishing(true);
try {
if (
!isSurveyValid(
localSurvey,
faultyQuestions,
setInvalidQuestions,
selectedLanguageCode,
setSelectedLanguageCode
)
) {
setIsSurveyPublishing(true);
if (isSurveyLogicCyclic(localSurvey.questions)) {
toast.error("Cyclic logic detected. Please fix it before saving.");
setIsSurveyPublishing(false);
return;
}
if (!validateSurvey(localSurvey)) {
setIsSurveyPublishing(false);
return;
}
const status = localSurvey.runOnDate ? "scheduled" : "inProgress";
const segment = (await handleSegmentWithIdTemp()) ?? null;
await updateSurveyAction({
...localSurvey,
status,
segment,
});
setIsSurveyPublishing(false);
await updateSurveyAction({ ...localSurvey, status });
router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`);
} catch (error) {
toast.error("An error occured while publishing the survey.");
@@ -257,7 +456,7 @@ export const SurveyMenuBar = ({
variant={localSurvey.status === "draft" ? "secondary" : "darkCTA"}
className="mr-3"
loading={isSurveySaving}
onClick={() => handleSurveySave()}>
onClick={() => saveSurveyAction()}>
Save
</Button>
{localSurvey.status === "draft" && audiencePrompt && (
@@ -293,7 +492,7 @@ export const SurveyMenuBar = ({
setConfirmDialogOpen(false);
router.back();
}}
onConfirm={() => handleSurveySave(true)}
onConfirm={() => saveSurveyAction(true)}
/>
</div>
</>

View File

@@ -1,234 +0,0 @@
"use client";
import { debounce } from "lodash";
import { SearchIcon } from "lucide-react";
import UnsplashImage from "next/image";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { TSurveyBackgroundBgType } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
import { getImagesFromUnsplashAction, triggerDownloadUnsplashImageAction } from "../actions";
interface ImageFromUnsplashSurveyBgProps {
handleBgChange: (url: string, bgType: TSurveyBackgroundBgType) => void;
}
interface UnsplashImage {
id: string;
alt_description: string;
urls: {
regularWithAttribution: string;
download?: string;
};
authorName?: string;
}
const defaultImages = [
{
id: "dog-1",
alt_description: "Dog",
urls: {
regularWithAttribution: "/image-backgrounds/dogs.webp",
},
},
{
id: "pencil",
alt_description: "Pencil",
urls: {
regularWithAttribution: "/image-backgrounds/pencil.webp",
},
},
{
id: "plant",
alt_description: "Plant",
urls: {
regularWithAttribution: "/image-backgrounds/plant.webp",
},
},
{
id: "dog-2",
alt_description: "Another Dog",
urls: {
regularWithAttribution: "/image-backgrounds/dog-2.webp",
},
},
{
id: "kitten-2",
alt_description: "Another Kitten",
urls: {
regularWithAttribution: "/image-backgrounds/kitten-2.webp",
},
},
{
id: "lollipop",
alt_description: "Lollipop",
urls: {
regularWithAttribution: "/image-backgrounds/lolipop.webp",
},
},
{
id: "oranges",
alt_description: "Oranges",
urls: {
regularWithAttribution: "/image-backgrounds/oranges.webp",
},
},
{
id: "flower",
alt_description: "Flower",
urls: {
regularWithAttribution: "/image-backgrounds/flowers.webp",
},
},
{
id: "supermario",
alt_description: "Super Mario",
urls: {
regularWithAttribution: "/image-backgrounds/supermario.webp",
},
},
{
id: "shapes",
alt_description: "Shapes",
urls: {
regularWithAttribution: "/image-backgrounds/shapes.webp",
},
},
{
id: "waves",
alt_description: "Waves",
urls: {
regularWithAttribution: "/image-backgrounds/waves.webp",
},
},
{
id: "kitten-1",
alt_description: "Kitten",
urls: {
regularWithAttribution: "/image-backgrounds/kittens.webp",
},
},
];
export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashSurveyBgProps) => {
const inputFocus = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [query, setQuery] = useState("");
const [images, setImages] = useState<UnsplashImage[]>(defaultImages);
const [page, setPage] = useState(1);
useEffect(() => {
const fetchData = async (searchQuery: string, currentPage: number) => {
try {
setIsLoading(true);
const imagesFromUnsplash = await getImagesFromUnsplashAction(searchQuery, currentPage);
for (let i = 0; i < imagesFromUnsplash.length; i++) {
const authorName = new URL(imagesFromUnsplash[i].urls.regularWithAttribution).searchParams.get(
"authorName"
);
imagesFromUnsplash[i].authorName = authorName;
}
setImages((prevImages) => [...prevImages, ...imagesFromUnsplash]);
} catch (error) {
toast.error(error.message);
} finally {
setIsLoading(false);
}
};
const debouncedFetchData = debounce((q) => fetchData(q, page), 500);
if (query.trim() !== "") {
debouncedFetchData(query);
}
return () => {
debouncedFetchData.cancel();
};
}, [query, page, setImages]);
useEffect(() => {
inputFocus.current?.focus();
}, []);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
setPage(1);
setImages([]);
};
const handleImageSelected = async (imageUrl: string, downloadImageUrl?: string) => {
try {
handleBgChange(imageUrl, "image");
if (downloadImageUrl) {
await triggerDownloadUnsplashImageAction(downloadImageUrl);
}
} catch (error) {
toast.error(error.message);
}
};
const handleLoadMore = () => {
setPage((prevPage) => prevPage + 1);
};
return (
<div className="relative mt-2 w-full">
<div className="relative">
<SearchIcon className="absolute left-2 top-1/2 h-6 w-4 -translate-y-1/2 text-slate-500" />
<Input
value={query}
onChange={handleChange}
placeholder="Try 'lollipop' or 'mountain'..."
className="pl-8"
ref={inputFocus}
aria-label="Search for images"
/>
</div>
<div className="relative mt-4 grid grid-cols-3 gap-1">
{images.length > 0 &&
images.map((image) => (
<div key={image.id} className="group relative">
<UnsplashImage
width={300}
height={200}
src={image.urls.regularWithAttribution}
alt={image.alt_description}
onClick={() => handleImageSelected(image.urls.regularWithAttribution, image.urls.download)}
className="h-full cursor-pointer rounded-lg object-cover"
/>
{image.authorName && (
<span className="absolute bottom-1 right-1 hidden rounded bg-black bg-opacity-75 px-2 py-1 text-xs text-white group-hover:block">
{image.authorName}
</span>
)}
</div>
))}
{isLoading && (
<div className="col-span-3 flex items-center justify-center p-3">
<LoadingSpinner />
</div>
)}
{images.length > 0 && !isLoading && query.trim() !== "" && (
<Button
size="sm"
variant="secondary"
className="col-span-3 mt-3 flex items-center justify-center"
type="button"
onClick={handleLoadMore}>
Load More
</Button>
)}
{!isLoading && images.length === 0 && query.trim() !== "" && (
<div className="col-span-3 flex items-center justify-center text-sm text-slate-500">
No images found for &apos;{query}&apos;
</div>
)}
</div>
</div>
);
};

View File

@@ -5,7 +5,6 @@ import { useState } from "react";
import toast from "react-hot-toast";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
@@ -46,15 +45,10 @@ export default function UpdateQuestionId({
}
};
const isButtonDisabled = () => {
if (currentValue === question.id || currentValue.trim() === "") return true;
else return false;
};
return (
<div>
<Label htmlFor="questionId">Question ID</Label>
<div className="mt-2 inline-flex w-full space-x-2">
<div className="mt-2 inline-flex w-full">
<Input
id="questionId"
name="questionId"
@@ -62,12 +56,10 @@ export default function UpdateQuestionId({
onChange={(e) => {
setCurrentValue(e.target.value);
}}
disabled={localSurvey.status !== "draft" && !question.isDraft}
className={`h-10 ${isInputInvalid ? "border-red-300 focus:border-red-300" : ""}`}
onBlur={saveAction}
disabled={!(localSurvey.status === "draft" || question.isDraft)}
className={isInputInvalid ? "border-red-300 focus:border-red-300" : ""}
/>
<Button variant="darkCTA" size="sm" onClick={saveAction} disabled={isButtonDisabled()}>
Save
</Button>
</div>
</div>
);

View File

@@ -38,9 +38,7 @@ export default function WhenToSendCard({
propActionClasses,
membershipRole,
}: WhenToSendCardProps) {
const [open, setOpen] = useState(
localSurvey.type === "app" || localSurvey.type === "website" ? true : false
);
const [open, setOpen] = useState(localSurvey.type === "web" ? true : false);
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const [actionClasses, setActionClasses] = useState<TActionClass[]>(propActionClasses);

View File

@@ -1,13 +1,9 @@
// extend this object in order to add more validation rules
import { isEqual } from "lodash";
import { toast } from "react-hot-toast";
import { extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { checkForEmptyFallBackValue } from "@formbricks/lib/utils/recall";
import { ZSegmentFilters } from "@formbricks/types/segment";
import {
TI18nString,
TSurvey,
TSurveyCTAQuestion,
TSurveyConsentQuestion,
TSurveyLanguage,
@@ -17,12 +13,9 @@ import {
TSurveyOpenTextQuestion,
TSurveyPictureSelectionQuestion,
TSurveyQuestion,
TSurveyQuestionType,
TSurveyQuestions,
TSurveyThankYouCard,
TSurveyWelcomeCard,
ZSurveyInlineTriggers,
surveyHasBothTriggers,
} from "@formbricks/types/surveys";
// Utility function to check if label is valid for all required languages
@@ -273,231 +266,3 @@ export const isSurveyLogicCyclic = (questions: TSurveyQuestions) => {
return false;
};
export const isSurveyValid = (
survey: TSurvey,
faultyQuestions: string[],
setInvalidQuestions: (questions: string[]) => void,
selectedLanguageCode: string,
setSelectedLanguageCode: (languageCode: string) => void
) => {
const existingQuestionIds = new Set();
// Ensuring at least one question is added to the survey.
if (survey.questions.length === 0) {
toast.error("Please add at least one question");
return false;
}
// Checking the validity of the welcome and thank-you cards if they are enabled.
if (survey.welcomeCard.enabled) {
if (!isCardValid(survey.welcomeCard, "start", survey.languages)) {
faultyQuestions.push("start");
}
}
if (survey.thankYouCard.enabled) {
if (!isCardValid(survey.thankYouCard, "end", survey.languages)) {
faultyQuestions.push("end");
}
}
// Verifying that any provided PIN is exactly four digits long.
const pin = survey.pin;
if (pin && pin.toString().length !== 4) {
toast.error("PIN must be a four digit number.");
return false;
}
// Assessing each question for completeness and correctness,
for (let index = 0; index < survey.questions.length; index++) {
const question = survey.questions[index];
const isFirstQuestion = index === 0;
const isValid = validateQuestion(question, survey.languages, isFirstQuestion);
if (!isValid) {
faultyQuestions.push(question.id);
}
}
// if there are any faulty questions, the user won't be allowed to save the survey
if (faultyQuestions.length > 0) {
setInvalidQuestions(faultyQuestions);
setSelectedLanguageCode("default");
toast.error("Please fill all required fields.");
return false;
}
for (const question of survey.questions) {
const existingLogicConditions = new Set();
if (existingQuestionIds.has(question.id)) {
toast.error("There are 2 identical question IDs. Please update one.");
return false;
}
existingQuestionIds.add(question.id);
if (
question.type === TSurveyQuestionType.MultipleChoiceSingle ||
question.type === TSurveyQuestionType.MultipleChoiceMulti
) {
const haveSameChoices =
question.choices.some((element) => element.label[selectedLanguageCode]?.trim() === "") ||
question.choices.some((element, index) =>
question.choices
.slice(index + 1)
.some(
(nextElement) =>
nextElement.label[selectedLanguageCode]?.trim() === element.label[selectedLanguageCode].trim()
)
);
if (haveSameChoices) {
toast.error("You have empty or duplicate choices.");
return false;
}
}
if (question.type === TSurveyQuestionType.Matrix) {
const hasDuplicates = (labels: TI18nString[]) => {
const flattenedLabels = labels
.map((label) => Object.keys(label).map((lang) => `${lang}:${label[lang].trim().toLowerCase()}`))
.flat();
return new Set(flattenedLabels).size !== flattenedLabels.length;
};
// Function to check for empty labels in each language
const hasEmptyLabels = (labels: TI18nString[]) => {
return labels.some((label) => Object.values(label).some((value) => value.trim() === ""));
};
if (hasEmptyLabels(question.rows) || hasEmptyLabels(question.columns)) {
toast.error("Empty row or column labels in one or more languages");
setInvalidQuestions([question.id]);
return false;
}
if (hasDuplicates(question.rows)) {
toast.error("You have duplicate row labels.");
return false;
}
if (hasDuplicates(question.columns)) {
toast.error("You have duplicate column labels.");
return false;
}
}
for (const logic of question.logic || []) {
const validFields = ["condition", "destination", "value"].filter(
(field) => logic[field] !== undefined
).length;
if (validFields < 2) {
setInvalidQuestions([question.id]);
toast.error("Incomplete logic jumps detected: Fill or remove them in the Questions tab.");
return false;
}
if (question.required && logic.condition === "skipped") {
toast.error("A logic condition is missing: Please update or delete it in the Questions tab.");
return false;
}
const thisLogic = `${logic.condition}-${logic.value}`;
if (existingLogicConditions.has(thisLogic)) {
setInvalidQuestions([question.id]);
toast.error(
"There are two competing logic conditons: Please update or delete one in the Questions tab."
);
return false;
}
existingLogicConditions.add(thisLogic);
}
}
// Checking the validity of redirection URLs to ensure they are properly formatted.
if (
survey.redirectUrl &&
!survey.redirectUrl.includes("https://") &&
!survey.redirectUrl.includes("http://")
) {
toast.error("Please enter a valid URL for redirecting respondents.");
return false;
}
// validate the user segment filters
const localSurveySegment = {
id: survey.segment?.id,
filters: survey.segment?.filters,
title: survey.segment?.title,
description: survey.segment?.description,
};
const surveySegment = {
id: survey.segment?.id,
filters: survey.segment?.filters,
title: survey.segment?.title,
description: survey.segment?.description,
};
// if the non-private segment in the survey and the strippedSurvey are different, don't save
if (!survey.segment?.isPrivate && !isEqual(localSurveySegment, surveySegment)) {
toast.error("Please save the audience filters before saving the survey");
return false;
}
if (!!survey.segment?.filters?.length) {
const parsedFilters = ZSegmentFilters.safeParse(survey.segment.filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message ||
"Invalid targeting: Please check your audience filters";
toast.error(errMsg);
return false;
}
}
// if inlineTriggers are present validate with zod
if (!!survey.inlineTriggers) {
const parsedInlineTriggers = ZSurveyInlineTriggers.safeParse(survey.inlineTriggers);
if (!parsedInlineTriggers.success) {
toast.error("Invalid Custom Actions: Please check your custom actions");
return false;
}
}
// validate that both triggers and inlineTriggers are not present
if (surveyHasBothTriggers(survey)) {
toast.error("Survey cannot have both custom and saved actions, please remove one.");
return false;
}
const questionWithEmptyFallback = checkForEmptyFallBackValue(survey, selectedLanguageCode);
if (questionWithEmptyFallback) {
toast.error("Fallback missing");
return false;
}
// Detecting any cyclic dependencies in survey logic.
if (isSurveyLogicCyclic(survey.questions)) {
toast.error("Cyclic logic detected. Please fix it before saving.");
return false;
}
if (survey.type === "app" && survey.segment?.id === "temp") {
const { filters } = survey.segment;
const parsedFilters = ZSegmentFilters.safeParse(filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message ||
"Invalid targeting: Please check your audience filters";
toast.error(errMsg);
return;
}
}
return true;
};

View File

@@ -4,7 +4,7 @@ import { getAdvancedTargetingPermission, getMultiLanguagePermission } from "@for
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -87,7 +87,6 @@ export default async function SurveysEditPage({ params }) {
isUserTargetingAllowed={isUserTargetingAllowed}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
/>
);
}

View File

@@ -28,7 +28,7 @@ interface PreviewSurveyProps {
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
}
let surveyNameTemp: string;
let surveyNameTemp;
const previewParentContainerVariant: Variants = {
expanded: {
@@ -156,7 +156,7 @@ export const PreviewSurvey = ({
const onFinished = () => {
// close modal if there are no questions left
if ((survey.type === "website" || survey.type === "app") && !survey.thankYouCard.enabled) {
if (survey.type === "web" && !survey.thankYouCard.enabled) {
setIsModalOpen(false);
setTimeout(() => {
setQuestionId(survey.questions[0]?.id);
@@ -165,7 +165,7 @@ export const PreviewSurvey = ({
}
};
// this useEffect is for refreshing the survey preview only if user is switching between templates on survey templates page and hence we are checking for survey.id === "someUniqeId1" which is a common Id for all templates
// this useEffect is fo refreshing the survey preview only if user is switching between templates on survey templates page and hence we are checking for survey.id === "someUniqeId1" which is a common Id for all templates
useEffect(() => {
if (survey.name !== surveyNameTemp && survey.id === "someUniqueId1") {
resetQuestionProgress();
@@ -260,13 +260,13 @@ export const PreviewSurvey = ({
/>
</Modal>
) : (
<div className="h-full w-full">
<div className="w-full px-3">
<div className="absolute left-5 top-5">
{!styling.isLogoHidden && product.logo?.url && (
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
)}
</div>
<div className="flex h-full items-end">
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
<SurveyInline
survey={survey}
isBrandingEnabled={product.linkSurveyBranding}

View File

@@ -30,7 +30,7 @@ export default function SurveyStarter({
const newSurveyFromTemplate = async (template: TTemplate) => {
setIsCreateSurveyLoading(true);
const surveyType = environment?.widgetSetupCompleted ? "app" : "link";
const surveyType = environment?.widgetSetupCompleted ? "web" : "link";
const augmentedTemplate: TSurveyInput = {
...template.preset,
type: surveyType,

View File

@@ -67,7 +67,7 @@ export const TemplateList = ({
const addSurvey = async (activeTemplate) => {
setLoading(true);
const surveyType = environment?.widgetSetupCompleted ? "app" : "link";
const surveyType = environment?.widgetSetupCompleted ? "web" : "link";
const augmentedTemplate: TSurveyInput = {
...activeTemplate.preset,
type: surveyType,

View File

@@ -2602,7 +2602,7 @@ export const minimalSurvey: TSurvey = {
createdAt: new Date(),
updatedAt: new Date(),
name: "Minimal Survey",
type: "app",
type: "web",
environmentId: "someEnvId1",
createdBy: null,
status: "draft",
@@ -2653,7 +2653,7 @@ export const getExampleSurveyTemplate = (webAppUrl: string) => ({
}) as TSurveyCTAQuestion
),
name: "Example survey",
type: "website" as TSurveyType,
type: "web" as TSurveyType,
autoComplete: 2,
triggers: ["New Session"],
status: "inProgress" as TSurveyStatus,

View File

@@ -6,12 +6,12 @@ import Image from "next/image";
import { OptionCard } from "@formbricks/ui/OptionCard";
interface PathwaySelectProps {
setSelectedPathway: (pathway: "link" | "website" | null) => void;
setSelectedPathway: (pathway: "link" | "in-app" | null) => void;
setCurrentStep: (currentStep: number) => void;
isFormbricksCloud: boolean;
}
type PathwayOptionType = "link" | "website";
type PathwayOptionType = "link" | "in-app";
export default function PathwaySelect({
setSelectedPathway,
@@ -29,7 +29,7 @@ export default function PathwaySelect({
localStorage.setItem("onboardingCurrentStep", "5");
}
} else {
localStorage.setItem("onboardingPathway", "website");
localStorage.setItem("onboardingPathway", "in-app");
setCurrentStep(2);
localStorage.setItem("onboardingCurrentStep", "2");
}
@@ -54,12 +54,12 @@ export default function PathwaySelect({
<Image src={LinkMockup} alt="" height={350} />
</OptionCard>
<OptionCard
cssId="onboarding-website-survey-card"
cssId="onboarding-inapp-survey-card"
size="lg"
title="Website Surveys"
description="Run a survey on a website."
title="In-app Surveys"
description="Run a survey on a website or in-app."
onSelect={() => {
handleSelect("website");
handleSelect("in-app");
}}>
<Image src={InappMockup} alt="" height={350} />
</OptionCard>

View File

@@ -183,31 +183,24 @@ export const SignupForm = ({
<IsPasswordValid password={password} setIsValid={setIsValid} />
</div>
)}
{showLogin && (
<Button
type="submit"
variant="darkCTA"
className="w-full justify-center"
loading={signingUp}
disabled={formRef.current ? !isButtonEnabled || !isValid : !isButtonEnabled}>
Continue with Email
</Button>
)}
{!showLogin && (
<Button
type="button"
onClick={() => {
<Button
onClick={(e: any) => {
e.preventDefault();
if (!showLogin) {
setShowLogin(true);
setButtonEnabled(false);
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => nameRef.current?.focus(), 100);
}}
variant="darkCTA"
className="w-full justify-center">
Continue with Email
</Button>
)}
} else if (formRef.current) {
formRef.current.requestSubmit();
}
}}
variant="darkCTA"
className="w-full justify-center"
loading={signingUp}
disabled={formRef.current ? !isButtonEnabled || !isValid : !isButtonEnabled}>
Continue with Email
</Button>
</form>
)}
{googleOAuthEnabled && (

View File

@@ -7,11 +7,8 @@ export async function GET(_: NextRequest, { params }: { params: { slug: string }
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-core":
path = `../../packages/js-core/dist/index.umd.cjs`;
break;
case "surveys":
path = `../../packages/surveys/dist/index.umd.cjs`;

View File

@@ -9,7 +9,7 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TJsAppStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { TJsStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
interface Context {
params: {
@@ -87,7 +87,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
}
// return state
const state: TJsAppStateSync = {
const state: TJsStateSync = {
person: { id: person.id, userId: person.userId },
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),

View File

@@ -9,7 +9,7 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TJsAppStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { TJsStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
interface Context {
params: {
@@ -86,7 +86,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
}
// return state
const state: TJsAppStateSync = {
const state: TJsStateSync = {
person: { id: person.id, userId: person.userId },
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),

View File

@@ -1,6 +1,6 @@
import { TJsAppState, TJsLegacyState } from "@formbricks/types/js";
import { TJsLegacyState, TJsState } from "@formbricks/types/js";
export const transformLegacySurveys = (state: TJsAppState): TJsLegacyState => {
export const transformLegacySurveys = (state: TJsState): TJsLegacyState => {
const updatedState: any = { ...state };
updatedState.surveys = updatedState.surveys.map((survey) => {
const updatedSurvey = { ...survey };

View File

@@ -101,9 +101,7 @@ export const getUpdatedState = async (environmentId: string, personId?: string):
surveys = await getSyncSurveys(environmentId, (person as TPerson).id);
} else {
surveys = await getSurveys(environmentId);
surveys = surveys.filter(
(survey) => (survey.type === "app" || survey.type === "website") && survey.status === "inProgress"
);
surveys = surveys.filter((survey) => survey.type === "web" && survey.status === "inProgress");
}
surveys = transformLegacySurveys(surveys);

Some files were not shown because too many files have changed in this diff Show More