From ec78038caa19209c3df47410fdc64c726b81b8dd Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Fri, 18 Jul 2025 17:41:14 +0530 Subject: [PATCH] fix: survey preview not working for suid enabled link surveys backport (#6259) --- .../summary/components/SurveyAnalysisCTA.tsx | 30 ++++++++---- .../components/ShareSurveyLink/index.test.tsx | 46 +++++++++++++------ .../components/ShareSurveyLink/index.tsx | 28 +++++++---- .../modules/survey/hooks/useSingleUseId.tsx | 2 +- 4 files changed, 74 insertions(+), 32 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx index 9a76aae39b..57d4917444 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.tsx @@ -5,6 +5,7 @@ import { ShareSurveyModal } from "@/app/(app)/environments/[environmentId]/surve import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; 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 { Badge } from "@/modules/ui/components/badge"; import { Button } from "@/modules/ui/components/button"; @@ -12,7 +13,7 @@ import { IconBar } from "@/modules/ui/components/iconbar"; import { useTranslate } from "@tolgee/react"; import { BellRing, Eye, SquarePenIcon } from "lucide-react"; 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 { TEnvironment } from "@formbricks/types/environment"; import { TSegment } from "@formbricks/types/segment"; @@ -48,17 +49,16 @@ export const SurveyAnalysisCTA = ({ isFormbricksCloud, }: SurveyAnalysisCTAProps) => { const { t } = useTranslate(); - const searchParams = useSearchParams(); - const pathname = usePathname(); const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); const [loading, setLoading] = useState(false); - const [modalState, setModalState] = useState({ start: searchParams.get("share") === "true", share: false, }); - const surveyUrl = useMemo(() => `${publicDomain}/s/${survey.id}`, [survey.id, publicDomain]); + const { refreshSingleUseId } = useSingleUseId(survey); const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted; @@ -102,9 +102,18 @@ export const SurveyAnalysisCTA = ({ setLoading(false); }; - const getPreviewUrl = () => { - const separator = surveyUrl.includes("?") ? "&" : "?"; - return `${surveyUrl}${separator}preview=true`; + const getPreviewUrl = async () => { + const surveyUrl = new URL(`${publicDomain}/s/${survey.id}`); + + 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); @@ -119,7 +128,10 @@ export const SurveyAnalysisCTA = ({ { icon: Eye, tooltip: t("common.preview"), - onClick: () => window.open(getPreviewUrl(), "_blank"), + onClick: async () => { + const previewUrl = await getPreviewUrl(); + window.open(previewUrl, "_blank"); + }, isVisible: survey.type === "link", }, { diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx index 10b831a0e0..f85604f73e 100644 --- a/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx +++ b/apps/web/modules/analysis/components/ShareSurveyLink/index.test.tsx @@ -4,6 +4,7 @@ import { toast } from "react-hot-toast"; import { afterEach, describe, expect, test, vi } from "vitest"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; +import { getSurveyUrl } from "../../utils"; vi.mock("react-hot-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 = { id: "survey-id", name: "Test Survey", @@ -161,7 +180,7 @@ describe("ShareSurveyLink", () => { 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( { const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab"); 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"); }); - 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`; render( { const previewButton = screen.getByLabelText("environments.surveys.preview_survey_in_a_new_tab"); 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"); }); @@ -215,7 +240,9 @@ describe("ShareSurveyLink", () => { }); test("updates the survey URL when the language is changed", () => { - const { rerender } = render( + const mockGetSurveyUrl = vi.mocked(getSurveyUrl); + + render( { const germanOption = screen.getByText("German"); fireEvent.click(germanOption); - rerender( - - ); - expect(setSurveyUrl).toHaveBeenCalled(); - expect(surveyUrl).toContain("lang=de"); + expect(mockGetSurveyUrl).toHaveBeenCalledWith(survey, publicDomain, "de"); + expect(setSurveyUrl).toHaveBeenCalledWith(`${publicDomain}/s/${survey.id}?lang=de`); }); }); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx index e2029b352f..2cb4bf8e67 100644 --- a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx +++ b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx @@ -1,5 +1,6 @@ "use client"; +import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import { Copy, SquareArrowOutUpRight } from "lucide-react"; @@ -32,6 +33,22 @@ export const ShareSurveyLink = ({ 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 (
@@ -53,14 +70,9 @@ export const ShareSurveyLink = ({ title={t("environments.surveys.preview_survey_in_a_new_tab")} aria-label={t("environments.surveys.preview_survey_in_a_new_tab")} disabled={!surveyUrl} - onClick={() => { - let previewUrl = surveyUrl; - if (previewUrl.includes("?")) { - previewUrl += "&preview=true"; - } else { - previewUrl += "?preview=true"; - } - window.open(previewUrl, "_blank"); + onClick={async () => { + const url = await getPreviewUrl(); + window.open(url, "_blank"); }}> {t("common.preview")} diff --git a/apps/web/modules/survey/hooks/useSingleUseId.tsx b/apps/web/modules/survey/hooks/useSingleUseId.tsx index cd91311316..63ee15cfd6 100644 --- a/apps/web/modules/survey/hooks/useSingleUseId.tsx +++ b/apps/web/modules/survey/hooks/useSingleUseId.tsx @@ -14,7 +14,7 @@ export const useSingleUseId = (survey: TSurvey | TSurveyList) => { if (survey.singleUse?.enabled) { const response = await generateSingleUseIdsAction({ surveyId: survey.id, - isEncrypted: !!survey.singleUse?.isEncrypted, + isEncrypted: Boolean(survey.singleUse?.isEncrypted), count: 1, });