Compare commits

...

17 Commits

Author SHA1 Message Date
Piyush Gupta
eae5d67a2b fix: weekly summary 2025-02-26 10:34:33 +05:30
Dhruwang Jariwala
5c2c1bbfcd fix: removed remove-unused flag (#4814) 2025-02-25 12:52:29 +00:00
Piyush Gupta
54e84858b5 docs: adds query param in single-use-id docs (#4811) 2025-02-25 11:18:59 +00:00
Piyush Gupta
833d0789d7 fix: oauth docs formatting (#4807) 2025-02-25 09:47:52 +00:00
Dhruwang Jariwala
1a974f3dd8 fix: survey placement and close on click outside (#4745)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-02-25 08:33:52 +00:00
Salim B
146173883f docs: Fix formatting and other small tweaks (#4798)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2025-02-25 06:09:45 +00:00
Johannes
ebb02a5723 docs: add suspense to RN (#4801) 2025-02-25 05:54:32 +00:00
Johannes
c96f7fed18 docs: fix images (#4800) 2025-02-24 09:51:15 +00:00
mintlify[bot]
861eff3cd2 docs: Update menu (#4793)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-02-21 04:52:05 -08:00
Piyush Gupta
b66c0d17d0 feat: adds Is set and Is not set operator for hidden fields in logic editor (#4785)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-02-21 11:47:31 +00:00
Dhruwang Jariwala
0e748050f3 fix: tolgee flow (#4765) 2025-02-21 08:56:06 +00:00
Matti Nannt
ae3524b79f chore: add api v2 draft docs (#4783)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-20 16:03:01 +01:00
mintlify[bot]
0ce58b592a docs: fix images in actions and targeting (#4781)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-02-20 05:21:27 -08:00
mintlify[bot]
578346840e docs: Rewrite Actions and Advanced Targeting Docs (#4779)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2025-02-20 12:49:13 +00:00
Piyush Gupta
56bcb46d6c fix: User Management Docs (#4775)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2025-02-20 09:28:14 +00:00
Piyush Gupta
91405c48e0 chore: remove SHORT_URL_BASE env key (#4766) 2025-02-20 08:22:09 +00:00
Paribesh Nepal
b40dff621a feat: Support HEIC format for images (#4719)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-02-20 04:00:49 +00:00
76 changed files with 1811 additions and 357 deletions

View File

@@ -30,13 +30,36 @@ jobs:
npx tolgee tag \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-extracted \
--filter-tag "draft: ${BRANCH_NAME}" \
--filter-tag "draft:${BRANCH_NAME}" \
--tag production \
--untag "draft: ${BRANCH_NAME}"
--untag "draft:${BRANCH_NAME}"
- name: Tag Deprecated Keys
- name: Tag unused production keys as Deprecated
run: |
npx tolgee tag \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-not-extracted --filter-tag production \
--tag deprecated --untag production
- name: Tag unused draft:current-branch keys as Deprecated
run: |
BRANCH_NAME=${GITHUB_REF##*/}
npx tolgee tag \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-not-extracted --filter-tag "draft:${BRANCH_NAME}" \
--tag deprecated --untag "draft:${BRANCH_NAME}"
- name: Sync with backup
run: |
npx tolgee sync \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--backup ./tolgee-backup \
--continue-on-warning \
--yes
- name: Upload backup as artifact
uses: actions/upload-artifact@v4
with:
name: tolgee-backup-${{ github.sha }}
path: ./tolgee-backup
retention-days: 90

5
.gitignore vendored
View File

@@ -59,4 +59,7 @@ packages/lib/uploads
apps/web/public/js
packages/database/migrations
packages/database/migrations
# tolgee
branch.json

View File

@@ -1 +1 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json

View File

@@ -1 +1 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json

View File

@@ -1,5 +1,17 @@
pnpm lint-staged
pnpm tolgee-pull || true
echo "{\"branchName\": \"main\"}" > ../branch.json
git add branch.json packages/lib/messages/*.json
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Load environment variables from .env files
if [ -f .env ]; then
export $(cat .env | grep -v '#' | xargs)
fi
pnpm lint-staged
if [ -z "$NEXT_PUBLIC_TOLGEE_API_KEY" ]; then
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
else
pnpm run tolgee-pull
git add packages/lib/messages
fi

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
vercel.json

View File

@@ -72,6 +72,7 @@ export const getSurveysForEnvironmentState = reactCache(
},
displayPercentage: true,
delay: true,
projectOverwrites: true,
},
});

View File

@@ -138,7 +138,7 @@ export const EditLogo = ({ project, environmentId, isReadOnly }: EditLogoProps)
) : (
<FileInput
id="logo-input"
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={environmentId}
onFileUpload={(files: string[]) => {
setLogoUrl(files[0]);
@@ -151,7 +151,7 @@ export const EditLogo = ({ project, environmentId, isReadOnly }: EditLogoProps)
<Input
ref={fileInputRef}
type="file"
accept="image/jpeg, image/png, image/webp"
accept="image/jpeg, image/png, image/webp, image/heic"
className="hidden"
disabled={isReadOnly}
onChange={handleFileChange}

View File

@@ -308,7 +308,7 @@ export const QuestionFormInput = ({
{showImageUploader && id === "headline" && (
<FileInput
id="question-image"
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={localSurvey.environmentId}
onFileUpload={(url: string[] | undefined, fileType: "image" | "video") => {
if (url) {

View File

@@ -114,7 +114,7 @@ export const EditWelcomeCard = ({
<div className="mt-3 flex w-full items-center justify-center">
<FileInput
id="welcome-card-image"
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={environmentId}
onFileUpload={(url: string[]) => {
updateSurvey({ fileUrl: url[0] });

View File

@@ -16,7 +16,7 @@ export const UploadImageSurveyBg = ({
<div className="flex w-full items-center justify-center">
<FileInput
id="survey-bg-file-input"
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={environmentId}
onFileUpload={(url: string[]) => {
if (url.length > 0) {

View File

@@ -133,7 +133,7 @@ export const PictureSelectionForm = ({
<div className="mt-3 flex w-full items-center justify-center">
<FileInput
id="choices-file-input"
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={environmentId}
onFileUpload={handleFileInputChanges}
fileUrl={question?.choices?.map((choice) => choice.imageUrl)}

View File

@@ -29,18 +29,21 @@ export const SurveyPlacementCard = ({
const { projectOverwrites } = localSurvey ?? {};
const { placement, clickOutsideClose, darkOverlay } = projectOverwrites ?? {};
const setProjectOverwrites = (projectOverwrites: TSurveyProjectOverwrites) => {
const setProjectOverwrites = (projectOverwrites: TSurveyProjectOverwrites | null) => {
setLocalSurvey({ ...localSurvey, projectOverwrites: projectOverwrites });
};
const togglePlacement = () => {
if (setProjectOverwrites) {
setProjectOverwrites({
...projectOverwrites,
placement: !!placement ? null : "bottomRight",
clickOutsideClose: false,
darkOverlay: false,
});
if (!!placement) {
setProjectOverwrites(null);
} else {
setProjectOverwrites({
placement: "bottomRight",
clickOutsideClose: false,
darkOverlay: false,
});
}
}
};

View File

@@ -479,6 +479,14 @@ export const getLogicRules = (t: TFnType) => {
label: t("environments.surveys.edit.does_not_end_with"),
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
{
label: t("environments.surveys.edit.is_set"),
value: ZSurveyLogicConditionsOperator.Enum.isSet,
},
{
label: t("environments.surveys.edit.is_not_set"),
value: ZSurveyLogicConditionsOperator.Enum.isNotSet,
},
],
},
};

View File

@@ -216,6 +216,8 @@ export const getMatchValueProps = (
"isPartiallySubmitted",
"isSkipped",
"isSubmitted",
"isSet",
"isNotSet",
].includes(condition.operator)
) {
return { show: false, options: [] };

View File

@@ -68,7 +68,7 @@ export const FileInput = ({
toast.error(t("common.only_one_file_allowed"));
}
const allowedFiles = getAllowedFiles(files, allowedFileExtensions, maxSizeInMB);
const allowedFiles = await getAllowedFiles(files, allowedFileExtensions, maxSizeInMB);
if (allowedFiles.length === 0) {
return;
@@ -137,7 +137,7 @@ export const FileInput = ({
};
const handleUploadMore = async (files: File[]) => {
const allowedFiles = getAllowedFiles(files, allowedFileExtensions, maxSizeInMB);
const allowedFiles = await getAllowedFiles(files, allowedFileExtensions, maxSizeInMB);
if (allowedFiles.length === 0) {
return;
}

View File

@@ -0,0 +1,29 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { z } from "zod";
const ZConvertHeicToJpegInput = z.object({
file: z.instanceof(File),
});
export const convertHeicToJpegAction = authenticatedActionClient
.schema(ZConvertHeicToJpegInput)
.action(async ({ parsedInput }) => {
if (!parsedInput.file || !parsedInput.file.name.endsWith(".heic")) return parsedInput.file;
const convert = (await import("heic-convert")).default;
const arrayBuffer = await parsedInput.file.arrayBuffer();
const nodeBuffer = Buffer.from(arrayBuffer) as unknown as ArrayBufferLike;
const convertedBuffer = await convert({
buffer: nodeBuffer,
format: "JPEG",
quality: 0.9,
});
return new File([convertedBuffer], parsedInput.file.name.replace(/\.heic$/, ".jpg"), {
type: "image/jpeg",
});
});

View File

@@ -2,6 +2,7 @@
import { toast } from "react-hot-toast";
import { TAllowedFileExtension } from "@formbricks/types/common";
import { convertHeicToJpegAction } from "./actions";
export const uploadFile = async (
file: File | Blob,
@@ -15,8 +16,6 @@ export const uploadFile = async (
const fileBuffer = await file.arrayBuffer();
// check the file size
const bufferBytes = fileBuffer.byteLength;
const bufferKB = bufferBytes / 1024;
@@ -74,7 +73,6 @@ export const uploadFile = async (
});
}
// Add the actual file to be uploaded
formData.append("file", file);
const uploadResponse = await fetch(signedUrl, {
@@ -96,34 +94,63 @@ export const uploadFile = async (
}
};
export const getAllowedFiles = (
const isFileSizeExceed = (fileSizeInMB: number, maxSizeInMB?: number) => {
if (maxSizeInMB && fileSizeInMB > maxSizeInMB) {
return true;
}
return false;
};
export const getAllowedFiles = async (
files: File[],
allowedFileExtensions: string[],
maxSizeInMB?: number
): File[] => {
): Promise<File[]> => {
const sizeExceedFiles: string[] = [];
const unsupportedExtensionFiles: string[] = [];
const convertedFiles: File[] = [];
const allowedFiles = files.filter((file) => {
for (const file of files) {
if (!file || !file.type) {
return false;
continue;
}
const extension = file.name.split(".").pop();
const fileSizeInMB = file.size / 1000000; // Kb -> Mb
const extension = file.name.split(".").pop()?.toLowerCase();
const fileSizeInMB = file.size / 1000000;
if (!allowedFileExtensions.includes(extension as TAllowedFileExtension)) {
unsupportedExtensionFiles.push(file.name);
return false; // Exclude file if extension not allowed
} else if (maxSizeInMB && fileSizeInMB > maxSizeInMB) {
sizeExceedFiles.push(file.name);
return false; // Exclude files larger than the maximum size
continue;
}
return true;
});
if (isFileSizeExceed(fileSizeInMB, maxSizeInMB)) {
sizeExceedFiles.push(file.name);
continue;
}
if (extension === "heic") {
const convertedFileResponse = await convertHeicToJpegAction({ file });
if (!convertedFileResponse?.data) {
unsupportedExtensionFiles.push(file.name);
continue;
} else {
const convertedFileSizeInMB = convertedFileResponse.data.size / 1000000;
if (isFileSizeExceed(convertedFileSizeInMB, maxSizeInMB)) {
sizeExceedFiles.push(file.name);
continue;
}
const convertedFile = new File([convertedFileResponse.data], file.name.replace(/\.heic$/, ".jpg"), {
type: "image/jpeg",
});
convertedFiles.push(convertedFile);
continue;
}
}
convertedFiles.push(file);
}
// Constructing toast messages based on the issues found
let toastMessage = "";
if (sizeExceedFiles.length > 0) {
toastMessage += `Files exceeding size limit (${maxSizeInMB} MB): ${sizeExceedFiles.join(", ")}. `;
@@ -134,7 +161,7 @@ export const getAllowedFiles = (
if (toastMessage) {
toast.error(toastMessage);
}
return allowedFiles;
return convertedFiles;
};
export const checkForYoutubePrivacyMode = (url: string): boolean => {

View File

@@ -229,7 +229,7 @@ export const InputCombobox = ({
className="min-w-0 rounded-none border-0 border-r border-slate-300 bg-white focus:border-slate-400"
{...inputProps}
id={`${id}-input`}
value={localValue as string | number}
value={localValue ?? undefined}
onChange={onInputChange}
/>
)}

View File

@@ -83,6 +83,7 @@
"file-loader": "6.2.0",
"framer-motion": "11.15.0",
"googleapis": "144.0.0",
"heic-convert": "2.1.0",
"https-proxy-agent": "7.0.6",
"jiti": "2.4.1",
"jsonwebtoken": "9.0.2",
@@ -129,6 +130,7 @@
"@formbricks/eslint-config": "workspace:*",
"@neshca/cache-handler": "1.9.0",
"@types/bcryptjs": "2.4.6",
"@types/heic-convert": "2.1.0",
"@types/lodash": "4.17.13",
"@types/markdown-it": "14.1.2",
"@types/nodemailer": "6.4.17",

View File

@@ -3,7 +3,7 @@
import { TolgeeProvider, TolgeeStaticData } from "@tolgee/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { branchName } from "../../../branch.json";
import branch from "../../../branch.json";
import { TolgeeBase } from "./shared";
type Props = {
@@ -13,7 +13,7 @@ type Props = {
};
const tolgee = TolgeeBase().init({
tagNewKeys: [`draft: ${branchName}`],
tagNewKeys: [`draft:${branch.branchName}`],
});
export const TolgeeNextProvider = ({ language, staticData, children }: Props) => {
@@ -25,6 +25,7 @@ export const TolgeeNextProvider = ({ language, staticData, children }: Props) =>
router.refresh();
});
return () => unsubscribe();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tolgee, router]);
return (

View File

@@ -1,5 +1,5 @@
import { createServerInstance } from "@tolgee/react/server";
import { branchName } from "../../../branch.json";
import branch from "../../../branch.json";
import { getLocale } from "./language";
import { TolgeeBase } from "./shared";
@@ -7,7 +7,7 @@ export const { getTolgee, getTranslate, T } = createServerInstance({
getLocale: getLocale,
createTolgee: async (language) => {
return TolgeeBase().init({
tagNewKeys: [`draft: ${branchName}`],
tagNewKeys: [`draft:${branch.branchName}`],
observerOptions: {
fullKeyEncode: true,
},

View File

@@ -1,9 +1,5 @@
{
"functions": {
"app/**/*.ts": {
"maxDuration": 10,
"memory": 512
},
"app/api/cron/**/*.ts": {
"maxDuration": 180,
"memory": 512
@@ -11,6 +7,10 @@
"app/api/v1/client/**/*.ts": {
"maxDuration": 10,
"memory": 200
},
"app/**/*.ts": {
"maxDuration": 10,
"memory": 512
}
}
}
}

View File

@@ -1 +1 @@
{ "branchName": "main" }
{"branchName": "473-weekly-summary-not-working"}

View File

@@ -50,9 +50,6 @@ x-environment: &environment
############################################## OPTIONAL (APP CONFIGURATION) ##############################################
# Set the below value if you have and want to use a custom URL for the links created by the Link Shortener
# SHORT_URL_BASE:
# Set the below to 0 to enable Email Verification for new signups (will required Email Configuration)
EMAIL_VERIFICATION_DISABLED: 1

View File

@@ -6952,7 +6952,7 @@
},
"/api/v1/management/surveys/{surveyId}/singleUseIds": {
"get": {
"description": "Generates multiple single use survey links for a survey based on its id. Count of links can be controlled using the limit query param(min: 1, max: 5000, default: 10).",
"description": "Generates multiple single use survey links for a survey based on its id.",
"parameters": [
{
"example": "{{apiKey}}",
@@ -6970,6 +6970,21 @@
"schema": {
"type": "string"
}
},
{
"description": "Number of links to generate.",
"example": 10,
"in": "query",
"name": "limit",
"required": false,
"schema": {
"default": 10,
"exclusiveMaximum": false,
"exclusiveMinimum": false,
"maximum": 5000,
"minimum": 1,
"type": "integer"
}
}
],
"responses": {

View File

@@ -0,0 +1,14 @@
---
title: "API v2 Reference (Draft)"
icon: "code-compare"
---
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has *different* authentication requirements, and provides access to different data and settings.
### API Key Setup
Checkout the [API Key Setup](/api-reference/rest-api) to access the Management APIs with an API Key.
If youve forked the collection and are running it, update the `apiKey` and `environmentId` in the collection variables with your values. We also provide post-run scripts to help auto-assign variables when running scripts.
Need more help? Visit our [Website](https://formbricks.com/) or join our [Discord](https://formbricks.com/discord)!

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
---
title: "How to setup SAML with Identity Providers"
title: "Setup SAML with Identity Providers"
description: "This guide explains the settings you need to use to configure SAML with your Identity Provider. Once configured, obtain an XML metadata file and upload it on your Formbricks instance."
---
### SAML Registration with Identity Providers
@@ -8,7 +9,7 @@ This guide explains the settings you need to use to configure SAML with your Ide
> **Note:** Please do not add a trailing slash at the end of the URLs. Create them exactly as shown below.
**Assertion consumer service URL / Single Sign-On URL / Destination URL:** https://app.formbricks.com/api/auth/saml/callback
**Assertion consumer service URL / Single Sign-On URL / Destination URL:** [https://app.formbricks.com/api/auth/saml/callback](https://app.formbricks.com/api/auth/saml/callback)
**Entity ID / Identifier / Audience URI / Audience Restriction:** [https://saml.formbricks.com](https://saml.formbricks.com)
@@ -22,55 +23,72 @@ This guide explains the settings you need to use to configure SAML with your Ide
**Mapping Attributes / Attribute Statements:**
- http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier -> id
- http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress -> email
- http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname -> firstName
- http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname -> lastName
* [http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier) -> id
* [http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress) -> email
* [http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname) -> firstName
* [http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname) -> lastName
### SAML With Okta
<Steps>
<Step title="Create an application with your OIDC provider">
<Step title="Create an application with your SAML provider">
For example, in Okta, once you create an account, you can click on Applications on the sidebar menu:
<img src="/images/development/guides/auth-and-provision/okta/okta-applications.webp" />
</Step>
<Step title="Click on Create App Integration">
<img src="/images/development/guides/auth-and-provision/okta/create-app-integration.webp" />
</Step>
<Step title="Select SAML 2.0 in the modal form, and click Next">
<img src="/images/development/guides/auth-and-provision/okta/select-saml-2.0.webp" />
</Step>
<Step title="Fill the general settings as shown and click Next">
<img src="/images/development/guides/auth-and-provision/okta/general-settings.webp" />
</Step>
<Step title="Fill the fields mapping as shown and click Next">
<img src="/images/development/guides/auth-and-provision/okta/fields-mapping.webp" />
</Step>
<Step title="Enter the SAML Integration Settings as shown and click Next">
<img src="/images/development/guides/auth-and-provision/okta/saml-integration-settings.webp" />
</Step>
<Step title="Check the internal app checkbox and click Finish">
<img src="/images/development/guides/auth-and-provision/okta/internal-app.webp" />
</Step>
<Step title="Check that the app is created successfully">
<img src="/images/development/guides/auth-and-provision/okta/app-created.webp" />
</Step>
<Step title="Click on the app and head over to the Assignments tab">
<img src="/images/development/guides/auth-and-provision/okta/assignments-tab.webp" />
</Step>
<Step title="Click on Assign button and select Assign to People">
<img src="/images/development/guides/auth-and-provision/okta/assign-to-people.webp" />
</Step>
<Step title="Select the users you want to assign the app to and click Assign">
<img src="/images/development/guides/auth-and-provision/okta/select-users.webp" />
</Step>
<Step title="Head over to the Sign On tab and scroll to the bottom to get the metadata, click on the Actions button">
<img src="/images/development/guides/auth-and-provision/okta/actions-button.webp" />
</Step>
<Step title="Click on View IdP metadata">
<img src="/images/development/guides/auth-and-provision/okta/view-idp-metadata.webp" />
</Step>
<Step title="Copy the metadata and paste it in the Formbricks SAML configuration"></Step>
<Step title="Copy the metadata and paste it in the Formbricks SAML configuration" />
</Steps>
That's it. Now when you try to login with SSO, your application on Okta will handle the authentication.
That's it. Now when you try to login with SSO, your application on Okta will handle the authentication.

View File

@@ -1,71 +1,76 @@
<svg width="198" height="263" viewBox="0 0 198 263" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 183.149H79.2V222.749C79.2 244.619 61.4705 262.349 39.6 262.349C17.7295 262.349 0 244.619 0 222.749V183.149Z" fill="url(#paint0_linear_301_72)"/>
<path d="M0 91.5747H158.4C180.27 91.5747 198 109.304 198 131.175C198 153.045 180.27 170.775 158.4 170.775H0V91.5747Z" fill="url(#paint1_linear_301_72)"/>
<path d="M0 64.9181C0 29.0648 29.0648 0 64.918 0H158.4C180.27 0 198 17.7295 198 39.6C198 61.4705 180.27 79.2 158.4 79.2H0V64.9181Z" fill="url(#paint2_linear_301_72)"/>
<mask id="mask0_301_72" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="198" height="263">
<path d="M0 183.151H79.2V222.751C79.2 244.621 61.4705 262.351 39.6 262.351C17.7295 262.351 0 244.621 0 222.751V183.151Z" fill="url(#paint3_linear_301_72)"/>
<path d="M0 91.5762H158.4C180.27 91.5762 198 109.306 198 131.176C198 153.047 180.27 170.776 158.4 170.776H0V91.5762Z" fill="url(#paint4_linear_301_72)"/>
<path d="M0 64.9181C0 29.0648 29.0648 0 64.918 0H158.4C180.27 0 198 17.7295 198 39.6C198 61.4705 180.27 79.2 158.4 79.2H0V64.9181Z" fill="url(#paint5_linear_301_72)"/>
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="220" height="220" fill="url(#paint0_radial_740_2)"/>
<path d="M50.2842 141.289H98.1312V165.213C98.1312 178.425 87.4203 189.136 74.2077 189.136C60.9951 189.136 50.2842 178.425 50.2842 165.213V141.289Z" fill="url(#paint1_linear_740_2)"/>
<path d="M50.2842 85.9663H145.978C159.191 85.9663 169.902 96.6772 169.902 109.89C169.902 123.102 159.191 133.813 145.978 133.813H50.2842V85.9663Z" fill="url(#paint2_linear_740_2)"/>
<path d="M50.2842 69.8617C50.2842 48.2017 67.8431 30.6428 89.5031 30.6428H145.978C159.191 30.6428 169.902 41.3538 169.902 54.5664C169.902 67.779 159.191 78.4899 145.978 78.4899H50.2842V69.8617Z" fill="url(#paint3_linear_740_2)"/>
<mask id="mask0_740_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="50" y="30" width="120" height="160">
<path d="M50.2842 141.291H98.1312V165.214C98.1312 178.427 87.4203 189.138 74.2077 189.138C60.9951 189.138 50.2842 178.427 50.2842 165.214V141.291Z" fill="url(#paint4_linear_740_2)"/>
<path d="M50.2842 85.9675H145.978C159.191 85.9675 169.902 96.6785 169.902 109.891C169.902 123.104 159.191 133.815 145.978 133.815H50.2842V85.9675Z" fill="url(#paint5_linear_740_2)"/>
<path d="M50.2842 69.862C50.2842 48.202 67.8431 30.6431 89.5031 30.6431H145.978C159.191 30.6431 169.902 41.354 169.902 54.5666C169.902 67.7792 159.191 78.4901 145.978 78.4901H50.2842V69.862Z" fill="url(#paint6_linear_740_2)"/>
</mask>
<g mask="url(#mask0_301_72)">
<g filter="url(#filter0_d_301_72)">
<mask id="mask1_301_72" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="198" height="263">
<path d="M0 183.151H79.2V222.751C79.2 244.621 61.4705 262.351 39.6 262.351C17.7295 262.351 0 244.621 0 222.751V183.151Z" fill="black" fill-opacity="0.1"/>
<path d="M0 64.9181C0 29.0648 29.0648 0 64.918 0H158.4C180.27 0 198 17.7295 198 39.6C198 61.4705 180.27 79.2 158.4 79.2H0V64.9181Z" fill="black" fill-opacity="0.1"/>
<path d="M0 91.5762H158.4C180.27 91.5762 198 109.306 198 131.176C198 153.047 180.27 170.776 158.4 170.776H0V91.5762Z" fill="black" fill-opacity="0.1"/>
<g mask="url(#mask0_740_2)">
<g filter="url(#filter0_d_740_2)">
<mask id="mask1_740_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="50" y="30" width="120" height="160">
<path d="M50.2842 141.291H98.1312V165.214C98.1312 178.427 87.4203 189.138 74.2077 189.138C60.9951 189.138 50.2842 178.427 50.2842 165.214V141.291Z" fill="black" fill-opacity="0.1"/>
<path d="M50.2842 69.8622C50.2842 48.2022 67.8431 30.6433 89.5031 30.6433H145.978C159.191 30.6433 169.902 41.3542 169.902 54.5668C169.902 67.7794 159.191 78.4904 145.978 78.4904H50.2842V69.8622Z" fill="black" fill-opacity="0.1"/>
<path d="M50.2842 85.9678H145.978C159.191 85.9678 169.902 96.6787 169.902 109.891C169.902 123.104 159.191 133.815 145.978 133.815H50.2842V85.9678Z" fill="black" fill-opacity="0.1"/>
</mask>
<g mask="url(#mask1_301_72)">
<path d="M4.1553 -68.2164C35.1799 -98.4956 113.85 -68.2164 113.85 -68.2164H4.1553C-3.4647 -60.7794 -8.21048 -49.6893 -8.21048 -33.6001C-8.21048 47.996 80.1781 77.6677 80.1781 134.538C80.1781 190.209 -4.52334 224.555 -8.09431 300.203H113.85C113.85 300.203 -8.21048 384.271 -8.21048 305.148C-8.21048 303.48 -8.17121 301.832 -8.09431 300.203H-61.875L-51.3525 -68.2164H4.1553Z" fill="black" fill-opacity="0.1"/>
<g mask="url(#mask1_740_2)">
<path d="M52.7947 -10.5675C71.5376 -28.8601 119.065 -10.5675 119.065 -10.5675H52.7947C48.1912 -6.07459 45.3241 0.625288 45.3241 10.3453C45.3241 59.6399 98.7223 77.5654 98.7223 111.922C98.7223 145.555 47.5517 166.304 45.3943 212.005H119.065C119.065 212.005 45.3241 262.794 45.3241 214.993C45.3241 213.985 45.3479 212.99 45.3943 212.005H12.9038L19.2607 -10.5675H52.7947Z" fill="black" fill-opacity="0.1"/>
</g>
</g>
<g filter="url(#filter1_f_301_72)">
<circle cx="-24.75" cy="227.7" r="74.25" fill="#00C4B8"/>
<g filter="url(#filter1_f_740_2)">
<circle cx="35.3322" cy="168.204" r="44.8566" fill="#00C4B8"/>
</g>
<g filter="url(#filter2_f_301_72)">
<circle cx="-24.75" cy="39.6006" r="74.25" fill="#00C4B8"/>
<g filter="url(#filter2_f_740_2)">
<circle cx="35.3322" cy="54.5671" r="44.8566" fill="#00C4B8"/>
</g>
</g>
<defs>
<filter id="filter0_d_301_72" x="-6.4918" y="-38.9508" width="191.752" height="340.253" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter0_d_740_2" x="46.3623" y="7.11196" width="115.843" height="205.557" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="32.459"/>
<feGaussianBlur stdDeviation="19.4754"/>
<feOffset dx="19.6095"/>
<feGaussianBlur stdDeviation="11.7657"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_301_72"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_301_72" result="shape"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_740_2"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_740_2" result="shape"/>
</filter>
<filter id="filter1_f_301_72" x="-163.918" y="88.5317" width="278.336" height="278.336" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter1_f_740_2" x="-48.7433" y="84.1283" width="168.151" height="168.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="32.459" result="effect1_foregroundBlur_301_72"/>
<feGaussianBlur stdDeviation="19.6095" result="effect1_foregroundBlur_740_2"/>
</filter>
<filter id="filter2_f_301_72" x="-163.918" y="-99.5675" width="278.336" height="278.336" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter2_f_740_2" x="-48.7433" y="-29.5085" width="168.151" height="168.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="32.459" result="effect1_foregroundBlur_301_72"/>
<feGaussianBlur stdDeviation="19.6095" result="effect1_foregroundBlur_740_2"/>
</filter>
<linearGradient id="paint0_linear_301_72" x1="79.5444" y1="221.314" x2="-0.00681719" y2="221.636" gradientUnits="userSpaceOnUse">
<radialGradient id="paint0_radial_740_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(110 110) rotate(90) scale(110)">
<stop stop-color="#334155"/>
<stop offset="1" stop-color="#0F172A"/>
</radialGradient>
<linearGradient id="paint1_linear_740_2" x1="98.3393" y1="164.346" x2="50.2801" y2="164.54" gradientUnits="userSpaceOnUse">
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint1_linear_301_72" x1="198.861" y1="129.74" x2="2.49336e-08" y2="131.749" gradientUnits="userSpaceOnUse">
<linearGradient id="paint2_linear_740_2" x1="170.422" y1="109.023" x2="50.2842" y2="110.237" gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint2_linear_301_72" x1="198.861" y1="38.1652" x2="2.49336e-08" y2="40.1739" gradientUnits="userSpaceOnUse">
<linearGradient id="paint3_linear_740_2" x1="170.422" y1="53.6996" x2="50.2842" y2="54.9131" gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint3_linear_301_72" x1="79.5444" y1="221.316" x2="-0.00681719" y2="221.638" gradientUnits="userSpaceOnUse">
<linearGradient id="paint4_linear_740_2" x1="98.3393" y1="164.347" x2="50.2801" y2="164.541" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint4_linear_301_72" x1="198.861" y1="129.741" x2="2.49336e-08" y2="131.75" gradientUnits="userSpaceOnUse">
<linearGradient id="paint5_linear_740_2" x1="170.422" y1="109.024" x2="50.2842" y2="110.238" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint5_linear_301_72" x1="198.861" y1="38.1652" x2="2.49336e-08" y2="40.1739" gradientUnits="userSpaceOnUse">
<linearGradient id="paint6_linear_740_2" x1="170.422" y1="53.6998" x2="50.2842" y2="54.9133" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -758,9 +758,14 @@
"url": "development"
},
{
"name": "API Reference",
"name": "API v1 Reference",
"openapi": "/api-reference/openapi.json",
"url": "api-reference"
},
{
"name": "API v2 Reference (WIP 👷‍♂️)",
"openapi": "/api-v2-reference/openapi.yml",
"url": "api-v2-reference"
}
],
"topbarCtaButton": {

View File

@@ -909,7 +909,6 @@ Formbricks v1.2 introduces new features for Link Surveys and enhances security.
| Environment Variable | Required | Recommended Generation | Comments |
| -------------------- | -------- | ----------------------- | ----------------------------------------------------------- |
| `ENCRYPTION_KEY` | true | `openssl rand -hex 32` | Needed for 2 Factor Authentication |
| `SHORT_URL_BASE` | false | `<your-short-base-url>` | Needed if you want to enable shorter links for Link Surveys |
### Deprecated / Removed Environment Variables

View File

@@ -5,9 +5,7 @@ icon: "key"
---
<Note>
Single Sign-On (SSO) functionality, including OAuth integrations with Google,
Microsoft Entra ID, Github and OpenID Connect, requires a valid Formbricks
Enterprise License.
Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Entra ID, Github and OpenID Connect, requires a valid Formbricks Enterprise License.
</Note>
### Google OAuth
@@ -140,8 +138,7 @@ Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks insta
- Enter a **Description**, set an **Expires** period, then click **Add**.
<Note>
You will need to create a new client secret using these steps whenever your
chosen expiry period ends.
You will need to create a new client secret using these steps whenever your chosen expiry period ends.
</Note>
![ninth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251467/image_bkirq4.jpg)
@@ -149,9 +146,7 @@ Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks insta
- Copy the entry under **Value** to populate the `AZUREAD_CLIENT_SECRET` variable.
<Note>
Microsoft will only show this value to you immediately after creation, and you
will not be able to access it again. If you lose it, simply start from step 9
to create a new secret.
Microsoft will only show this value to you immediately after creation, and you will not be able to access it again. If you lose it, simply start from step 9 to create a new secret.
</Note>
![tenth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251234/image_jen6tp.jpg)
@@ -159,19 +154,17 @@ Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks insta
- Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
<Note>
You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g.,
"THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters.
You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g., "THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters.
</Note>
An example `.env` for Microsoft Entra ID in Formbricks would look like:
````yml Formbricks Env for Microsoft Entra ID SSO
```yml Formbricks Env for Microsoft Entra ID SSO
AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c
AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885
AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3"
```
- Restart your Formbricks instance.
- You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant.
@@ -191,8 +184,7 @@ Integrating your own OIDC (OpenID Connect) instance with your Formbricks instanc
- `OIDC_SIGNING_ALGORITHM`
<Note>
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}
/api/auth/callback/openid`.
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
</Note>
- Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
@@ -203,7 +195,8 @@ An example configuration for a FusionAuth OpenID Connect in Formbricks would loo
```yml Formbricks Env for FusionAuth OIDC
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
OIDC_ISSUER=http://localhost:9011 OIDC_DISPLAY_NAME=FusionAuth
OIDC_ISSUER=http://localhost:9011
OIDC_DISPLAY_NAME=FusionAuth
OIDC_SIGNING_ALGORITHM=HS256
```
@@ -213,4 +206,3 @@ OIDC_SIGNING_ALGORITHM=HS256
- Restart your Formbricks instance.
- You're all set! Users can now sign up & log in using their OIDC credentials.
````

View File

@@ -16,81 +16,81 @@ Make sure Docker and Docker Compose are installed on your system. These are usua
## Start
* **Create a New Directory for Formbricks**
1. **Create a New Directory for Formbricks**
Open a terminal and run the following commands to create and enter a new directory for Formbricks:
Open a terminal and run the following commands to create and enter a new directory for Formbricks:
```bash
mkdir formbricks-quickstart && cd formbricks-quickstart
```
```bash
mkdir formbricks-quickstart && cd formbricks-quickstart
```
* **Download the Docker-Compose File**
1. **Download the Docker-Compose File**
Get the docker-compose file from the Formbricks repository by running:
Get the docker-compose file from the Formbricks repository by running:
```bash
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
```
```bash
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
```
* **Generate NextAuth Secret**
1. **Generate NextAuth Secret**
You need a NextAuth secret for session signing and encryption. Run the command below to generate a random string using `openssl` and automatically insert it into the `docker-compose.yml` file:
You need a NextAuth secret for session signing and encryption. Run the command below to generate a random string using `openssl` and automatically insert it into the `docker-compose.yml` file:
```bash
sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.*/NEXTAUTH_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
```bash
sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.*/NEXTAUTH_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
* **Generate Encryption Key**
1. **Generate Encryption Key**
Next, you need to generate an Encryption Key. This will be used for authenticating and verifying 2 Factor Authentication. The `sed` command below generates a random string using `openssl`, then replaces the `ENCRYPTION_KEY:` placeholder in the `docker-compose.yml` file with this generated secret:
Next, you need to generate an Encryption Key. This will be used for authenticating and verifying 2 Factor Authentication. The `sed` command below generates a random string using `openssl`, then replaces the `ENCRYPTION_KEY:` placeholder in the `docker-compose.yml` file with this generated secret:
```bash
sed -i "/ENCRYPTION_KEY:$/s/ENCRYPTION_KEY:.*/ENCRYPTION_KEY: $(openssl rand -hex 32)/" docker-compose.yml
```
```bash
sed -i "/ENCRYPTION_KEY:$/s/ENCRYPTION_KEY:.*/ENCRYPTION_KEY: $(openssl rand -hex 32)/" docker-compose.yml
```
* **Generate Cron Secret**
1. **Generate Cron Secret**
You require a Cron secret to secure API access for running cron jobs. Run the command below to generate a random string using `openssl` and automatically insert it into the `docker-compose.yml` file:
You require a Cron secret to secure API access for running cron jobs. Run the command below to generate a random string using `openssl` and automatically insert it into the `docker-compose.yml` file:
```bash
sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
```bash
sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
* **Start the Docker Setup**
1. **Start the Docker Setup**
Now, youre ready to run Formbricks with Docker. Use the command below to start Formbricks along with a PostgreSQL database using Docker Compose:
Now, youre ready to run Formbricks with Docker. Use the command below to start Formbricks along with a PostgreSQL database using Docker Compose:
```bash
docker compose up -d
```
```bash
docker compose up -d
```
The `-d` flag runs the containers in the background, so they keep running even after you close the terminal.
The `-d` flag runs the containers in the background, so they keep running even after you close the terminal.
* **Open Formbricks in Your Browser**
1. **Open Formbricks in Your Browser**
Once the setup is running, open [**http://localhost:3000**](http://localhost:3000) in your browser to access Formbricks. The first time you visit, you'll see a setup wizard. Follow the steps to create your first user and start using Formbricks.
Once the setup is running, open [**http://localhost:3000**](http://localhost:3000) in your browser to access Formbricks. The first time you visit, you'll see a setup wizard. Follow the steps to create your first user and start using Formbricks.
## Update
Please take a look at our [migration guide](/self-hosting/advanced/migration) for version specific steps to update Formbricks.
* Pull the latest Formbricks image
1. Pull the latest Formbricks image
```bash
docker compose pull
```
```bash
docker compose pull
```
* Stop the Formbricks stack
1. Stop the Formbricks stack
```bash
docker compose down
```
```bash
docker compose down
```
* Re-start the Formbricks stack with the updated image
1. Re-start the Formbricks stack with the updated image
```bash
docker compose up -d
```
```bash
docker compose up -d
```
## Debug
@@ -137,13 +137,10 @@ formbricks-quickstart-formbricks-1 | Listening on port 3000 url: http://<random
You can close the logs again by hitting `CTRL + C`.
## [](https://formbricks.com/docs/self-hosting/docker#customizing-environment-variables)
<Note>
**Customizing environment variables**
To edit any of the available environment variables, check out our [Configure](/self-hosting/configuration) section!
To edit any of the available environment variables, check out our [Configuration](/self-hosting/configuration/environment-variables) section!
</Note>
If you have any questions or require help, feel free to reach out to us on [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions). 😃[
](https://formbricks.com/docs/developer-docs/rest-api)
If you have any questions or require help, feel free to reach out to us on [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions). 😃

View File

@@ -1,7 +1,6 @@
---
title: "User Management"
description:
"Assign different roles to organization members to grant them specific rights like creating surveys, viewing responses, or managing organization members."
description: "Assign different roles to organization members to grant them specific rights like creating surveys, viewing responses, or managing organization members."
icon: "user"
---
@@ -14,9 +13,8 @@ Learn about the different organization-level and team-level roles and how they a
Permissions in Formbricks are broadly handled using organization-level roles, which apply to all teams and projects in the organization. Users on a self-hosting and Enterprise plan, have access to team-level roles, which enable more granular permissions.
<Note>
Access Roles is a feature of the **Enterprise Edition**. In the **Community
Edition** and on the **Free** and **Startup** plan in the Cloud you can invite
unlimited organization members as `Owner`.
Access Roles is a feature of the **Enterprise Edition**. In the **Community Edition** and on the **Free**
and **Startup** plan in the Cloud you can invite unlimited organization members as `Owner`.
</Note>
Here are the different access permissions, ranked from highest to lowest access
@@ -30,16 +28,16 @@ Here are the different access permissions, ranked from highest to lowest access
All users and their organization-level roles are listed in **Organization Settings > General**. Users can hold any of the following org-level roles:
- **Owner** have full access to the organization, its data, and settings. Org Owners can perform Team Admin actions without needing to join the team.
- **Manager** have full management access to all teams and projects. They can also manage the organization's membership. Org Managers can perform Team Admin actions without needing to join the team. They cannot change other organization settings.
- **Billing** users can manage payment and compliance details in the organization.
- **Org Members** can view most data in the organization and act in the projects they are members of. They cannot join projects on their own and need to be assigned.
- **Org Managers** have full management access to all teams and projects. They can also manage the organization's membership. Org Managers can perform Team Admin actions without needing to join the team. They cannot change other organization settings.
- **Org Owners** have full access to the organization, its data, and settings. Org Owners can perform Team Admin actions without needing to join the team.
- **Member** can view most data in the organization and act in the projects they are members of. They cannot join projects on their own and need to be assigned.
### Permissions at project level
- **read**: read access to all resources (except settings) in the project.
- **read & write**: read & write access to all resources (except settings) in the project.
- **manage**: read & write access to all resources including settings in the project.
- **Read**: Read access to all resources (except settings) in the project.
- **Read & write**: Read & write access to all resources (except settings) in the project.
- **Manage**: Read & write access to all resources including settings in the project.
### Team-level Roles
@@ -53,31 +51,32 @@ For more information on user roles & permissions, see below:
| **Organization** | | | | |
| Update organization | ✅ | ❌ | ❌ | ❌ |
| Delete organization | ✅ | ❌ | ❌ | ❌ |
| Add new Member | ✅ | ✅ | ❌ | ❌ |
| Delete Member | ✅ | ✅ | ❌ | ❌ |
| Update Member Access | ✅ | ✅ | ❌ | ❌ |
| Update Billing | ✅ | ✅ | ✅ | ❌ |
| Add new member | ✅ | ✅ | ❌ | ❌ |
| Delete member | ✅ | ✅ | ❌ | ❌ |
| Update member access | ✅ | ✅ | ❌ | ❌ |
| Update billing | ✅ | ✅ | ✅ | ❌ |
| **Project** | | | | |
| Create Project | ✅ | ✅ | ❌ | ❌ |
| Update Project Name | ✅ | ✅ | ❌ | ✅\*\* |
| Update Project Recontact Options | ✅ | ✅ | ❌ | ✅\*\* |
| Update Look & Feel | ✅ | ✅ | ❌ | ✅\*\* |
| Update Survey Languages | ✅ | ✅ | ❌ | ✅\*\* |
| Delete Project | ✅ | ✅ | ❌ | ❌ |
| Create project | ✅ | ✅ | ❌ | ❌ |
| Update project name | ✅ | ✅ | ❌ | ✅\*\* |
| Update project recontact options | ✅ | ✅ | ❌ | ✅\*\* |
| Update look & feel | ✅ | ✅ | ❌ | ✅\*\* |
| Update survey languages | ✅ | ✅ | ❌ | ✅\*\* |
| Delete project | ✅ | ✅ | ❌ | ❌ |
| **Surveys** | | | | |
| Create New Survey | ✅ | ✅ | ❌ | ✅\* |
| Edit Survey | ✅ | ✅ | ❌ | ✅\* |
| Delete Survey | ✅ | ✅ | ❌ | ✅\* |
| Create new survey | ✅ | ✅ | ❌ | ✅\* |
| Edit survey | ✅ | ✅ | ❌ | ✅\* |
| Delete survey | ✅ | ✅ | ❌ | ✅\* |
| View survey results | ✅ | ✅ | ❌ | ✅ |
| **Response** | | | | |
| Delete response | ✅ | ✅ | ❌ | ✅\* |
| Add tags on response | ✅ | ✅ | ❌ | ✅\* |
| Edit tags on response | ✅ | ✅ | ❌ | ✅\* |
| Download survey responses (CSV) | ✅ | ✅ | ❌ | ✅\* |
| **Actions** | | | | |
| Create Action | ✅ | ✅ | ❌ | ✅\* |
| Update Action | ✅ | ✅ | ❌ | ✅\* |
| Delete Action | ✅ | ✅ | ❌ | ✅\* |
| **API Keys** | | | | |
| Create action | ✅ | ✅ | ❌ | ✅\* |
| Update action | ✅ | ✅ | ❌ | ✅\* |
| Delete action | ✅ | ✅ | ❌ | ✅\* |
| **API keys** | | | | |
| Create API key | ✅ | ✅ | ❌ | ✅\*\* |
| Update API key | ✅ | ✅ | ❌ | ✅\*\* |
| Delete API key | ✅ | ✅ | ❌ | ✅\*\* |
@@ -85,10 +84,10 @@ For more information on user roles & permissions, see below:
| Create tags | ✅ | ✅ | ❌ | ✅\* |
| Update tags | ✅ | ✅ | ❌ | ✅\* |
| Delete tags | ✅ | ✅ | ❌ | ✅\*\* |
| **People** | | | | |
| Delete Person | ✅ | ✅ | ❌ | ✅\* |
| **Contacts** | | | | |
| Delete contact | ✅ | ✅ | ❌ | ✅\* |
| **Integrations** | | | | |
| Manage Integrations | ✅ | ✅ | ❌ | ✅\* |
| Manage integrations | ✅ | ✅ | ❌ | ✅\* |
\* - for the read & write permissions team members
@@ -108,16 +107,13 @@ There are two ways to invite organization members: One by one or in bulk.
![Add member Button Position](/images/xm-and-surveys/core-features/access-roles/add-member.webp)
3. In the modal, add the Name, Email and Role of the organization member you want to invite:
![Individual Invite Modal Tab](/images/xm-and-surveys/core-features/access-roles/individual-invite.webp)
<Note>
Access Roles is a feature of the **Enterprise Edition**. In the **Community
Edition** and on the **Free** and **Startup** plan in the Cloud you can invite
unlimited organization members as `Owners`.
Access Roles is a feature of the **Enterprise Edition**. In the **Community Edition** and on the **Free**
and **Startup** plan in the Cloud you can invite unlimited organization members as `Owners`.
</Note>
Formbricks sends an email to the organization member with an invitation link. The organization member can accept the invitation or create a new account by clicking on the link.
@@ -128,7 +124,6 @@ Formbricks sends an email to the organization member with an invitation link. Th
![Where to find the Menu Item for Organization Settings](/images/xm-and-surveys/core-features/access-roles/organization-settings-menu.webp)
2. Click on the `Add member` button:
![Add member Button Position](/images/xm-and-surveys/core-features/access-roles/add-member.webp)

View File

@@ -71,11 +71,11 @@ window.addEventListener("message", (event) => {
});
```
## Emebd Mode
## Embed Mode
Embed your survey with a minimalist design, disregarding padding and background.
### How to enable it?
### How to enable Embed Mode
It can be enabled by simply appending **?embed=true** to your survey link or from UI
@@ -83,16 +83,15 @@ It can be enabled by simply appending **?embed=true** to your survey link or fro
2. Toggle **Embed mode**
![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp)
![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-toggle.webp)
### With Embed mode enabled
With Embed mode enabled:
![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-enabled.webp)
![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-enabled.webp)
### With Embed mode disabled
![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-disabled.webp)
With Embed mode disabled:
![Toggle embed mode](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/embed-mode-disabled.webp)
## Embedding Surveys in Emails
@@ -128,22 +127,20 @@ Gmail does not support HTML embedding natively. It's a WYSIWYG (What You See Is
- Open Gmail and compose a new email.
- Write your email content after which you want to embed the survey.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-without-survey.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-without-survey.webp)
- Right next to the Send button you will see a new button called **HTML Editor**. Click on it.
- This will open a new window with the **Design** tab active. Switch to the **Source** tab.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-source-tab.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-source-tab.webp)
- Now paste the copied HTML code from Formbricks into this window. On the right, you will see a preview of how the email will look.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-add-survey.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/plugin-add-survey.webp)
- Click on the **Close Editor** button to save the changes & close the editor.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-with-survey.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/embed-surveys/email-content-with-survey.webp)
- Voila! You have successfully embedded the survey in your email.

View File

@@ -15,7 +15,7 @@ PIN protection can be applied to your surveys easily through the survey editor.
2. **Select Response Options**: Find and select **`Response Options`** to access settings related to survey responses.
![Select Response Options](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-one.webp)
![Select Response Options](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-one.webp)
1. **Enable PIN Protection**: Find the option for "Protect Survey with a PIN" and
activate it. You will be prompted to enter a PIN that respondents must use to access
@@ -35,15 +35,15 @@ When a respondent attempts to access the survey, they are prompted to enter the
- **PIN Entry Prompt**: A screen will appear asking the respondent to enter the PIN to proceed. This acts as the first gatekeeping step before survey access is granted.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-three.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-three.webp)
- **Incorrect PIN Handling**: If an incorrect PIN is entered, the respondent will be informed and asked to try again, ensuring secure access to the survey.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-four.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-four.webp)
- **Correct PIN**: On entering the correct PIN, the user access the survey & can fill it accordingly.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-five.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/pin-protected-surveys/step-five.webp)
### **Benefits of PIN Protection**

View File

@@ -24,14 +24,13 @@ Using single-use links with Formbricks is quite straight-forward:
1. In the survey settings, toggle "Single Use Link" on:
![Single use survey settings](/images/xm-and-surveys/surveys/link-surveys/single-use-links/single-use-setting.webp)
![Single use survey settings](/images/xm-and-surveys/surveys/link-surveys/single-use-links/single-use-setting.webp)
2. When you publish your survey, the following modal will open:
![Share modal with 7 single use links which can be regenerated](/images/xm-and-surveys/surveys/link-surveys/single-use-links/share-modal.webp)
![Share modal with 7 single use links which can be regenerated](/images/xm-and-surveys/surveys/link-surveys/single-use-links/share-modal.webp)
Here, you can copy and generate as many single-use links as you need.
Here, you can copy and generate as many single-use links as you need.
## URL Encryption
@@ -45,7 +44,6 @@ You can find the suId of each submission in the submission meta data. To view it
![View suId in the submission meta data.](/images/xm-and-surveys/surveys/link-surveys/single-use-links/metadata.webp)
### 'Link used' message
You can customize the 'link used' messaging in the Survey Editor settings:

View File

@@ -8,8 +8,7 @@ Understand the source a survey respondent comes from when responding to your sur
Check out this video to learn more about source tracking in link surveys:
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/CytWhuyEMVI?si=nxRrdMmlsQ5P6wwp&amp;controls=0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/CytWhuyEMVI?si=nxRrdMmlsQ5P6wwp&amp;controls=0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## Purpose
@@ -38,8 +37,7 @@ https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google
3. **View Responses**: Use the collected source data to analyze where your survey respondents are coming from. You can hover over the user icon in the responses tab to see the source of the user.
![View Source in Response](/images/xm-and-surveys/surveys/link-surveys/source-tracking/view-response.webp)
![View Source in Response](/images/xm-and-surveys/surveys/link-surveys/source-tracking/view-response.webp)
4. **Analyse Data**: Download all the responses as a CSV/Excel and get access to the source information. This can provide valuable insights into your audience.

View File

@@ -17,12 +17,11 @@ This feature, designed for link surveys, can be enabled or disabled directly fro
2. **Access Settings**: Click on the **`Settings`** tab next to the Questions & Styling tab.
3. **Select Response Options**: Find and select **`Response Options`** to access settings related to survey responses.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-one.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-one.webp)
4. **Activate Email Verification**: Find the "Verify Email Before Accessing Survey" option and use the toggle to activate email verification. Specify what details should be visible to the public when they access the survey.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-two.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-two.webp)
### **User Experience Upon Accessing the Survey**
@@ -30,18 +29,17 @@ When email verification is enabled, the following process unfolds for the user:
1. **Email Entry Prompt**: Upon accessing the survey link, the user is prompted to verify their email before they can proceed.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-three.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-three.webp)
2. **Preview Option**: A "Preview survey questions" option is available for those who are just browsing or curious about the survey content without completing it. This allows a non-interactive view of the survey.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-four.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-four.webp)
3. **Verification Process**: After entering their email, respondents receive an email containing a survey link, which they can use to access the survey.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-five.webp)
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-five.webp)
**4. Survey Access**: After verifying their email, respondents can access and respond to the survey.
4. **Survey Access**: After verifying their email, respondents can access and respond to the survey.
![Choose a link survey template](/images/xm-and-surveys/surveys/link-surveys/verify-email-before-survey/step-six.webp)

View File

@@ -6,51 +6,77 @@ icon: "code"
## **How Do Actions Work?**
Actions in Formbricks App Surveys are deeply integrated with user activities within your app. When a user performs a specified action, the Formbricks widget detects this activity and can present a survey to that specific user if the trigger conditions match for that survey. This capability ensures that surveys are triggered at the right time. You can set up these actions through a user-friendly No-Code interface within the Formbricks dashboard.
<Steps>
<Step title="User performs action">
The user performs an action in your application.
</Step>
## **Why Are Actions Useful?**
<Step title="Formbricks widget detects action">
The embedded widget (SDK) detects the action, if you set up a [No Code Action](/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions) or a [Code Action](/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions) to do so.
</Step>
Actions are invaluable for enhancing survey relevance and effectiveness:
<Step title="Check for survey trigger">
If a survey is set to trigger on that action, Formbricks checks if the user or visitor qualifies.
</Step>
- **Personalized Engagement**: Surveys triggered by user actions ensure content is highly relevant and engaging, matching each users current context.
<Step title="Check for user or visitor qualification">
If the user or visitor already filled out a survey in the past couple of days (see [Global Waiting Time](/xm-and-surveys/surveys/website-app-surveys/recontact#project-wide-global-waiting-time)), the survey will not be shown to prevent survey fatigue.
- **User Attributes**: By tying surveys to specific user attributes, such as activity levels or feature usage, you can customize the survey experience to reflect individual user profiles.
Also, if this user or visitor has seen this specific survey before, Formbricks might not show it as this is dependent on the [Recontact Options](/xm-and-surveys/surveys/website-app-surveys/recontact).
</Step>
- **User Targeting**: Precise targeting based on user attributes ensures that surveys are shown only to users who meet certain criteria, enhancing the relevance and effectiveness of each survey.
<Step title="Show survey">
If all conditions match, the survey is shown.
</Step>
</Steps>
<Tip>
Tying surveys to specific user actions enables **context-aware surveying**: You make sure that your user research is relevant to the current user context which leads to sign**ificantly higher response and completions rates** as well as lower survey fatigue.
</Tip>
## **Setting Up No-Code Actions**
Formbricks offers an intuitive No-Code interface that allows you to configure actions without needing to write any code.
To add a No-Code Action:
<Note>
No Code Actions are **not available for surveys in mobile apps**. No Code Action tracking are heavily dependent on JavaScript and most mobile apps are compiled into a different programming language. To track user actions in mobile apps use [Code Actions.](/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions)
</Note>
1. Visit the Formbricks Dashboard & switch to the Actions tab:
<Steps>
<Step title="Visit the Actions tab via the main navigation">
![Action overview on Formbricks Open Source Survey Solution](/images/xm-and-surveys/surveys/website-app-surveys/actions/actions-view.png "Action overview on Formbricks Open Source Survey Solution")
</Step>
![setup checklist ui of survey popup for app surveys](/images/xm-and-surveys/surveys/website-app-surveys/actions/i1.webp)
<Step title="Click on 'Add Action' in the top right corner to see the following:">
![Add action to open source in app survey](/images/xm-and-surveys/surveys/website-app-surveys/actions/i2.webp "Add action to open source in app survey")
</Step>
</Steps>
1. Now click on “Add Action
![add action](/images/xm-and-surveys/surveys/website-app-surveys/actions/i2.webp)
Here are four types of No-Code actions you can set up:
There are four types of No-Code actions:
### **1. Click Action**
![Add click action to open source in app survey](/images/xm-and-surveys/surveys/website-app-surveys/actions/click-action.png "Add click action to open source in app survey")
Click Action is triggered when a user clicks on a specific element within your application. You can define the element's inner text or CSS selector to trigger the survey.
A Click Action is triggered when a user clicks on a specific element within your application. You can define the element's inner text, CSS selector or both to trigger the survey.
- **Inner Text**: Checks if the innerText of a clicked HTML element, like a button label, matches a specific text. This action allows you to display a survey based on text interactions within your application.
* **Inner Text**: Checks if the innerText of a clicked HTML element, like a button label, matches a specific text. This action allows you to display a survey based on text interactions within your application.
- **CSS Selector**: Verifies if a clicked HTML element matches a provided CSS selector, such as a class, ID, or any other CSS selector used in your website. It enables survey triggers based on element interactions.
* **CSS Selector**: Verifies if a clicked HTML element matches a provided CSS selector, such as a class, ID, or any other CSS selector used in your website. It enables survey triggers based on element interactions.
### **2. Page view Action**
* **Both**: Only if both is true, the action is triggered
### **2. Page View Action**
![Add page view action to open source in app survey](/images/xm-and-surveys/surveys/website-app-surveys/actions/page-view.png "Add page view action to open source in app survey")
This action is triggered when a user visits a page within your application.
### **3. Exit Intent Action**
![Add exit intent action to open source in app survey](/images/xm-and-surveys/surveys/website-app-surveys/actions/exit-intent.png "Add exit intent action to open source in app survey")
This action is triggered when a user is about to leave your application. It helps capture user feedback before they exit, providing valuable insights into user experiences and potential improvements.
### **4. 50% Scroll Action**
![Add 50% scroll action to open source in app survey](/images/xm-and-surveys/surveys/website-app-surveys/actions/scroll.png "Add 50% scroll action to open source in app survey")
This action is triggered when a user scrolls through 50% of a page within your application. It helps capture user feedback at a specific point in their journey, enabling you to gather insights based on user interactions.
@@ -58,39 +84,49 @@ This action is triggered when a user visits a specific page within your applicat
You can combine the url filters with any of the no-code actions to trigger the survey based on the URL match conditions.
### **URL Match Conditions**
### **Page Filter**
- **exactMatch**: Triggers the action when the URL exactly matches the specified string.
You can limit action tracking to specific subpages of your website or web app by using the Page Filter. Here you can use a variety of URL filter settings:
- **contains**: Activates when the URL contains the specified substring.
* **exactMatch**: Triggers the action when the URL exactly matches the specified string.
- **startsWith**: Fires when the URL starts with the specified string.
* **contains**: Activates when the URL contains the specified substring.
- **endsWith**: Executes when the URL ends with the specified string.
* **startsWith**: Fires when the URL starts with the specified string.
- **notMatch**: Triggers when the URL does not match the specified condition.
* **endsWith**: Executes when the URL ends with the specified string.
- **notContains**: Activates when the URL does not contain the specified substring.
* **notMatch**: Triggers when the URL does not match the specified condition.
* **notContains**: Activates when the URL does not contain the specified substring.
## **Setting Up Code Actions**
For more granular control, you can implement actions directly in your codebase:
For more granular control, you can implement actions directly in your code:
1. **Configure the Action**: First, add the action via the Formbricks web interface to make it available for survey configuration.
After that you can fire an action using `formbricks.track()`
<Steps>
<Step title="Configure action in Formbricks">
First, add the action via the Formbricks web interface to make it available for survey configuration:
2. **Track an Action**: Use formbricks.track() to send an action event to Formbricks.
![Add a code action to open source in app survey](/images/xm-and-surveys/surveys/website-app-surveys/actions/code-action.png "Add a code action to open source in app survey")
```javascript Track an action formbricks.track("Action Name");
</Step>
```
<Step title="Add action tracking to your code">
Use formbricks.track() to send an action event to Formbricks:
Here is an example of how to fire an action when a user clicks a button:
```
formbricks.track("action");
```
```javascript Track Button Click
const handleClick = () => {
formbricks.track("Button Clicked");
};
Here is an example of how to fire an action when a user clicks a button:
return <button onClick={handleClick}>Click Me</button>;
```
```javascript
const handleClick = () => {
formbricks.track("Button Clicked");
};
return <button onClick={handleClick}>Click Me</button>;
```
</Step>
</Steps>

View File

@@ -1,28 +1,67 @@
---
title: "Advanced Targeting"
description: "Advanced Targeting allows you to show surveys to the right group of people. You can target surveys based on user attributes, device type, and more instead of spraying and praying. This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code."
description: "Advanced Targeting allows you to show surveys to a specific segment of your users. You can target surveys based on user attributes, device type, and more. This helps you get more relevant insights while keeping survey fatigue at a minimum. After the initial setup, you can target any segment without touching code."
icon: "bullseye"
---
<Info>
Advanced Targeting is available on paid plans for both Formbricks Cloud and On Premise.
</Info>
## How to setup Advanced Targeting
### When to use Advanced Targeting?
<Note>
Advanced Targeting is only available on the Pro plan!
</Note>
Advanced Targeting helps you achieve a number of goals:
* On the Formbricks dashboard, click on **People** tab from the top navigation bar.
1. **Relevance**: Keep survey content relevant to respondents.
* Switch to the **Segments** tab & click on **Create Segment**.
2. **Cohort-analysis**: Survey specific user cohorts only.
* Give your segment a title & a description to help you remember what this segment is about.
3. **Statistical Relevance:** When surveying a smaller subset of users, statistical relevance is reached with a lot less responses.
* Now click on the **Add Filter** button to add a filter. You can filter based on user attributes, other segments, devices, and more.
## How does Advanced Targeting work?
* To group a set of filters together, click on the Three Dots icon on the right side of the filter and click on **Create Group**.
<Steps>
<Step title="Create Segment">
To get started, go to the Contacts tab and create a new Segment:
* Try playing around with different filters & conditions that we have provided to see how the segment size changes.
![Create a new segment](/images/xm-and-surveys/surveys/website-app-surveys/targeting/contacts.png "Create a new segment")
* Once you are happy with the segment, click on **Save Segment**.
</Step>
* Now, when you create a survey, you can select this segment to target your survey to.
<Step title="Configure Segment based on attributes">
In the Segment editor, you can configure your Segment with a combination of Attributes, Segments and Devices. If a user matches either or all of the criteria, they become part of the Segment. See [Segment Configuration](/xm-and-surveys/surveys/website-app-surveys/advanced-targeting#segment-configuration) below.
</Step>
<Step title="Create a survey of type Website & App">
Create a new survey and go to Settings to change it to Website & App survey:
![Create a new segment of type in-app](/images/xm-and-surveys/surveys/website-app-surveys/targeting/survey-type.png "Create a new segment")
</Step>
<Step title="Choose Segment in Targeting options">
![Choose Segment in Targeting options](/images/xm-and-surveys/surveys/website-app-surveys/targeting/target-audience.png "Choose Segment in Targeting options")
</Step>
<Step title="Publish your survey" icon="party-horn" iconType="solid" />
</Steps>
### Segment Configuration
There are three means to move Contacts in or out of Segments: **Attributes**, other **Segments** and **Devices**:
1. **Attributes**: If the value of a specific attribute matches, the user becomes part of the Segment.
![Attribute filter](/images/xm-and-surveys/surveys/website-app-surveys/targeting/attribute-filter.png "Attribute filter")
2. **Segments**: You can nest Segments meaning that if a user is or is not part of another Segment, they can be included or excluded
![Segments filter](/images/xm-and-surveys/surveys/website-app-surveys/targeting/segments-filter.png "Segments filter")
3. **Devices**: If a user uses a Phone or Desktop, you can include or exclude them
![Devices filter](/images/xm-and-surveys/surveys/website-app-surveys/targeting/device-filter.png "Devices filter")
4. **Filter Groups:** You can group any of the above conditions in group and connect them logically with `AND` or `OR`. This allows for maximum granularity.

View File

@@ -165,6 +165,7 @@ export default function FormbricksProvider() {
```typescript app/layout.tsx
// other imports
import FormbricksProvider from "./formbricks";
import { Suspense } from "react";
export default function RootLayout({
children,
@@ -173,7 +174,9 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<FormbricksProvider />
<Suspense>
<FormbricksProvider />
</Suspense>
<body>{children}</body>
</html>
);

View File

@@ -1,41 +1,32 @@
---
title: "Show Survey to % of Users"
description: "Formbricks allows you to display surveys to only a percentage of your targeted users."
title: "Survey % of Users"
description: "Formbricks allows you to display surveys to only a percentage of your targeted users. This is helpful for surveying on public facing website."
icon: "percent"
---
To target specific segments of your audience or manage survey exposure, Formbricks allows you to display surveys to only a percentage of your targeted users.
To target specific segments of your audience or manage survey exposure, Formbricks allows you to display surveys to only a random selection of your targeted users.
<Note>
This feature is applicable for website surveys and app surveys, where managing
participant engagement and response volume is crucial.
</Note>
## How to set up percentage-based visibility
## **Enabling Percentage-Based Visibility**
<Steps>
<Step title="Open settings in survey editor">
In the Survey Editor, click on the **`Settings`** tab
</Step>
Set up this feature to control how many users see your survey, using a simple slider to specify the percentage of your targeted audience that should be prompted to participate.
<Step title="Find 'Survey Trigger' card">
Locate the 'Survey Trigger' card in the settings panel
</Step>
### **Steps to Set Up Percentage-Based Visibility**
<Step title="Enable percentage toggle">
Find and enable the **`Show Survey to % of Users`** toggle
</Step>
1. **Open Survey Editor**: Navigate to the survey where you wish to configure visibility settings & click on Edit.
<Step title="Set percentage">
Enter the desired percentage (from 0.01% to 100%) of users to whom the survey will be shown
2. **Access Settings**: In the Survey Editor, click on the **`Settings`** tab
3. **Navigate to Survey Trigger Options**:
![Survey Trigger Options](/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-one.webp)
- Select **`Survey Trigger`** from the menu options.
- Choose **`Survey Display Settings`** to access visibility controls.
4. **Adjust User Visibility Percentage**:
- Find the **`Show Survey to % of Users`** toggle. Enable it.
- Enter the desired percentage (from 0.01% to 100%) of users to whom the survey will be shown.
![Choose a link survey template](/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-two.webp)
![Set percentage](/images/xm-and-surveys/surveys/website-app-surveys/targeting/percentage.png "Set percentage")
</Step>
</Steps>
<Note>
Please note that this feature operates based on mathematical probabilistic
@@ -51,12 +42,12 @@ For instance, if you want to expose your survey to only half of your targeted us
It's effective for:
- **A/B Testing**: Compare different user experiences by showing different surveys or survey variations to different segments of your audience.
* **A/B Testing**: Compare different user experiences by showing different surveys or survey variations to different segments of your audience.
- **Gradual Rollouts**: Gradually introduce a new survey to a portion of your users to monitor initial responses and adjust based on feedback before full deployment.
* **Gradual Rollouts**: Gradually introduce a new survey to a portion of your users to monitor initial responses and adjust based on feedback before full deployment.
- **Load Management**: Manage server load and response processing by limiting the number of responses collected at any given time.
* **Load Management**: Manage server load and response processing by limiting the number of responses collected at any given time.
## **Conclusion**
Using the **Show Survey to % of Users** feature allows for precise control over who sees your Formbricks surveys, making it an invaluable feature for targeted data collection and user experience management. By adjusting survey visibility settings, you can strategically engage segments of your audience, enhance survey testing phases, and optimize overall survey effectiveness.
Using the **Show Survey to % of Users** feature allows for precise control over who sees your Formbricks surveys, making it an invaluable feature for targeted data collection and user experience management. By adjusting survey visibility settings, you can strategically engage segments of your audience, enhance survey testing phases, and optimize overall survey effectiveness.

View File

@@ -31,7 +31,7 @@
"prepare": "husky install",
"storybook": "turbo run storybook",
"fb-migrate-dev": "pnpm --filter @formbricks/database create-migration && pnpm prisma generate",
"tolgee-pull": "tolgee pull && prettier --write ./packages/lib/messages/*.json"
"tolgee-pull": "BRANCH_NAME=$(node -p \"require('./branch.json').branchName\") && tolgee pull --tags \"draft:$BRANCH_NAME\" \"production\" && prettier --write ./packages/lib/messages/*.json"
},
"devDependencies": {
"@azure/microsoft-playwright-testing": "1.0.0-beta.6",

View File

@@ -107,6 +107,7 @@ const renderWidget = async (
},
surveyState
);
const projectOverwrites = survey.projectOverwrites ?? {};
const clickOutside = projectOverwrites.clickOutsideClose ?? project.clickOutsideClose;
const darkOverlay = projectOverwrites.darkOverlay ?? project.darkOverlay;

View File

@@ -8,8 +8,6 @@ export const IS_FORMBRICKS_CLOUD = env.IS_FORMBRICKS_CLOUD === "1";
export const WEBAPP_URL =
env.WEBAPP_URL || (env.VERCEL_URL ? `https://${env.VERCEL_URL}` : false) || "http://localhost:3000";
export const SHORT_URL_BASE = env.SHORT_URL_BASE ? env.SHORT_URL_BASE : WEBAPP_URL;
// encryption keys
export const FORMBRICKS_ENCRYPTION_KEY = env.FORMBRICKS_ENCRYPTION_KEY || undefined;
export const ENCRYPTION_KEY = env.ENCRYPTION_KEY;

View File

@@ -73,7 +73,6 @@ export const env = createEnv({
S3_SECRET_KEY: z.string().optional(),
S3_ENDPOINT_URL: z.string().optional(),
S3_FORCE_PATH_STYLE: z.enum(["1", "0"]).optional(),
SHORT_URL_BASE: z.string().url().optional().or(z.string().length(0)),
SIGNUP_DISABLED: z.enum(["1", "0"]).optional(),
SLACK_CLIENT_ID: z.string().optional(),
SLACK_CLIENT_SECRET: z.string().optional(),
@@ -197,7 +196,6 @@ export const env = createEnv({
S3_SECRET_KEY: process.env.S3_SECRET_KEY,
S3_ENDPOINT_URL: process.env.S3_ENDPOINT_URL,
S3_FORCE_PATH_STYLE: process.env.S3_FORCE_PATH_STYLE,
SHORT_URL_BASE: process.env.SHORT_URL_BASE,
SIGNUP_DISABLED: process.env.SIGNUP_DISABLED,
SLACK_CLIENT_ID: process.env.SLACK_CLIENT_ID,
SLACK_CLIENT_SECRET: process.env.SLACK_CLIENT_SECRET,

View File

@@ -1472,7 +1472,9 @@
"is_booked": "Ist gebucht",
"is_clicked": "Wird geklickt",
"is_completely_submitted": "Vollständig eingereicht",
"is_not_set": "Ist nicht festgelegt",
"is_partially_submitted": "Teilweise eingereicht",
"is_set": "Ist festgelegt",
"is_skipped": "Wird übersprungen",
"is_submitted": "Wird eingereicht",
"jump_to_question": "Zur Frage springen",

View File

@@ -1472,7 +1472,9 @@
"is_booked": "Is booked",
"is_clicked": "Is clicked",
"is_completely_submitted": "Is completely submitted",
"is_not_set": "Is not set",
"is_partially_submitted": "Is partially submitted",
"is_set": "Is set",
"is_skipped": "Is skipped",
"is_submitted": "Is submitted",
"jump_to_question": "Jump to question",

View File

@@ -1472,7 +1472,9 @@
"is_booked": "Est réservé",
"is_clicked": "Est cliqué",
"is_completely_submitted": "Est complètement soumis",
"is_not_set": "N'est pas défini",
"is_partially_submitted": "Est partiellement soumis",
"is_set": "Est défini",
"is_skipped": "Est ignoré",
"is_submitted": "Est soumis",
"jump_to_question": "Passer à la question",

View File

@@ -1472,7 +1472,9 @@
"is_booked": "Tá reservado",
"is_clicked": "É clicado",
"is_completely_submitted": "Está completamente submetido",
"is_not_set": "Não está definido",
"is_partially_submitted": "Parcialmente enviado",
"is_set": "Está definido",
"is_skipped": "é pulado",
"is_submitted": "é submetido",
"jump_to_question": "Pular para a pergunta",

View File

@@ -1472,7 +1472,9 @@
"is_booked": "已預訂",
"is_clicked": "已點擊",
"is_completely_submitted": "已完全提交",
"is_not_set": "未設定",
"is_partially_submitted": "已部分提交",
"is_set": "已設定",
"is_skipped": "已跳過",
"is_submitted": "已提交",
"jump_to_question": "跳至問題",

View File

@@ -456,6 +456,10 @@ const evaluateSingleCondition = (
const values = Object.values(leftValue);
return values.length > 0 && !values.includes("");
} else return false;
case "isSet":
return leftValue !== undefined && leftValue !== null && leftValue !== "";
case "isNotSet":
return leftValue === undefined || leftValue === null || leftValue === "";
default:
return false;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/react-native",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT",
"description": "Formbricks React Native SDK allows you to connect your app to Formbricks, display surveys and trigger events.",
"homepage": "https://formbricks.com",

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions -- required for template literals */
/* eslint-disable @typescript-eslint/no-unsafe-call -- required */
/* eslint-disable no-console -- debugging*/
import React, { type JSX, useEffect, useMemo, useRef, useState } from "react";
@@ -149,6 +150,10 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und
return await storage.uploadFile(file, params);
};
const surveyPlacement = survey.projectOverwrites?.placement ?? project.placement;
const clickOutside = survey.projectOverwrites?.clickOutsideClose ?? project.clickOutsideClose;
const darkOverlay = survey.projectOverwrites?.darkOverlay ?? project.darkOverlay;
return (
<Modal
animationType="slide"
@@ -168,7 +173,10 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und
isBrandingEnabled,
styling,
languageCode,
placement: surveyPlacement,
appUrl: appConfig.get().appUrl,
clickOutside: surveyPlacement === "center" ? clickOutside : true,
darkOverlay,
}),
}}
style={{ backgroundColor: "transparent" }}
@@ -333,6 +341,20 @@ export function SurveyWebView({ survey }: SurveyWebViewProps): JSX.Element | und
}
const renderHtml = (options: Partial<SurveyInlineProps> & { appUrl?: string }): string => {
const isCenter = options.placement === "center";
const getBackgroundColor = (): "rgba(51, 65, 85, 0.8)" | "rgba(255, 255, 255, 0.9)" | "transparent" => {
if (isCenter) {
if (options.darkOverlay) {
return "rgba(51, 65, 85, 0.8)";
}
return "rgba(255, 255, 255, 0.9)";
}
return "transparent";
};
return `
<!doctype html>
<html>
@@ -341,10 +363,70 @@ const renderHtml = (options: Partial<SurveyInlineProps> & { appUrl?: string }):
<title>Formbricks WebView Survey</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body style="overflow: hidden; height: 100vh; display: flex; flex-direction: column; justify-content: flex-end;">
<div id="formbricks-react-native" style="width: 100%;"></div>
</body>
<body style="overflow: hidden; height: 100vh; background: ${getBackgroundColor()}; margin: 0;">
<style>
.survey-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
padding: 20px;
box-sizing: border-box;
pointer-events: none;
}
#formbricks-react-native {
width: 100%;
max-width: 600px;
pointer-events: auto;
}
#formbricks-react-native > div {
width: 100%;
}
@media (max-width: 640px) {
.survey-container {
padding: 0;
align-items: flex-end !important;
justify-content: center !important;
}
#formbricks-react-native {
max-width: 100%;
}
}
/* Placement-specific styles */
.placement-bottomLeft {
align-items: flex-end;
justify-content: flex-start;
}
.placement-bottomRight {
align-items: flex-end;
justify-content: flex-end;
}
.placement-topLeft {
align-items: flex-start;
justify-content: flex-start;
}
.placement-topRight {
align-items: flex-start;
justify-content: flex-end;
}
.placement-center {
align-items: center;
justify-content: center;
}
</style>
<div class="survey-container placement-${options.placement ?? "center"}" id="survey-wrapper">
<div id="formbricks-react-native">
<div></div>
</div>
</div>
</body>
<script type="text/javascript">
const consoleLog = (type, log) => window.ReactNativeWebView.postMessage(JSON.stringify({'type': 'Console', 'data': {'type': type, 'log': log}}));
@@ -424,7 +506,7 @@ const renderHtml = (options: Partial<SurveyInlineProps> & { appUrl?: string }):
onClose,
onFileUpload
};
window.formbricksSurveys.renderSurveyInline(surveyProps);
}
@@ -435,7 +517,17 @@ const renderHtml = (options: Partial<SurveyInlineProps> & { appUrl?: string }):
script.onerror = (error) => {
console.error("Failed to load Formbricks Surveys library:", error);
};
document.head.appendChild(script);
// Add click handler to close survey when clicking outside
document.addEventListener('click', function(event) {
if(!${options.clickOutside}) return;
const surveyContainer = document.getElementById('formbricks-react-native');
if (surveyContainer && !surveyContainer.contains(event.target)) {
onClose();
}
});
</script>
</html>
`;

View File

@@ -25,6 +25,7 @@ export interface SurveyBaseProps {
isCardBorderVisible?: boolean;
startAtQuestionId?: string;
clickOutside?: boolean;
darkOverlay?: boolean;
hiddenFieldsRecord?: TResponseData;
shouldResetQuestionId?: boolean;
fullSizeCards?: boolean;
@@ -32,4 +33,5 @@ export interface SurveyBaseProps {
export interface SurveyInlineProps extends SurveyBaseProps {
containerId: string;
placement: "bottomLeft" | "bottomRight" | "topLeft" | "topRight" | "center";
}

View File

@@ -8,5 +8,5 @@
},
"exclude": ["dist", "build", "node_modules"],
"extends": "@formbricks/config-typescript/react-native-library.json",
"include": ["."]
"include": [".", "../database/src/index.ts"]
}

View File

@@ -19,6 +19,7 @@ export const ZPlacement = z.enum(["bottomLeft", "bottomRight", "topLeft", "topRi
export type TPlacement = z.infer<typeof ZPlacement>;
export const ZAllowedFileExtension = z.enum([
"heic",
"png",
"jpeg",
"jpg",

View File

@@ -296,6 +296,8 @@ export const ZSurveyLogicConditionsOperator = z.enum([
"isBooked",
"isPartiallySubmitted",
"isCompletelySubmitted",
"isSet",
"isNotSet",
]);
const operatorsWithoutRightOperand = [
@@ -306,6 +308,8 @@ const operatorsWithoutRightOperand = [
ZSurveyLogicConditionsOperator.Enum.isBooked,
ZSurveyLogicConditionsOperator.Enum.isPartiallySubmitted,
ZSurveyLogicConditionsOperator.Enum.isCompletelySubmitted,
ZSurveyLogicConditionsOperator.Enum.isSet,
ZSurveyLogicConditionsOperator.Enum.isNotSet,
] as const;
export const ZDynamicLogicField = z.enum(["question", "variable", "hiddenField"]);
@@ -1517,6 +1521,8 @@ const isInvalidOperatorsForHiddenFieldType = (operator: TSurveyLogicConditionsOp
"doesNotStartWith",
"endsWith",
"doesNotEndWith",
"isSet",
"isNotSet",
].includes(operator)
) {
isInvalidOperator = true;

86
pnpm-lock.yaml generated
View File

@@ -387,6 +387,9 @@ importers:
googleapis:
specifier: 144.0.0
version: 144.0.0(encoding@0.1.13)
heic-convert:
specifier: 2.1.0
version: 2.1.0
https-proxy-agent:
specifier: 7.0.6
version: 7.0.6
@@ -520,6 +523,9 @@ importers:
'@types/bcryptjs':
specifier: 2.4.6
version: 2.4.6
'@types/heic-convert':
specifier: 2.1.0
version: 2.1.0
'@types/lodash':
specifier: 4.17.13
version: 4.17.13
@@ -591,7 +597,7 @@ importers:
version: 8.18.0(eslint@8.57.0)(typescript@5.7.2)
'@vercel/style-guide':
specifier: 6.0.0
version: 6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.5(tsx@4.19.2))
version: 6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))
eslint-config-next:
specifier: 15.1.0
version: 15.1.0(eslint@8.57.0)(typescript@5.7.2)
@@ -5467,6 +5473,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/heic-convert@2.1.0':
resolution: {integrity: sha512-Cf5Sdc2Gm2pfZ0uN1zjj35wcf3mF1lJCMIzws5OdJynrdMJRTIRUGa5LegbVg0hatzOPkH2uAf2JRjPYgl9apg==}
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
@@ -8359,8 +8368,8 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hast-util-to-jsx-runtime@2.3.2:
resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==}
hast-util-to-jsx-runtime@2.3.3:
resolution: {integrity: sha512-pdpkP8YD4v+qMKn2lnKSiJvZvb3FunDmFYQvVOsoO08+eTNWdaWKPMrC5wwNICtU3dQWHhElj5Sf5jPEnv4qJg==}
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
@@ -8369,6 +8378,14 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
heic-convert@2.1.0:
resolution: {integrity: sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg==}
engines: {node: '>=12.0.0'}
heic-decode@2.0.0:
resolution: {integrity: sha512-NU+zsiDvdL+EebyTjrEqjkO2XYI7FgLhQzsbmO8dnnYce3S0PBSDm/ZyI4KpcGPXYEdb5W72vp/AQFuc4F8ASg==}
engines: {node: '>=8.0.0'}
help-me@3.0.0:
resolution: {integrity: sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==}
@@ -8944,6 +8961,9 @@ packages:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
jpeg-js@0.4.4:
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
js-cookie@2.2.1:
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
@@ -9133,6 +9153,10 @@ packages:
engines: {node: '>=16'}
hasBin: true
libheif-js@1.18.2:
resolution: {integrity: sha512-4Nk0dKhhRfVS4mECcX2jSDpNU6gcHQLneJjkGQq61N8COGtjSpSA3CI+1Q3kUYv5Vf+SwIqUtaDSdU6JO37c6w==}
engines: {node: '>=8.0.0'}
lighthouse-logger@1.4.2:
resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==}
@@ -10478,6 +10502,10 @@ packages:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
pngjs@6.0.0:
resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
engines: {node: '>=12.13.0'}
polished@4.3.1:
resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==}
engines: {node: '>=10'}
@@ -10747,8 +10775,8 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
property-information@7.0.0:
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -18841,6 +18869,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/heic-convert@2.1.0': {}
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/istanbul-lib-report@3.0.3':
@@ -19259,7 +19289,7 @@ snapshots:
next: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
'@vercel/style-guide@6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.5(tsx@4.19.2))':
'@vercel/style-guide@6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))':
dependencies:
'@babel/core': 7.26.0
'@babel/eslint-parser': 7.26.5(@babel/core@7.26.0)(eslint@8.57.0)
@@ -19267,7 +19297,7 @@ snapshots:
'@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)
'@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.7.2)
eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0))
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0)
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0)
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)
@@ -19279,7 +19309,7 @@ snapshots:
eslint-plugin-testing-library: 6.5.0(eslint@8.57.0)(typescript@5.7.2)
eslint-plugin-tsdoc: 0.2.17
eslint-plugin-unicorn: 51.0.1(eslint@8.57.0)
eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.5(tsx@4.19.2))
eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))
prettier-plugin-packagejson: 2.5.8(prettier@3.4.2)
optionalDependencies:
'@next/eslint-plugin-next': 15.1.0
@@ -19369,7 +19399,7 @@ snapshots:
optionalDependencies:
vite: 5.4.14(@types/node@22.10.2)(lightningcss@1.27.0)(terser@5.37.0)
'@vitest/mocker@3.0.5(vite@6.0.9(tsx@4.19.2))':
'@vitest/mocker@3.0.5(vite@6.0.9(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))':
dependencies:
'@vitest/spy': 3.0.5
estree-walker: 3.0.3
@@ -21378,9 +21408,9 @@ snapshots:
eslint: 8.57.0
eslint-plugin-turbo: 2.3.3(eslint@8.57.0)
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)):
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0):
dependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)
eslint-import-resolver-node@0.3.9:
dependencies:
@@ -21402,11 +21432,11 @@ snapshots:
is-glob: 4.0.3
stable-hash: 0.0.4
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0):
eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -21461,7 +21491,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -21628,7 +21658,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.5(tsx@4.19.2)):
eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)):
dependencies:
'@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.7.2)
eslint: 8.57.0
@@ -22399,7 +22429,7 @@ snapshots:
dependencies:
function-bind: 1.1.2
hast-util-to-jsx-runtime@2.3.2:
hast-util-to-jsx-runtime@2.3.3:
dependencies:
'@types/estree': 1.0.6
'@types/hast': 3.0.4
@@ -22411,7 +22441,7 @@ snapshots:
mdast-util-mdx-expression: 2.0.1
mdast-util-mdx-jsx: 3.2.0
mdast-util-mdxjs-esm: 2.0.1
property-information: 6.5.0
property-information: 7.0.0
space-separated-tokens: 2.0.2
style-to-object: 1.0.8
unist-util-position: 5.0.0
@@ -22425,6 +22455,16 @@ snapshots:
he@1.2.0: {}
heic-convert@2.1.0:
dependencies:
heic-decode: 2.0.0
jpeg-js: 0.4.4
pngjs: 6.0.0
heic-decode@2.0.0:
dependencies:
libheif-js: 1.18.2
help-me@3.0.0:
dependencies:
glob: 7.2.3
@@ -23028,6 +23068,8 @@ snapshots:
joycon@3.1.1: {}
jpeg-js@0.4.4: {}
js-cookie@2.2.1: {}
js-sdsl@4.3.0: {}
@@ -23245,6 +23287,8 @@ snapshots:
dependencies:
isomorphic.js: 0.2.5
libheif-js@1.18.2: {}
lighthouse-logger@1.4.2:
dependencies:
debug: 2.6.9
@@ -24933,6 +24977,8 @@ snapshots:
pngjs@5.0.0: {}
pngjs@6.0.0: {}
polished@4.3.1:
dependencies:
'@babel/runtime': 7.26.7
@@ -25137,7 +25183,7 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
property-information@6.5.0: {}
property-information@7.0.0: {}
proxy-from-env@1.1.0: {}
@@ -25325,7 +25371,7 @@ snapshots:
'@types/hast': 3.0.4
'@types/react': 19.0.1
devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.2
hast-util-to-jsx-runtime: 2.3.3
html-url-attributes: 3.0.1
mdast-util-to-hast: 13.2.0
react: 19.0.0
@@ -27274,7 +27320,7 @@ snapshots:
vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0):
dependencies:
'@vitest/expect': 3.0.5
'@vitest/mocker': 3.0.5(vite@6.0.9(tsx@4.19.2))
'@vitest/mocker': 3.0.5(vite@6.0.9(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))
'@vitest/pretty-format': 3.0.5
'@vitest/runner': 3.0.5
'@vitest/snapshot': 3.0.5

View File

@@ -159,7 +159,6 @@
"S3_REGION",
"S3_SECRET_KEY",
"SENTRY_DSN",
"SHORT_URL_BASE",
"SIGNUP_DISABLED",
"SLACK_CLIENT_ID",
"SLACK_CLIENT_SECRET",