Compare commits

...

5 Commits

Author SHA1 Message Date
Matti Nannt
0cb3881cd0 fix: return weekly-summary cron early when SMTP not configured (#6344) 2025-07-31 22:03:35 +02:00
Piyush Gupta
ec78038caa fix: survey preview not working for suid enabled link surveys backport (#6259) 2025-07-18 14:11:14 +02:00
Anshuman Pandey
908614b4e2 fix: backports removal of suid UI from survey editor (#6258) 2025-07-18 15:45:12 +05:30
pandeymangg
c584901337 removes the suid UI from the survey editor 2025-07-18 14:34:42 +05:30
Anshuman Pandey
087699fb57 fix: backports read only survey url change (#6256) 2025-07-18 10:38:54 +02:00
7 changed files with 116 additions and 191 deletions

View File

@@ -5,6 +5,7 @@ import { ShareSurveyModal } from "@/app/(app)/environments/[environmentId]/surve
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown"; import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog"; import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions"; import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
import { Badge } from "@/modules/ui/components/badge"; import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -12,7 +13,7 @@ import { IconBar } from "@/modules/ui/components/iconbar";
import { useTranslate } from "@tolgee/react"; import { useTranslate } from "@tolgee/react";
import { BellRing, Eye, SquarePenIcon } from "lucide-react"; import { BellRing, Eye, SquarePenIcon } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { TEnvironment } from "@formbricks/types/environment"; import { TEnvironment } from "@formbricks/types/environment";
import { TSegment } from "@formbricks/types/segment"; import { TSegment } from "@formbricks/types/segment";
@@ -48,17 +49,16 @@ export const SurveyAnalysisCTA = ({
isFormbricksCloud, isFormbricksCloud,
}: SurveyAnalysisCTAProps) => { }: SurveyAnalysisCTAProps) => {
const { t } = useTranslate(); const { t } = useTranslate();
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [modalState, setModalState] = useState<ModalState>({ const [modalState, setModalState] = useState<ModalState>({
start: searchParams.get("share") === "true", start: searchParams.get("share") === "true",
share: false, share: false,
}); });
const surveyUrl = useMemo(() => `${publicDomain}/s/${survey.id}`, [survey.id, publicDomain]); const { refreshSingleUseId } = useSingleUseId(survey);
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted; const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
@@ -102,9 +102,18 @@ export const SurveyAnalysisCTA = ({
setLoading(false); setLoading(false);
}; };
const getPreviewUrl = () => { const getPreviewUrl = async () => {
const separator = surveyUrl.includes("?") ? "&" : "?"; const surveyUrl = new URL(`${publicDomain}/s/${survey.id}`);
return `${surveyUrl}${separator}preview=true`;
if (survey.singleUse?.enabled) {
const newId = await refreshSingleUseId();
if (newId) {
surveyUrl.searchParams.set("suId", newId);
}
}
surveyUrl.searchParams.set("preview", "true");
return surveyUrl.toString();
}; };
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false); const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
@@ -119,7 +128,10 @@ export const SurveyAnalysisCTA = ({
{ {
icon: Eye, icon: Eye,
tooltip: t("common.preview"), tooltip: t("common.preview"),
onClick: () => window.open(getPreviewUrl(), "_blank"), onClick: async () => {
const previewUrl = await getPreviewUrl();
window.open(previewUrl, "_blank");
},
isVisible: survey.type === "link", isVisible: survey.type === "link",
}, },
{ {

View File

@@ -4,14 +4,13 @@ import { updateSingleUseLinksAction } from "@/app/(app)/environments/[environmen
import { DisableLinkModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal"; import { DisableLinkModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/disable-link-modal";
import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links"; import { DocumentationLinks } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/documentation-links";
import { ShareSurveyLink } from "@/modules/analysis/components/ShareSurveyLink"; import { ShareSurveyLink } from "@/modules/analysis/components/ShareSurveyLink";
import { getSurveyUrl } from "@/modules/analysis/utils";
import { generateSingleUseIdsAction } from "@/modules/survey/list/actions"; import { generateSingleUseIdsAction } from "@/modules/survey/list/actions";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle"; import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input"; import { Input } from "@/modules/ui/components/input";
import { useTranslate } from "@tolgee/react"; import { useTranslate } from "@tolgee/react";
import { CirclePlayIcon } from "lucide-react"; import { CirclePlayIcon, CopyIcon } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@@ -33,6 +32,7 @@ export const AnonymousLinksTab = ({
setSurveyUrl, setSurveyUrl,
locale, locale,
}: AnonymousLinksTabProps) => { }: AnonymousLinksTabProps) => {
const surveyUrlWithCustomSuid = `${surveyUrl}?suId=CUSTOM-ID`;
const router = useRouter(); const router = useRouter();
const { t } = useTranslate(); const { t } = useTranslate();
@@ -173,11 +173,9 @@ export const AnonymousLinksTab = ({
count, count,
}); });
const baseSurveyUrl = getSurveyUrl(survey, publicDomain, "default");
if (!!response?.data?.length) { if (!!response?.data?.length) {
const singleUseIds = response.data; const singleUseIds = response.data;
const surveyLinks = singleUseIds.map((singleUseId) => `${baseSurveyUrl}?suId=${singleUseId}`); const surveyLinks = singleUseIds.map((singleUseId) => `${surveyUrl}?suId=${singleUseId}`);
// Create content with just the links // Create content with just the links
const csvContent = surveyLinks.join("\n"); const csvContent = surveyLinks.join("\n");
@@ -258,14 +256,36 @@ export const AnonymousLinksTab = ({
/> />
{!singleUseEncryption ? ( {!singleUseEncryption ? (
<Alert variant="info" size="default"> <div className="flex w-full flex-col gap-4">
<AlertTitle> <Alert variant="info" size="default">
{t("environments.surveys.share.anonymous_links.custom_single_use_id_title")} <AlertTitle>
</AlertTitle> {t("environments.surveys.share.anonymous_links.custom_single_use_id_title")}
<AlertDescription> </AlertTitle>
{t("environments.surveys.share.anonymous_links.custom_single_use_id_description")}
</AlertDescription> <AlertDescription>
</Alert> {t("environments.surveys.share.anonymous_links.custom_single_use_id_description")}
</AlertDescription>
</Alert>
<div className="grid w-full grid-cols-6 items-center gap-2">
<div className="col-span-5 truncate rounded-md border border-slate-200 px-2 py-1">
<span className="truncate text-sm text-slate-900">{surveyUrlWithCustomSuid}</span>
</div>
<Button
variant="secondary"
onClick={() => {
navigator.clipboard.writeText(surveyUrlWithCustomSuid);
toast.success(t("common.copied_to_clipboard"));
}}
className="col-span-1 gap-1 text-sm">
{t("common.copy")}
<CopyIcon />
</Button>
</div>
</div>
) : null} ) : null}
{singleUseEncryption && ( {singleUseEncryption && (

View File

@@ -1,8 +1,9 @@
import { responses } from "@/app/lib/api/response"; import { responses } from "@/app/lib/api/response";
import { CRON_SECRET } from "@/lib/constants"; import { CRON_SECRET, SMTP_HOST } from "@/lib/constants";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "@/modules/email"; import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "@/modules/email";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { logger } from "@formbricks/logger";
import { getNotificationResponse } from "./lib/notificationResponse"; import { getNotificationResponse } from "./lib/notificationResponse";
import { getOrganizationIds } from "./lib/organization"; import { getOrganizationIds } from "./lib/organization";
import { getProjectsByOrganizationId } from "./lib/project"; import { getProjectsByOrganizationId } from "./lib/project";
@@ -16,6 +17,11 @@ export const POST = async (): Promise<Response> => {
return responses.notAuthenticatedResponse(); return responses.notAuthenticatedResponse();
} }
if (!SMTP_HOST) {
logger.info("SMTP_HOST is not configured, skipping weekly summary email");
return responses.successResponse({}, true);
}
const emailSendingPromises: Promise<void>[] = []; const emailSendingPromises: Promise<void>[] = [];
// Fetch all organization IDs // Fetch all organization IDs

View File

@@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
import { afterEach, describe, expect, test, vi } from "vitest"; import { afterEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user"; import { TUserLocale } from "@formbricks/types/user";
import { getSurveyUrl } from "../../utils";
vi.mock("react-hot-toast", () => ({ vi.mock("react-hot-toast", () => ({
toast: { toast: {
@@ -11,6 +12,24 @@ vi.mock("react-hot-toast", () => ({
}, },
})); }));
// Mock the useSingleUseId hook
vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({
useSingleUseId: vi.fn(() => ({
singleUseId: "test-single-use-id",
refreshSingleUseId: vi.fn().mockResolvedValue("test-single-use-id"),
})),
}));
// Mock the survey utils
vi.mock("../../utils", () => ({
getSurveyUrl: vi.fn((survey, publicDomain, language) => {
if (language && language !== "en") {
return `${publicDomain}/s/${survey.id}?lang=${language}`;
}
return `${publicDomain}/s/${survey.id}`;
}),
}));
const survey: TSurvey = { const survey: TSurvey = {
id: "survey-id", id: "survey-id",
name: "Test Survey", name: "Test Survey",
@@ -161,7 +180,7 @@ describe("ShareSurveyLink", () => {
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard"); expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
}); });
test("opens the preview link in a new tab when preview button is clicked (no query params)", () => { test("opens the preview link in a new tab when preview button is clicked (no query params)", async () => {
render( render(
<ShareSurveyLink <ShareSurveyLink
survey={survey} survey={survey}
@@ -175,10 +194,13 @@ describe("ShareSurveyLink", () => {
const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab"); const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab");
fireEvent.click(previewButton); fireEvent.click(previewButton);
// Wait for the async function to complete
await new Promise((resolve) => setTimeout(resolve, 0));
expect(global.open).toHaveBeenCalledWith(`${surveyUrl}?preview=true`, "_blank"); expect(global.open).toHaveBeenCalledWith(`${surveyUrl}?preview=true`, "_blank");
}); });
test("opens the preview link in a new tab when preview button is clicked (with query params)", () => { test("opens the preview link in a new tab when preview button is clicked (with query params)", async () => {
const surveyWithParamsUrl = `${publicDomain}/s/survey-id?foo=bar`; const surveyWithParamsUrl = `${publicDomain}/s/survey-id?foo=bar`;
render( render(
<ShareSurveyLink <ShareSurveyLink
@@ -193,6 +215,9 @@ describe("ShareSurveyLink", () => {
const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab"); const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab");
fireEvent.click(previewButton); fireEvent.click(previewButton);
// Wait for the async function to complete
await new Promise((resolve) => setTimeout(resolve, 0));
expect(global.open).toHaveBeenCalledWith(`${surveyWithParamsUrl}&preview=true`, "_blank"); expect(global.open).toHaveBeenCalledWith(`${surveyWithParamsUrl}&preview=true`, "_blank");
}); });
@@ -215,7 +240,9 @@ describe("ShareSurveyLink", () => {
}); });
test("updates the survey URL when the language is changed", () => { test("updates the survey URL when the language is changed", () => {
const { rerender } = render( const mockGetSurveyUrl = vi.mocked(getSurveyUrl);
render(
<ShareSurveyLink <ShareSurveyLink
survey={survey} survey={survey}
publicDomain={publicDomain} publicDomain={publicDomain}
@@ -231,16 +258,7 @@ describe("ShareSurveyLink", () => {
const germanOption = screen.getByText("German"); const germanOption = screen.getByText("German");
fireEvent.click(germanOption); fireEvent.click(germanOption);
rerender( expect(mockGetSurveyUrl).toHaveBeenCalledWith(survey, publicDomain, "de");
<ShareSurveyLink expect(setSurveyUrl).toHaveBeenCalledWith(`${publicDomain}/s/${survey.id}?lang=de`);
survey={survey}
publicDomain={publicDomain}
surveyUrl={surveyUrl}
setSurveyUrl={setSurveyUrl}
locale={locale}
/>
);
expect(setSurveyUrl).toHaveBeenCalled();
expect(surveyUrl).toContain("lang=de");
}); });
}); });

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react"; import { useTranslate } from "@tolgee/react";
import { Copy, SquareArrowOutUpRight } from "lucide-react"; import { Copy, SquareArrowOutUpRight } from "lucide-react";
@@ -32,6 +33,22 @@ export const ShareSurveyLink = ({
setSurveyUrl(url); setSurveyUrl(url);
}; };
const { refreshSingleUseId } = useSingleUseId(survey);
const getPreviewUrl = async () => {
const previewUrl = new URL(surveyUrl);
if (survey.singleUse?.enabled) {
const newId = await refreshSingleUseId();
if (newId) {
previewUrl.searchParams.set("suId", newId);
}
}
previewUrl.searchParams.set("preview", "true");
return previewUrl.toString();
};
return ( return (
<div className={"flex max-w-full flex-col items-center justify-center gap-2 md:flex-row"}> <div className={"flex max-w-full flex-col items-center justify-center gap-2 md:flex-row"}>
<SurveyLinkDisplay surveyUrl={surveyUrl} key={surveyUrl} /> <SurveyLinkDisplay surveyUrl={surveyUrl} key={surveyUrl} />
@@ -53,14 +70,9 @@ export const ShareSurveyLink = ({
title={t("environments.surveys.preview_survey_in_a_new_tab")} title={t("environments.surveys.preview_survey_in_a_new_tab")}
aria-label={t("environments.surveys.preview_survey_in_a_new_tab")} aria-label={t("environments.surveys.preview_survey_in_a_new_tab")}
disabled={!surveyUrl} disabled={!surveyUrl}
onClick={() => { onClick={async () => {
let previewUrl = surveyUrl; const url = await getPreviewUrl();
if (previewUrl.includes("?")) { window.open(url, "_blank");
previewUrl += "&preview=true";
} else {
previewUrl += "?preview=true";
}
window.open(previewUrl, "_blank");
}}> }}>
{t("common.preview")} {t("common.preview")}
<SquareArrowOutUpRight /> <SquareArrowOutUpRight />

View File

@@ -7,12 +7,10 @@ import { DatePicker } from "@/modules/ui/components/date-picker";
import { Input } from "@/modules/ui/components/input"; import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { Slider } from "@/modules/ui/components/slider"; import { Slider } from "@/modules/ui/components/slider";
import { Switch } from "@/modules/ui/components/switch";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react"; import { useTranslate } from "@tolgee/react";
import { ArrowUpRight, CheckIcon } from "lucide-react"; import { CheckIcon } from "lucide-react";
import Link from "next/link";
import { KeyboardEventHandler, useEffect, useState } from "react"; import { KeyboardEventHandler, useEffect, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { TSurvey } from "@formbricks/types/surveys/types"; import { TSurvey } from "@formbricks/types/surveys/types";
@@ -47,12 +45,6 @@ export const ResponseOptionsCard = ({
subheading: t("environments.surveys.edit.survey_completed_subheading"), subheading: t("environments.surveys.edit.survey_completed_subheading"),
}); });
const [singleUseMessage, setSingleUseMessage] = useState({
heading: t("environments.surveys.edit.survey_already_answered_heading"),
subheading: t("environments.surveys.edit.survey_already_answered_subheading"),
});
const [singleUseEncryption, setSingleUseEncryption] = useState(true);
const [runOnDate, setRunOnDate] = useState<Date | null>(null); const [runOnDate, setRunOnDate] = useState<Date | null>(null);
const [closeOnDate, setCloseOnDate] = useState<Date | null>(null); const [closeOnDate, setCloseOnDate] = useState<Date | null>(null);
const [recaptchaThreshold, setRecaptchaThreshold] = useState<number>(localSurvey.recaptcha?.threshold ?? 0); const [recaptchaThreshold, setRecaptchaThreshold] = useState<number>(localSurvey.recaptcha?.threshold ?? 0);
@@ -163,53 +155,6 @@ export const ResponseOptionsCard = ({
setLocalSurvey({ ...localSurvey, surveyClosedMessage: message }); 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 handleHideBackButtonToggle = () => { const handleHideBackButtonToggle = () => {
setLocalSurvey({ ...localSurvey, isBackButtonHidden: !localSurvey.isBackButtonHidden }); setLocalSurvey({ ...localSurvey, isBackButtonHidden: !localSurvey.isBackButtonHidden });
}; };
@@ -223,14 +168,6 @@ export const ResponseOptionsCard = ({
setSurveyClosedMessageToggle(true); 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.runOnDate) { if (localSurvey.runOnDate) {
setRunOnDate(localSurvey.runOnDate); setRunOnDate(localSurvey.runOnDate);
setRunOnDateToggle(true); setRunOnDateToggle(true);
@@ -240,13 +177,7 @@ export const ResponseOptionsCard = ({
setCloseOnDate(localSurvey.closeOnDate); setCloseOnDate(localSurvey.closeOnDate);
setCloseOnDateToggle(true); setCloseOnDateToggle(true);
} }
}, [ }, [localSurvey, surveyClosedMessage.heading, surveyClosedMessage.subheading]);
localSurvey,
singleUseMessage.heading,
singleUseMessage.subheading,
surveyClosedMessage.heading,
surveyClosedMessage.subheading,
]);
const toggleAutocomplete = () => { const toggleAutocomplete = () => {
if (autoComplete) { if (autoComplete) {
@@ -471,80 +402,6 @@ export const ResponseOptionsCard = ({
</div> </div>
</AdvancedOptionToggle> </AdvancedOptionToggle>
{/* Single User Survey Options */}
<AdvancedOptionToggle
htmlId="singleUserSurveyOptions"
isChecked={!!localSurvey.singleUse?.enabled}
onToggle={handleSingleUseSurveyToggle}
title={t("environments.surveys.edit.single_use_survey_links")}
description={t("environments.surveys.edit.single_use_survey_links_description")}
childBorder={true}>
<div className="flex w-full items-center space-x-1 p-4 pb-4">
<div className="w-full cursor-pointer items-center bg-slate-50">
<div className="row mb-2 flex cursor-default items-center space-x-2">
<Label htmlFor="howItWorks">{t("environments.surveys.edit.how_it_works")}</Label>
</div>
<ul className="mb-3 ml-4 cursor-default list-inside list-disc space-y-1">
<li className="text-sm text-slate-600">
{t(
"environments.surveys.edit.blocks_survey_if_the_survey_url_has_no_single_use_id_suid"
)}
</li>
<li className="text-sm text-slate-600">
{t(
"environments.surveys.edit.blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already"
)}
</li>
<li className="text-sm text-slate-600">
<Link
href="https://formbricks.com/docs/link-surveys/single-use-links"
target="_blank"
className="underline">
{t("common.read_docs")} <ArrowUpRight className="inline" size={16} />
</Link>
</li>
</ul>
<Label htmlFor="headline">{t("environments.surveys.edit.link_used_message")}</Label>
<Input
autoFocus
id="heading"
className="mb-4 mt-2 bg-white"
name="heading"
value={singleUseMessage.heading}
onChange={(e) => handleSingleUseSurveyMessageChange({ heading: e.target.value })}
/>
<Label htmlFor="headline">{t("environments.surveys.edit.subheading")}</Label>
<Input
className="mb-4 mt-2 bg-white"
id="subheading"
name="subheading"
value={singleUseMessage.subheading}
onChange={(e) => handleSingleUseSurveyMessageChange({ subheading: e.target.value })}
/>
<Label htmlFor="headline">{t("environments.surveys.edit.url_encryption")}</Label>
<div>
<div className="mt-2 flex items-center space-x-1">
<Switch
id="encryption-switch"
checked={singleUseEncryption}
onCheckedChange={hangleSingleUseEncryptionToggle}
/>
<Label htmlFor="encryption-label">
<div className="ml-2">
<p className="text-sm font-normal text-slate-600">
{t(
"environments.surveys.edit.enable_encryption_of_single_use_id_suid_in_survey_url"
)}
</p>
</div>
</Label>
</div>
</div>
</div>
</div>
</AdvancedOptionToggle>
{/* Verify Email Section */} {/* Verify Email Section */}
<AdvancedOptionToggle <AdvancedOptionToggle
htmlId="verifyEmailBeforeSubmission" htmlId="verifyEmailBeforeSubmission"

View File

@@ -14,7 +14,7 @@ export const useSingleUseId = (survey: TSurvey | TSurveyList) => {
if (survey.singleUse?.enabled) { if (survey.singleUse?.enabled) {
const response = await generateSingleUseIdsAction({ const response = await generateSingleUseIdsAction({
surveyId: survey.id, surveyId: survey.id,
isEncrypted: !!survey.singleUse?.isEncrypted, isEncrypted: Boolean(survey.singleUse?.isEncrypted),
count: 1, count: 1,
}); });