-
-
- {survey.status !== "draft" && (
- <>
-
- >
- )}
- {survey.status === "draft" && (
-
Draft
- )}
+ className="absolute h-full w-full">
+
+
+
+ {survey.status !== "draft" && (
+ <>
+
+ >
+ )}
+ {survey.status === "draft" && (
+ Draft
+ )}
+
+
-
-
-
- ))}
+
+ );
+ })}
>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/SingleResponse.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/SingleResponse.tsx
index 8fca36bd48..402a2ce596 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/SingleResponse.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/SingleResponse.tsx
@@ -147,6 +147,11 @@ export default function SingleResponse({
)}
+ {data.singleUseId && (
+
+ {data.singleUseId}
+
+ )}
{data.finished && (
Completed
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkModalButton.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkModalButton.tsx
index 76f32f0f64..4251e3d590 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkModalButton.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkModalButton.tsx
@@ -7,12 +7,14 @@ import { useState } from "react";
import clsx from "clsx";
import { TProduct } from "@formbricks/types/v1/product";
import ShareEmbedSurvey from "./ShareEmbedSurvey";
+import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
interface LinkSurveyShareButtonProps {
survey: TSurvey;
className?: string;
surveyBaseUrl: string;
product: TProduct;
+ singleUseIds?: string[];
}
export default function LinkSurveyShareButton({
@@ -20,8 +22,10 @@ export default function LinkSurveyShareButton({
className,
surveyBaseUrl,
product,
+ singleUseIds,
}: LinkSurveyShareButtonProps) {
const [showLinkModal, setShowLinkModal] = useState(false);
+ const isSingleUse = survey.singleUse?.enabled ?? false;
return (
<>
@@ -34,8 +38,14 @@ export default function LinkSurveyShareButton({
onClick={() => setShowLinkModal(true)}>
-
- {showLinkModal && (
+ {showLinkModal && isSingleUse && singleUseIds ? (
+
+ ) : (
void;
+ singleUseIds: string[];
+}
+
+export default function LinkSingleUseSurveyModal({
+ survey,
+ open,
+ setOpen,
+ singleUseIds,
+}: LinkSingleUseSurveyModalProps) {
+ const defaultSurveyUrl = `${window.location.protocol}//${window.location.host}/s/${survey.id}`;
+ const [selectedSingleUseIds, setSelectedSingleIds] = useState([]);
+
+ const linkTextRef = useRef(null);
+ const router = useRouter();
+
+ const handleLinkOnClick = (index: number) => {
+ setSelectedSingleIds([...selectedSingleUseIds, index]);
+ const surveyUrl = `${defaultSurveyUrl}?suId=${singleUseIds[index]}`;
+ navigator.clipboard.writeText(surveyUrl);
+ toast.success("URL copied to clipboard!");
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx
index 0cf2a083bc..f789d35872 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx
@@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import ShareEmbedSurvey from "./ShareEmbedSurvey";
import { TProduct } from "@formbricks/types/v1/product";
+import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
import { TEnvironment } from "@formbricks/types/v1/environment";
interface SummaryMetadataProps {
@@ -14,6 +15,7 @@ interface SummaryMetadataProps {
survey: TSurvey;
surveyBaseUrl: string;
product: TProduct;
+ singleUseIds?: string[];
}
export default function SuccessMessage({
@@ -21,7 +23,10 @@ export default function SuccessMessage({
survey,
surveyBaseUrl,
product,
+ singleUseIds,
}: SummaryMetadataProps) {
+ const isSingleUse = survey.singleUse?.enabled ?? false;
+
const searchParams = useSearchParams();
const [showLinkModal, setShowLinkModal] = useState(false);
const [confetti, setConfetti] = useState(false);
@@ -52,7 +57,14 @@ export default function SuccessMessage({
return (
<>
- {showLinkModal && (
+ {showLinkModal && isSingleUse && singleUseIds ? (
+
+ ) : (
{
@@ -56,6 +58,7 @@ const SummaryPage = ({
survey={survey}
surveyId={surveyId}
surveyBaseUrl={surveyBaseUrl}
+ singleUseIds={singleUseIds}
product={product}
/>
{
+ return Array(5)
+ .fill(null)
+ .map(() => {
+ return generateSurveySingleUseId(isEncrypted);
+ });
+};
export default async function Page({ params }) {
const session = await getServerSession(authOptions);
if (!session) {
throw new Error("Unauthorized");
}
+
const [{ responses, survey }, environment] = await Promise.all([
getAnalysisData(params.surveyId, params.environmentId),
getEnvironment(params.environmentId),
]);
+ const isSingleUseSurvey = survey.singleUse?.enabled ?? false;
+ const singleUseIds = generateSingleUseIds(survey.singleUse?.isEncrypted ?? false);
if (!environment) {
throw new Error("Environment not found");
}
@@ -38,6 +50,7 @@ export default async function Page({ params }) {
survey={survey}
surveyId={params.surveyId}
surveyBaseUrl={SURVEY_BASE_URL}
+ singleUseIds={isSingleUseSurvey ? singleUseIds : undefined}
product={product}
environmentTags={tags}
/>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx
index 65a54b59ff..5d6d861b1e 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx
@@ -31,8 +31,16 @@ interface SummaryHeaderProps {
survey: TSurvey;
surveyBaseUrl: string;
product: TProduct;
+ singleUseIds?: string[];
}
-const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }: SummaryHeaderProps) => {
+const SummaryHeader = ({
+ surveyId,
+ environment,
+ survey,
+ surveyBaseUrl,
+ product,
+ singleUseIds,
+}: SummaryHeaderProps) => {
const router = useRouter();
const isCloseOnDateEnabled = survey.closeOnDate !== null;
@@ -47,7 +55,12 @@ const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }
{survey.type === "link" && (
-
+
)}
{(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? (
@@ -75,6 +88,7 @@ const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }
survey={survey}
surveyBaseUrl={surveyBaseUrl}
product={product}
+ singleUseIds={singleUseIds}
/>
>
@@ -157,6 +171,7 @@ const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }
survey={survey}
surveyBaseUrl={surveyBaseUrl}
product={product}
+ singleUseIds={singleUseIds}
/>
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx
index 1d81e55082..23c05f558b 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx
@@ -1,7 +1,17 @@
"use client";
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
-import { AdvancedOptionToggle, DatePicker, Input, Label } from "@formbricks/ui";
+import {
+ AdvancedOptionToggle,
+ DatePicker,
+ Input,
+ Label,
+ Switch,
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@formbricks/ui";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useEffect, useState } from "react";
@@ -10,9 +20,14 @@ import toast from "react-hot-toast";
interface ResponseOptionsCardProps {
localSurvey: TSurveyWithAnalytics;
setLocalSurvey: (survey: TSurveyWithAnalytics) => void;
+ isEncryptionKeySet: boolean;
}
-export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: ResponseOptionsCardProps) {
+export default function ResponseOptionsCard({
+ localSurvey,
+ setLocalSurvey,
+ isEncryptionKeySet,
+}: ResponseOptionsCardProps) {
const [open, setOpen] = useState(false);
const autoComplete = localSurvey.autoComplete !== null;
const [redirectToggle, setRedirectToggle] = useState(false);
@@ -27,6 +42,12 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
subheading: "This free & open-source survey has been closed",
});
+ const [singleUseMessage, setSingleUseMessage] = useState({
+ heading: "The survey has already been answered.",
+ subheading: "You can only use this link once.",
+ });
+
+ const [singleUseEncryption, setSingleUseEncryption] = useState(isEncryptionKeySet);
const [verifyEmailSurveyDetails, setVerifyEmailSurveyDetails] = useState({
name: "",
subheading: "",
@@ -104,6 +125,53 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
setLocalSurvey({ ...localSurvey, surveyClosedMessage: message });
};
+ const handleSingleUseSurveyToggle = () => {
+ if (!localSurvey.singleUse?.enabled) {
+ setLocalSurvey({
+ ...localSurvey,
+ singleUse: { enabled: true, ...singleUseMessage, isEncrypted: singleUseEncryption },
+ });
+ } else {
+ setLocalSurvey({ ...localSurvey, singleUse: { enabled: false, isEncrypted: false } });
+ }
+ };
+
+ const handleSingleUseSurveyMessageChange = ({
+ heading,
+ subheading,
+ }: {
+ heading?: string;
+ subheading?: string;
+ }) => {
+ const message = {
+ heading: heading ?? singleUseMessage.heading,
+ subheading: subheading ?? singleUseMessage.subheading,
+ };
+
+ const localSurveySingleUseEnabled = localSurvey.singleUse?.enabled ?? false;
+ setSingleUseMessage(message);
+ setLocalSurvey({
+ ...localSurvey,
+ singleUse: { enabled: localSurveySingleUseEnabled, ...message, isEncrypted: singleUseEncryption },
+ });
+ };
+
+ const hangleSingleUseEncryptionToggle = () => {
+ if (!singleUseEncryption) {
+ setSingleUseEncryption(true);
+ setLocalSurvey({
+ ...localSurvey,
+ singleUse: { enabled: true, ...singleUseMessage, isEncrypted: true },
+ });
+ } else {
+ setSingleUseEncryption(false);
+ setLocalSurvey({
+ ...localSurvey,
+ singleUse: { enabled: true, ...singleUseMessage, isEncrypted: false },
+ });
+ }
+ };
+
const handleVerifyEmailSurveyDetailsChange = ({
name,
subheading,
@@ -134,6 +202,14 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
setSurveyClosedMessageToggle(true);
}
+ if (localSurvey.singleUse?.enabled) {
+ setSingleUseMessage({
+ heading: localSurvey.singleUse.heading ?? singleUseMessage.heading,
+ subheading: localSurvey.singleUse.subheading ?? singleUseMessage.subheading,
+ });
+ setSingleUseEncryption(localSurvey.singleUse.isEncrypted);
+ }
+
if (localSurvey.verifyEmail) {
setVerifyEmailSurveyDetails({
name: localSurvey.verifyEmail.name!,
@@ -302,6 +378,81 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
+ {/* Single User Survey Options */}
+