- {fileUrls?.map((file, index) => {
- const fileName = getOriginalFileNameFromUrl(file);
+ {fileUrls?.map((fileUrl, index) => {
+ const fileName = getOriginalFileNameFromUrl(fileUrl);
return (
`.${ext}`).join(",")}
className="fb-hidden"
- onChange={(e) => {
+ onChange={async (e) => {
const inputElement = e.target as HTMLInputElement;
if (inputElement.files) {
handleFileSelection(inputElement.files);
diff --git a/packages/surveys/src/components/general/QuestionConditional.tsx b/packages/surveys/src/components/general/QuestionConditional.tsx
index 121f81e5a2..ccd3d16d88 100644
--- a/packages/surveys/src/components/general/QuestionConditional.tsx
+++ b/packages/surveys/src/components/general/QuestionConditional.tsx
@@ -11,6 +11,7 @@ import { NPSQuestion } from "@/components/questions/NPSQuestion";
import { OpenTextQuestion } from "@/components/questions/OpenTextQuestion";
import { PictureSelectionQuestion } from "@/components/questions/PictureSelectionQuestion";
import { RatingQuestion } from "@/components/questions/RatingQuestion";
+import { TJsFileUploadParams } from "@formbricks/types/js";
import { TResponseData, TResponseDataValue, TResponseTtc } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
@@ -21,7 +22,7 @@ interface QuestionConditionalProps {
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
- onFileUpload: (file: File, config?: TUploadFileConfig) => Promise;
+ onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise;
isFirstQuestion: boolean;
isLastQuestion: boolean;
languageCode: string;
diff --git a/packages/surveys/src/components/general/Survey.tsx b/packages/surveys/src/components/general/Survey.tsx
index 0a95fa4634..9feaa7af27 100644
--- a/packages/surveys/src/components/general/Survey.tsx
+++ b/packages/surveys/src/components/general/Survey.tsx
@@ -81,6 +81,7 @@ export const Survey = ({
return survey.questions.find((q) => q.id === questionId);
}
}, [questionId, survey, history]);
+
const contentRef = useRef(null);
const showProgressBar = !styling.hideProgressBar;
const getShowSurveyCloseButton = (offset: number) => {
@@ -297,7 +298,7 @@ export const Survey = ({
string;
+ replaceRecallInfo: (text: string, responseData: TResponseData, variables: TSurveyVariables) => string;
isCurrent: boolean;
responseData: TResponseData;
}
@@ -142,11 +142,19 @@ export const WelcomeCard = ({
)}
diff --git a/packages/surveys/src/components/questions/FileUploadQuestion.tsx b/packages/surveys/src/components/questions/FileUploadQuestion.tsx
index 13acb749c1..92bb1dc90b 100644
--- a/packages/surveys/src/components/questions/FileUploadQuestion.tsx
+++ b/packages/surveys/src/components/questions/FileUploadQuestion.tsx
@@ -5,6 +5,7 @@ import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
import { useState } from "preact/hooks";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
+import { TJsFileUploadParams } from "@formbricks/types/js";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import type { TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
@@ -18,7 +19,7 @@ interface FileUploadQuestionProps {
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
- onFileUpload: (file: File, config?: TUploadFileConfig) => Promise
;
+ onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise;
isFirstQuestion: boolean;
isLastQuestion: boolean;
surveyId: string;
diff --git a/packages/surveys/src/components/questions/MatrixQuestion.tsx b/packages/surveys/src/components/questions/MatrixQuestion.tsx
index f2fd420f98..bfb8431e76 100644
--- a/packages/surveys/src/components/questions/MatrixQuestion.tsx
+++ b/packages/surveys/src/components/questions/MatrixQuestion.tsx
@@ -40,7 +40,6 @@ export const MatrixQuestion = ({
}: MatrixQuestionProps) => {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
- const isSubmitButtonVisible = question.required ? Object.entries(value).length !== 0 : true;
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
const handleSelect = useCallback(
@@ -152,6 +151,7 @@ export const MatrixQuestion = ({
dir="auto"
type="radio"
tabIndex={-1}
+ required={true}
id={`${row}-${column}`}
name={getLocalizedValue(row, languageCode)}
value={getLocalizedValue(column, languageCode)}
@@ -182,14 +182,12 @@ export const MatrixQuestion = ({
/>
)}
- {isSubmitButtonVisible && (
- {}}
- tabIndex={0}
- />
- )}
+ {}}
+ tabIndex={0}
+ />
);
diff --git a/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx b/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx
index a2cdcce730..4d6eea1858 100644
--- a/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx
+++ b/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx
@@ -174,7 +174,7 @@ export const StackedCardsContainer = ({
{cardArrangement === "simple" ? (
diff --git a/packages/surveys/src/lib/recall.ts b/packages/surveys/src/lib/recall.ts
index 5782b367d6..621be79f12 100644
--- a/packages/surveys/src/lib/recall.ts
+++ b/packages/surveys/src/lib/recall.ts
@@ -3,9 +3,13 @@ import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { formatDateWithOrdinal, isValidDateString } from "@formbricks/lib/utils/datetime";
import { extractFallbackValue, extractId, extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TResponseData } from "@formbricks/types/responses";
-import { TSurveyQuestion } from "@formbricks/types/surveys/types";
+import { TSurveyQuestion, TSurveyVariables } from "@formbricks/types/surveys/types";
-export const replaceRecallInfo = (text: string, responseData: TResponseData): string => {
+export const replaceRecallInfo = (
+ text: string,
+ responseData: TResponseData,
+ variables: TSurveyVariables
+): string => {
let modifiedText = text;
while (modifiedText.includes("recall:")) {
@@ -16,7 +20,13 @@ export const replaceRecallInfo = (text: string, responseData: TResponseData): st
if (!recallItemId) return modifiedText; // Return the text if no ID could be extracted
const fallback = extractFallbackValue(recallInfo).replaceAll("nbsp", " ");
- let value = null;
+ let value: string | null = null;
+
+ // Fetching value from variables based on recallItemId
+ if (variables.length) {
+ const variable = variables.find((variable) => variable.id === recallItemId);
+ value = variable?.value?.toString() ?? fallback;
+ }
// Fetching value from responseData or attributes based on recallItemId
if (responseData[recallItemId]) {
@@ -42,13 +52,15 @@ export const replaceRecallInfo = (text: string, responseData: TResponseData): st
export const parseRecallInformation = (
question: TSurveyQuestion,
languageCode: string,
- responseData: TResponseData
+ responseData: TResponseData,
+ variables: TSurveyVariables
) => {
const modifiedQuestion = structuredClone(question);
if (question.headline && question.headline[languageCode]?.includes("recall:")) {
modifiedQuestion.headline[languageCode] = replaceRecallInfo(
getLocalizedValue(modifiedQuestion.headline, languageCode),
- responseData
+ responseData,
+ variables
);
}
if (
@@ -58,7 +70,8 @@ export const parseRecallInformation = (
) {
modifiedQuestion.subheader[languageCode] = replaceRecallInfo(
getLocalizedValue(modifiedQuestion.subheader, languageCode),
- responseData
+ responseData,
+ variables
);
}
return modifiedQuestion;
diff --git a/packages/types/actions.ts b/packages/types/actions.ts
index 8524228f85..7418abd1e1 100644
--- a/packages/types/actions.ts
+++ b/packages/types/actions.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { ZActionClass } from "./action-classes";
-import { ZId } from "./environment";
+import { ZId } from "./common";
export const ZAction = z.object({
id: ZId,
diff --git a/packages/types/common.ts b/packages/types/common.ts
index e6b7397fef..eb45cff676 100644
--- a/packages/types/common.ts
+++ b/packages/types/common.ts
@@ -37,3 +37,7 @@ export const ZAllowedFileExtension = z.enum([
]);
export type TAllowedFileExtension = z.infer;
+
+export const ZId = z.string().cuid2();
+
+export const ZUuid = z.string().uuid();
diff --git a/packages/types/document-insights.ts b/packages/types/document-insights.ts
index df592dc7d9..7356d0f3fb 100644
--- a/packages/types/document-insights.ts
+++ b/packages/types/document-insights.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { ZId } from "./environment";
+import { ZId } from "./common";
export const ZDocumentInsight = z.object({
documentId: ZId,
diff --git a/packages/types/documents.ts b/packages/types/documents.ts
index 68e2674157..2e6df6ab79 100644
--- a/packages/types/documents.ts
+++ b/packages/types/documents.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { ZId } from "./environment";
+import { ZId } from "./common";
export const ZDocumentSentiment = z.enum(["positive", "negative", "neutral"]);
diff --git a/packages/types/environment.ts b/packages/types/environment.ts
index aa45c2500c..9111ad3901 100644
--- a/packages/types/environment.ts
+++ b/packages/types/environment.ts
@@ -25,8 +25,6 @@ export const ZEnvironmentUpdateInput = z.object({
websiteSetupCompleted: z.boolean(),
});
-export const ZId = z.string().cuid2();
-
export const ZEnvironmentCreateInput = z.object({
type: z.enum(["development", "production"]).optional(),
appSetupCompleted: z.boolean().optional(),
diff --git a/packages/types/formbricks-surveys.ts b/packages/types/formbricks-surveys.ts
index e009c8fc23..30b414d274 100644
--- a/packages/types/formbricks-surveys.ts
+++ b/packages/types/formbricks-surveys.ts
@@ -1,6 +1,7 @@
-import { type TProductStyling } from "./product";
-import { type TResponseData, type TResponseUpdate } from "./responses";
-import { type TUploadFileConfig } from "./storage";
+import type { TJsFileUploadParams } from "./js";
+import type { TProductStyling } from "./product";
+import type { TResponseData, TResponseUpdate } from "./responses";
+import type { TUploadFileConfig } from "./storage";
import type { TSurvey, TSurveyStyling } from "./surveys/types";
export interface SurveyBaseProps {
@@ -20,7 +21,7 @@ export interface SurveyBaseProps {
prefillResponseData?: TResponseData;
skipPrefilled?: boolean;
languageCode: string;
- onFileUpload: (file: File, config?: TUploadFileConfig) => Promise;
+ onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise;
responseCount?: number;
isCardBorderVisible?: boolean;
startAtQuestionId?: string;
diff --git a/packages/types/insights.ts b/packages/types/insights.ts
index b4f96cc71d..32e12b42e7 100644
--- a/packages/types/insights.ts
+++ b/packages/types/insights.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { ZId } from "./environment";
+import { ZId } from "./common";
export const ZInsightCategory = z.enum(["featureRequest", "complaint", "praise"]);
diff --git a/packages/types/js.ts b/packages/types/js.ts
index 383a5eca62..d9dc9022ff 100644
--- a/packages/types/js.ts
+++ b/packages/types/js.ts
@@ -2,7 +2,8 @@ import { z } from "zod";
import { ZActionClass } from "./action-classes";
import { ZAttributes } from "./attributes";
import { ZProduct } from "./product";
-import { ZResponseHiddenFieldValue } from "./responses";
+import { ZResponseHiddenFieldValue, ZResponseUpdate } from "./responses";
+import { ZUploadFileConfig } from "./storage";
import { ZSurvey } from "./surveys/types";
export const ZJsPerson = z.object({
@@ -186,3 +187,22 @@ export const ZJsTrackProperties = z.object({
});
export type TJsTrackProperties = z.infer;
+
+export const ZJsFileUploadParams = z.object({
+ file: z.object({ type: z.string(), name: z.string(), base64: z.string() }),
+ params: ZUploadFileConfig,
+});
+
+export type TJsFileUploadParams = z.infer;
+
+export const ZJsRNWebViewOnMessageData = z.object({
+ onFinished: z.boolean().nullish(),
+ onDisplay: z.boolean().nullish(),
+ onResponse: z.boolean().nullish(),
+ responseUpdate: ZResponseUpdate.nullish(),
+ onRetry: z.boolean().nullish(),
+ onClose: z.boolean().nullish(),
+ onFileUpload: z.boolean().nullish(),
+ fileUploadParams: ZJsFileUploadParams.nullish(),
+ uploadId: z.string().nullish(),
+});
diff --git a/packages/types/responses.ts b/packages/types/responses.ts
index 2e7c637e0b..d31a5fda07 100644
--- a/packages/types/responses.ts
+++ b/packages/types/responses.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { ZAttributes } from "./attributes";
-import { ZId } from "./environment";
+import { ZId } from "./common";
import { ZSurvey, ZSurveyLogicCondition } from "./surveys/types";
import { ZTag } from "./tags";
diff --git a/packages/types/surveys/types.ts b/packages/types/surveys/types.ts
index 574362fac0..1658aa91a2 100644
--- a/packages/types/surveys/types.ts
+++ b/packages/types/surveys/types.ts
@@ -1,8 +1,7 @@
import { z } from "zod";
import { ZActionClass, ZActionClassNoCodeConfig } from "../action-classes";
import { ZAttributes } from "../attributes";
-import { ZAllowedFileExtension, ZColor, ZPlacement } from "../common";
-import { ZId } from "../environment";
+import { ZAllowedFileExtension, ZColor, ZId, ZPlacement } from "../common";
import { ZInsight } from "../insights";
import { ZLanguage } from "../product";
import { ZSegment } from "../segment";
@@ -144,6 +143,36 @@ export const ZSurveyHiddenFields = z.object({
export type TSurveyHiddenFields = z.infer;
+export const ZSurveyVariable = z
+ .discriminatedUnion("type", [
+ z.object({
+ id: z.string().cuid2(),
+ name: z.string(),
+ type: z.literal("number"),
+ value: z.number().default(0),
+ }),
+ z.object({
+ id: z.string().cuid2(),
+ name: z.string(),
+ type: z.literal("text"),
+ value: z.string().default(""),
+ }),
+ ])
+ .superRefine((data, ctx) => {
+ // variable name can only contain lowercase letters, numbers, and underscores
+ if (!/^[a-z0-9_]+$/.test(data.name)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "Variable name can only contain lowercase letters, numbers, and underscores",
+ path: ["variables"],
+ });
+ }
+ });
+export const ZSurveyVariables = z.array(ZSurveyVariable);
+
+export type TSurveyVariable = z.infer;
+export type TSurveyVariables = z.infer;
+
export const ZSurveyProductOverwrites = z.object({
brandColor: ZColor.nullish(),
highlightBorderColor: ZColor.nullish(),
@@ -605,6 +634,29 @@ export const ZSurvey = z
}
}),
hiddenFields: ZSurveyHiddenFields,
+ variables: ZSurveyVariables.superRefine((variables, ctx) => {
+ // variable ids must be unique
+ const variableIds = variables.map((v) => v.id);
+ const uniqueVariableIds = new Set(variableIds);
+ if (uniqueVariableIds.size !== variableIds.length) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "Variable IDs must be unique",
+ path: ["variables"],
+ });
+ }
+
+ // variable names must be unique
+ const variableNames = variables.map((v) => v.name);
+ const uniqueVariableNames = new Set(variableNames);
+ if (uniqueVariableNames.size !== variableNames.length) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "Variable names must be unique",
+ path: ["variables"],
+ });
+ }
+ }),
delay: z.number(),
autoComplete: z.number().min(1, { message: "Response limit must be greater than 0" }).nullable(),
runOnDate: z.date().nullable(),
@@ -1393,7 +1445,7 @@ export type TSortOption = z.infer;
export const ZSurveyRecallItem = z.object({
id: z.string(),
label: z.string(),
- type: z.enum(["question", "hiddenField", "attributeClass"]),
+ type: z.enum(["question", "hiddenField", "attributeClass", "variable"]),
});
export type TSurveyRecallItem = z.infer;
diff --git a/packages/ui/Editor/components/ToolbarPlugin.tsx b/packages/ui/Editor/components/ToolbarPlugin.tsx
index 70696e022e..d53d929bde 100644
--- a/packages/ui/Editor/components/ToolbarPlugin.tsx
+++ b/packages/ui/Editor/components/ToolbarPlugin.tsx
@@ -161,9 +161,17 @@ const FloatingLinkEditor = ({ editor }: { editor: LexicalEditor }) => {
setEditMode(true);
}, []);
+ const linkAttributes = {
+ target: "_blank",
+ rel: "noopener noreferrer",
+ };
+
const handleSubmit = () => {
if (lastSelection && linkUrl) {
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
+ url: linkUrl,
+ ...linkAttributes,
+ });
}
setEditMode(false);
};
@@ -415,8 +423,6 @@ export const ToolbarPlugin = (props: TextEditorProps) => {
if (!isLink) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
url: "https://",
- target: "_blank",
- rel: "noopener noreferrer",
});
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
diff --git a/packages/ui/Input/index.tsx b/packages/ui/Input/index.tsx
index 6c2abcb975..cf391afc5e 100644
--- a/packages/ui/Input/index.tsx
+++ b/packages/ui/Input/index.tsx
@@ -16,7 +16,7 @@ const Input = React.forwardRef(({ className, isInv
className={cn(
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
className,
- isInvalid && "border-error focus:border-error border"
+ isInvalid && "border border-red-500 focus:border-red-500"
)}
ref={ref}
{...props}
diff --git a/packages/ui/MediaBackground/index.tsx b/packages/ui/MediaBackground/index.tsx
index 4e3487e3ae..828362c655 100644
--- a/packages/ui/MediaBackground/index.tsx
+++ b/packages/ui/MediaBackground/index.tsx
@@ -168,7 +168,7 @@ export const MediaBackground: React.FC = ({
);
default:
- return
;
+ return
;
}
};
@@ -185,7 +185,7 @@ export const MediaBackground: React.FC
= ({
className={`relative h-[90%] max-h-[40rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
{/* below element is use to create notch for the mobile device mockup */}
- {renderBackground()}
+ {survey.type === "link" && renderBackground()}
{renderContent()}
);
diff --git a/packages/ui/PreviewSurvey/components/Modal.tsx b/packages/ui/PreviewSurvey/components/Modal.tsx
index 76a4401b34..a33269071d 100644
--- a/packages/ui/PreviewSurvey/components/Modal.tsx
+++ b/packages/ui/PreviewSurvey/components/Modal.tsx
@@ -29,7 +29,7 @@ export const Modal = ({
const [show, setShow] = useState(true);
const modalRef = useRef