mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-22 06:00:51 -06:00
Compare commits
30 Commits
v1.2.2
...
@formbrick
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
888dbbcfd2 | ||
|
|
73711b4631 | ||
|
|
bd0b8ecd66 | ||
|
|
fefc27aadd | ||
|
|
8eb0cf3207 | ||
|
|
6a40ed705d | ||
|
|
53ef8771f3 | ||
|
|
ac8cf987d3 | ||
|
|
11ede2e517 | ||
|
|
aa6d6df178 | ||
|
|
c2675a9d49 | ||
|
|
84ce0c267c | ||
|
|
a2df7abf85 | ||
|
|
7acd2ccabb | ||
|
|
a34606ab03 | ||
|
|
dd0f0ead39 | ||
|
|
d6c9ce7c5b | ||
|
|
0b253e6ba5 | ||
|
|
dd0091e52c | ||
|
|
c45248ada8 | ||
|
|
ca21c9cea7 | ||
|
|
10ab71b20f | ||
|
|
2acd18d8d5 | ||
|
|
27b99d9761 | ||
|
|
536e610895 | ||
|
|
c40fedda90 | ||
|
|
c74b3034fd | ||
|
|
72fb1b3b30 | ||
|
|
5859b51b8b | ||
|
|
d51e17fe2e |
@@ -122,7 +122,7 @@ GOOGLE_SHEETS_CLIENT_SECRET=
|
||||
GOOGLE_SHEETS_REDIRECT_URL=
|
||||
|
||||
# Oauth credentials for Airtable integration
|
||||
AIR_TABLE_CLIENT_ID=
|
||||
AIRTABLE_CLIENT_ID=
|
||||
|
||||
# Enterprise License Key
|
||||
ENTERPRISE_LICENSE_KEY=
|
||||
|
||||
@@ -10,13 +10,13 @@ jobs:
|
||||
cron-reportUsageToStripe:
|
||||
env:
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.API_KEY }}
|
||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL }}/api/cron/report-usage \
|
||||
-X GET \
|
||||
-H 'x-api-key: ${{ env.API_KEY }}' \
|
||||
-X POST \
|
||||
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
|
||||
--fail
|
||||
|
||||
93
.github/workflows/release-docker-github.yml
vendored
Normal file
93
.github/workflows/release-docker-github.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
name: Docker
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Install the cosign tool except on PR
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
|
||||
with:
|
||||
cosign-release: 'v2.1.1'
|
||||
|
||||
# Set up BuildKit Docker container builder to be able to build
|
||||
# multi-platform images and export cache
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Sign the resulting Docker image digest except on PRs.
|
||||
# This will only write to the public Rekor transparency log when the Docker
|
||||
# repository is public to avoid leaking data. If you would like to publish
|
||||
# transparency data even for private images, pass --force to cosign below.
|
||||
# https://github.com/sigstore/cosign
|
||||
- name: Sign the published Docker image
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
env:
|
||||
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
# This step uses the identity token to provision an ephemeral certificate
|
||||
# against the sigstore community Fulcio instance.
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
@@ -60,11 +60,10 @@ Community managers will follow these Community Impact Guidelines in determining
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
|
||||
|
||||
@@ -6,7 +6,7 @@ Discover a myriad of ways to leave your mark on Formbricks — whether it's by s
|
||||
|
||||
## 🐛 Issue Hunters
|
||||
|
||||
Did you stumble upon a bug? Encountered a hiccup in deployment? Perhaps you have some user feedback to share? Your quickest route to help us out is by [raising an issue](https://github.com/formbricks/formbricks/issues/new/choose). We're on standby to respond swiftly.
|
||||
Did you stumble upon a bug? Encountered a hiccup in deployment? Perhaps you have some user feedback to share? Your quickest route to help us out is by [raising an issue](https://github.com/formbricks/formbricks/issues/new/choose). We're on standby to respond swiftly.
|
||||
|
||||
## 💡 Feature Architects
|
||||
|
||||
@@ -20,13 +20,12 @@ Ready to dive into the code and make a real impact? Here's your path:
|
||||
|
||||
1. **Fork the Repository:** Fork our repository or use [Gitpod](https://formbricks.com/docs/contributing/gitpod)
|
||||
|
||||
2. **Tweak and Transform:** Work your coding magic and apply your changes.
|
||||
1. **Tweak and Transform:** Work your coding magic and apply your changes.
|
||||
|
||||
3. **Pull Request Act:** If you're ready to go, craft a new pull request closely following our PR template 🙏
|
||||
1. **Pull Request Act:** If you're ready to go, craft a new pull request closely following our PR template 🙏
|
||||
|
||||
Would you prefer a chat before you dive into a lot of work? Our [Discord server](https://formbricks.com/discord) is your harbor. Share your thoughts, and we'll meet you there with open arms. We're responsive and friendly, promise!
|
||||
|
||||
|
||||
## 🚀 Aspiring Features
|
||||
|
||||
If you spot a feature that isn't part of our official plan but could propel Formbricks forward, don't hesitate. Raise it as an enhancement issue, and let us know you're ready to take the lead. We'll be quick to respond.
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<div id="top"></div>
|
||||
|
||||
[<img src="ph.png">](https://www.producthunt.com/posts/formbricks)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://formbricks.com">
|
||||
<img width="120" alt="Open Source Experience Management Solution Qualtrics Alternative Logo" src="https://github.com/formbricks/formbricks/assets/72809645/0086704f-bee7-4d38-9cc8-fa42ee59e004">
|
||||
@@ -18,7 +15,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/formbricks/formbricks/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL-purple" alt="License"></a> <a href="https://formbricks.com/discord"><img src="https://img.shields.io/discord/979077669410979880?label=Discord&logo=discord&logoColor=%23fff" alt="Join Formbricks Discord"></a> <a href="https://github.com/formbricks/formbricks/stargazers"><img src="https://img.shields.io/github/stars/formbricks/formbricks?logo=github" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=32303986"><img src="https://img.shields.io/badge/Hacker%20News-122-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="https://www.producthunt.com/products/snoopforms"><img src="https://img.shields.io/badge/Product%20Hunt-%232%20Product%20of%20the%20Day-orange?logo=producthunt&logoColor=%23fff" alt="Product Hunt"></a>
|
||||
<a href="[https://www.producthunt.com/products/formbricks](https://www.producthunt.com/posts/formbricks)"><img src="https://img.shields.io/badge/Product%20Hunt-455-orange?logo=producthunt&logoColor=%23fff" alt="Product Hunt"></a>
|
||||
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next/"><img src="https://img.shields.io/badge/2023-blue?logo=github&label=Github%20Accelerator" alt="Github Accelerator"></a>
|
||||
<a href="https://github.com/formbricks/formbricks/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||
</p>
|
||||
|
||||
@@ -1,38 +1,8 @@
|
||||
import formbricks from "@formbricks/js";
|
||||
import type { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import "../styles/globals.css";
|
||||
|
||||
declare const window: any;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
formbricks.init({
|
||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
debug: true,
|
||||
});
|
||||
window.formbricks = formbricks;
|
||||
}
|
||||
}
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Connect next.js router to Formbricks
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
const handleRouteChange = formbricks?.registerRouteChange;
|
||||
router.events.on("routeChangeComplete", handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handleRouteChange);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
||||
@@ -2,9 +2,13 @@ import formbricks from "@formbricks/js";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import fbsetup from "../../public/fb-setup.png";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
declare const window: any;
|
||||
|
||||
export default function AppPage({}) {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
@@ -14,6 +18,30 @@ export default function AppPage({}) {
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
const isUserId = window.location.href.includes("userId=true");
|
||||
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,
|
||||
debug: true,
|
||||
});
|
||||
window.formbricks = formbricks;
|
||||
}
|
||||
|
||||
// Connect next.js router to Formbricks
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
const handleRouteChange = formbricks?.registerRouteChange;
|
||||
router.events.on("routeChangeComplete", handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handleRouteChange);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col justify-between md:flex-row">
|
||||
@@ -204,25 +232,37 @@ export default function AppPage({}) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
formbricks.setUserId("THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
Set User ID
|
||||
</button>
|
||||
</div>
|
||||
{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-gray-700 dark:hover:bg-gray-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-gray-700 dark:hover:bg-gray-600">
|
||||
Activate User Identification
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
This button sets an external{" "}
|
||||
This button activates/deactivates{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/attributes/identify-users"
|
||||
target="_blank"
|
||||
className="underline dark:text-blue-500">
|
||||
user ID
|
||||
user identification
|
||||
</a>{" "}
|
||||
to 'THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING'
|
||||
with the userId 'THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING'
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,10 @@ export const metadata = {
|
||||
|
||||
# Code Actions
|
||||
|
||||
Actions can also be set in the code base. You can fire an action using `formbricks.track()`
|
||||
Actions can also be set in the codebase to trigger surveys. Please add the code action first in the Formbricks web interface to be able to configure your surveys to use this action.
|
||||
|
||||
After that you can fire an action using `formbricks.track()`
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Track an action">
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ This set of API can be used to
|
||||
<CodeGroup title="Request" tag="POST" label="/api/v1/management/surveys">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X DELETE \
|
||||
curl -X POST \
|
||||
'https://app.formbricks.com/api/v1/management/surveys' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
|
||||
@@ -10,23 +10,29 @@ export const metadata = {
|
||||
|
||||
At Formbricks, we value user privacy. By default, Formbricks doesn't collect or store any personal information from your users. However, we understand that it can be helpful for you to know which user submitted the feedback and also functionality like recontacting users and controlling the waiting period between surveys requires identifying the users. That's why we provide a way for you to share existing user data from your app, so you can view it in our dashboard.
|
||||
|
||||
Once the Formbricks widget is loaded on your web app, our SDK exposes methods for identifying user attributes. Let's set it up!
|
||||
If you would like to use the User Identification feature of Formbricks, target surveys to specific user segments and see more information about the user who responded to a survey, you can identify users by setting a User ID, email, and custom attributes. This guide will walk you through how to do that.
|
||||
|
||||
## Setting User ID
|
||||
|
||||
You can use the `setUserId` function to identify a user with any string. It's best to use the default identifier you use in your app (e.g. unique id from database) but you can also anonymize these as long as they are unique for every user. This function can be called multiple times with the same value safely and stores the identifier in local storage. We recommend you set the User ID whenever the user logs in to your website, as well as after the installation snippet (if the user is already logged in).
|
||||
To enable the User identification feature you need to set the `userId` in the init() call of Formbricks. Only when the `userId` is set the person will be visible in the Formbricks dashboard. The `userId` can be any string and it's best to use the default identifier you use in your app (e.g. unique id from database or the email address if it's unique) but you can also anonymize these as long as they are unique for every user.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Setting User ID">
|
||||
|
||||
```javascript
|
||||
formbricks.setUserId("USER_ID");
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
userId: "<user_id>",
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
## Setting User Email
|
||||
|
||||
You can use the setEmail function to set the user's email:
|
||||
The `userId` is the main identifier used in Formbricks and user identification is only enabled when it is set. In addition to the userId you can also set attributes that describes the user better. The email address can be set using the setEmail function:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Setting Email">
|
||||
|
||||
@@ -39,11 +45,12 @@ formbricks.setEmail("user@example.com");
|
||||
### Setting Custom User Attributes
|
||||
|
||||
You can use the setAttribute function to set any custom attribute for the user (e.g. name, plan, etc.):
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Setting Custom Attributes">
|
||||
|
||||
```javascript
|
||||
formbricks.setAttribute("attribute_key", "attribute_value");
|
||||
formbricks.setAttribute("Plan", "free");
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -51,6 +58,7 @@ formbricks.setAttribute("attribute_key", "attribute_value");
|
||||
### Logging Out Users
|
||||
|
||||
When a user logs out of your webpage, make sure to log them out of Formbricks as well. This will prevent new activity from being associated with an incorrect user. Use the logout function:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Logging out User">
|
||||
|
||||
@@ -59,4 +67,4 @@ formbricks.logout();
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
@@ -4,7 +4,8 @@ import DemoApp from "./demoapp.webp";
|
||||
|
||||
export const metadata = {
|
||||
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",
|
||||
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
|
||||
@@ -13,13 +14,14 @@ export const metadata = {
|
||||
|
||||
To play around with the in-app [User Actions](/docs/actions/why), you can use the Demo App. It's a simple React app that you can run locally and use to trigger actions and set [Attributes](/docs/attributes/why).
|
||||
|
||||
<Image src={DemoApp} alt="Demo App Preview" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image src={DemoApp} alt="Demo App Preview" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
|
||||
## Functionality
|
||||
|
||||
### Code Action
|
||||
|
||||
This button sends a <a href="/docs/actions/code">Code Action</a> to the Formbricks API called 'Code Action'. You will find it in the Actions Tab.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Track Code action">
|
||||
|
||||
@@ -32,6 +34,7 @@ formbricks.track("Code Action");
|
||||
### No Code Action
|
||||
|
||||
This button sends a <a href="/docs/actions/no-code">No Code Action</a> as long as you created it beforehand in the Formbricks App. For it to work, you need to add the No Code Action within Formbricks.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Track No-Code action">
|
||||
|
||||
@@ -44,6 +47,7 @@ This button sends a <a href="/docs/actions/no-code">No Code Action</a> as long a
|
||||
### Set Plan to "Free"
|
||||
|
||||
This button sets the <a href="/docs/attributes/custom-attributes">attribute</a> 'Plan' to 'Free'. If the attribute does not exist, it creates it.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Set Plan to Free">
|
||||
|
||||
@@ -56,6 +60,7 @@ formbricks.setAttribute("Plan", "Free");
|
||||
### Set Plan to "Paid"
|
||||
|
||||
This button sets the <a href="/docs/attributes/custom-attributes">attribute</a> 'Plan' to 'Paid'. If the attribute does not exist, it creates it.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Set Plan to Paid">
|
||||
|
||||
@@ -68,6 +73,7 @@ formbricks.setAttribute("Plan", "Paid");
|
||||
### Set Email
|
||||
|
||||
This button sets the <a href="/docs/attributes/identify-users">user email</a> 'test@web.com'
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Set Email">
|
||||
|
||||
@@ -79,13 +85,14 @@ formbricks.setEmail("test@web.com");
|
||||
</Col>
|
||||
### Set UserId
|
||||
|
||||
This button sets an external <a href="/docs/attributes/identify-users">user ID</a> to 'THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING'
|
||||
This button sets an external <a href="/docs/attributes/identify-users">user ID</a> in the Formbricks init call to 'THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING'
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Set User ID">
|
||||
|
||||
```tsx
|
||||
formbricks.setUserId("THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING");
|
||||
userId: "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
@@ -90,6 +90,8 @@ To get the project running locally on your machine you need to have the followin
|
||||
|
||||
**You can now access the Formbricks app on [http://localhost:3000](http://localhost:3000)**. You will be automatically redirected to the login. To use your local installation of formbricks, create a new account.
|
||||
|
||||
<Note>A fresh setup does not have a default account. Please create a new account and proceed accordingly.</Note>
|
||||
|
||||
For viewing the confirmation email and other emails the system sends you, you can access mailhog at [http://localhost:8025](http://localhost:8025)
|
||||
|
||||
### Build
|
||||
|
||||
@@ -45,7 +45,7 @@ All you need to do is copy a `<script>` tag to your HTML head, and that’s abou
|
||||
```html {{ title: 'index.html' }}
|
||||
<!-- START Formbricks Surveys -->
|
||||
<script type="text/javascript">
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.1.2/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "<your-environment-id>", apiHost: "<api-host>"})},500)}();
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.2.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "<your-environment-id>", apiHost: "<api-host>"})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
```
|
||||
|
||||
@@ -8,14 +8,12 @@ import I5 from "./5-options-survey-popup-in-app-for-feedback.webp";
|
||||
import I6 from "./6-setup-in-app-survey-popup-feedback-box.webp";
|
||||
import I7 from "./7-in-app-survey-popup-for-feedback.webp";
|
||||
import I8 from "./8-pop-up-form-in-web-app-survey.webp";
|
||||
import I9 from "./9-set-up-in-app-micro-survey-popup.webp";
|
||||
import I10 from "./10-micro-survey-pop-up-in-app.webp";
|
||||
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 metadata = {
|
||||
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.",
|
||||
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
|
||||
@@ -32,7 +30,7 @@ While you can [self-host](/docs/self-hosting/deployment) Formbricks, the quickes
|
||||
src={I1}
|
||||
alt="Choose in app survey template"
|
||||
quality="100"
|
||||
className="max-w-full sm:max-w-3xl rounded-lg "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
## Create your first survey
|
||||
@@ -43,7 +41,7 @@ To be able to see a survey in your app, you need to create one. We’ll choose o
|
||||
src={I2}
|
||||
alt="Settings for popup survey inside web app"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
As you can see in the orange note here, we have not yet connected Formbricks Cloud with our app. We will do so in just a minute, let’s first setup the survey correctly.
|
||||
@@ -54,7 +52,7 @@ Select “Web App” in the How to ask settings:
|
||||
src={I3}
|
||||
alt="Survey settings for popup micro surve"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Scroll down to Survey Trigger and choose “New Session”. This will cause this survey to appear when the Formbricks Widget tracks a new user session:
|
||||
@@ -63,7 +61,7 @@ Scroll down to Survey Trigger and choose “New Session”. This will cause this
|
||||
src={I4}
|
||||
alt="In app survey trigger for feedback popup micro survey"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
In **Recontact Options** we choose the following settings, so that we can play around with the survey more easily. By default, each survey will be shown only once to each user to prevent survey fatigue:
|
||||
@@ -72,7 +70,7 @@ In **Recontact Options** we choose the following settings, so that we can play a
|
||||
src={I5}
|
||||
alt="Options for survey popup in app micro survey"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Now hit **Publish** and you’ll be forwarded to the Summary Page. This is where you’ll find the responses to this survey. On the Summary Page click through to the Setup Checklist:
|
||||
@@ -81,7 +79,7 @@ Now hit **Publish** and you’ll be forwarded to the Summary Page. This is where
|
||||
src={I6}
|
||||
alt="pop up survey settings for inapp web survey"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Set up the Formbricks Widget in your app
|
||||
@@ -92,7 +90,7 @@ On the Setup Checklist you have two elements. At the top you find the Widget Sta
|
||||
src={I7}
|
||||
alt="feedback popup in app survey"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
In the manual below, this code snippet contains all the information you need:
|
||||
@@ -104,9 +102,11 @@ In the manual below, this code snippet contains all the information you need:
|
||||
src={I8}
|
||||
alt="settings for in app survey popping up"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
If you like to use the user identification feature, please follow the [user identification guide](/docs/attributes/identify-users).
|
||||
|
||||
## Load Formbricks widget in your app
|
||||
|
||||
In a local instance of your app, you'll embed the Formbricks Widget. Dependent on your frontend tech, the setup differs a bit:
|
||||
@@ -124,60 +124,5 @@ Now, restart your app in your terminal to make sure the widget is loaded. Once i
|
||||
src={ReactApp}
|
||||
alt="In app survey in React app for micro surveys"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Most frequent errors and how to debug them
|
||||
|
||||
In case you don’t see your survey right away, here's what you can do. Go through these in orderly to find the error fast:
|
||||
|
||||
### Formbricks Cloud and your app are not connected properly.
|
||||
|
||||
Go back to [app.formbricks.com](http://app.formbricks.com) and go to the Setup Checklist in the Settings. If the status is still indicated as “Not connected” your app hasn't yet pinged the Formbricks Cloud:
|
||||
|
||||
<Image
|
||||
src={I9}
|
||||
alt="setup checklist ui of survey popup for in app surveys"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
**How to fix it:**
|
||||
|
||||
1. Check if your app loads the Formbricks widget correctly. Make sure you have `debug` mode enabled in your integration and you should see the Formbricks debug logs in your browser console while being in your app (right click in the browser, `Inspect`, switch to the console tab). If you don’t see them, double check your integration or if you are unable to solve this issue, please [join our Discord](https://formbricks.com/discord) and we’ll help you out.
|
||||
|
||||
---
|
||||
|
||||
### Survey not loaded
|
||||
|
||||
If your app is connected with Formbricks Cloud, the survey might have not been loaded properly. Check the debug logs and search for the list of surveys loaded. It should look like so:
|
||||
|
||||
<Image
|
||||
src={I11}
|
||||
alt="survey logs for in app survey pop up micro"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
**How to fix it:**
|
||||
|
||||
The widget only loads surveys which are **public** and **in progress**. Go to Formbricks Cloud and to the Survey Summary page. Check if your survey is live:
|
||||
|
||||
<Image
|
||||
src={I10}
|
||||
alt="ui of survey popup for in app micro surveys"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
### Survey not triggered
|
||||
|
||||
If the survey is loaded by the widget it might not have been triggered properly.
|
||||
|
||||
**How to fix:**
|
||||
|
||||
1. Open your local app in an incognito tab or window. The New Session event is only fired if a user was inactive for 60 minutes or was logged out of Formbricks via formrbicks.logout().
|
||||
2. Check the debug logs for “Event ‘New Session” tracked”. If you see it in the logs and the survey still did not get displayed, [please let us know.](mailto:support@formbricks.com)
|
||||
|
||||
That should fix it! If not, please [join our Discord](https://formbricks.com/discord)! We’re more than happy to help you get started 😊
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,96 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import I1 from "./1-set-up-in-app-micro-survey-popup.webp";
|
||||
import I2 from "./2-micro-survey-pop-up-in-app.webp";
|
||||
import I3 from "./3-survey-logs-in-app-survey-popup.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Formbricks Troubleshooting Guide",
|
||||
description:
|
||||
"Troubleshoot your Formbricks integration and get your in-app surveys up and running in no time. Most frequent errors and how to debug them.",
|
||||
};
|
||||
|
||||
#### Getting Started
|
||||
|
||||
# Troubleshooting Guide
|
||||
|
||||
In case you don’t see your survey right away, here's what you can do. Go through these to find the error fast:
|
||||
|
||||
## Formbricks Cloud and your app are not connected properly.
|
||||
|
||||
Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted instance's URL and go to the Setup Checklist in the Settings. If the status is still indicated as “Not connected” your app hasn't yet pinged the Formbricks Cloud:
|
||||
|
||||
<Image
|
||||
src={I1}
|
||||
alt="setup checklist ui of survey popup for in app surveys"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
**How to fix it:**
|
||||
|
||||
1. Check if your app loads the Formbricks widget correctly.
|
||||
2. Make sure you have `debug` mode enabled in your integration and you should see the Formbricks debug logs in your browser console while being in your app (right click in the browser, `Inspect`, switch to the console tab). If you don’t see them, double check your integration.
|
||||
|
||||
---
|
||||
|
||||
## Survey not loaded
|
||||
|
||||
If your app is connected with Formbricks Cloud, the survey might have not been loaded properly. Check the debug logs and search for the list of surveys loaded. It should look like so:
|
||||
|
||||
<Image
|
||||
src={I3}
|
||||
alt="survey logs for in app survey pop up micro"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
**How to fix it:**
|
||||
|
||||
The widget only loads surveys which are **public** and **in progress**. Go to Formbricks Cloud and to the Survey Summary page. Check if your survey is live:
|
||||
|
||||
<Image
|
||||
src={I2}
|
||||
alt="ui of survey popup for in app micro surveys"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Survey not triggered
|
||||
|
||||
If the survey is loaded by the widget it might not have been triggered properly.
|
||||
|
||||
**How to fix:**
|
||||
|
||||
1. Open your local app in an incognito tab or window. The New Session event is only fired if a user was inactive for 60 minutes or was logged out of Formbricks via formrbicks.logout().
|
||||
2. Check the debug logs for “Event ‘New Session” tracked”. If you see it in the logs and the survey still did not get displayed, [please let us know.](mailto:support@formbricks.com)
|
||||
|
||||
---
|
||||
|
||||
## Survey not displayed in HTML page
|
||||
|
||||
If the survey is loaded by the widget in the HTML page, try the below steps:
|
||||
|
||||
**How to fix:**
|
||||
|
||||
1. Make sure you have added the [script](/docs/getting-started/framework-guides#html) in the head of the HTML page.
|
||||
2. Verify that you have set the \<environment-id\> and \<host\> as per your Formbricks instance.
|
||||
3. Verify that you have the latest version of the JS Package.
|
||||
4. Check the debug logs to see if you still see any errors.
|
||||
|
||||
---
|
||||
|
||||
## Cannot read undefined of .init()
|
||||
|
||||
If you see this error in the console, it means that the Formbricks JS package is not loaded properly.
|
||||
|
||||
**How to fix:**
|
||||
|
||||
1. Update to the latest version of the JS Package.
|
||||
2. Verify this wherever you call initialise the Formbricks instance in your code.
|
||||
3. It should now start working.
|
||||
|
||||
---
|
||||
|
||||
If you are still facing issues, please [Open an Issue on GitHub](https://github.com/formbricks/formbricks/issues) or [join our Discord](https://formbricks.com/discord)! We’re more than happy to help you get started 😊
|
||||
@@ -160,8 +160,8 @@ Enabling the Airtable Integration in a self-hosted environment requires creating
|
||||
|
||||
### By now, your environment variables should include the below ones:
|
||||
|
||||
- `AIR_TABLE_CLIENT_ID`
|
||||
- `AIR_TABLE_REDIRECT_URL`
|
||||
- `AIRTABLE_CLIENT_ID`
|
||||
- `AIRTABLE_REDIRECT_URL`
|
||||
|
||||
Voila! You have successfully enabled the Airtable integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks Cloud](#formbricks-cloud) section to link an Airtable with Formbricks.
|
||||
|
||||
|
||||
@@ -16,4 +16,6 @@ At Formbricks, we understand that different users have different needs, and we s
|
||||
|
||||
Please note that regardless of the method you choose, Formbricks is designed to be easy-to-use and flexible. So choose the method that best fits your comfort level and requirements, and start leveraging the **power of Formbricks** today!
|
||||
|
||||
<Note>Running Formbricks requires at least **1 vCPU**, **2 GBs of RAM**, **8 GBs of SSD**</Note>
|
||||
|
||||
Looking for something not mentioned here? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you!
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
export const metadata = {
|
||||
title: "External auth providers",
|
||||
description:
|
||||
"Set up and integrate multiple external authentication providers with Formbricks. Our step-by-step guide covers Google OAuth and more, ensuring a seamless login experience for your users.",
|
||||
};
|
||||
|
||||
## Google OAuth Authentication
|
||||
|
||||
Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance.
|
||||
|
||||
### Requirements
|
||||
|
||||
- A Google Cloud Platform (GCP) account.
|
||||
- A Formbricks instance running and accessible.
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Create a GCP Project**:
|
||||
|
||||
- Navigate to the [GCP Console](https://console.cloud.google.com/).
|
||||
- From the projects list, select a project or create a new one.
|
||||
|
||||
2. **Setting up OAuth 2.0**:
|
||||
|
||||
- If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**.
|
||||
- On the left, click **Credentials**.
|
||||
- Click **Create Credentials**, then select **OAuth client ID**.
|
||||
|
||||
3. **Configure OAuth Consent Screen**:
|
||||
|
||||
- If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**.
|
||||
- Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted.
|
||||
|
||||
4. **Create OAuth 2.0 Client IDs**:
|
||||
- Select the application type **Web application** for your project and enter any additional information required.
|
||||
- Ensure to specify authorized JavaScript origins and authorized redirect URIs.
|
||||
|
||||
```
|
||||
Authorized JavaScript origins: {WEBAPP_URL}
|
||||
Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google
|
||||
```
|
||||
|
||||
5. **Update Environment Variables in Docker**:
|
||||
- To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container.
|
||||
- In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform:
|
||||
- Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID):
|
||||
|
||||
```
|
||||
docker exec -it container_id /bin/bash
|
||||
export GOOGLE_AUTH_ENABLED=1
|
||||
export GOOGLE_CLIENT_ID=your-client-id-here
|
||||
export GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
exit
|
||||
```
|
||||
|
||||
```
|
||||
GOOGLE_AUTH_ENABLED=1
|
||||
GOOGLE_CLIENT_ID=your-client-id-here
|
||||
GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
```
|
||||
|
||||
6. **Restart Your Formbricks Instance**:
|
||||
- **Note:** Restarting your Docker containers may cause a brief period of downtime. Plan accordingly.
|
||||
- Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. Here's how you can do it:
|
||||
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
|
||||
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration:
|
||||
@@ -193,6 +193,7 @@ export const navigation: Array<NavGroup> = [
|
||||
links: [
|
||||
{ title: "Quickstart: In app", href: "/docs/getting-started/quickstart-in-app-survey" },
|
||||
{ title: "Framework Guides", href: "/docs/getting-started/framework-guides" },
|
||||
{ title: "Troubleshooting", href: "/docs/getting-started/troubleshooting" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -248,6 +249,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Production", href: "/docs/self-hosting/production" },
|
||||
{ title: "Docker", href: "/docs/self-hosting/docker" },
|
||||
{ title: "Migration Guide", href: "/docs/self-hosting/migration-guide" },
|
||||
{ title: "External auth providers", href: "/docs/self-hosting/external-auth-providers" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,13 +4,13 @@ import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
const DummyUI: React.FC = () => {
|
||||
const eventClasses = [
|
||||
const actionClasses = [
|
||||
{ id: "1", name: "View Dashboard" },
|
||||
{ id: "2", name: "Upgrade to Pro" },
|
||||
{ id: "3", name: "Cancel Plan" },
|
||||
];
|
||||
|
||||
const [triggers, setTriggers] = useState<string[]>([eventClasses[0].id]);
|
||||
const [triggers, setTriggers] = useState<string[]>([actionClasses[0].id]);
|
||||
|
||||
const setTriggerEvent = (index: number, eventClassId: string) => {
|
||||
setTriggers((prevTriggers) =>
|
||||
@@ -19,7 +19,7 @@ const DummyUI: React.FC = () => {
|
||||
};
|
||||
|
||||
const addTriggerEvent = () => {
|
||||
setTriggers((prevTriggers) => [...prevTriggers, eventClasses[0].id]);
|
||||
setTriggers((prevTriggers) => [...prevTriggers, actionClasses[0].id]);
|
||||
};
|
||||
|
||||
const removeTriggerEvent = (index: number) => {
|
||||
@@ -41,12 +41,12 @@ const DummyUI: React.FC = () => {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{eventClasses.map((eventClass) => (
|
||||
{actionClasses.map((actionClass) => (
|
||||
<SelectItem
|
||||
key={eventClass.id}
|
||||
key={actionClass.id}
|
||||
className="xs:text-base px-0.5 py-1 text-xs text-slate-800 dark:bg-slate-700 dark:text-slate-300 dark:ring-slate-700"
|
||||
value={eventClass.id}>
|
||||
{eventClass.name}
|
||||
value={actionClass.id}>
|
||||
{actionClass.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -60,7 +60,7 @@ if (typeof window !== "undefined") {
|
||||
</>
|
||||
) : activeTab === "html" ? (
|
||||
<CodeBlock>{`<script type="text/javascript">
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.1.2/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init("claDadXk29dak92dK9","https://app.formbricks.com")},500)}();
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.2.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init("claDadXk29dak92dK9","https://app.formbricks.com")},500)}();
|
||||
</script>`}</CodeBlock>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -127,6 +127,11 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"Makes frontend development cycle 10x faster with API Client, Mock Server, Intercept & Modify HTTP Requests and Session Replays.",
|
||||
href: "https://requestly.io",
|
||||
},
|
||||
{
|
||||
name: "Revert",
|
||||
description: "The open-source unified API to build B2B integrations remarkably fast",
|
||||
href: "https://revert.dev",
|
||||
},
|
||||
{
|
||||
name: "Rivet",
|
||||
description: "Open-source solution to deploy, scale, and operate your multiplayer game.",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../.env
|
||||
@@ -1,3 +1,5 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import ConfirmationPage from "./components/ConfirmationPage";
|
||||
|
||||
export default function BillingConfirmation({ searchParams }) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
import { formbricksEnabled } from "@/app/lib/formbricks";
|
||||
import formbricks from "@formbricks/js";
|
||||
import { useEffect } from "react";
|
||||
@@ -15,8 +15,8 @@ export default function FormbricksClient({ session }) {
|
||||
formbricks.init({
|
||||
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
|
||||
apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
|
||||
userId: session.user.id,
|
||||
});
|
||||
formbricks.setUserId(session.user.id);
|
||||
formbricks.setEmail(session.user.email);
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { env } from "@/env.mjs";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
import type { Session } from "next-auth";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
import AddNoCodeActionModal from "./AddNoCodeActionModal";
|
||||
import AddNoCodeActionModal from "./AddActionModal";
|
||||
import ActionDetailModal from "./ActionDetailModal";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
"use client";
|
||||
|
||||
import { createActionClassAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions";
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/CssSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/InnerHtmlSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/PageUrlSelector";
|
||||
import { TActionClass, TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
import { TabBar } from "@formbricks/ui/TabBar";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { testURLmatch } from "../lib/testURLmatch";
|
||||
|
||||
interface AddNoCodeActionModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
setActionClassArray?;
|
||||
isViewer: boolean;
|
||||
}
|
||||
|
||||
function isValidCssSelector(selector?: string) {
|
||||
if (!selector || selector.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const element = document.createElement("div");
|
||||
try {
|
||||
element.querySelector(selector);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function AddNoCodeActionModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
setActionClassArray,
|
||||
isViewer,
|
||||
}: AddNoCodeActionModalProps) {
|
||||
const { register, control, handleSubmit, watch, reset } = useForm();
|
||||
const [isPageUrl, setIsPageUrl] = useState(false);
|
||||
const [isCssSelector, setIsCssSelector] = useState(false);
|
||||
const [isInnerHtml, setIsInnerText] = useState(false);
|
||||
const [isCreatingAction, setIsCreatingAction] = useState(false);
|
||||
const [testUrl, setTestUrl] = useState("");
|
||||
const [isMatch, setIsMatch] = useState("");
|
||||
const [type, setType] = useState("noCode");
|
||||
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
|
||||
const filteredNoCodeConfig: TActionClassNoCodeConfig = {};
|
||||
|
||||
if (isPageUrl && pageUrl?.rule && pageUrl?.value) {
|
||||
filteredNoCodeConfig.pageUrl = { rule: pageUrl.rule, value: pageUrl.value };
|
||||
}
|
||||
if (isInnerHtml && innerHtml?.value) {
|
||||
filteredNoCodeConfig.innerHtml = { value: innerHtml.value };
|
||||
}
|
||||
if (isCssSelector && cssSelector?.value) {
|
||||
filteredNoCodeConfig.cssSelector = { value: cssSelector.value };
|
||||
}
|
||||
|
||||
return filteredNoCodeConfig;
|
||||
};
|
||||
|
||||
const handleMatchClick = () => {
|
||||
const match = testURLmatch(
|
||||
testUrl,
|
||||
watch("noCodeConfig.[pageUrl].value"),
|
||||
watch("noCodeConfig.[pageUrl].rule")
|
||||
);
|
||||
setIsMatch(match);
|
||||
if (match === "yes") toast.success("Your survey would be shown on this URL.");
|
||||
if (match === "no") toast.error("Your survey would not be shown.");
|
||||
};
|
||||
|
||||
const submitEventClass = async (data: Partial<TActionClassInput>): Promise<void> => {
|
||||
const { noCodeConfig } = data;
|
||||
try {
|
||||
if (isViewer) {
|
||||
throw new Error("You are not authorised to perform this action.");
|
||||
}
|
||||
setIsCreatingAction(true);
|
||||
if (data.name === "") throw new Error("Please give your action a name");
|
||||
if (type === "noCode") {
|
||||
if (!isPageUrl && !isCssSelector && !isInnerHtml)
|
||||
throw new Error("Please select at least one selector");
|
||||
|
||||
if (isCssSelector && !isValidCssSelector(noCodeConfig?.cssSelector?.value)) {
|
||||
throw new Error("Please enter a valid CSS Selector");
|
||||
}
|
||||
|
||||
if (isPageUrl && noCodeConfig?.pageUrl?.rule === undefined) {
|
||||
throw new Error("Please select a rule for page URL");
|
||||
}
|
||||
}
|
||||
|
||||
const updatedAction: TActionClassInput = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
environmentId,
|
||||
type,
|
||||
} as TActionClassInput;
|
||||
|
||||
if (type === "noCode") {
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(noCodeConfig as TActionClassNoCodeConfig);
|
||||
updatedAction.noCodeConfig = filteredNoCodeConfig;
|
||||
}
|
||||
|
||||
const newActionClass: TActionClass = await createActionClassAction(updatedAction);
|
||||
if (setActionClassArray) {
|
||||
setActionClassArray((prevActionClassArray: TActionClass[]) => [
|
||||
...prevActionClassArray,
|
||||
newActionClass,
|
||||
]);
|
||||
}
|
||||
reset();
|
||||
resetAllStates(false);
|
||||
toast.success("Action added successfully.");
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setIsCreatingAction(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetAllStates = (open: boolean) => {
|
||||
setIsCssSelector(false);
|
||||
setIsPageUrl(false);
|
||||
setIsInnerText(false);
|
||||
setTestUrl("");
|
||||
setIsMatch("");
|
||||
reset();
|
||||
setOpen(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={() => resetAllStates(false)} noPadding closeOnOutsideClick={false}>
|
||||
<div className="flex h-full flex-col rounded-lg">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex w-full items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mr-1.5 h-6 w-6 text-slate-500">
|
||||
<CursorArrowRaysIcon />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">Track New User Action</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
Track a user action to display surveys or create user segment.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TabBar
|
||||
tabs={[
|
||||
{ id: "noCode", label: "No Code" },
|
||||
{ id: "code", label: "Code" },
|
||||
]}
|
||||
activeId={type}
|
||||
setActiveId={setType}
|
||||
/>
|
||||
{type === "noCode" ? (
|
||||
<form onSubmit={handleSubmit(submitEventClass)}>
|
||||
<div className="flex justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-4">
|
||||
<div className="grid w-full grid-cols-2 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<Label>What did your user do?</Label>
|
||||
<Input placeholder="E.g. Clicked Download" {...register("name", { required: true })} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Label>Description</Label>
|
||||
<Input placeholder="User clicked Download Button " {...register("description")} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Select By</Label>
|
||||
</div>
|
||||
<CssSelector
|
||||
isCssSelector={isCssSelector}
|
||||
setIsCssSelector={setIsCssSelector}
|
||||
register={register}
|
||||
/>
|
||||
<PageUrlSelector
|
||||
isPageUrl={isPageUrl}
|
||||
setIsPageUrl={setIsPageUrl}
|
||||
register={register}
|
||||
control={control}
|
||||
testUrl={testUrl}
|
||||
setTestUrl={setTestUrl}
|
||||
isMatch={isMatch}
|
||||
setIsMatch={setIsMatch}
|
||||
handleMatchClick={handleMatchClick}
|
||||
/>
|
||||
<InnerHtmlSelector
|
||||
isInnerHtml={isInnerHtml}
|
||||
setIsInnerHtml={setIsInnerText}
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
<div className="flex space-x-2">
|
||||
<Button type="button" variant="minimal" onClick={() => resetAllStates(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="darkCTA" type="submit" loading={isCreatingAction}>
|
||||
Track Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(submitEventClass)}>
|
||||
<div className="flex justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-4">
|
||||
<div className="grid w-full grid-cols-2 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<Label>Identifier</Label>
|
||||
<Input placeholder="E.g. clicked-download" {...register("name", { required: true })} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Label>Description</Label>
|
||||
<Input placeholder="User clicked Download Button " {...register("description")} />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<Alert>
|
||||
<Terminal className="h-4 w-4" />
|
||||
<AlertTitle>How do Code Actions work?</AlertTitle>
|
||||
<AlertDescription>
|
||||
You can track code action anywhere in your app using{" "}
|
||||
<span className="rounded bg-gray-100 px-2 py-1 text-xs">
|
||||
formbricks.track("{watch("name")}")
|
||||
</span>{" "}
|
||||
in your code. Read more in our{" "}
|
||||
<a href="https://formbricks.com/docs/actions/code" target="_blank" className="underline">
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
<div className="flex space-x-2">
|
||||
<Button type="button" variant="minimal" onClick={() => resetAllStates(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="darkCTA" type="submit" loading={isCreatingAction}>
|
||||
Track Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { createActionClassAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions";
|
||||
import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/CssSelector";
|
||||
import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/InnerHtmlSelector";
|
||||
import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/PageUrlSelector";
|
||||
import { TActionClass, TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Modal } from "@formbricks/ui/Modal";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { testURLmatch } from "../lib/testURLmatch";
|
||||
|
||||
interface AddNoCodeActionModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
setActionClassArray?;
|
||||
isViewer: boolean;
|
||||
}
|
||||
|
||||
function isValidCssSelector(selector?: string) {
|
||||
if (!selector || selector.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const element = document.createElement("div");
|
||||
try {
|
||||
element.querySelector(selector);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function AddNoCodeActionModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
setActionClassArray,
|
||||
isViewer,
|
||||
}: AddNoCodeActionModalProps) {
|
||||
const { register, control, handleSubmit, watch, reset } = useForm();
|
||||
const [isPageUrl, setIsPageUrl] = useState(false);
|
||||
const [isCssSelector, setIsCssSelector] = useState(false);
|
||||
const [isInnerHtml, setIsInnerText] = useState(false);
|
||||
const [isCreatingAction, setIsCreatingAction] = useState(false);
|
||||
const [testUrl, setTestUrl] = useState("");
|
||||
const [isMatch, setIsMatch] = useState("");
|
||||
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
|
||||
const filteredNoCodeConfig: TActionClassNoCodeConfig = {};
|
||||
|
||||
if (isPageUrl && pageUrl?.rule && pageUrl?.value) {
|
||||
filteredNoCodeConfig.pageUrl = { rule: pageUrl.rule, value: pageUrl.value };
|
||||
}
|
||||
if (isInnerHtml && innerHtml?.value) {
|
||||
filteredNoCodeConfig.innerHtml = { value: innerHtml.value };
|
||||
}
|
||||
if (isCssSelector && cssSelector?.value) {
|
||||
filteredNoCodeConfig.cssSelector = { value: cssSelector.value };
|
||||
}
|
||||
|
||||
return filteredNoCodeConfig;
|
||||
};
|
||||
|
||||
const handleMatchClick = () => {
|
||||
const match = testURLmatch(
|
||||
testUrl,
|
||||
watch("noCodeConfig.[pageUrl].value"),
|
||||
watch("noCodeConfig.[pageUrl].rule")
|
||||
);
|
||||
setIsMatch(match);
|
||||
if (match === "yes") toast.success("Your survey would be shown on this URL.");
|
||||
if (match === "no") toast.error("Your survey would not be shown.");
|
||||
};
|
||||
|
||||
const submitEventClass = async (data: Partial<TActionClassInput>): Promise<void> => {
|
||||
const { noCodeConfig } = data;
|
||||
try {
|
||||
if (isViewer) {
|
||||
throw new Error("You are not authorised to perform this action.");
|
||||
}
|
||||
setIsCreatingAction(true);
|
||||
if (data.name === "") throw new Error("Please give your action a name");
|
||||
if (!isPageUrl && !isCssSelector && !isInnerHtml)
|
||||
throw new Error("Please select at least one selector");
|
||||
|
||||
if (isCssSelector && !isValidCssSelector(noCodeConfig?.cssSelector?.value)) {
|
||||
throw new Error("Please enter a valid CSS Selector");
|
||||
}
|
||||
|
||||
if (isPageUrl && noCodeConfig?.pageUrl?.rule === undefined) {
|
||||
throw new Error("Please select a rule for page URL");
|
||||
}
|
||||
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(noCodeConfig as TActionClassNoCodeConfig);
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
environmentId,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as TActionClassInput;
|
||||
|
||||
const newActionClass: TActionClass = await createActionClassAction(updatedData);
|
||||
if (setActionClassArray) {
|
||||
setActionClassArray((prevActionClassArray: TActionClass[]) => [
|
||||
...prevActionClassArray,
|
||||
newActionClass,
|
||||
]);
|
||||
}
|
||||
reset();
|
||||
resetAllStates(false);
|
||||
toast.success("Action added successfully.");
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setIsCreatingAction(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetAllStates = (open: boolean) => {
|
||||
setIsCssSelector(false);
|
||||
setIsPageUrl(false);
|
||||
setIsInnerText(false);
|
||||
setTestUrl("");
|
||||
setIsMatch("");
|
||||
reset();
|
||||
setOpen(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={() => resetAllStates(false)} noPadding closeOnOutsideClick={false}>
|
||||
<div className="flex h-full flex-col rounded-lg">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex w-full items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mr-1.5 h-6 w-6 text-slate-500">
|
||||
<CursorArrowRaysIcon />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">Track New User Action</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
Track a user action to display surveys or create user segment.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(submitEventClass)}>
|
||||
<div className="flex justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-4">
|
||||
<div className="grid w-full grid-cols-2 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<Label>What did your user do?</Label>
|
||||
<Input placeholder="E.g. Clicked Download" {...register("name", { required: true })} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Label>Description</Label>
|
||||
<Input placeholder="User clicked Download Button " {...register("description")} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Select By</Label>
|
||||
</div>
|
||||
<CssSelector
|
||||
isCssSelector={isCssSelector}
|
||||
setIsCssSelector={setIsCssSelector}
|
||||
register={register}
|
||||
/>
|
||||
<PageUrlSelector
|
||||
isPageUrl={isPageUrl}
|
||||
setIsPageUrl={setIsPageUrl}
|
||||
register={register}
|
||||
control={control}
|
||||
testUrl={testUrl}
|
||||
setTestUrl={setTestUrl}
|
||||
isMatch={isMatch}
|
||||
setIsMatch={setIsMatch}
|
||||
handleMatchClick={handleMatchClick}
|
||||
/>
|
||||
<InnerHtmlSelector
|
||||
isInnerHtml={isInnerHtml}
|
||||
setIsInnerHtml={setIsInnerText}
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
<div className="flex space-x-2">
|
||||
<Button type="button" variant="minimal" onClick={() => resetAllStates(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="darkCTA" type="submit" loading={isCreatingAction}>
|
||||
Track Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import type { AttributeClass } from "@prisma/client";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updatetAttributeClass } from "@formbricks/lib/attributeClass/service";
|
||||
import { updateAttributeClass } from "@formbricks/lib/attributeClass/service";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AttributeSettingsTabProps {
|
||||
@@ -25,7 +25,7 @@ export default function AttributeSettingsTab({ attributeClass, setOpen }: Attrib
|
||||
const onSubmit = async (data) => {
|
||||
setisAttributeBeingSubmitted(true);
|
||||
setOpen(false);
|
||||
await updatetAttributeClass(attributeClass.id, data);
|
||||
await updateAttributeClass(attributeClass.id, data);
|
||||
router.refresh();
|
||||
setisAttributeBeingSubmitted(false);
|
||||
};
|
||||
@@ -33,7 +33,7 @@ export default function AttributeSettingsTab({ attributeClass, setOpen }: Attrib
|
||||
const handleArchiveToggle = async () => {
|
||||
setisAttributeBeingSubmitted(true);
|
||||
const data = { archived: !attributeClass.archived };
|
||||
await updatetAttributeClass(attributeClass.id, data);
|
||||
await updateAttributeClass(attributeClass.id, data);
|
||||
setisAttributeBeingSubmitted(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function copyToOtherEnvironmentAction(
|
||||
include: {
|
||||
triggers: {
|
||||
include: {
|
||||
eventClass: true,
|
||||
actionClass: true,
|
||||
},
|
||||
},
|
||||
attributeFilters: {
|
||||
@@ -109,9 +109,9 @@ export async function copyToOtherEnvironmentAction(
|
||||
let targetEnvironmentTriggers: string[] = [];
|
||||
// map the local triggers to the target environment
|
||||
for (const trigger of existingSurvey.triggers) {
|
||||
const targetEnvironmentTrigger = await prisma.eventClass.findFirst({
|
||||
const targetEnvironmentTrigger = await prisma.actionClass.findFirst({
|
||||
where: {
|
||||
name: trigger.eventClass.name,
|
||||
name: trigger.actionClass.name,
|
||||
environment: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
@@ -119,18 +119,18 @@ export async function copyToOtherEnvironmentAction(
|
||||
});
|
||||
if (!targetEnvironmentTrigger) {
|
||||
// if the trigger does not exist in the target environment, create it
|
||||
const newTrigger = await prisma.eventClass.create({
|
||||
const newTrigger = await prisma.actionClass.create({
|
||||
data: {
|
||||
name: trigger.eventClass.name,
|
||||
name: trigger.actionClass.name,
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
description: trigger.eventClass.description,
|
||||
type: trigger.eventClass.type,
|
||||
noCodeConfig: trigger.eventClass.noCodeConfig
|
||||
? JSON.parse(JSON.stringify(trigger.eventClass.noCodeConfig))
|
||||
description: trigger.actionClass.description,
|
||||
type: trigger.actionClass.type,
|
||||
noCodeConfig: trigger.actionClass.noCodeConfig
|
||||
? JSON.parse(JSON.stringify(trigger.actionClass.noCodeConfig))
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
@@ -183,8 +183,8 @@ export async function copyToOtherEnvironmentAction(
|
||||
questions: JSON.parse(JSON.stringify(existingSurvey.questions)),
|
||||
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
|
||||
triggers: {
|
||||
create: targetEnvironmentTriggers.map((eventClassId) => ({
|
||||
eventClassId: eventClassId,
|
||||
create: targetEnvironmentTriggers.map((actionClassId) => ({
|
||||
actionClassId: actionClassId,
|
||||
})),
|
||||
},
|
||||
attributeFilters: {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { getLatestActionByEnvironmentId } from "@formbricks/lib/action/service";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { ArrowDownIcon, CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { ArrowDownIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -11,14 +9,7 @@ interface WidgetStatusIndicatorProps {
|
||||
}
|
||||
|
||||
export default async function WidgetStatusIndicator({ environmentId, type }: WidgetStatusIndicatorProps) {
|
||||
const [environment, latestAction] = await Promise.all([
|
||||
getEnvironment(environmentId),
|
||||
getLatestActionByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!environment?.widgetSetupCompleted && latestAction) {
|
||||
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
|
||||
}
|
||||
const [environment] = await Promise.all([getEnvironment(environmentId)]);
|
||||
|
||||
const stati = {
|
||||
notImplemented: {
|
||||
@@ -27,26 +18,18 @@ export default async function WidgetStatusIndicator({ environmentId, type }: Wid
|
||||
title: "Connect Formbricks to your app.",
|
||||
subtitle: "You have not yet connected Formbricks to your app. Follow setup guide.",
|
||||
},
|
||||
running: { icon: CheckIcon, color: "green", title: "Receiving data.", subtitle: "Last action received:" },
|
||||
issue: {
|
||||
icon: ExclamationTriangleIcon,
|
||||
color: "amber",
|
||||
title: "There might be an issue.",
|
||||
subtitle: "Last action received:",
|
||||
running: {
|
||||
icon: CheckIcon,
|
||||
color: "green",
|
||||
title: "Receiving data.",
|
||||
subtitle: "You have successfully connected Formbricks to your app.",
|
||||
},
|
||||
};
|
||||
|
||||
let status: "notImplemented" | "running" | "issue";
|
||||
|
||||
if (latestAction) {
|
||||
const currentTime = new Date();
|
||||
const timeDifference = currentTime.getTime() - new Date(latestAction.createdAt).getTime();
|
||||
|
||||
if (timeDifference <= 24 * 60 * 60 * 1000) {
|
||||
status = "running";
|
||||
} else {
|
||||
status = "issue";
|
||||
}
|
||||
if (environment.widgetSetupCompleted) {
|
||||
status = "running";
|
||||
} else {
|
||||
status = "notImplemented";
|
||||
}
|
||||
@@ -59,23 +42,18 @@ export default async function WidgetStatusIndicator({ environmentId, type }: Wid
|
||||
className={clsx(
|
||||
"flex flex-col items-center justify-center space-y-2 rounded-lg py-6 text-center",
|
||||
status === "notImplemented" && "bg-slate-100",
|
||||
status === "running" && "bg-green-100",
|
||||
status === "issue" && "bg-amber-100"
|
||||
status === "running" && "bg-green-100"
|
||||
)}>
|
||||
<div
|
||||
className={clsx(
|
||||
"h-12 w-12 rounded-full bg-white p-2",
|
||||
status === "notImplemented" && "text-slate-700",
|
||||
status === "running" && "text-green-700",
|
||||
status === "issue" && "text-amber-700"
|
||||
status === "running" && "text-green-700"
|
||||
)}>
|
||||
<currentStatus.icon />
|
||||
</div>
|
||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
||||
<p className="text-sm text-slate-700">
|
||||
{currentStatus.subtitle}{" "}
|
||||
{latestAction && <span>{timeSince(latestAction.createdAt.toISOString())}</span>}
|
||||
</p>
|
||||
<p className="text-sm text-slate-700">{currentStatus.subtitle}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -84,16 +62,12 @@ export default async function WidgetStatusIndicator({ environmentId, type }: Wid
|
||||
<Link href={`/environments/${environment.id}/settings/setup`}>
|
||||
<div className="group my-4 flex justify-center">
|
||||
<div className=" flex rounded-full bg-slate-100 px-2 py-1">
|
||||
<p className="mr-2 text-sm text-slate-400 group-hover:underline">
|
||||
{currentStatus.subtitle}{" "}
|
||||
{latestAction && <span>{timeSince(latestAction.createdAt.toISOString())}</span>}
|
||||
</p>
|
||||
<p className="mr-2 text-sm text-slate-400 group-hover:underline">{currentStatus.subtitle}</p>
|
||||
<div
|
||||
className={clsx(
|
||||
"h-5 w-5 rounded-full p-0.5",
|
||||
status === "notImplemented" && "bg-slate-100 text-slate-700",
|
||||
status === "running" && "bg-green-100 text-green-700",
|
||||
status === "issue" && "bg-amber-100 text-amber-700"
|
||||
status === "running" && "bg-green-100 text-green-700"
|
||||
)}>
|
||||
<currentStatus.icon />
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@ export const fetchTables = async (environmentId: string, baseId: string) => {
|
||||
headers: { environmentId: environmentId },
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
return res.json() as Promise<TIntegrationAirtableTables>;
|
||||
const resJson = await res.json();
|
||||
return resJson.data as Promise<TIntegrationAirtableTables>;
|
||||
};
|
||||
|
||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
||||
@@ -21,6 +21,6 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
const resJSON = await res.json();
|
||||
const authUrl = resJSON.authUrl;
|
||||
const authUrl = resJSON.data.authUrl;
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import AirtableWrapper from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
import { AIR_TABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
@@ -9,7 +9,7 @@ import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import GoBackButton from "@formbricks/ui/GoBackButton";
|
||||
|
||||
export default async function Airtable({ params }) {
|
||||
const enabled = !!AIR_TABLE_CLIENT_ID;
|
||||
const enabled = !!AIRTABLE_CLIENT_ID;
|
||||
const [surveys, integrations, environment] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
|
||||
@@ -9,6 +9,6 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
const resJSON = await res.json();
|
||||
const authUrl = resJSON.authUrl;
|
||||
const authUrl = resJSON.data.authUrl;
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export default async function EnvironmentLayout({ children, params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
||||
|
||||
@@ -5,14 +5,13 @@ import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSessionCount } from "@formbricks/lib/session/service";
|
||||
|
||||
export default async function AttributesSection({ personId }: { personId: string }) {
|
||||
const person = await getPerson(personId);
|
||||
if (!person) {
|
||||
throw new Error("No such person found");
|
||||
}
|
||||
const numberOfSessions = await getSessionCount(personId);
|
||||
|
||||
const responses = await getResponsesByPersonId(personId);
|
||||
|
||||
const numberOfResponses = responses?.length || 0;
|
||||
@@ -35,6 +34,8 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{person.attributes.userId ? (
|
||||
<span>{person.attributes.userId}</span>
|
||||
) : person.userId ? (
|
||||
<span>{person.userId}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">Not provided</span>
|
||||
)}
|
||||
@@ -56,8 +57,8 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">Sessions</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{numberOfSessions}</dd>
|
||||
{/* <dt className="text-sm font-medium text-slate-500">Sessions</dt> */}
|
||||
{/* <dd className="mt-1 text-sm text-slate-900">{numberOfSessions}</dd> */}
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">Responses</dt>
|
||||
|
||||
@@ -12,7 +12,8 @@ export default function Loading() {
|
||||
{
|
||||
id: "demoId1",
|
||||
createdAt: new Date(),
|
||||
sessionId: "",
|
||||
// sessionId: "",
|
||||
personId: "",
|
||||
properties: {},
|
||||
actionClass: {
|
||||
id: "demoId1",
|
||||
@@ -28,7 +29,8 @@ export default function Loading() {
|
||||
{
|
||||
id: "demoId2",
|
||||
createdAt: new Date(),
|
||||
sessionId: "",
|
||||
// sessionId: "",
|
||||
personId: "",
|
||||
properties: {},
|
||||
actionClass: {
|
||||
id: "demoId2",
|
||||
|
||||
@@ -80,7 +80,7 @@ export default async function PeoplePage({
|
||||
</div>
|
||||
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
<div className="ph-no-capture text-slate-900">
|
||||
{truncateMiddle(getAttributeValue(person, "userId"), 24)}
|
||||
{truncateMiddle(getAttributeValue(person, "userId"), 24) || person.userId}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
||||
import { Alert, AlertDescription } from "@formbricks/ui/Alert";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { updateProductAction } from "../actions";
|
||||
|
||||
interface EditFormbricksBrandingProps {
|
||||
type: "linkSurvey" | "inAppSurvey";
|
||||
product: TProduct;
|
||||
canRemoveBranding: boolean;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export function EditFormbricksBranding({
|
||||
type,
|
||||
product,
|
||||
canRemoveBranding,
|
||||
environmentId,
|
||||
}: EditFormbricksBrandingProps) {
|
||||
const [isBrandingEnabled, setIsBrandingEnabled] = useState(
|
||||
type === "linkSurvey" ? product.linkSurveyBranding : product.inAppSurveyBranding
|
||||
);
|
||||
const [updatingBranding, setUpdatingBranding] = useState(false);
|
||||
|
||||
const toggleBranding = async () => {
|
||||
try {
|
||||
setUpdatingBranding(true);
|
||||
const newBrandingState = !isBrandingEnabled;
|
||||
setIsBrandingEnabled(newBrandingState);
|
||||
let inputProduct: Partial<TProductUpdateInput> = {
|
||||
[type === "linkSurvey" ? "linkSurveyBranding" : "inAppSurveyBranding"]: newBrandingState,
|
||||
};
|
||||
await updateProductAction(product.id, inputProduct);
|
||||
toast.success(
|
||||
newBrandingState ? "Formbricks branding will be shown." : "Formbricks branding will now be hidden."
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
} finally {
|
||||
setUpdatingBranding(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full items-center">
|
||||
{!canRemoveBranding && (
|
||||
<div className="mb-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
To remove the Formbricks branding from the <span className="font-semibold">{type} surveys</span>
|
||||
, please{" "}
|
||||
{type === "linkSurvey" ? (
|
||||
<span className="underline">
|
||||
<Link href={`/environments/${environmentId}/settings/billing`}>upgrade your plan.</Link>
|
||||
</span>
|
||||
) : (
|
||||
<span className="underline">
|
||||
<Link href={`/environments/${environmentId}/settings/billing`}>add your creditcard.</Link>
|
||||
</span>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-6 flex items-center space-x-2">
|
||||
<Switch
|
||||
id={`branding-${type}`}
|
||||
checked={isBrandingEnabled}
|
||||
onCheckedChange={toggleBranding}
|
||||
disabled={!canRemoveBranding || updatingBranding}
|
||||
/>
|
||||
<Label htmlFor={`branding-${type}`}>
|
||||
Show Formbricks Branding in {type === "linkSurvey" ? "Link" : "In-App"} Surveys
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription } from "@formbricks/ui/Alert";
|
||||
import { updateProductAction } from "../actions";
|
||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Link from "next/link";
|
||||
|
||||
interface EditSignatureProps {
|
||||
product: TProduct;
|
||||
canRemoveSignature: boolean;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export function EditFormbricksSignature({ product, canRemoveSignature, environmentId }: EditSignatureProps) {
|
||||
const [formbricksSignature, setFormbricksSignature] = useState(product.formbricksSignature);
|
||||
const [updatingSignature, setUpdatingSignature] = useState(false);
|
||||
|
||||
const toggleSignature = async () => {
|
||||
try {
|
||||
setUpdatingSignature(true);
|
||||
const newSignatureState = !formbricksSignature;
|
||||
setFormbricksSignature(newSignatureState);
|
||||
let inputProduct: Partial<TProductUpdateInput> = {
|
||||
formbricksSignature: newSignatureState,
|
||||
};
|
||||
await updateProductAction(product.id, inputProduct);
|
||||
toast.success(
|
||||
newSignatureState ? "Formbricks signature will be shown." : "Formbricks signature will now be hidden."
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
} finally {
|
||||
setUpdatingSignature(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full items-center">
|
||||
{!canRemoveSignature && (
|
||||
<div className="mb-4">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
To remove the Formbricks branding from the link surveys, please{" "}
|
||||
<span className="underline">
|
||||
<Link href={`/environments/${environmentId}/settings/billing`}>upgrade</Link>
|
||||
</span>{" "}
|
||||
your plan.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="signature"
|
||||
checked={formbricksSignature}
|
||||
onCheckedChange={toggleSignature}
|
||||
disabled={!canRemoveSignature || updatingSignature}
|
||||
/>
|
||||
<Label htmlFor="signature">Show 'Powered by Formbricks' Signature in Link Surveys</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import SettingsCard from "../components/SettingsCard";
|
||||
import SettingsTitle from "../components/SettingsTitle";
|
||||
import { EditFormbricksSignature } from "./components/EditSignature";
|
||||
import { EditBrandColor } from "./components/EditBrandColor";
|
||||
import { EditPlacement } from "./components/EditPlacement";
|
||||
import { EditHighlightBorder } from "./components/EditHighlightBorder";
|
||||
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { DEFAULT_BRAND_COLOR, REVALIDATION_INTERVAL } 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";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
import { getServerSession } from "next-auth";
|
||||
import SettingsCard from "../components/SettingsCard";
|
||||
import SettingsTitle from "../components/SettingsTitle";
|
||||
import { EditBrandColor } from "./components/EditBrandColor";
|
||||
import { EditHighlightBorder } from "./components/EditHighlightBorder";
|
||||
import { EditPlacement } from "./components/EditPlacement";
|
||||
import { EditFormbricksBranding } from "./components/EditBranding";
|
||||
|
||||
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||
const [session, team, product] = await Promise.all([
|
||||
@@ -33,8 +32,10 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const canRemoveLinkBranding = team.billing.features.linkSurvey.status !== "inactive";
|
||||
const canRemoveInAppBranding = team.billing.features.inAppSurvey.status !== "inactive";
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
const canRemoveSignature = team.billing.features.linkSurvey.status !== "inactive";
|
||||
const { isDeveloper, isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
const isBrandColorEditDisabled = isDeveloper ? true : isViewer;
|
||||
|
||||
@@ -68,11 +69,18 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
/>
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title="Formbricks Signature"
|
||||
title="Formbricks Branding"
|
||||
description="We love your support but understand if you toggle it off.">
|
||||
<EditFormbricksSignature
|
||||
<EditFormbricksBranding
|
||||
type="linkSurvey"
|
||||
product={product}
|
||||
canRemoveSignature={canRemoveSignature}
|
||||
canRemoveBranding={canRemoveLinkBranding}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
<EditFormbricksBranding
|
||||
type="inAppSurvey"
|
||||
product={product}
|
||||
canRemoveBranding={canRemoveInAppBranding}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getProfile } from "@formbricks/lib/profile/service";
|
||||
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||
const { environmentId } = params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const profile = session ? await getProfile(session.user.id) : null;
|
||||
const profile = session && session.user ? await getProfile(session.user.id) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -108,7 +108,7 @@ if (typeof window !== "undefined") {
|
||||
</p>
|
||||
<CodeBlock language="js">{`<!-- START Formbricks Surveys -->
|
||||
<script type="text/javascript">
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.1.2/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.2.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->`}</CodeBlock>
|
||||
<p className="text-lg font-semibold text-slate-800">You're done 🎉</p>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function LinkTab({ surveyUrl, survey, brandColor }: EmailTabProps
|
||||
<SurveyInline
|
||||
brandColor={brandColor}
|
||||
survey={survey}
|
||||
formbricksSignature={false}
|
||||
isBrandingEnabled={false}
|
||||
autoFocus={false}
|
||||
isRedirectDisabled={false}
|
||||
key={survey.id}
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function EditWelcomeCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="mt-8 flex items-center">
|
||||
<div className="mt-8 flex items-center">
|
||||
<div className="mr-2">
|
||||
<Switch
|
||||
id="timeToFinish"
|
||||
@@ -176,7 +176,7 @@ export default function EditWelcomeCard({
|
||||
Display an estimate of completion time for survey
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
|
||||
const options = [
|
||||
{
|
||||
id: "web",
|
||||
name: "Web App",
|
||||
name: "In-App Survey",
|
||||
icon: ComputerDesktopIcon,
|
||||
description: "Embed a survey in your web app to collect responses.",
|
||||
comingSoon: false,
|
||||
@@ -65,7 +65,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
|
||||
},
|
||||
{
|
||||
id: "mobile",
|
||||
name: "Mobile app",
|
||||
name: "Mobile App Survey",
|
||||
icon: DevicePhoneMobileIcon,
|
||||
description: "Survey users inside a mobile app (iOS & Android).",
|
||||
comingSoon: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import AddNoCodeActionModal from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/AddNoCodeActionModal";
|
||||
import AddNoCodeActionModal from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/components/AddActionModal";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
import { CheckCircleIcon, FunnelIcon, PlusIcon, TrashIcon, UserGroupIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { Info } from "lucide-react";
|
||||
import { useEffect, useState } from "react"; /* */
|
||||
|
||||
const filterConditions = [
|
||||
@@ -99,6 +101,24 @@ export default function WhoToSendCard({ localSurvey, setLocalSurvey, attributeCl
|
||||
<Collapsible.CollapsibleContent className="">
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="mx-6 mb-4 mt-3">
|
||||
<Alert variant="info">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertTitle>User Identification</AlertTitle>
|
||||
<AlertDescription>
|
||||
To target your audience you need to identify your users within your app. You can read more
|
||||
about how to do this in our{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/attributes/identify-users"
|
||||
className="underline"
|
||||
target="_blank">
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="mx-6 flex items-center rounded-lg border border-slate-200 p-4 text-slate-800">
|
||||
<div>
|
||||
{localSurvey.attributeFilters?.length === 0 ? (
|
||||
|
||||
@@ -204,7 +204,7 @@ export default function PreviewSurvey({
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
/>
|
||||
@@ -219,7 +219,7 @@ export default function PreviewSurvey({
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
/>
|
||||
</div>
|
||||
@@ -274,7 +274,7 @@ export default function PreviewSurvey({
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
/>
|
||||
@@ -287,7 +287,7 @@ export default function PreviewSurvey({
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { UsageAttributesUpdater } from "@/app/(app)/components/FormbricksClient";
|
||||
import SurveyDropDownMenu from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyDropDownMenu";
|
||||
import SurveyStarter from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyStarter";
|
||||
import { SurveyStatusIndicator } from "@formbricks/ui/SurveyStatusIndicator";
|
||||
import { generateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { ComputerDesktopIcon, LinkIcon, PlusIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
import { generateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { SurveyStatusIndicator } from "@formbricks/ui/SurveyStatusIndicator";
|
||||
import { ComputerDesktopIcon, LinkIcon, PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function SurveysList({ environmentId }: { environmentId: string }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -18,7 +18,7 @@ const welcomeCardDefault: TSurveyWelcomeCard = {
|
||||
enabled: false,
|
||||
headline: "Welcome!",
|
||||
html: "Thanks for providing your feedback - let's go!",
|
||||
timeToFinish: false,
|
||||
timeToFinish: true,
|
||||
};
|
||||
|
||||
export const templates: TTemplate[] = [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import type { Session } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
type Greeting = {
|
||||
next: () => void;
|
||||
@@ -13,6 +14,27 @@ type Greeting = {
|
||||
|
||||
const Greeting: React.FC<Greeting> = ({ next, skip, name, session }) => {
|
||||
const legacyUser = !session ? false : new Date(session?.user?.createdAt) < new Date("2023-05-03T00:00:00"); // if user is created before onboarding deployment
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
next();
|
||||
}
|
||||
};
|
||||
const button = buttonRef.current;
|
||||
if (button) {
|
||||
button.focus();
|
||||
button.addEventListener("keydown", handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (button) {
|
||||
button.removeEventListener("keydown", handleKeyDown);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full max-w-xl flex-col justify-around gap-8 px-8">
|
||||
@@ -30,7 +52,7 @@ const Greeting: React.FC<Greeting> = ({ next, skip, name, session }) => {
|
||||
<Button size="lg" variant="minimal" onClick={skip}>
|
||||
I'll do it later
|
||||
</Button>
|
||||
<Button size="lg" variant="darkCTA" onClick={next}>
|
||||
<Button size="lg" variant="darkCTA" onClick={next} ref={buttonRef} tabIndex={0}>
|
||||
Begin (1 min)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
import { formbricksEnabled, updateResponse } from "@/app/lib/formbricks";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProfileObjective } from "@formbricks/types/profile";
|
||||
import { TProfile } from "@formbricks/types/profile";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { handleTabNavigation } from "../utils";
|
||||
|
||||
type ObjectiveProps = {
|
||||
next: () => void;
|
||||
@@ -35,18 +36,26 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const [isProfileUpdating, setIsProfileUpdating] = useState(false);
|
||||
|
||||
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [fieldsetRef, setSelectedChoice]);
|
||||
|
||||
const handleNextClick = async () => {
|
||||
if (selectedChoice) {
|
||||
const selectedObjective = objectives.find((objective) => objective.label === selectedChoice);
|
||||
if (selectedObjective) {
|
||||
try {
|
||||
setIsProfileUpdating(true);
|
||||
const updatedProfile = {
|
||||
...profile,
|
||||
await updateProfileAction({
|
||||
objective: selectedObjective.id,
|
||||
name: profile.name ?? undefined,
|
||||
};
|
||||
await updateProfileAction(updatedProfile);
|
||||
});
|
||||
setIsProfileUpdating(false);
|
||||
} catch (e) {
|
||||
setIsProfileUpdating(false);
|
||||
@@ -73,14 +82,14 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
return (
|
||||
<div className="flex w-full max-w-xl flex-col gap-8 px-8">
|
||||
<div className="px-4">
|
||||
<label className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
<label htmlFor="choices" className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
What do you want to achieve?
|
||||
</label>
|
||||
<label className="block text-sm font-normal leading-6 text-slate-500">
|
||||
We have 85+ templates, help us select the best for your need.
|
||||
</label>
|
||||
<div className="mt-4">
|
||||
<fieldset>
|
||||
<fieldset id="choices" aria-label="What do you want to achieve?" ref={fieldsetRef}>
|
||||
<legend className="sr-only">Choices</legend>
|
||||
<div className=" relative space-y-2 rounded-md">
|
||||
{objectives.map((choice) => (
|
||||
@@ -103,6 +112,11 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
onChange={(e) => {
|
||||
setSelectedChoice(e.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleNextClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{choice.label}
|
||||
|
||||
@@ -88,12 +88,7 @@ export default function Onboarding({ session, environmentId, profile, product }:
|
||||
<Greeting next={next} skip={doLater} name={profile.name ? profile.name : ""} session={session} />
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<Role
|
||||
next={next}
|
||||
skip={skipStep}
|
||||
setFormbricksResponseId={setFormbricksResponseId}
|
||||
profile={profile}
|
||||
/>
|
||||
<Role next={next} skip={skipStep} setFormbricksResponseId={setFormbricksResponseId} />
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<Objective
|
||||
|
||||
@@ -96,6 +96,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId, product })
|
||||
placeholder="e.g. Formbricks"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
aria-label="Your product name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
import { createResponse, formbricksEnabled } from "@/app/lib/formbricks";
|
||||
import { TProfile } from "@formbricks/types/profile";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { handleTabNavigation } from "../utils";
|
||||
|
||||
type RoleProps = {
|
||||
next: () => void;
|
||||
skip: () => void;
|
||||
setFormbricksResponseId: (id: string) => void;
|
||||
profile: TProfile;
|
||||
};
|
||||
|
||||
type RoleChoice = {
|
||||
@@ -21,9 +20,18 @@ type RoleChoice = {
|
||||
id: "project_manager" | "engineer" | "founder" | "marketing_specialist" | "other";
|
||||
};
|
||||
|
||||
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profile }) => {
|
||||
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [fieldsetRef, setSelectedChoice]);
|
||||
|
||||
const roles: Array<RoleChoice> = [
|
||||
{ label: "Project Manager", id: "project_manager" },
|
||||
@@ -39,8 +47,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profil
|
||||
if (selectedRole) {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
const updatedProfile = { ...profile, role: selectedRole.id };
|
||||
await updateProfileAction(updatedProfile);
|
||||
await updateProfileAction({ role: selectedRole.id });
|
||||
setIsUpdating(false);
|
||||
} catch (e) {
|
||||
setIsUpdating(false);
|
||||
@@ -66,19 +73,20 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profil
|
||||
return (
|
||||
<div className="flex w-full max-w-xl flex-col gap-8 px-8">
|
||||
<div className="px-4">
|
||||
<label className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
<label htmlFor="choices" className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
What is your role?
|
||||
</label>
|
||||
<label className="block text-sm font-normal leading-6 text-slate-500">
|
||||
Make your Formbricks experience more personalised.
|
||||
</label>
|
||||
<div className="mt-4">
|
||||
<fieldset>
|
||||
<fieldset id="choices" aria-label="What is your role?" ref={fieldsetRef}>
|
||||
<legend className="sr-only">Choices</legend>
|
||||
<div className=" relative space-y-2 rounded-md">
|
||||
{roles.map((choice) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
htmlFor={choice.id}
|
||||
className={cn(
|
||||
selectedChoice === choice.label
|
||||
? "z-10 border-slate-400 bg-slate-100"
|
||||
@@ -90,12 +98,18 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profil
|
||||
type="radio"
|
||||
id={choice.id}
|
||||
value={choice.label}
|
||||
name="role"
|
||||
checked={choice.label === selectedChoice}
|
||||
className="checked:text-brand-dark focus:text-brand-dark h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
setSelectedChoice(e.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleNextClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{choice.label}
|
||||
|
||||
@@ -7,11 +7,12 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProfile } from "@formbricks/lib/profile/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Onboarding from "./components/Onboarding";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function OnboardingPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error("No session found");
|
||||
redirect("/auth/login");
|
||||
}
|
||||
const userId = session?.user.id;
|
||||
const environment = await getFirstEnvironmentByUserId(userId);
|
||||
|
||||
34
apps/web/app/(app)/onboarding/utils.ts
Normal file
34
apps/web/app/(app)/onboarding/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// util.js
|
||||
export const handleTabNavigation = (fieldsetRef, setSelectedChoice) => (event) => {
|
||||
if (event.key !== "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const radioButtons = fieldsetRef.current?.querySelectorAll('input[type="radio"]');
|
||||
if (!radioButtons || radioButtons.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedRadioButton = fieldsetRef.current?.querySelector(
|
||||
'input[type="radio"]:focus'
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (!focusedRadioButton) {
|
||||
// If no radio button is focused, then it will focus on the first one by default
|
||||
const firstRadioButton = radioButtons[0] as HTMLInputElement;
|
||||
firstRadioButton.focus();
|
||||
setSelectedChoice(firstRadioButton.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedIndex = Array.from(radioButtons).indexOf(focusedRadioButton);
|
||||
const lastIndex = radioButtons.length - 1;
|
||||
|
||||
// Calculating the next index, considering wrapping from the last to the first element
|
||||
const nextIndex = focusedIndex === lastIndex ? 0 : focusedIndex + 1;
|
||||
const nextRadioButton = radioButtons[nextIndex] as HTMLInputElement;
|
||||
nextRadioButton.focus();
|
||||
setSelectedChoice(nextRadioButton.value);
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
|
||||
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const session = await getServerSession();
|
||||
const session = await getServerSession(authOptions);
|
||||
if (session) {
|
||||
redirect(`/`);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
UsedContent,
|
||||
RightAccountContent,
|
||||
} from "./components/InviteContentComponents";
|
||||
import { env } from "@/env.mjs";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
|
||||
export default async function JoinTeam({ searchParams }) {
|
||||
const currentUser = await getServerSession(authOptions);
|
||||
|
||||
@@ -51,7 +51,7 @@ async function reportTeamUsage(team: TTeam) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
const headersList = headers();
|
||||
const apiKey = headersList.get("x-api-key");
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
WEBAPP_URL,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { createOrUpdateIntegration } from "@formbricks/lib/integration/service";
|
||||
import { google } from "googleapis";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
@@ -15,19 +16,19 @@ export async function GET(req: NextRequest) {
|
||||
const code = queryParams.get("code");
|
||||
|
||||
if (!environmentId) {
|
||||
return NextResponse.json({ error: "Invalid environmentId" });
|
||||
return responses.badRequestResponse("Invalid environmentId");
|
||||
}
|
||||
|
||||
if (code && typeof code !== "string") {
|
||||
return NextResponse.json({ message: "`code` must be a string" }, { status: 400 });
|
||||
return responses.badRequestResponse("`code` must be a string");
|
||||
}
|
||||
|
||||
const client_id = GOOGLE_SHEETS_CLIENT_ID;
|
||||
const client_secret = GOOGLE_SHEETS_CLIENT_SECRET;
|
||||
const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL;
|
||||
if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 });
|
||||
if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 });
|
||||
if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 });
|
||||
if (!client_id) return responses.internalServerErrorResponse("Google client id is missing");
|
||||
if (!client_secret) return responses.internalServerErrorResponse("Google client secret is missing");
|
||||
if (!redirect_uri) return responses.internalServerErrorResponse("Google redirect url is missing");
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
let key;
|
||||
@@ -61,22 +62,7 @@ export async function GET(req: NextRequest) {
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prisma.integration.upsert({
|
||||
where: {
|
||||
type_environmentId: {
|
||||
environmentId,
|
||||
type: "googleSheets",
|
||||
},
|
||||
},
|
||||
update: {
|
||||
...googleSheetIntegration,
|
||||
environment: { connect: { id: environmentId } },
|
||||
},
|
||||
create: {
|
||||
...googleSheetIntegration,
|
||||
environment: { connect: { id: environmentId } },
|
||||
},
|
||||
});
|
||||
const result = await createOrUpdateIntegration(environmentId, googleSheetIntegration);
|
||||
|
||||
if (result) {
|
||||
return NextResponse.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/google-sheets`);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { google } from "googleapis";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { NextRequest } from "next/server";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -20,24 +21,24 @@ export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!environmentId) {
|
||||
return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 });
|
||||
return responses.badRequestResponse("environmentId is missing");
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ Error: "Invalid session" }, { status: 400 });
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId);
|
||||
if (!canUserAccessEnvironment) {
|
||||
return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 });
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const client_id = GOOGLE_SHEETS_CLIENT_ID;
|
||||
const client_secret = GOOGLE_SHEETS_CLIENT_SECRET;
|
||||
const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL;
|
||||
if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 });
|
||||
if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 });
|
||||
if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 });
|
||||
if (!client_id) return responses.internalServerErrorResponse("Google client id is missing");
|
||||
if (!client_secret) return responses.internalServerErrorResponse("Google client secret is missing");
|
||||
if (!redirect_uri) return responses.internalServerErrorResponse("Google redirect url is missing");
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
@@ -47,5 +48,5 @@ export async function GET(req: NextRequest) {
|
||||
state: environmentId!,
|
||||
});
|
||||
|
||||
return NextResponse.json({ authUrl }, { status: 200 });
|
||||
return responses.successResponse({ authUrl });
|
||||
}
|
||||
|
||||
10
apps/web/app/api/v1/(legacy)/js/actions/route.ts
Normal file
10
apps/web/app/api/v1/(legacy)/js/actions/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
116
apps/web/app/api/v1/(legacy)/js/lib/surveys.ts
Normal file
116
apps/web/app/api/v1/(legacy)/js/lib/surveys.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { displayCache } from "@formbricks/lib/display/cache";
|
||||
import { getDisplaysByPersonId } from "@formbricks/lib/display/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { productCache } from "@formbricks/lib/product/cache";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
// Helper function to calculate difference in days between two dates
|
||||
const diffInDays = (date1: Date, date2: Date) => {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
};
|
||||
|
||||
export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
return await getSyncSurveys(environmentId, person);
|
||||
},
|
||||
[`getSyncSurveysCached-${environmentId}`],
|
||||
{
|
||||
tags: [
|
||||
displayCache.tag.byPersonId(person.id),
|
||||
surveyCache.tag.byEnvironmentId(environmentId),
|
||||
productCache.tag.byEnvironmentId(environmentId),
|
||||
],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
export const getSyncSurveys = async (
|
||||
environmentId: string,
|
||||
person: TPerson
|
||||
): Promise<TSurveyWithTriggers[]> => {
|
||||
// get recontactDays from product
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
let surveys = await getSurveys(environmentId);
|
||||
|
||||
// filtered surveys for running and web
|
||||
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
|
||||
|
||||
const displays = await getDisplaysByPersonId(person.id);
|
||||
|
||||
// filter surveys that meet the displayOption criteria
|
||||
surveys = surveys.filter((survey) => {
|
||||
if (survey.displayOption === "respondMultiple") {
|
||||
return true;
|
||||
} else if (survey.displayOption === "displayOnce") {
|
||||
return displays.filter((display) => display.surveyId === survey.id).length === 0;
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return (
|
||||
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
|
||||
0
|
||||
);
|
||||
} else {
|
||||
throw Error("Invalid displayOption");
|
||||
}
|
||||
});
|
||||
|
||||
const attributeClasses = await getAttributeClasses(environmentId);
|
||||
|
||||
// filter surveys that meet the attributeFilters criteria
|
||||
const potentialSurveysWithAttributes = surveys.filter((survey) => {
|
||||
const attributeFilters = survey.attributeFilters;
|
||||
if (attributeFilters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// check if meets all attribute filters criterias
|
||||
return attributeFilters.every((attributeFilter) => {
|
||||
const attributeClassName = attributeClasses.find(
|
||||
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
|
||||
)?.name;
|
||||
if (!attributeClassName) {
|
||||
throw Error("Invalid attribute filter class");
|
||||
}
|
||||
const personAttributeValue = person.attributes[attributeClassName];
|
||||
if (attributeFilter.condition === "equals") {
|
||||
return personAttributeValue === attributeFilter.value;
|
||||
} else if (attributeFilter.condition === "notEquals") {
|
||||
return personAttributeValue !== attributeFilter.value;
|
||||
} else {
|
||||
throw Error("Invalid attribute filter condition");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const latestDisplay = displays[0];
|
||||
|
||||
// filter surveys that meet the recontactDays criteria
|
||||
surveys = potentialSurveysWithAttributes.filter((survey) => {
|
||||
if (!latestDisplay) {
|
||||
return true;
|
||||
} else if (survey.recontactDays !== null) {
|
||||
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
|
||||
if (!lastDisplaySurvey) {
|
||||
return true;
|
||||
}
|
||||
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
|
||||
} else if (product.recontactDays !== null) {
|
||||
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return surveys;
|
||||
};
|
||||
133
apps/web/app/api/v1/(legacy)/js/lib/sync.ts
Normal file
133
apps/web/app/api/v1/(legacy)/js/lib/sync.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/lib/surveys";
|
||||
import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { captureTelemetry } from "@formbricks/lib/telemetry";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TJsLegacyState } from "@formbricks/types/js";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
|
||||
const captureNewSessionTelemetry = async (jsVersion?: string): Promise<void> => {
|
||||
await captureTelemetry("state update", { jsVersion: jsVersion ?? "unknown" });
|
||||
};
|
||||
|
||||
export const getUpdatedState = async (
|
||||
environmentId: string,
|
||||
personId: string,
|
||||
jsVersion?: string
|
||||
): Promise<TJsLegacyState> => {
|
||||
let environment: TEnvironment | null;
|
||||
|
||||
if (jsVersion) {
|
||||
captureNewSessionTelemetry(jsVersion);
|
||||
}
|
||||
|
||||
// check if environment exists
|
||||
environment = await getEnvironment(environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
if (!environment?.widgetSetupCompleted) {
|
||||
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
|
||||
}
|
||||
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
|
||||
// check if Monthly Active Users limit is reached
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
const hasUserTargetingSubscription =
|
||||
team?.billing?.features.userTargeting.status &&
|
||||
team?.billing?.features.userTargeting.status in ["active", "canceled"];
|
||||
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
|
||||
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
|
||||
|
||||
if (isMauLimitReached) {
|
||||
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
|
||||
throw new Error(errorMessage);
|
||||
|
||||
// if (!personId) {
|
||||
// // don't allow new people
|
||||
// throw new Error(errorMessage);
|
||||
// }
|
||||
// const session = await getSession(sessionId);
|
||||
// if (!session) {
|
||||
// // don't allow new sessions
|
||||
// throw new Error(errorMessage);
|
||||
// }
|
||||
// // check if session was created this month (user already active this month)
|
||||
// const now = new Date();
|
||||
// const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
// if (new Date(session.createdAt) < firstDayOfMonth) {
|
||||
// throw new Error(errorMessage);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
const person = await getPerson(personId);
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveysCached(environmentId, person),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsLegacyState = {
|
||||
person: person!,
|
||||
session: {},
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
};
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export const getPublicUpdatedState = async (environmentId: string) => {
|
||||
// check if environment exists
|
||||
const environment = await getEnvironment(environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
// TODO: check if Monthly Active Users limit is reached
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSurveys(environmentId),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const state: TJsLegacyState = {
|
||||
surveys,
|
||||
session: {},
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
person: null,
|
||||
};
|
||||
|
||||
return state;
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
import { personCache } from "@formbricks/lib/person/cache";
|
||||
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { ZJsPeopleLegacyAttributeInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: Request, { params }): Promise<NextResponse> {
|
||||
try {
|
||||
const { personId } = params;
|
||||
|
||||
if (!personId || personId === "legacy") {
|
||||
return responses.internalServerErrorResponse("setAttribute requires an identified user", true);
|
||||
}
|
||||
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZJsPeopleLegacyAttributeInput.safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, key, value } = inputValidation.data;
|
||||
|
||||
const existingPerson = await getPerson(personId);
|
||||
|
||||
if (!existingPerson) {
|
||||
return responses.notFoundResponse("Person", personId, true);
|
||||
}
|
||||
|
||||
let attributeClass = await getAttributeClassByName(environmentId, key);
|
||||
|
||||
// create new attribute class if not found
|
||||
if (attributeClass === null) {
|
||||
attributeClass = await createAttributeClass(environmentId, key, "code");
|
||||
}
|
||||
|
||||
if (!attributeClass) {
|
||||
return responses.internalServerErrorResponse("Unable to create attribute class", true);
|
||||
}
|
||||
|
||||
// upsert attribute (update or create)
|
||||
await updatePersonAttribute(personId, attributeClass.id, value);
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
surveyCache.revalidate({
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const state = await getUpdatedState(environmentId, personId);
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { ZJsPeopleUserIdInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZJsPeopleUserIdInput.safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, userId } = inputValidation.data;
|
||||
|
||||
const personWithUserId = await getOrCreatePersonByUserId(userId, environmentId);
|
||||
|
||||
const state = await getUpdatedState(environmentId, personWithUserId.id);
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
|
||||
}
|
||||
}
|
||||
32
apps/web/app/api/v1/(legacy)/js/people/route.ts
Normal file
32
apps/web/app/api/v1/(legacy)/js/people/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { createPerson } from "@formbricks/lib/person/service";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function OPTIONS() {
|
||||
// cors headers
|
||||
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// we need to create a new person
|
||||
// call the createPerson service from here
|
||||
|
||||
const { environmentId, userId } = await req.json();
|
||||
|
||||
if (!environmentId) {
|
||||
return responses.badRequestResponse("environmentId is required", { environmentId }, true);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return responses.badRequestResponse("userId is required", { environmentId }, true);
|
||||
}
|
||||
|
||||
try {
|
||||
const person = await createPerson(environmentId, userId);
|
||||
|
||||
return responses.successResponse({ status: "success", person }, true);
|
||||
} catch (err) {
|
||||
return responses.internalServerErrorResponse("Something went wrong", true);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getSyncSurveysCached } from "@/app/api/v1/js/sync/lib/surveys";
|
||||
import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/sync/lib/surveys";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import {
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
@@ -7,33 +7,22 @@ import {
|
||||
PRICING_USERTARGETING_FREE_MTU,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { createPerson, getPerson } from "@formbricks/lib/person/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { createSession, extendSession, getSession } from "@formbricks/lib/session/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import {
|
||||
getMonthlyActiveTeamPeopleCount,
|
||||
getMonthlyTeamResponseCount,
|
||||
getTeamByEnvironmentId,
|
||||
} from "@formbricks/lib/team/service";
|
||||
import { captureTelemetry } from "@formbricks/lib/telemetry";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TJsState } from "@formbricks/types/js";
|
||||
import { TJsLegacyState } from "@formbricks/types/js";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TSession } from "@formbricks/types/sessions";
|
||||
|
||||
const captureNewSessionTelemetry = async (jsVersion?: string): Promise<void> => {
|
||||
await captureTelemetry("session created", { jsVersion: jsVersion ?? "unknown" });
|
||||
};
|
||||
|
||||
export const getUpdatedState = async (
|
||||
environmentId: string,
|
||||
personId?: string,
|
||||
sessionId?: string,
|
||||
jsVersion?: string
|
||||
): Promise<TJsState> => {
|
||||
export const getUpdatedState = async (environmentId: string, personId?: string): Promise<TJsLegacyState> => {
|
||||
let environment: TEnvironment | null;
|
||||
let person: TPerson;
|
||||
let session: TSession | null;
|
||||
let person: TPerson | {};
|
||||
const session = {};
|
||||
|
||||
// check if environment exists
|
||||
environment = await getEnvironment(environmentId);
|
||||
@@ -58,64 +47,23 @@ export const getUpdatedState = async (
|
||||
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
|
||||
if (isMauLimitReached) {
|
||||
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
|
||||
if (!personId || !sessionId) {
|
||||
if (!personId) {
|
||||
// don't allow new people or sessions
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const session = await getSession(sessionId);
|
||||
if (!session) {
|
||||
// don't allow new sessions
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
// check if session was created this month (user already active this month)
|
||||
const now = new Date();
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
if (new Date(session.createdAt) < firstDayOfMonth) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!personId) {
|
||||
// create a new person
|
||||
person = await createPerson(environmentId);
|
||||
// create a new session
|
||||
session = await createSession(person.id);
|
||||
person = { id: "legacy" };
|
||||
} else {
|
||||
// check if person exists
|
||||
const existingPerson = await getPerson(personId);
|
||||
if (!existingPerson) {
|
||||
// create a new person
|
||||
person = await createPerson(environmentId);
|
||||
} else {
|
||||
if (existingPerson) {
|
||||
person = existingPerson;
|
||||
}
|
||||
}
|
||||
if (!sessionId) {
|
||||
// create a new session
|
||||
session = await createSession(person.id);
|
||||
} else {
|
||||
// check validity of person & session
|
||||
session = await getSession(sessionId);
|
||||
if (!session) {
|
||||
// create a new session
|
||||
session = await createSession(person.id);
|
||||
captureNewSessionTelemetry(jsVersion);
|
||||
} else {
|
||||
// check if session is expired
|
||||
if (session.expiresAt < new Date()) {
|
||||
// create a new session
|
||||
session = await createSession(person.id);
|
||||
captureNewSessionTelemetry(jsVersion);
|
||||
} else {
|
||||
// extend session (if about to expire)
|
||||
const isSessionAboutToExpire =
|
||||
new Date(session.expiresAt).getTime() - new Date().getTime() < 1000 * 60 * 10;
|
||||
|
||||
if (isSessionAboutToExpire) {
|
||||
session = await extendSession(sessionId);
|
||||
}
|
||||
}
|
||||
person = { id: "legacy" };
|
||||
}
|
||||
}
|
||||
// check if App Survey limit is reached
|
||||
@@ -131,9 +79,20 @@ export const getUpdatedState = async (
|
||||
monthlyResponsesCount >= PRICING_APPSURVEYS_FREE_RESPONSES;
|
||||
}
|
||||
|
||||
const isPerson = Object.keys(person).length > 0;
|
||||
|
||||
let surveys;
|
||||
if (isAppSurveyLimitReached) {
|
||||
surveys = [];
|
||||
} else if (isPerson) {
|
||||
surveys = await getSyncSurveysCached(environmentId, person as TPerson);
|
||||
} else {
|
||||
surveys = await getSurveys(environmentId);
|
||||
surveys = surveys.filter((survey) => survey.type === "web");
|
||||
}
|
||||
|
||||
// get/create rest of the state
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
!isAppSurveyLimitReached ? getSyncSurveysCached(environmentId, person) : [],
|
||||
const [noCodeActionClasses, product] = await Promise.all([
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
@@ -143,8 +102,8 @@ export const getUpdatedState = async (
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsState = {
|
||||
person: person!,
|
||||
const state: TJsLegacyState = {
|
||||
person,
|
||||
session,
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { ZJsSyncInput } from "@formbricks/types/js";
|
||||
import { ZJsSyncLegacyInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
@@ -13,7 +13,7 @@ export async function POST(req: Request): Promise<NextResponse> {
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZJsSyncInput.safeParse(jsonInput);
|
||||
const inputValidation = ZJsSyncLegacyInput.safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -23,9 +23,9 @@ export async function POST(req: Request): Promise<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, personId, sessionId } = inputValidation.data;
|
||||
const { environmentId, personId } = inputValidation.data;
|
||||
|
||||
const state = await getUpdatedState(environmentId, personId, sessionId, inputValidation.data.jsVersion);
|
||||
const state = await getUpdatedState(environmentId, personId);
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
@@ -1,11 +1,11 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { createDisplayLegacy } from "@formbricks/lib/display/service";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { createDisplay } from "@formbricks/lib/display/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
|
||||
import { TDisplay, ZDisplayCreateInput } from "@formbricks/types/displays";
|
||||
import { TDisplay, ZDisplayLegacyCreateInput } from "@formbricks/types/displays";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
@@ -13,8 +13,11 @@ export async function OPTIONS(): Promise<NextResponse> {
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
const jsonInput: unknown = await request.json();
|
||||
const inputValidation = ZDisplayCreateInput.safeParse(jsonInput);
|
||||
const jsonInput = await request.json();
|
||||
if (jsonInput.personId === "legacy") {
|
||||
delete jsonInput.personId;
|
||||
}
|
||||
const inputValidation = ZDisplayLegacyCreateInput.safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -24,12 +27,14 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
const displayInput = inputValidation.data;
|
||||
const { surveyId, responseId } = inputValidation.data;
|
||||
let { personId } = inputValidation.data;
|
||||
|
||||
// find environmentId from surveyId
|
||||
let survey;
|
||||
|
||||
try {
|
||||
survey = await getSurvey(displayInput.surveyId);
|
||||
survey = await getSurvey(surveyId);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
@@ -45,7 +50,11 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
// create display
|
||||
let display: TDisplay;
|
||||
try {
|
||||
display = await createDisplay(displayInput);
|
||||
display = await createDisplayLegacy({
|
||||
surveyId,
|
||||
personId,
|
||||
responseId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
@@ -57,7 +66,7 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
|
||||
if (teamDetails?.teamOwnerId) {
|
||||
await capturePosthogEvent(teamDetails.teamOwnerId, "display created", teamDetails.teamId, {
|
||||
surveyId: displayInput.surveyId,
|
||||
surveyId,
|
||||
});
|
||||
} else {
|
||||
console.warn("Posthog capture not possible. No team owner found");
|
||||
109
apps/web/app/api/v1/client/(legacy)/responses/route.ts
Normal file
109
apps/web/app/api/v1/client/(legacy)/responses/route.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { createResponse } from "@formbricks/lib/response/service";
|
||||
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
const responseInput: TResponseInput = await request.json();
|
||||
if (responseInput.personId === "legacy") {
|
||||
responseInput.personId = null;
|
||||
}
|
||||
const agent = UAParser(request.headers.get("user-agent"));
|
||||
const inputValidation = ZResponseInput.safeParse(responseInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
let survey: TSurvey | null;
|
||||
|
||||
try {
|
||||
survey = await getSurvey(responseInput.surveyId);
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", responseInput.surveyId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
} else {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const teamDetails = await getTeamDetails(survey.environmentId);
|
||||
|
||||
let response: TResponse;
|
||||
try {
|
||||
const meta = {
|
||||
source: responseInput?.meta?.source,
|
||||
url: responseInput?.meta?.url,
|
||||
userAgent: {
|
||||
browser: agent?.browser.name,
|
||||
device: agent?.device.type,
|
||||
os: agent?.os.name,
|
||||
},
|
||||
};
|
||||
|
||||
// check if personId is anonymous
|
||||
if (responseInput.personId === "anonymous") {
|
||||
// remove this from the request
|
||||
responseInput.personId = null;
|
||||
}
|
||||
|
||||
response = await createResponse({
|
||||
...responseInput,
|
||||
meta,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
} else {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
sendToPipeline({
|
||||
event: "responseCreated",
|
||||
environmentId: survey.environmentId,
|
||||
surveyId: response.surveyId,
|
||||
response: response,
|
||||
});
|
||||
|
||||
if (responseInput.finished) {
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: survey.environmentId,
|
||||
surveyId: response.surveyId,
|
||||
response: response,
|
||||
});
|
||||
}
|
||||
|
||||
if (teamDetails?.teamOwnerId) {
|
||||
await capturePosthogEvent(teamDetails.teamOwnerId, "response created", teamDetails.teamId, {
|
||||
surveyId: response.surveyId,
|
||||
surveyType: survey.type,
|
||||
});
|
||||
} else {
|
||||
console.warn("Posthog capture not possible. No team owner found");
|
||||
}
|
||||
|
||||
return responses.successResponse(response, true);
|
||||
}
|
||||
@@ -4,16 +4,25 @@ import { createAction } from "@formbricks/lib/action/service";
|
||||
import { ZActionInput } from "@formbricks/types/actions";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: Request): Promise<NextResponse> {
|
||||
export async function POST(req: Request, context: Context): Promise<NextResponse> {
|
||||
try {
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZActionInput.safeParse(jsonInput);
|
||||
const inputValidation = ZActionInput.safeParse({
|
||||
...jsonInput,
|
||||
environmentId: context.params.environmentId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -23,19 +32,7 @@ export async function POST(req: Request): Promise<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, sessionId, name, properties } = inputValidation.data;
|
||||
|
||||
// hotfix: don't create action for "Exit Intent (Desktop)", 50% Scroll events
|
||||
if (["Exit Intent (Desktop)", "50% Scroll"].includes(name)) {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
createAction({
|
||||
environmentId,
|
||||
sessionId,
|
||||
name,
|
||||
properties,
|
||||
});
|
||||
await createAction(inputValidation.data);
|
||||
|
||||
return responses.successResponse({}, true);
|
||||
} catch (error) {
|
||||
@@ -0,0 +1,40 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { updateDisplay } from "@formbricks/lib/display/service";
|
||||
import { ZDisplayUpdateInput } from "@formbricks/types/displays";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
displayId: string;
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, context: Context): Promise<NextResponse> {
|
||||
const { displayId } = context.params;
|
||||
const jsonInput = await request.json();
|
||||
const inputValidation = ZDisplayUpdateInput.safeParse({
|
||||
...jsonInput,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const display = await updateDisplay(displayId, inputValidation.data);
|
||||
return responses.successResponse(display, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(error.message, true);
|
||||
}
|
||||
}
|
||||
65
apps/web/app/api/v1/client/[environmentId]/displays/route.ts
Normal file
65
apps/web/app/api/v1/client/[environmentId]/displays/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { createDisplay } from "@formbricks/lib/display/service";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
|
||||
import { TDisplay, ZDisplayCreateInput } from "@formbricks/types/displays";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(request: Request, context: Context): Promise<NextResponse> {
|
||||
const jsonInput = await request.json();
|
||||
const inputValidation = ZDisplayCreateInput.safeParse({
|
||||
...jsonInput,
|
||||
environmentId: context.params.environmentId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// find teamId & teamOwnerId from environmentId
|
||||
const teamDetails = await getTeamDetails(inputValidation.data.environmentId);
|
||||
|
||||
// create display
|
||||
let display: TDisplay;
|
||||
try {
|
||||
display = await createDisplay(inputValidation.data);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
} else {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (teamDetails?.teamOwnerId) {
|
||||
await capturePosthogEvent(teamDetails.teamOwnerId, "display created", teamDetails.teamId);
|
||||
} else {
|
||||
console.warn("Posthog capture not possible. No team owner found");
|
||||
}
|
||||
|
||||
return responses.successResponse(
|
||||
{
|
||||
...display,
|
||||
createdAt: display.createdAt.toISOString(),
|
||||
updatedAt: display.updatedAt.toISOString(),
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSyncSurveysCached } from "@formbricks/lib/survey/service";
|
||||
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TJsState, ZJsPeopleUserIdInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_: Request,
|
||||
{
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
environmentId: string;
|
||||
userId: string;
|
||||
};
|
||||
}
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
// validate using zod
|
||||
const inputValidation = ZJsPeopleUserIdInput.safeParse({
|
||||
environmentId: params.environmentId,
|
||||
userId: params.userId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, userId } = inputValidation.data;
|
||||
|
||||
// check if person exists
|
||||
const person = await getOrCreatePersonByUserId(userId, environmentId);
|
||||
|
||||
if (!person) {
|
||||
return responses.badRequestResponse(`Person with userId ${userId} not found`);
|
||||
}
|
||||
|
||||
let environment: TEnvironment | null;
|
||||
|
||||
// check if environment exists
|
||||
environment = await getEnvironment(environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
if (!environment?.widgetSetupCompleted) {
|
||||
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
|
||||
}
|
||||
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
|
||||
// check if Monthly Active Users limit is reached
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
const hasUserTargetingSubscription =
|
||||
team?.billing?.features.userTargeting.status &&
|
||||
team?.billing?.features.userTargeting.status in ["active", "canceled"];
|
||||
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
|
||||
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
|
||||
|
||||
// TODO: Problem is that if isMauLimitReached, all sync request will fail
|
||||
// But what we essentially want, is to fail only for new people syncing for the first time
|
||||
|
||||
if (isMauLimitReached) {
|
||||
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveysCached(environmentId, person),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsState = {
|
||||
person,
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
};
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TJsState, ZJsPublicSyncInput } from "@formbricks/types/js";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_: NextRequest,
|
||||
{ params }: { params: { environmentId: string } }
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
// validate using zod
|
||||
const environmentIdValidation = ZJsPublicSyncInput.safeParse({
|
||||
environmentId: params.environmentId,
|
||||
});
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId } = environmentIdValidation.data;
|
||||
|
||||
const environment = await getEnvironment(environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
if (!environment?.widgetSetupCompleted) {
|
||||
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSurveys(environmentId),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const state: TJsState = {
|
||||
surveys: surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web"),
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
person: null,
|
||||
};
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(`Unable to complete response: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
@@ -8,13 +8,20 @@ import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
personId: string;
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: Request, { params }): Promise<NextResponse> {
|
||||
export async function POST(req: Request, context: Context): Promise<NextResponse> {
|
||||
try {
|
||||
const { personId } = params;
|
||||
const { personId, environmentId } = context.params;
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
@@ -28,7 +35,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, sessionId, key, value } = inputValidation.data;
|
||||
const { key, value } = inputValidation.data;
|
||||
|
||||
const existingPerson = await getPerson(personId);
|
||||
|
||||
@@ -59,7 +66,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const state = await getUpdatedState(environmentId, personId, sessionId);
|
||||
const state = await getUpdatedState(environmentId, personId);
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
32
apps/web/app/api/v1/client/[environmentId]/people/route.ts
Normal file
32
apps/web/app/api/v1/client/[environmentId]/people/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { createPerson } from "@formbricks/lib/person/service";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function OPTIONS() {
|
||||
// cors headers
|
||||
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// we need to create a new person
|
||||
// call the createPerson service from here
|
||||
|
||||
const { environmentId, userId } = await req.json();
|
||||
|
||||
if (!environmentId) {
|
||||
return responses.badRequestResponse("environmentId is required", { environmentId }, true);
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return responses.badRequestResponse("userId is required", { environmentId }, true);
|
||||
}
|
||||
|
||||
try {
|
||||
const person = await createPerson(environmentId, userId);
|
||||
|
||||
return responses.successResponse({ status: "success", person }, true);
|
||||
} catch (err) {
|
||||
return responses.internalServerErrorResponse("Something went wrong", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { updateResponse } from "@formbricks/lib/response/service";
|
||||
import { ZResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: { responseId: string } }
|
||||
): Promise<NextResponse> {
|
||||
const { responseId } = params;
|
||||
|
||||
if (!responseId) {
|
||||
return responses.badRequestResponse("Response ID is missing", undefined, true);
|
||||
}
|
||||
|
||||
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),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// update response
|
||||
let response;
|
||||
try {
|
||||
response = await updateResponse(responseId, inputValidation.data);
|
||||
} catch (error) {
|
||||
if (error instanceof ResourceNotFoundError) {
|
||||
return responses.notFoundResponse("Response", responseId, true);
|
||||
}
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
}
|
||||
if (error instanceof DatabaseError) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// get survey to get environmentId
|
||||
let survey;
|
||||
try {
|
||||
survey = await getSurvey(response.surveyId);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
}
|
||||
if (error instanceof DatabaseError) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// send response update to pipeline
|
||||
// don't await to not block the response
|
||||
sendToPipeline({
|
||||
event: "responseUpdated",
|
||||
environmentId: survey.environmentId,
|
||||
surveyId: survey.id,
|
||||
response,
|
||||
});
|
||||
|
||||
if (response.finished) {
|
||||
// send response to pipeline
|
||||
// don't await to not block the response
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: survey.environmentId,
|
||||
surveyId: survey.id,
|
||||
response: response,
|
||||
});
|
||||
}
|
||||
return responses.successResponse(response, true);
|
||||
}
|
||||
@@ -54,6 +54,12 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
},
|
||||
};
|
||||
|
||||
// check if personId is anonymous
|
||||
if (responseInput.personId === "anonymous") {
|
||||
// remove this from the request
|
||||
responseInput.personId = null;
|
||||
}
|
||||
|
||||
response = await createResponse({
|
||||
...responseInput,
|
||||
meta,
|
||||
@@ -6,12 +6,20 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { putFileToLocalStorage } from "@formbricks/lib/storage/service";
|
||||
import { UPLOADS_DIR } from "@formbricks/lib/constants";
|
||||
import { env } from "@/env.mjs";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { validateLocalSignedUrl } from "@formbricks/lib/crypto";
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
interface Context {
|
||||
params: {
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest, context: Context): Promise<NextResponse> {
|
||||
const environmentId = context.params.environmentId;
|
||||
|
||||
const accessType = "private"; // private files are accessible only by authorized users
|
||||
const headersList = headers();
|
||||
|
||||
@@ -47,16 +55,12 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
const [survey, team] = await Promise.all([getSurvey(surveyId), getTeamByEnvironmentId(environmentId)]);
|
||||
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", surveyId);
|
||||
}
|
||||
|
||||
const { environmentId } = survey;
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
|
||||
}
|
||||
@@ -4,13 +4,21 @@ import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import uploadPrivateFile from "./lib/uploadPrivateFile";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
// api endpoint for uploading private files
|
||||
// uploaded files will be private, only the user who has access to the environment can access the file
|
||||
// uploading private files requires no authentication
|
||||
// use this to let users upload files to a survey for example
|
||||
// this api endpoint will return a signed url for uploading the file to s3 and another url for uploading file to the local storage
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
export async function POST(req: NextRequest, context: Context): Promise<NextResponse> {
|
||||
const environmentId = context.params.environmentId;
|
||||
|
||||
const { fileName, fileType, surveyId } = await req.json();
|
||||
|
||||
if (!surveyId) {
|
||||
@@ -25,16 +33,12 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
return responses.badRequestResponse("contentType is required");
|
||||
}
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
const [survey, team] = await Promise.all([getSurvey(surveyId), getTeamByEnvironmentId(environmentId)]);
|
||||
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", surveyId);
|
||||
}
|
||||
|
||||
const { environmentId } = survey;
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { getSettings } from "@/app/lib/api/clientSettings";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { createPerson } from "@formbricks/lib/person/service";
|
||||
import { createSession } from "@formbricks/lib/session/service";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_: Request,
|
||||
{ params }: { params: { environmentId: string } }
|
||||
): Promise<NextResponse> {
|
||||
const { environmentId } = params;
|
||||
|
||||
if (!environmentId) {
|
||||
return responses.badRequestResponse(
|
||||
"Missing environmentId",
|
||||
{
|
||||
missing_field: "environmentId",
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const person = await createPerson(environmentId);
|
||||
const session = await createSession(person.id);
|
||||
const settings = await getSettings(environmentId, person.id);
|
||||
|
||||
return responses.successResponse(
|
||||
{
|
||||
person,
|
||||
session,
|
||||
settings,
|
||||
},
|
||||
true
|
||||
);
|
||||
} catch (error) {
|
||||
return responses.internalServerErrorResponse(error.message, true);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { connectAirtable, fetchAirtableAuthToken } from "@formbricks/lib/airtable/service";
|
||||
import { AIR_TABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import * as z from "zod";
|
||||
|
||||
async function getEmail(token: string) {
|
||||
@@ -26,27 +27,27 @@ export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!environmentId) {
|
||||
return NextResponse.json({ error: "Invalid environmentId" });
|
||||
return responses.badRequestResponse("Invalid environmentId");
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ Error: "Invalid session" }, { status: 400 });
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
if (code && typeof code !== "string") {
|
||||
return NextResponse.json({ message: "`code` must be a string" }, { status: 400 });
|
||||
return responses.badRequestResponse("`code` must be a string");
|
||||
}
|
||||
const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId);
|
||||
if (!canUserAccessEnvironment) {
|
||||
return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 });
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const client_id = AIR_TABLE_CLIENT_ID;
|
||||
const client_id = AIRTABLE_CLIENT_ID;
|
||||
const redirect_uri = WEBAPP_URL + "/api/v1/integrations/airtable/callback";
|
||||
const code_verifier = Buffer.from(environmentId + session.user.id + environmentId).toString("base64");
|
||||
|
||||
if (!client_id) return NextResponse.json({ Error: "Airtable client id is missing" }, { status: 400 });
|
||||
if (!redirect_uri) return NextResponse.json({ Error: "Airtable redirect url is missing" }, { status: 400 });
|
||||
if (!client_id) return responses.internalServerErrorResponse("Airtable client id is missing");
|
||||
if (!redirect_uri) return responses.internalServerErrorResponse("Airtable redirect url is missing");
|
||||
|
||||
const formData = {
|
||||
grant_type: "authorization_code",
|
||||
@@ -59,7 +60,7 @@ export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const key = await fetchAirtableAuthToken(formData);
|
||||
if (!key) {
|
||||
return NextResponse.json({ Error: "Failed to fetch Airtable auth token" }, { status: 500 });
|
||||
return responses.notFoundResponse("airtable auth token", key);
|
||||
}
|
||||
const email = await getEmail(key.access_token);
|
||||
|
||||
@@ -71,8 +72,7 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/airtable`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
NextResponse.json({ Error: error }, { status: 500 });
|
||||
responses.internalServerErrorResponse(error);
|
||||
}
|
||||
|
||||
NextResponse.json({ Error: "unknown error occurred" }, { status: 400 });
|
||||
responses.badRequestResponse("unknown error occurred");
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { NextRequest } from "next/server";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { AIR_TABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
|
||||
const scope = `data.records:read data.records:write schema.bases:read schema.bases:write user.email:read`;
|
||||
|
||||
@@ -13,23 +14,22 @@ export async function GET(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!environmentId) {
|
||||
return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 });
|
||||
return responses.badRequestResponse("environmentId is missing");
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ Error: "Invalid session" }, { status: 400 });
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId);
|
||||
if (!canUserAccessEnvironment) {
|
||||
return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 });
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const client_id = AIR_TABLE_CLIENT_ID;
|
||||
const client_id = AIRTABLE_CLIENT_ID;
|
||||
const redirect_uri = WEBAPP_URL + "/api/v1/integrations/airtable/callback";
|
||||
if (!client_id) return NextResponse.json({ Error: "Airtable client id is missing" }, { status: 400 });
|
||||
if (!redirect_uri) return NextResponse.json({ Error: "Airtable redirect url is missing" }, { status: 400 });
|
||||
|
||||
if (!client_id) return responses.internalServerErrorResponse("Airtable client id is missing");
|
||||
if (!redirect_uri) return responses.internalServerErrorResponse("Airtable redirect url is missing");
|
||||
const codeVerifier = Buffer.from(environmentId + session.user.id + environmentId).toString("base64");
|
||||
|
||||
const codeChallengeMethod = "S256";
|
||||
@@ -51,5 +51,5 @@ export async function GET(req: NextRequest) {
|
||||
authUrl.searchParams.append("code_challenge_method", codeChallengeMethod);
|
||||
authUrl.searchParams.append("code_challenge", codeChallenge);
|
||||
|
||||
return NextResponse.json({ authUrl: authUrl.toString() }, { status: 200 });
|
||||
return responses.successResponse({ authUrl: authUrl.toString() });
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { NextRequest } from "next/server";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import * as z from "zod";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
@@ -15,30 +16,28 @@ export async function GET(req: NextRequest) {
|
||||
const baseId = z.string().safeParse(queryParams.get("baseId"));
|
||||
|
||||
if (!baseId.success) {
|
||||
return NextResponse.json({ Error: "Base Id is Required" }, { status: 400 });
|
||||
return responses.missingFieldResponse("Base Id is Required");
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ Error: "Invalid session" }, { status: 400 });
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
if (!environmentId) {
|
||||
return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 });
|
||||
return responses.badRequestResponse("environmentId is missing");
|
||||
}
|
||||
|
||||
const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId);
|
||||
if (!canUserAccessEnvironment || !environmentId) {
|
||||
return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 });
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
const integration = (await getIntegrationByType(environmentId, "airtable")) as TIntegrationAirtable;
|
||||
console.log(integration);
|
||||
|
||||
if (!integration) {
|
||||
return NextResponse.json({ Error: "integration not found" }, { status: 401 });
|
||||
return responses.notFoundResponse("Integration not found", environmentId);
|
||||
}
|
||||
|
||||
const tables = await getTables(integration.config.key, baseId.data);
|
||||
|
||||
return NextResponse.json(tables, { status: 200 });
|
||||
return responses.successResponse(tables);
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { getDisplaysByPersonId, updateDisplay } from "@formbricks/lib/display/service";
|
||||
import { personCache } from "@formbricks/lib/person/cache";
|
||||
import { deletePerson, selectPerson, transformPrismaPerson } from "@formbricks/lib/person/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { ZJsPeopleUserIdInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: Request, { params }): Promise<NextResponse> {
|
||||
try {
|
||||
const { personId } = params;
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZJsPeopleUserIdInput.safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, userId, sessionId } = inputValidation.data;
|
||||
|
||||
let returnedPerson;
|
||||
// check if person with this userId exists
|
||||
const person = await prisma.person.findFirst({
|
||||
where: {
|
||||
environmentId,
|
||||
attributes: {
|
||||
some: {
|
||||
attributeClass: {
|
||||
name: "userId",
|
||||
},
|
||||
value: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: selectPerson,
|
||||
});
|
||||
// if person exists, reconnect displays, session and delete old user
|
||||
if (person) {
|
||||
const displays = await getDisplaysByPersonId(personId);
|
||||
|
||||
await Promise.all(displays.map((display) => updateDisplay(display.id, { personId: person.id })));
|
||||
|
||||
// reconnect session to new person
|
||||
await prisma.session.update({
|
||||
where: {
|
||||
id: sessionId,
|
||||
},
|
||||
data: {
|
||||
person: {
|
||||
connect: {
|
||||
id: person.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// delete old person
|
||||
await deletePerson(personId);
|
||||
|
||||
returnedPerson = person;
|
||||
} else {
|
||||
// update person with userId
|
||||
returnedPerson = await prisma.person.update({
|
||||
where: {
|
||||
id: personId,
|
||||
},
|
||||
data: {
|
||||
attributes: {
|
||||
create: {
|
||||
value: userId,
|
||||
attributeClass: {
|
||||
connect: {
|
||||
name_environmentId: {
|
||||
name: "userId",
|
||||
environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: selectPerson,
|
||||
});
|
||||
|
||||
personCache.revalidate({
|
||||
id: returnedPerson.id,
|
||||
environmentId: returnedPerson.environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
const transformedPerson = transformPrismaPerson(returnedPerson);
|
||||
|
||||
personCache.revalidate({
|
||||
id: transformedPerson.id,
|
||||
environmentId: environmentId,
|
||||
});
|
||||
|
||||
surveyCache.revalidate({
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const state = await getUpdatedState(environmentId, transformedPerson.id, sessionId);
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { NextResponse } from "next/server";
|
||||
import {
|
||||
deleteAttributeClass,
|
||||
getAttributeClass,
|
||||
updatetAttributeClass,
|
||||
updateAttributeClass,
|
||||
} from "@formbricks/lib/attributeClass/service";
|
||||
import { TAttributeClass, ZAttributeClassUpdateInput } from "@formbricks/types/attributeClasses";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
@@ -82,7 +82,7 @@ export async function PUT(
|
||||
transformErrorToDetails(inputValidation.error)
|
||||
);
|
||||
}
|
||||
const updatedAttributeClass = await updatetAttributeClass(params.attributeClassId, inputValidation.data);
|
||||
const updatedAttributeClass = await updateAttributeClass(params.attributeClassId, inputValidation.data);
|
||||
if (updatedAttributeClass) {
|
||||
return responses.successResponse(updatedAttributeClass);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user