Merge branch 'main' of https://github.com/formbricks/formbricks into feat/embed-xp

This commit is contained in:
Piyush Gupta
2023-09-29 20:04:36 +05:30
92 changed files with 1163 additions and 245 deletions

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Code Actions",
title: "Implementing Code Actions in Formbricks | Real-time User Action Tracking",
description:
"Integrate code actions in Formbricks using formbricks.track() to trigger surveys based on user actions, like button clicks, for precise insights. All open-source.",
"Dive into the world of Formbricks' code actions. Learn how to seamlessly integrate formbricks.track() method into your codebase, enabling real-time tracking of user actions like button clicks, visiting a specfic URL. Up your survey game with precise and exact triggers.",
};
#### Actions

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "No-Code Actions",
title: "Implementing No-Code Actions in Formbricks | Real-time User Action Tracking",
description:
"Utilize Formbricks' No-Code Actions like Page URL, innerText, and CSS Selector for easy survey triggers and enhanced user insights.",
"Discover the power of Formbricks' No-Code Actions. Easily set up triggers based on Page URL, innerText, and CSS Selectors without touching a line of code. Inccrease user engagement and get insights at precise moments in the user journey.",
};
#### Actions

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "What are actions and why are they useful?",
title: "Using Actions in Formbricks | Fine-tuning User Moments",
description:
"Actions in Formbricks enable targeted survey displays during specific user journey moments. Enhance user segmentation by tracking actions for granular surveying.",
"Dive deep into how actions in Formbricks help products and teams to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
};
#### Actions

View File

@@ -4,9 +4,9 @@ import AddApiKey from "./add-api-key.webp";
import ApiKeySecret from "./api-key-secret.webp";
export const meta = {
title: "API Key Setup",
title: "Formbricks API Key: Setup and Testing",
description:
"Generate, store, and delete personal API keys for secure Formbricks access. Ensure safekeeping to prevent unauthorized account control.",
"This guide provides step-by-step instructions to generate, store, and delete API keys, ensuring safe and authenticated access to your Formbricks account.",
};
#### API

View File

@@ -1,9 +1,9 @@
import { Fence } from "@/components/shared/Fence";
export const meta = {
title: "Responses API",
title: "Formbricks Public Client API Guide: Manage Survey Displays & Responses",
description:
"Explore the Formbricks Public Client API for client-side tasks and integration into your website.",
"Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on how to mark surveys as displayed as well as responded for individual persons, ensuring seamless client-side interactions without compromising data security.",
};
#### Management API

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "API Overview",
title: "Formbricks API Overview: Public Client & Management API Breakdown",
description:
"Explore Formbricks' APIs: Public Client API for client-side tasks, and Management API for account management with secure API Key authentication.",
"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

View File

@@ -1,9 +1,9 @@
import { Fence } from "@/components/shared/Fence";
export const meta = {
title: "Responses API",
title: "Formbricks People API: Fetch or Create Person Overview",
description:
"Explore the Formbricks Public Client API for client-side tasks and integration into your website.",
"Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.",
};
#### Management API

View File

@@ -1,9 +1,9 @@
import { Fence } from "@/components/shared/Fence";
export const meta = {
title: "Responses API",
title: "Formbricks Responses API Documentation - Manage Your Survey Data Seamlessly",
description:
"Explore the Formbricks Public Client API for client-side tasks and integration into your website.",
"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.",
};
#### Management API
@@ -14,7 +14,7 @@ The Public Client API is designed for the JavaScript SDK and does not require au
---
## List all responses {{ tag: 'GET', label: '/api/v1/responses' }}
## List all responses {{ tag: 'GET', label: '/api/v1/management/responses' }}
<Row>
<Col>
@@ -39,11 +39,11 @@ The Public Client API is designed for the JavaScript SDK and does not require au
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/responses">
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/responses' \
'https://app.formbricks.com/api/v1/management/responses' \
--header \
'x-api-key: <your-api-key>'
```

View File

@@ -1,9 +1,9 @@
import { Fence } from "@/components/shared/Fence";
export const meta = {
title: "Surveys API",
title: "Formbricks Surveys API Documentation - How to Retrieve All Surveys",
description:
"Explore the Formbricks Public Client API for client-side tasks and integration into your website.",
"Explore the comprehensive guide to the Formbricks Surveys API. Learn how to effectively retrieve all the surveys in your environment with the necessary headers and API key setup. Includes sample request and response formats.",
};
#### Management API
@@ -14,7 +14,7 @@ The Survey API currently has one endpoint that allows you to get all the surveys
---
## List all surveys {{ tag: 'GET', label: '/api/v1/surveys' }}
## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }}
<Row>
<Col>
@@ -32,11 +32,11 @@ The Survey API currently has one endpoint that allows you to get all the surveys
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/surveys">
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/surveys' \
'https://app.formbricks.com/api/v1/management/surveys' \
--header \
'x-api-key: <your-api-key>'
```

View File

@@ -1,6 +1,6 @@
export const meta = {
title: "Webhook API Overview",
description: "Learn how to use the Formbricks Webhook API.",
title: "Formbricks Webhook API Documentation - List, Retrieve, Create, and Delete Webhooks",
description: "Explore the comprehensive guide to the Formbricks Webhooks API. This is all you need to interact and play with the Formbricks Webhooks and integrate them into any third party app of your choice",
};
#### Webhook API

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Setting attributes with code",
title: "Guide for Setting Custom Attributes | Formbricks Documentation",
description:
"Set attributes in code using setAttribute function. Enhance user segmentation, target surveys effectively, and gather valuable insights for better decisions. All open-source.",
"Learn how to set attributes in code using setAttribute function. Enhance user segmentation, target surveys effectively, and gather valuable insights for better decisions. Easily send user-specific details for better survey segmentation and gain deeper insights.",
};
#### Attributes

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Identifying Users",
title: "User Identification in Formbricks | Enhancing Survey Feedback",
description:
"Identify users with Formbricks by setting User ID, email, and custom attributes. Enhance survey targeting and recontacting while maintaining user privacy.",
"A comprehensive guide on identifying users in Formbricks without compromising privacy. Learn how to set User ID, email, and custom attributes to optimize survey targeting, recontact users, and control survey intervals, all while respecting user anonymity.",
};
#### Attributes

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "What are attributes and why are they useful?",
title: "Understanding User Attributes in Formbricks Surveys",
description:
"How to use attributes for user segmentation, enhancing survey targeting & results. Improve feedback quality and make data-driven decisions.",
"Dive into the importance of attributes in surveys. Learn how key-value pairs can significantly improve survey targeting, enhance feedback quality, and guide data-driven decisions with Formbricks.",
};
#### Attributes

View File

@@ -11,8 +11,8 @@ import PublishSurvey from "./publish-survey.webp";
import SelectAction from "./select-action.webp";
export const meta = {
title: "Learn from Churn",
description: "To know how to decrease churn, you have to understand it. Use a micro-survey.",
title: "Mastering Churn Surveys with Formbricks | Essential Tips & Steps",
description: "Learn how to effectively utilize Formbricks' Churn Surveys to gain deeper insights into user departures. Dive into a step-by-step guide to craft, trigger, and optimize your churn surveys, ensuring you capture invaluable feedback at critical junctures",
};
#### Best Practices

View File

@@ -11,8 +11,8 @@ import WhenToAsk from "./when-to-ask.webp";
import CopyIds from "./copy-ids.webp";
export const meta = {
title: "Docs Feedback",
description: "Docs Feedback allows you to measure how clear your documentation is.",
title: "Integrate Docs Feedback in Your Website: A Step-by-Step Guide on getting feedback on your Documentation with Formbricks",
description: "Learn the step-by-step process to effectively measure the clarity of your documentation using Formbricks Cloud. Dive into best practices, setting up cloud environments, integrating feedback widgets on your frontend, and connecting to the Formbricks API for a seamless user experience.",
};
#### Best Practices

View File

@@ -10,8 +10,8 @@ import RecontactOptions from "./recontact-options.webp";
import SelectAction from "./select-action.webp";
export const meta = {
title: "Feature Chaser",
description: "Follow up with users who used a specific feature. Gather feedback and improve your product.",
title: "Setting Up Feature Chaser Surveys with Formbricks: A Comprehensive Guide",
description: "Learn how to harness the power of Formbricks to gather targeted user feedback on specific features. Dive deep into creating, triggering, and publishing the Feature Chaser survey to enhance your product with actionable insights for specific users.",
};
#### Best Practices

View File

@@ -12,8 +12,8 @@ import SelectAction from "./select-feedback-button-action.webp";
import RecontactOptions from "./set-recontact-options.webp";
export const meta = {
title: "Feedback Box",
description: "The Feedback Box gives your users a direct channel to share their feedback and feel heard.",
title: "Implementing the Feedback Box with Formbricks: A Step-by-Step Tutorial",
description: "Unlock user insights effortlessly! Discover how to set up the Feedback Box in your app using Formbricks, allowing your users to provide real-time feedback. Follow our comprehensive guide to enhance user experience and respond rapidly to feedback",
};
#### Best Practices

View File

@@ -10,8 +10,8 @@ import RecontactOptions from "./recontact-options.webp";
import SelectAction from "./select-action.webp";
export const meta = {
title: "Improve Trial Conversion",
description: "Understand how to improve the trial conversions to get more paying customers.",
title: "Boost Your Trial Conversion Rates with Formbricks: Comprehensive Guide",
description: "Unlock the secret to converting more trial users into paying customers using Formbricks. Understand insights behind trial cancellations and tailor your offering to fit user needs. Dive into our step-by-step tutorial and improve your conversion strategy today",
};
#### Best Practices

View File

@@ -13,8 +13,8 @@ import RecontactOptions from "./recontact-options.webp";
import SelectAction from "./select-action.webp";
export const meta = {
title: "In-app Interview Prompt",
description: "Invite only power users to schedule an interview with your product team.",
title: "Maximize User Interview Participation with In-app Interview Prompts",
description: "Engage with your power users seamlessly using Formbricks' In-app Interview Prompt. Ditch traditional email invites and experience way more more respondents. Dive into our comprehensive guide on setting up auto-scheduled interviews today and enhance your user understanding",
};
#### Best Practices

View File

@@ -10,8 +10,8 @@ import RecontactOptions from "./recontact-options.webp";
import SelectAction from "./select-action.webp";
export const meta = {
title: "Product-Market Fit Survey",
description: "The Product-Market Fit survey helps you measure, well, Product-Market Fit (PMF).",
title: "How to Set Up a Product-Market Fit Survey Using Formbricks - Step-by-Step Guide",
description: "Learn to leverage Formbricks to create and implement a Product-Market Fit survey in your web app. Follow our detailed step-by-step guide to measure and understand your PMF effectively. Ensure high data quality, efficient triggers, and actionable insights.",
};
#### Best Practices

View File

@@ -3,8 +3,8 @@ import Image from "next/image";
import DemoApp from "./demoapp.webp";
export const meta = {
title: "Demo App",
description: "To test in-app surveys, trigger actions and set attributes, you can use the Demo App.",
title: "Formbricks Demo App Guide: Play around with Formbricks",
description: "To test in-app surveys, trigger actions and set attributes, you can use the Demo App. This guide provides hands-on examples of sending both code and no-code actions",
};
#### Contributing

View File

@@ -1,6 +1,6 @@
export const meta = {
title: "Contribution Guide",
description: "How to contribute to Formbricks",
title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks",
description: "Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Discord",
};
#### Contributing

View File

@@ -1,6 +1,6 @@
export const meta = {
title: "Setup Dev Environment",
description: "Setup a development environment for Formbricks.",
title: "Formbricks Development Setup: Complete Guide to Local Environment Configuration for Dev",
description: "Step-by-step guide to setting up your local development environment for Formbricks. Includes installing essential tools like Node.JS, pnpm, and Docker, and accessing the entire Formbricks stack including the Demo app and the main website",
};
#### Contributing

View File

@@ -5,9 +5,9 @@ import UncaughtPromise from "./uncaught-promise.webp";
import Logout from "./logout.webp";
export const meta = {
title: "Troubleshooting",
title: "Formbricks Troubleshooting Guide: How to Solve & Debug Common Issues",
description:
"Formbricks is a complex application in constant development. Sometimes, things don't go as planned. Here are some tips to help you troubleshoot.",
"Facing issues with Formbricks? This troubleshooting guide covers frequently encountered problems, from Prisma migrations to package errors and more. Detailed solutions, accompanied by visual aids, ensure a smoother user experience with Formbricks",
};
#### Contributing

View File

@@ -6,8 +6,8 @@ import WidgetConnected from "./widget-connected.webp";
import ReactApp from "./react-in-app-survey-app-popup-form.webp";
export const metadata = {
title: "Framework Guides",
description: "Explore all the possible ways you can integrate Formbricks into your application.",
title: "Integrate Formbricks: Comprehensive Framework Guide & Integration Tutorial",
description: "Master the integration of Formbricks into your application with our detailed guides. From HTML to ReactJS, NextJS, and VueJS, get step-by-step instructions and ensure seamless setup.",
};
# Framework Guides

View File

@@ -14,8 +14,8 @@ import I11 from "./11-survey-logs-in-app-survey-popup.webp";
import ReactApp from "../framework-guides/react-in-app-survey-app-popup-form.webp";
export const meta = {
title: "Collect in app survey responses in 10 minutes",
description: "Get your first in app survey response in 10 minutes.",
title: "Formbricks Quickstart Guide: In-App Surveys Made Simple",
description: "Launch your first in-app survey effortlessly. Dive into our step-by-step guide to set up, integrate, and debug Formbricks in your web app in under 15 minutes.",
};
#### Getting Started

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,5 +1,12 @@
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import IntegrationTab from "./integrations-tab.webp";
import ConnectWithGoogle from "./connect-with-google.webp";
import GoogleConnected from "./google-connected.webp";
import LinkSurveyWithSheet from "./link-survey-with-sheet.webp";
import LinkWithQuestions from "./link-with-questions.webp";
import ListLinkedSurveys from "./list-linked-surveys.webp";
import DeleteConnection from "./delete-connection.webp";
import Image from "next/image";
export const meta = {
@@ -18,12 +25,144 @@ The Google Sheets integration allows you to automatically send responses to a Go
self-hosted version of Formbricks.
</Note>
## Formbricks Cloud
1. Go to the Integrations tab in your [Formbricks Cloud dashboard](https://app.formbricks.com/) and click on the "Connect" button under Google Sheets integration.
<Image
src={IntegrationTab}
alt="Formbricks Integrations Tab"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. Now click on the "Connect with Google" button to authenticate yourself with Google.
<Image
src={ConnectWithGoogle}
alt="Connect Formbricks with your Google"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. You will now be taken to the Google OAuth page where you can select the Google account you want to use for the integration.
4. Once you have selected the account and completed the authentication process, you will be taken back to Formbricks Cloud and see the connected status as below:
<Image
src={GoogleConnected}
alt="Formbricks is now connected with Google"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>
Before the next step, make sure that you have a Formbricks Survey with at least one question and a Google Sheet in the Google account you integrated.
</Note>
6. Now click on the "Link New Sheet" button to link a new Google Sheet with Formbricks and a modal will open up.
<Image
src={LinkSurveyWithSheet}
alt="Link Formbricks with a Google Sheet"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
7. Select the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button.
<Image
src={LinkWithQuestions}
alt="Select question to link with Google Sheet"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
8. On submitting, the modal will close and you will see the linked Google Sheet in the list of linked Google Sheets.
<Image
src={ListLinkedSurveys}
alt="List of linked Google Sheets"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Congratulations! You have successfully linked a Google Sheet with Formbricks. Now whenever a response is submitted for the linked Survey, it will be automatically added to the linked Google Sheet.
## Setup in self-hosted Formbricks
Enabling the Google Sheets Integration in a self-hosted environment isn't easy and requires a setup using Google Cloud and changing the environment variables of your Formbricks instance.
The environment variables you need to set are:
<Note>This process is really complicated and we recommend using Formbricks Cloud for this feature.</Note>
We will first create a Google Cloud Project and then enable the Google Sheets API for it. Then we will create an OAuth Client ID and Client Secret for our Formbricks instance and set them as environment variables.
1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and **create a new project**.
2. Now select the project you just created and go to the "**APIs & Services**" section.
3. Click on the "**Enable APIs and Services**" button and search for "**Google Sheets API**" and enable it.
4. Now go to the "**OAuth Consent screen**" section and select the **"External" User Type** if you want any Google User to be able to use the integration or select "Internal" if you want only the users of your Google Workspace to be able to use the integration.
5. Now provide it the details such as
- App name (Will **show up in the OAuth modal** when the user is asked to authenticate with Google)
- User support email (ideally should be **your email** for any support queries by the Users or Google)
- Developer contact information (ideally should be **your email** for any **support queries by Google**)
6. Now click on the "Save and Continue" button and you will be taken to the Scopes step.
7. Click on the "**Add or Remove Scopes**" button and add the scopes `https://www.googleapis.com/auth/userinfo.email`, `https://www.googleapis.com/auth/spreadsheets` & `https://www.googleapis.com/auth/drive` and click on the "Update" button:
8. Now Verify the scopes and click on the "Save and Continue" button.
9. Now go to the **"Test Users" section, skip the step**, and click the "Save and Continue" button.
10. You will now be shown a summary of the OAuth Consent Screen. Verify the details and Click on the "**Back to Dashboard**" button.
11. Now go to the "**Credentials**" section and click on the "**Create Credentials**" button and select "**OAuth Client ID**".
12. Select "**Web Application**" as the Application Type and provide it a name (this name will **not be visible** to your end users).
13. Now add your **public facing URL** in the "**Authorized JavaScript Origins**" section:
- https://`<your-public-facing-url`>
14. Now add the following URL in the "**Authorized redirect URIs**" section and click on the "**Create**" button:
- https://`<your-public-facing-url`>/api/google-sheet/callback
15. You will now be shown the **Client ID** and **Client Secret**. Copy them and set them as the **environment variables** in your Formbricks instance as:
- `GOOGLE_SHEETS_CLIENT_ID` - Client ID
- `GOOGLE_SHEETS_CLIENT_SECRET` - Client Secret
16. Also use the **same Authorized redirect URI** in the `GOOGLE_SHEETS_REDIRECT_URL` environment variable.
### By now, your environment variables should include the below ones as well:
- `GOOGLE_SHEETS_CLIENT_ID`
- `GOOGLE_SHEETS_CLIENT_SECRET`
- `GOOGLE_SHEETS_REDIRECT_URL`
Voila! You have successfully enabled the Google Sheets integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks Cloud](#formbricks-cloud) section to link a Google Sheet with Formbricks.
## Remove Integration with Google Account
To remove the integration with Google Account,
1. Visit the Integrations tab in your Formbricks Cloud dashboard.
2. Select "Manage" button in the Google Sheets card.
3. Click on the "Connected with `<your-email-here`>" just before the "Link new Sheet" button.
4. It will now ask for a confirmation to remove the integration. Click on the "Delete" button to remove the integration. You can always come back and connect again with the same Google Account.
<Image
src={DeleteConnection}
alt="Delete Google Integration with Formbricks"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## What data do you need?
- Your **Email ID** for authentication (We use this to identify you)
- Your **Google Sheets Names and IDs** (We fetch this to list and show you the options of choosing a sheet to integrate with)
- Write access to **selected Google Sheet** (The google sheet you choose to integrate it with, we write survey responses to it)
For the above, we ask for:
1. **User Email**: To identify you (that's it, nothing else, we're opensource, see this in our codebase [here](https://github.com/formbricks/formbricks/blob/main/apps/web/app/api/google-sheet/callback/route.ts#L47C17-L47C25))
1. **Google Drive API**: To list all your google sheets (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/services/googleSheet.ts#L13))
1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/services/googleSheet.ts#L70))
<Note>
We do not store any other information of yours! We value Privacy more than you and rest assured you're safe
with us!
</Note>
Still struggling or something not working as expected? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you!

View File

@@ -15,8 +15,8 @@ import Result from "./result.webp";
import SelectAction from "./select-action.webp";
export const meta = {
title: "Make.com Setup",
description: "Wire up Formbricks with Make and 1000+ other apps",
title: "Formbricks Integration with Make.com: A Step-by-Step Guide",
description: "Discover how to seamlessly integrate Formbricks with Make.com. Dive into our comprehensive guide to set up scenarios, connect with a plethora of apps, and send your survey data to more than 1000 platforms.",
};
#### Integrations

View File

@@ -19,8 +19,8 @@ import SuccessConnection from "./success-connection.png";
import UpdateQuestionId from "./update-question-id.png";
export const meta = {
title: "n8n Setup",
description: "Wire up Formbricks with n8n and 350+ other apps",
title: "Comprehensive Guide to Integrating Formbricks with n8n",
description: "Unlock the potential of combining Formbricks with n8n for a streamlined workflow experience. Dive into our step-by-step guide and send your survey data effortlessly to 350+ applications. Streamline your data processes now!",
};
#### Integrations

View File

@@ -15,8 +15,8 @@ import UpdateQuestionId from "./update-question-id.webp";
import ZapierMessage from "./zapier-message.webp";
export const meta = {
title: "Zapier Setup",
description: "Wire up Formbricks with Zapier and 5000+ other apps",
title: "Step-by-Step Guide to Integrating Formbricks with Zapier",
description: "Master the integration of Formbricks with Zapier using our detailed guide. Seamlessly connect your surveys to 5000+ apps, automate data transfers, and enhance feedback management. Start optimizing your workflow today.",
};
#### Integrations

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "How Formbricks works",
title: "Inside Look: Formbricks In-Product Micro-Surveys",
description:
"Master in-product micro-surveys with Formbricks: user-friendly form builder, precise targeting, seamless integration, and insightful analytics for SaaS products.",
"Unlock the full potential of Formbricks: From intuitive form-building and event-based triggers to effortless integrations and deep analytics. Master the art of in-product surveys for your SaaS or digital platform.",
};
#### Introduction

View File

@@ -4,9 +4,9 @@ import { HeroPattern } from "@/components/docs/HeroPattern";
import { Button } from "@/components/docs/Button";
export const metadata = {
title: "Formbricks Open Source Experience Management",
title: "Formbricks: Privacy-first Experience Management",
description:
"Natively embed qualitative user research into your B2B SaaS. Leverage Best Practices for user discovery to increase Product-Market Fit",
"Enhance your product with Formbricks the leading open-source solution for in-product micro-surveys. Dive deep into user research, amplify product-market fit, and uncover the 'why' behind your analytics.",
};
export const sections = [

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Why is Formbricks better?",
title: "Formbricks vs. Generic Survey Tools: A Comparative Insight",
description:
"The ultimate survey solution for SaaS, offering in-depth micro-surveys, precise targeting, and seamless integrations. Discover the difference today!",
"Discover how Formbricks excels as a specialized in-product micro-survey platform for SaaS. Get unmatched targeting, seamless integrations, and make informed decisions with our open-source advantage.",
};
#### Introduction

View File

@@ -3,8 +3,8 @@ import Image from "next/image";
import QuestionId from "./question-id.webp";
export const meta = {
title: "Data Prefilling in Link Surveys",
description: "Prefill data in your surveys to make it easier for your users to provide feedback.",
title: "URL Data Prefilling for Link Surveys in Formbricks",
description: "Master the art of data prefilling in Formbricks link surveys. Dive into our guide on how to use URL parameters to prepopulate answers, boosting conversion rates and enhancing user experience. Learn through examples and ensure correct validation for each question type.",
};
#### Link Surveys

View File

@@ -3,9 +3,9 @@ import Image from "next/image";
import PeopleView from "./people-view.webp";
export const meta = {
title: "User Identification in Link Surveys",
title: "Effective User Identification in Formbricks Link Surveys",
description:
"Identify users in link surveys via URL parameter. Connect responses to existing users in Formbricks.",
"Discover how to seamlessly connect responses from Formbricks link surveys to existing users in your database. Learn the intricacies of the userId URL parameter to enhance user tracking, profiling, and segmentation, ensuring more personalized interactions and data-driven decisions.",
};
#### Link Surveys

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Deploying Formbricks to production",
title: "Comprehensive Guide to Self-Hosting Formbricks",
description:
"Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.",
"Discover versatile options to deploy Formbricks tailored to your expertise level. From Ubuntu setups using shell scripts, swift Docker deployments, to manual source configurations, harness the flexibility and power of Formbricks to fit your unique hosting needs. Dive in today!",
};
#### Self-Hosting

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Deploying Formbricks with Docker",
title: "Guide to Deploying Formbricks Using Docker",
description:
"Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.",
"Step-by-step tutorial on how to effortlessly set up and run Formbricks via Docker. Explore the quick deployment process with Docker-Compose, learn how to update Formbricks, and troubleshoot common issues. Ideal for those looking for a hassle-free Formbricks experience",
};
#### Self-Hosting

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Deploying Formbricks from Source Code",
title: "Formbricks Self-Hosting Guide: Deploy from Source Code for Complete Customization",
description:
"Build the Formbricks Image right from the open-sourced codebase and make changes however needed. Deploy it then with the freedom of customizing even the compile-time environment variables.",
"Build the Formbricks Image right from the open-sourced codebase and make changes however needed. Deploy it then with the freedom of customizing even the compile-time environment variables. Gain more control and flexibility by customizing compile-time environment variables and configuring your instance. Dive into our comprehensive guide for building and running Formbricks with Docker.",
};
#### Self-Hosting

View File

@@ -1,7 +1,7 @@
export const meta = {
title: "Deploying Formbricks to production",
title: "Step by Step Guide on Deploying Formbricks to Production on Ubuntu",
description:
"Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.",
"Master the swift deployment of Formbricks on an Ubuntu server with our step-by-step guide. Use a single command to automate Docker, Postgres DB, SSL certificate configuration, and more. Encounter issues? Dive into our troubleshooting steps or join our community on Discord for assistance.",
};
#### Self-Hosting

View File

@@ -5,7 +5,7 @@ export default function DemoPage() {
return (
<LayoutWaitlist
title="Formbricks Demo"
description="Leverage 30+ templates to kick-start your experience management.">
description="Play around with our pre-defined 30+ templates and them to kick-start your survey & experience management.">
<DemoView />
</LayoutWaitlist>
);

View File

@@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function DocsFeedbackPage() {
return (
<Layout
title="Docs Feedback"
title="Get User Feedback in the easiest way possible with Formbricks"
description="The better your docs, the higher your user adoption. Measure granularly how clear your documentation is.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">

View File

@@ -7,8 +7,8 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function FeatureChaserPage() {
return (
<Layout
title="Feature Chaser"
description="Show a survey about a new feature shown only to people who used it.">
title="Feature Chaser with Formbricks"
description="Show a survey about a new feature shown only to people who used it and gain insightful data.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Feature Chaser" difficulty="Easy" setupMinutes="10" />

View File

@@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function FeedbackBoxPage() {
return (
<Layout
title="Feedback Box"
title="Feedback Box with Formbricks"
description="Open a direct channel to your users by allowing them to share feedback with your team.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">

View File

@@ -3,6 +3,7 @@ import { Callout } from "@/components/shared/Callout";
export const meta = {
title: "How to create a GDPR compliant form",
description: "Steps to understand how GDPR might be needed for you with Formbricks.",
};
<Callout title="No legal advice" type="note">

View File

@@ -3,6 +3,7 @@ import { Callout } from "@/components/shared/Callout";
export const meta = {
title: "GDPR FAQs",
description: "Frequently asked questions about GDPR and Formbricks",
};
<Callout title="No legal advice" type="note">

View File

@@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function MissedTrialPagePage() {
return (
<Layout
title="Improve Trial Conversion"
title="Improve Trial Conversion with Formbricks"
description="Take the guessing out, convert more trials to paid users with insights.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">

View File

@@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function InterviewPromptPage() {
return (
<Layout
title="Interview Prompt"
title="Interview Prompt with Formbricks"
description="Ask only power users users to book a time in your calendar. Get those juicy details.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">

View File

@@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function LearnFromChurnPage() {
return (
<Layout
title="Learn from Churn"
title="Learn from Churn with Formbricks"
description="Churn is hard, but insightful. Learn from users who changed their mind.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">

View File

@@ -15,7 +15,7 @@ import Image from "next/image";
export default function MeasurePMFPage() {
return (
<Layout
title="Product-Market Fit Survey"
title="Product-Market Fit Survey with Formbricks"
description="Measure Product-Market Fit to understand how to develop your product further.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">

View File

@@ -6,7 +6,7 @@ import UseCaseHeader from "@/components/shared/UseCaseHeader";
export default function OnboardingSegmentationPage() {
return (
<Layout
title="Onboarding Segmentation"
title="Onboarding Segmentation with Formbricks"
description="Add a survey to your onboarding to loop in Formbricks right from the start.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">

View File

@@ -79,6 +79,7 @@ export default function AddNoCodeActionModal({
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig);
const updatedData: TActionClassInput = {
...data,
environmentId,
noCodeConfig: filteredNoCodeConfig,
type: "noCode",
} as TActionClassInput;

View File

@@ -0,0 +1,39 @@
import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey";
import { TAuthenticationApiKey } from "@formbricks/types/v1/auth";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { responses } from "@/lib/api/response";
import { NextResponse } from "next/server";
export async function authenticateRequest(request: Request): Promise<TAuthenticationApiKey | null> {
const apiKey = request.headers.get("x-api-key");
if (apiKey) {
const apiKeyData = await getApiKeyFromKey(apiKey);
if (apiKeyData) {
const authentication: TAuthenticationApiKey = {
type: "apiKey",
environmentId: apiKeyData.environmentId,
};
return authentication;
}
return null;
}
return null;
}
export function handleErrorResponse(error: any): NextResponse {
switch (error.message) {
case "NotAuthenticated":
return responses.notAuthenticatedResponse();
case "Unauthorized":
return responses.unauthorizedResponse();
default:
if (
error instanceof DatabaseError ||
error instanceof InvalidInputError ||
error instanceof ResourceNotFoundError
) {
return responses.badRequestResponse(error.message);
}
return responses.internalServerErrorResponse("Some error occurred");
}
}

View File

@@ -0,0 +1,93 @@
import { responses } from "@/lib/api/response";
import { NextResponse } from "next/server";
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/services/actionClass";
import { TActionClass, ZActionClassInput } from "@formbricks/types/v1/actionClasses";
import { authenticateRequest } from "@/app/api/v1/auth";
import { transformErrorToDetails } from "@/lib/api/validator";
import { TAuthenticationApiKey } from "@formbricks/types/v1/auth";
import { handleErrorResponse } from "@/app/api/v1/auth";
async function fetchAndAuthorizeActionClass(
authentication: TAuthenticationApiKey,
actionClassId: string
): Promise<TActionClass | null> {
const actionClass = await getActionClass(actionClassId);
if (!actionClass) {
return null;
}
if (actionClass.environmentId !== authentication.environmentId) {
throw new Error("Unauthorized");
}
return actionClass;
}
export async function GET(
request: Request,
{ params }: { params: { actionClassId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const actionClass = await fetchAndAuthorizeActionClass(authentication, params.actionClassId);
if (actionClass) {
return responses.successResponse(actionClass);
}
return responses.notFoundResponse("Action Class", params.actionClassId);
} catch (error) {
return handleErrorResponse(error);
}
}
export async function PUT(
request: Request,
{ params }: { params: { actionClassId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const actionClass = await fetchAndAuthorizeActionClass(authentication, params.actionClassId);
if (!actionClass) {
return responses.notFoundResponse("Action Class", params.actionClassId);
}
const actionCLassUpdate = await request.json();
const inputValidation = ZActionClassInput.safeParse(actionCLassUpdate);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error)
);
}
const updatedActionClass = await updateActionClass(
inputValidation.data.environmentId,
params.actionClassId,
inputValidation.data
);
if (updatedActionClass) {
return responses.successResponse(updatedActionClass);
}
return responses.internalServerErrorResponse("Some error ocured while updating action");
} catch (error) {
return handleErrorResponse(error);
}
}
export async function DELETE(
request: Request,
{ params }: { params: { actionClassId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const actionClass = await fetchAndAuthorizeActionClass(authentication, params.actionClassId);
if (!actionClass) {
return responses.notFoundResponse("Action Class", params.actionClassId);
}
if (actionClass.type === "automatic") {
return responses.badRequestResponse("Automatic action classes cannot be deleted");
}
const deletedActionClass = await deleteActionClass(authentication.environmentId, params.actionClassId);
return responses.successResponse(deletedActionClass);
} catch (error) {
return handleErrorResponse(error);
}
}

View File

@@ -0,0 +1,49 @@
import { responses } from "@/lib/api/response";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { authenticateRequest } from "@/app/api/v1/auth";
import { NextResponse } from "next/server";
import { TActionClass, ZActionClassInput } from "@formbricks/types/v1/actionClasses";
import { createActionClass, getActionClasses } from "@formbricks/lib/services/actionClass";
import { transformErrorToDetails } from "@/lib/api/validator";
export async function GET(request: Request) {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const actionClasses: TActionClass[] = await getActionClasses(authentication.environmentId!);
return responses.successResponse(actionClasses);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}
export async function POST(request: Request): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const actionClassInput = await request.json();
const inputValidation = ZActionClassInput.safeParse(actionClassInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
const actionClass: TActionClass = await createActionClass(
authentication.environmentId!,
inputValidation.data
);
return responses.successResponse(actionClass);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}

View File

@@ -0,0 +1,93 @@
import { responses } from "@/lib/api/response";
import { handleErrorResponse } from "@/app/api/v1/auth";
import { NextResponse } from "next/server";
import {
deleteAttributeClass,
getAttributeClass,
updatetAttributeClass,
} from "@formbricks/lib/services/attributeClass";
import { TAttributeClass, ZAttributeClassUpdateInput } from "@formbricks/types/v1/attributeClasses";
import { transformErrorToDetails } from "@/lib/api/validator";
import { authenticateRequest } from "@/app/api/v1/auth";
import { TAuthenticationApiKey } from "@formbricks/types/v1/auth";
async function fetchAndAuthorizeAttributeClass(
authentication: TAuthenticationApiKey,
attributeId: string
): Promise<TAttributeClass | null> {
const attributeClass = await getAttributeClass(attributeId);
if (!attributeClass) {
return null;
}
if (attributeClass.environmentId !== authentication.environmentId) {
throw new Error("Unauthorized");
}
return attributeClass;
}
export async function GET(
request: Request,
{ params }: { params: { attributeClassId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const attributeClass = await fetchAndAuthorizeAttributeClass(authentication, params.attributeClassId);
if (attributeClass) {
return responses.successResponse(attributeClass);
}
return responses.notFoundResponse("Attribute Class", params.attributeClassId);
} catch (error) {
return handleErrorResponse(error);
}
}
export async function DELETE(
request: Request,
{ params }: { params: { attributeClassId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const attributeClass = await fetchAndAuthorizeAttributeClass(authentication, params.attributeClassId);
if (!attributeClass) {
return responses.notFoundResponse("Attribute Class", params.attributeClassId);
}
if (attributeClass.type === "automatic") {
return responses.badRequestResponse("Automatic Attribute Classes cannot be deleted");
}
const deletedAttributeClass = await deleteAttributeClass(params.attributeClassId);
return responses.successResponse(deletedAttributeClass);
} catch (error) {
return handleErrorResponse(error);
}
}
export async function PUT(
request: Request,
{ params }: { params: { attributeClassId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const attributeClass = await fetchAndAuthorizeAttributeClass(authentication, params.attributeClassId);
if (!attributeClass) {
return responses.notFoundResponse("Attribute Class", params.attributeClassId);
}
const attributeClassUpdate = await request.json();
const inputValidation = ZAttributeClassUpdateInput.safeParse(attributeClassUpdate);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error)
);
}
const updatedAttributeClass = await updatetAttributeClass(params.attributeClassId, inputValidation.data);
if (updatedAttributeClass) {
return responses.successResponse(updatedAttributeClass);
}
return responses.internalServerErrorResponse("Some error ocured while updating action");
} catch (error) {
return handleErrorResponse(error);
}
}

View File

@@ -0,0 +1,53 @@
import { responses } from "@/lib/api/response";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { authenticateRequest } from "@/app/api/v1/auth";
import { NextResponse } from "next/server";
import { transformErrorToDetails } from "@/lib/api/validator";
import { TAttributeClass, ZAttributeClassInput } from "@formbricks/types/v1/attributeClasses";
import { createAttributeClass, getAttributeClasses } from "@formbricks/lib/services/attributeClass";
export async function GET(request: Request) {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const atributeClasses: TAttributeClass[] = await getAttributeClasses(authentication.environmentId!);
return responses.successResponse(atributeClasses);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}
export async function POST(request: Request): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const attributeClassInput = await request.json();
const inputValidation = ZAttributeClassInput.safeParse(attributeClassInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
const attributeClass: TAttributeClass | null = await createAttributeClass(
authentication.environmentId,
inputValidation.data.name,
inputValidation.data.type
);
if (!attributeClass) {
return responses.internalServerErrorResponse("Failed creating attribute class");
}
return responses.successResponse(attributeClass);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}

View File

@@ -0,0 +1,77 @@
import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
import { responses } from "@/lib/api/response";
import { deletePerson, getPerson } from "@formbricks/lib/services/person";
import { TAuthenticationApiKey } from "@formbricks/types/v1/auth";
import { TPerson } from "@formbricks/types/v1/people";
import { NextResponse } from "next/server";
async function fetchAndAuthorizePerson(
authentication: TAuthenticationApiKey,
personId: string
): Promise<TPerson | null> {
const person = await getPerson(personId);
if (!person) {
return null;
}
if (person.environmentId !== authentication.environmentId) {
throw new Error("Unauthorized");
}
return person;
}
export async function GET(
request: Request,
{ params }: { params: { personId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const person = await fetchAndAuthorizePerson(authentication, params.personId);
if (person) {
return responses.successResponse(person);
}
return responses.notFoundResponse("Person", params.personId);
} catch (error) {
return handleErrorResponse(error);
}
}
// Please use the methods provided by the client API to update a person
/* export async function PUT(
request: Request,
{ params }: { params: { personId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
await fetchAndAuthorizePerson(authentication, params.personId);
const personUpdate = await request.json();
const inputValidation = ZPersonUpdateInput.safeParse(personUpdate);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error)
);
}
return responses.successResponse(await updatePerson(params.personId, inputValidation.data));
} catch (error) {
return handleErrorResponse(error);
}
} */
export async function DELETE(request: Request, { params }: { params: { personId: string } }) {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const person = await fetchAndAuthorizePerson(authentication, params.personId);
if (!person) {
return responses.notFoundResponse("Person", params.personId);
}
await deletePerson(params.personId);
return responses.successResponse({ success: "Person deleted successfully" });
} catch (error) {
return handleErrorResponse(error);
}
}

View File

@@ -0,0 +1,35 @@
import { authenticateRequest } from "@/app/api/v1/auth";
import { responses } from "@/lib/api/response";
import { getPeople } from "@formbricks/lib/services/person";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { TPerson } from "@formbricks/types/v1/people";
export async function GET(request: Request) {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const people: TPerson[] = await getPeople(authentication.environmentId!);
return responses.successResponse(people);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}
// Please use the client API to create a new person
/* export async function POST(request: Request): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const person: TPerson = await createPerson(authentication.environmentId);
return responses.successResponse(person);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
} */

View File

@@ -0,0 +1,88 @@
import { responses } from "@/lib/api/response";
import { NextResponse } from "next/server";
import { transformErrorToDetails } from "@/lib/api/validator";
import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/services/response";
import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses";
import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper";
import { getSurvey } from "@formbricks/lib/services/survey";
import { authenticateRequest } from "@/app/api/v1/auth";
import { handleErrorResponse } from "@/app/api/v1/auth";
async function fetchAndValidateResponse(authentication: any, responseId: string): Promise<TResponse> {
const response = await getResponse(responseId);
if (!response || !(await canUserAccessResponse(authentication, response))) {
throw new Error("Unauthorized");
}
return response;
}
const canUserAccessResponse = async (authentication: any, response: TResponse): Promise<boolean> => {
const survey = await getSurvey(response.surveyId);
if (!survey) return false;
if (authentication.type === "session") {
return await hasUserEnvironmentAccess(authentication.session.user, survey.environmentId);
} else if (authentication.type === "apiKey") {
return survey.environmentId === authentication.environmentId;
} else {
throw Error("Unknown authentication type");
}
};
export async function GET(
request: Request,
{ params }: { params: { responseId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
await fetchAndValidateResponse(authentication, params.responseId);
const response = await fetchAndValidateResponse(authentication, params.responseId);
if (response) {
return responses.successResponse(response);
}
return responses.notFoundResponse("Response", params.responseId);
} catch (error) {
return handleErrorResponse(error);
}
}
export async function DELETE(
request: Request,
{ params }: { params: { responseId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const response = await fetchAndValidateResponse(authentication, params.responseId);
if (!response) {
return responses.notFoundResponse("Response", params.responseId);
}
const deletedResponse = await deleteResponse(params.responseId);
return responses.successResponse(deletedResponse);
} catch (error) {
return handleErrorResponse(error);
}
}
export async function PUT(
request: Request,
{ params }: { params: { responseId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
await fetchAndValidateResponse(authentication, params.responseId);
const responseUpdate = await request.json();
const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error)
);
}
return responses.successResponse(await updateResponse(params.responseId, inputValidation.data));
} catch (error) {
return handleErrorResponse(error);
}
}

View File

@@ -0,0 +1,20 @@
import { responses } from "@/lib/api/response";
import { getEnvironmentResponses } from "@formbricks/lib/services/response";
import { authenticateRequest } from "@/app/api/v1/auth";
import { DatabaseError } from "@formbricks/types/v1/errors";
export async function GET(request: Request) {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const responseArray = await getEnvironmentResponses(authentication.environmentId!);
return responses.successResponse(responseArray);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}
// Please use the client API to create a new response

View File

@@ -0,0 +1,78 @@
import { responses } from "@/lib/api/response";
import { NextResponse } from "next/server";
import { getSurvey, updateSurvey, deleteSurvey } from "@formbricks/lib/services/survey";
import { TSurvey, ZSurveyInput } from "@formbricks/types/v1/surveys";
import { transformErrorToDetails } from "@/lib/api/validator";
import { authenticateRequest } from "@/app/api/v1/auth";
import { handleErrorResponse } from "@/app/api/v1/auth";
async function fetchAndAuthorizeSurvey(authentication: any, surveyId: string): Promise<TSurvey | null> {
const survey = await getSurvey(surveyId);
if (!survey) {
return null;
}
if (survey.environmentId !== authentication.environmentId) {
throw new Error("Unauthorized");
}
return survey;
}
export async function GET(
request: Request,
{ params }: { params: { surveyId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const survey = await fetchAndAuthorizeSurvey(authentication, params.surveyId);
if (survey) {
return responses.successResponse(survey);
}
return responses.notFoundResponse("Survey", params.surveyId);
} catch (error) {
return handleErrorResponse(error);
}
}
export async function DELETE(
request: Request,
{ params }: { params: { surveyId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const survey = await fetchAndAuthorizeSurvey(authentication, params.surveyId);
if (!survey) {
return responses.notFoundResponse("Survey", params.surveyId);
}
const deletedSurvey = await deleteSurvey(params.surveyId);
return responses.successResponse(deletedSurvey);
} catch (error) {
return handleErrorResponse(error);
}
}
export async function PUT(
request: Request,
{ params }: { params: { surveyId: string } }
): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const survey = await fetchAndAuthorizeSurvey(authentication, params.surveyId);
if (!survey) {
return responses.notFoundResponse("Survey", params.surveyId);
}
const surveyUpdate = await request.json();
const inputValidation = ZSurveyInput.safeParse(surveyUpdate);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error)
);
}
return responses.successResponse(await updateSurvey(inputValidation.data));
} catch (error) {
return handleErrorResponse(error);
}
}

View File

@@ -0,0 +1,49 @@
import { responses } from "@/lib/api/response";
import { authenticateRequest } from "@/app/api/v1/auth";
import { NextResponse } from "next/server";
import { transformErrorToDetails } from "@/lib/api/validator";
import { createSurvey, getSurveys } from "@formbricks/lib/services/survey";
import { ZSurveyInput } from "@formbricks/types/v1/surveys";
import { DatabaseError } from "@formbricks/types/v1/errors";
export async function GET(request: Request) {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const surveys = await getSurveys(authentication.environmentId!);
return responses.successResponse(surveys);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}
export async function POST(request: Request): Promise<NextResponse> {
try {
const authentication = await authenticateRequest(request);
if (!authentication) return responses.notAuthenticatedResponse();
const surveyInput = await request.json();
const inputValidation = ZSurveyInput.safeParse(surveyInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
const environmentId = authentication.environmentId;
const surveyData = { ...inputValidation.data, environmentId: undefined };
const survey = await createSurvey(environmentId, surveyData);
return responses.successResponse(survey);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}

View File

@@ -1,50 +0,0 @@
import { responses } from "@/lib/api/response";
import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey";
import { getEnvironmentResponses, getSurveyResponses } from "@formbricks/lib/services/response";
import { headers } from "next/headers";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { getSurvey } from "@formbricks/lib/services/survey";
export async function GET(request: Request) {
const apiKey = headers().get("x-api-key");
if (!apiKey) {
return responses.notAuthenticatedResponse();
}
let apiKeyData;
try {
apiKeyData = await getApiKeyFromKey(apiKey);
if (!apiKeyData) {
return responses.notAuthenticatedResponse();
}
} catch (error) {
return responses.notAuthenticatedResponse();
}
// get surveyId from searchParams
const { searchParams } = new URL(request.url);
const surveyId = searchParams.get("surveyId");
// get responses from database
try {
if (!surveyId) {
const environmentResponses = await getEnvironmentResponses(apiKeyData.environmentId);
return responses.successResponse(environmentResponses);
}
// check if survey is part of environment
const survey = await getSurvey(surveyId);
if (!survey) {
return responses.notFoundResponse(surveyId, "survey");
}
if (survey.environmentId !== apiKeyData.environmentId) {
return responses.notFoundResponse(surveyId, "survey");
}
// get responses for survey
const surveyResponses = await getSurveyResponses(surveyId);
return responses.successResponse(surveyResponses);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}

View File

@@ -1,32 +0,0 @@
import { responses } from "@/lib/api/response";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey";
import { getSurveys } from "@formbricks/lib/services/survey";
import { headers } from "next/headers";
export async function GET() {
const apiKey = headers().get("x-api-key");
if (!apiKey) {
return responses.notAuthenticatedResponse();
}
let apiKeyData;
try {
apiKeyData = await getApiKeyFromKey(apiKey);
if (!apiKeyData) {
return responses.notAuthenticatedResponse();
}
} catch (error) {
return responses.notAuthenticatedResponse();
}
// get surveys from database
try {
const surveys = await getSurveys(apiKeyData.environmentId);
return responses.successResponse(surveys);
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}

View File

@@ -12,12 +12,7 @@ const nextConfig = {
experimental: {
serverActions: true,
},
transpilePackages: [
"@formbricks/database",
"@formbricks/ee",
"@formbricks/ui",
"@formbricks/lib",
],
transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"],
images: {
remotePatterns: [
{
@@ -30,6 +25,25 @@ const nextConfig = {
},
],
},
async redirects() {
return [
{
source: "/api/v1/responses",
destination: "/api/v1/management/responses",
permanent: true,
},
{
source: "/api/v1/surveys",
destination: "/api/v1/management/surveys",
permanent: true,
},
{
source: "/api/v1/me",
destination: "/api/v1/management/me",
permanent: true,
},
];
},
async headers() {
return [
{

View File

@@ -127,6 +127,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
attributes: attributes,
createdAt: person.createdAt,
updatedAt: person.updatedAt,
environmentId: environmentId,
};
};

View File

@@ -172,6 +172,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
attributes: attributes,
createdAt: person.createdAt,
updatedAt: person.updatedAt,
environmentId: environmentId,
};
};

View File

@@ -14,8 +14,8 @@ import { addSyncEventListener, removeSyncEventListener } from "./sync";
let areRemoveEventListenersAdded = false;
export const addEventListeners = (): void => {
addSyncEventListener();
export const addEventListeners = (debug: boolean = false): void => {
addSyncEventListener(debug);
addPageUrlEventListeners();
addClickEventListener();
addExitIntentListener();

View File

@@ -97,7 +97,7 @@ export const initialize = async (
}
logger.debug("Adding event listeners");
addEventListeners();
addEventListeners(c.debug);
addCleanupEventListeners();
isInitialized = true;

View File

@@ -64,6 +64,22 @@ export const getActionClassesCached = (environmentId: string) =>
}
)();
export const getActionClass = async (actionClassId: string): Promise<TActionClass | null> => {
validateInputs([actionClassId, ZId]);
try {
let actionClass = await prisma.eventClass.findUnique({
where: {
id: actionClassId,
},
select,
});
return actionClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching action`);
}
};
export const deleteActionClass = async (
environmentId: string,
actionClassId: string

View File

@@ -3,9 +3,9 @@ import "server-only";
import { prisma } from "@formbricks/database";
import {
TAttributeClass,
TAttributeClassType,
TAttributeClassUpdateInput,
ZAttributeClassUpdateInput,
TAttributeClassType,
} from "@formbricks/types/v1/attributeClasses";
import { ZId } from "@formbricks/types/v1/environment";
import { validateInputs } from "../utils/validate";
@@ -52,6 +52,21 @@ export const getAttributeClasses = cache(async (environmentId: string): Promise<
}
});
export const getAttributeClass = async (attributeClassId: string): Promise<TAttributeClass | null> => {
validateInputs([attributeClassId, ZId]);
try {
let attributeClass = await prisma.attributeClass.findUnique({
where: {
id: attributeClassId,
},
});
return attributeClass;
} catch (error) {
throw new DatabaseError(`Database error when fetching attributeClass with id ${attributeClassId}`);
}
};
export const updatetAttributeClass = async (
attributeClassId: string,
data: Partial<TAttributeClassUpdateInput>
@@ -119,3 +134,18 @@ export const createAttributeClass = async (
revalidateTag(attributeClassesCacheTag(environmentId));
return transformPrismaAttributeClass(attributeClass);
};
export const deleteAttributeClass = async (attributeClassId: string): Promise<TAttributeClass> => {
validateInputs([attributeClassId, ZId]);
try {
const deletedAttributeClass = await prisma.attributeClass.delete({
where: {
id: attributeClassId,
},
});
return deletedAttributeClass;
} catch (error) {
throw new DatabaseError(`Database error when deleting webhook with ID ${attributeClassId}`);
}
};

View File

@@ -23,6 +23,7 @@ const selectDisplay = {
id: true,
createdAt: true,
updatedAt: true,
environmentId: true,
attributes: {
select: {
value: true,

View File

@@ -30,7 +30,7 @@ export async function createOrUpdateIntegration(
return integration;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.log(error);
console.error(error);
throw new DatabaseError("Database operation failed");
}
throw error;

View File

@@ -3,7 +3,7 @@ import "server-only";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/v1/environment";
import { DatabaseError } from "@formbricks/types/v1/errors";
import { TPerson } from "@formbricks/types/v1/people";
import { TPerson, TPersonUpdateInput } from "@formbricks/types/v1/people";
import { Prisma } from "@prisma/client";
import { revalidateTag, unstable_cache } from "next/cache";
import { cache } from "react";
@@ -15,6 +15,7 @@ export const selectPerson = {
id: true,
createdAt: true,
updatedAt: true,
environmentId: true,
attributes: {
where: {
attributeClass: {
@@ -34,6 +35,7 @@ export const selectPerson = {
type TransformPersonInput = {
id: string;
environmentId: string;
attributes: {
value: string;
attributeClass: {
@@ -56,6 +58,7 @@ export const transformPrismaPerson = (person: TransformPersonInput): TPerson =>
return {
id: person.id,
attributes: attributes,
environmentId: person.environmentId,
createdAt: person.createdAt,
updatedAt: person.updatedAt,
};
@@ -201,6 +204,26 @@ export const deletePerson = async (personId: string): Promise<void> => {
}
};
export const updatePerson = async (personId: string, personInput: TPersonUpdateInput): Promise<TPerson> => {
try {
const personPrisma = await prisma.person.update({
where: {
id: personId,
},
data: personInput,
select: selectPerson,
});
const person = transformPrismaPerson(personPrisma);
return person;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};
export const getOrCreatePersonByUserId = async (userId: string, environmentId: string): Promise<TPerson> => {
// Check if a person with the userId attribute exists
const personPrisma = await prisma.person.findFirst({

View File

@@ -140,3 +140,29 @@ export const deleteProfile = async (personId: string): Promise<void> => {
throw error;
}
};
export async function getUserIdFromEnvironment(environmentId: string) {
const environment = await prisma.environment.findUnique({
where: { id: environmentId },
select: {
product: {
select: {
team: {
select: {
memberships: {
select: {
user: {
select: {
id: true,
},
},
},
},
},
},
},
},
},
});
return environment?.product.team.memberships[0].user.id;
}

View File

@@ -31,6 +31,7 @@ const responseSelection = {
id: true,
createdAt: true,
updatedAt: true,
environmentId: true,
attributes: {
select: {
value: true,
@@ -223,6 +224,7 @@ export const getSurveyResponses = cache(async (surveyId: string): Promise<TRespo
});
export const preloadEnvironmentResponses = (environmentId: string) => {
validateInputs([environmentId, ZId]);
void getEnvironmentResponses(environmentId);
};
@@ -303,3 +305,21 @@ export const updateResponse = async (
throw error;
}
};
export async function deleteResponse(responseId: string) {
validateInputs([responseId, ZId]);
try {
const deletedResponse = await prisma.response.delete({
where: {
id: responseId,
},
});
return deletedResponse;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
}

View File

@@ -1,15 +1,20 @@
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/v1/environment";
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors";
import { TSurvey, TSurveyWithAnalytics, ZSurvey, ZSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
import {
TSurvey,
TSurveyAttributeFilter,
TSurveyWithAnalytics,
ZSurvey,
ZSurveyWithAnalytics,
} from "@formbricks/types/v1/surveys";
import { Prisma } from "@prisma/client";
import { TSurveyAttributeFilter } from "@formbricks/types/v1/surveys";
import { revalidateTag } from "next/cache";
import { cache } from "react";
import "server-only";
import { revalidateTag } from "next/cache";
import { z } from "zod";
import { captureTelemetry } from "../telemetry";
import { validateInputs } from "../utils/validate";
import { ZId } from "@formbricks/types/v1/environment";
const getSurveysCacheTag = (environmentId: string): string => `env-${environmentId}-surveys`;
@@ -123,7 +128,6 @@ export const getSurveyWithAnalytics = cache(
const survey = ZSurveyWithAnalytics.parse(transformedSurvey);
return survey;
} catch (error) {
console.log(error);
if (error instanceof z.ZodError) {
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
}
@@ -256,10 +260,10 @@ export const getSurveysWithAnalytics = cache(
}
);
export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
export async function updateSurvey(updatedSurvey: Partial<TSurvey>): Promise<TSurvey> {
const surveyId = updatedSurvey.id;
let data: any = {};
let survey: Partial<any> = { ...updatedSurvey };
let survey: any = { ...updatedSurvey };
if (updatedSurvey.triggers && updatedSurvey.triggers.length > 0) {
const modifiedTriggers = updatedSurvey.triggers.map((trigger) => {
@@ -423,14 +427,15 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
const modifiedSurvey: TSurvey = {
...prismaSurvey, // Properties from prismaSurvey
triggers: updatedSurvey.triggers, // Include triggers from updatedSurvey
attributeFilters: updatedSurvey.attributeFilters, // Include attributeFilters from updatedSurvey
triggers: updatedSurvey.triggers ? updatedSurvey.triggers : [], // Include triggers from updatedSurvey
attributeFilters: updatedSurvey.attributeFilters ? updatedSurvey.attributeFilters : [], // Include attributeFilters from updatedSurvey
};
revalidateTag(getSurveysCacheTag(modifiedSurvey.environmentId));
return modifiedSurvey;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}

View File

@@ -3,12 +3,6 @@ import { VNode } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { cn } from "../lib/utils";
// CSS classes object
const mobileClasses = {
show: "translate-y-full",
hide: "translate-y-0",
};
interface ModalProps {
children: VNode;
isOpen: boolean;
@@ -29,7 +23,6 @@ export default function Modal({
onClose,
}: ModalProps) {
const [show, setShow] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const isCenter = placement === "center";
const modalRef = useRef(null);
@@ -56,21 +49,6 @@ export default function Modal({
};
}, [show, clickOutside, onClose, isCenter]);
const handleMobileClasses = (isMobile: boolean, show: boolean) => {
return isMobile ? (show ? mobileClasses.show : mobileClasses.hide) : "";
};
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 640);
};
window.addEventListener("resize", handleResize);
handleResize();
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
// This classes will be applied only when screen size is greater than sm, hence sm is common prefix for all
const getPlacementStyle = (placement: PlacementType) => {
switch (placement) {
@@ -114,8 +92,8 @@ export default function Modal({
"relative h-full w-full",
isCenter
? darkOverlay
? "sm:bg-gray-700/80"
: "sm:bg-white/50"
? "bg-gray-700/80"
: "bg-white/50"
: "bg-none transition-all duration-500 ease-in-out"
)}>
<div
@@ -123,9 +101,7 @@ export default function Modal({
className={cn(
getPlacementStyle(placement),
show ? "opacity-100" : "opacity-0",
"pointer-events-auto absolute h-fit w-full overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm",
isMobile ? "top-full" : "",
handleMobileClasses(isMobile, show)
"pointer-events-auto absolute bottom-0 h-fit w-full overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
)}>
{!isCenter && (
<div class="absolute right-0 top-0 block pr-4 pt-4">

View File

@@ -37,10 +37,11 @@ export const ZActionClass = z.object({
export type TActionClass = z.infer<typeof ZActionClass>;
export const ZActionClassInput = z.object({
environmentId: z.string(),
name: z.string(),
description: z.union([z.string(), z.null()]),
noCodeConfig: z.union([ZActionClassNoCodeConfig, z.null()]),
type: z.enum(["code", "noCode", "automatic"]),
description: z.string().optional(),
noCodeConfig: ZActionClassNoCodeConfig.nullish(),
type: z.enum(["code", "noCode"]),
});
export type TActionClassInput = z.infer<typeof ZActionClassInput>;

View File

@@ -9,16 +9,27 @@ export const ZAttributeClass = z.object({
createdAt: z.date(),
updatedAt: z.date(),
name: z.string(),
description: z.string(),
description: z.string().nullable(),
type: ZAttributeClassType,
environmentId: z.string(),
archived: z.boolean(),
});
export const ZAttributeClassUpdateInput = z.object({
export const ZAttributeClassInput = z.object({
name: z.string(),
description: z.string(),
archived: z.boolean(),
type: z.enum(["code"]),
environmentId: z.string(),
});
export const ZAttributeClassUpdateInput = z.object({
name: z.string(),
description: z.string().optional(),
archived: z.boolean().optional(),
});
export type TAttributeClassUpdateInput = z.infer<typeof ZAttributeClassUpdateInput>;
export type TAttributeClassInput = z.infer<typeof ZAttributeClassInput>;
export type TAttributeClass = z.infer<typeof ZAttributeClass>;

32
packages/types/v1/auth.ts Normal file
View File

@@ -0,0 +1,32 @@
import { z } from "zod";
const StaticImageData = z.object({
// Placeholder schema for StaticImageData
// ... (fill in with actual keys and types for the StaticImageData)
});
const ZAuthSession = z.object({
user: z.object({
id: z.string(),
createdAt: z.string(),
teams: z.array(
z.object({
id: z.string(),
plan: z.string(),
role: z.string(),
})
),
email: z.string(),
name: z.string(),
onboardingCompleted: z.boolean(),
image: StaticImageData.optional(),
}),
});
const ZAuthenticationApiKey = z.object({
type: z.literal("apiKey"),
environmentId: z.string(),
});
export type TAuthSession = z.infer<typeof ZAuthSession>;
export type TAuthenticationApiKey = z.infer<typeof ZAuthenticationApiKey>;

View File

@@ -8,6 +8,12 @@ export const ZPerson = z.object({
attributes: ZPersonAttributes,
createdAt: z.date(),
updatedAt: z.date(),
environmentId: z.string().cuid2(),
});
export const ZPersonUpdateInput = z.object({
attributes: ZPersonAttributes,
});
export type TPersonUpdateInput = z.infer<typeof ZPersonUpdateInput>;
export type TPerson = z.infer<typeof ZPerson>;

View File

@@ -1,5 +1,5 @@
import { z } from "zod";
import { ZPersonAttributes } from "./people";
import { ZPerson, ZPersonAttributes } from "./people";
import { ZSurvey } from "./surveys";
import { ZTag } from "./tags";
@@ -46,14 +46,7 @@ export const ZResponse = z.object({
createdAt: z.date(),
updatedAt: z.date(),
surveyId: z.string().cuid2(),
person: z
.object({
id: z.string().cuid2(),
attributes: z.record(z.union([z.string(), z.number()])),
createdAt: z.date(),
updatedAt: z.date(),
})
.nullable(),
person: ZPerson.nullable(),
personAttributes: ZResponsePersonAttributes,
finished: z.boolean(),
data: ZResponseData,

View File

@@ -226,37 +226,65 @@ export const ZSurveyQuestions = z.array(ZSurveyQuestion);
export type TSurveyQuestions = z.infer<typeof ZSurveyQuestions>;
export const ZSurveyAttributeFilter = z.object({
attributeClassId: z.string(),
attributeClassId: z.string().cuid2(),
condition: z.enum(["equals", "notEquals"]),
value: z.string(),
});
export type TSurveyAttributeFilter = z.infer<typeof ZSurveyAttributeFilter>;
const ZSurveyDisplayOption = z.enum(["displayOnce", "displayMultiple", "respondMultiple"]);
const ZSurveyType = z.enum(["web", "email", "link", "mobile"]);
const ZSurveyStatus = z.enum(["draft", "inProgress", "paused", "completed"]);
export const ZSurvey = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
updatedAt: z.date(),
name: z.string(),
type: z.enum(["web", "email", "link", "mobile"]),
type: ZSurveyType,
environmentId: z.string(),
status: z.enum(["draft", "inProgress", "paused", "completed"]),
status: ZSurveyStatus,
attributeFilters: z.array(ZSurveyAttributeFilter),
displayOption: z.enum(["displayOnce", "displayMultiple", "respondMultiple"]),
autoClose: z.union([z.number(), z.null()]),
displayOption: ZSurveyDisplayOption,
autoClose: z.number().nullable(),
triggers: z.array(ZActionClass),
redirectUrl: z.string().url().nullable(),
recontactDays: z.union([z.number(), z.null()]),
recontactDays: z.number().nullable(),
questions: ZSurveyQuestions,
thankYouCard: ZSurveyThankYouCard,
delay: z.number(),
autoComplete: z.union([z.number(), z.null()]),
autoComplete: z.number().nullable(),
closeOnDate: z.date().nullable(),
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
verifyEmail: ZSurveyVerifyEmail.nullable(),
});
export const ZSurveyInput = z.object({
name: z.string(),
type: ZSurveyType.optional(),
environmentId: z.string(),
status: ZSurveyStatus.optional(),
displayOption: ZSurveyDisplayOption.optional(),
autoClose: z.number().optional(),
redirectUrl: z.string().url().optional(),
recontactDays: z.number().optional(),
questions: ZSurveyQuestions.optional(),
thankYouCard: ZSurveyThankYouCard.optional(),
delay: z.number().optional(),
autoComplete: z.number().optional(),
closeOnDate: z.date().optional(),
surveyClosedMessage: ZSurveyClosedMessage.optional(),
verifyEmail: ZSurveyVerifyEmail.optional(),
// TODO: Update survey create endpoint to accept attributeFilters and triggers like the survey update endpoint
// attributeFilters: z.array(ZSurveyAttributeFilter).optional(),
//triggers: z.array(ZActionClass).optional(),
});
export type TSurvey = z.infer<typeof ZSurvey>;
export type TSurveyInput = z.infer<typeof ZSurveyInput>;
export const ZSurveyWithAnalytics = ZSurvey.extend({
analytics: z.object({