diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/ResponsesLimitReachedBanner.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/ResponsesLimitReachedBanner.tsx new file mode 100644 index 0000000000..cc7f431e0e --- /dev/null +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/ResponsesLimitReachedBanner.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants"; +import { useResponses } from "@/lib/responses/responses"; +import Link from "next/Link"; + +export default function ResponsesLimitReachedBanner({ environmentId, surveyId }) { + const { responsesData } = useResponses(environmentId, surveyId); + const reachedLimit = responsesData?.reachedLimit; + const count = responsesData?.count; + + return ( + <> + {reachedLimit && ( +
+
+

+ Free limit reached + + You can only see {RESPONSES_LIMIT_FREE} of the {count} responses you received. +

+ + Upgrade now + +
+
+
+ )} + + ); +} diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx index cc89353979..27a725076b 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx @@ -9,9 +9,11 @@ import { useMemo } from "react"; import SingleResponse from "./SingleResponse"; export default function ResponseTimeline({ environmentId, surveyId }) { - const { responses, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId); + const { responsesData, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId); const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId); + const responses = responsesData?.responses; + const matchQandA = useMemo(() => { if (survey && responses) { // Create a mapping of question IDs to their headlines diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx index d1dbc4a6ab..7b61784871 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx @@ -1,6 +1,7 @@ import ContentWrapper from "@/components/shared/ContentWrapper"; import SurveyResultsTabs from "../SurveyResultsTabs"; import ResponseTimeline from "./ResponseTimeline"; +import ResponsesLimitReachedBanner from "../ResponsesLimitReachedBanner"; export default function ResponsesPage({ params }) { return ( @@ -10,6 +11,7 @@ export default function ResponsesPage({ params }) { environmentId={params.environmentId} surveyId={params.surveyId} /> + diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx index f775638d81..908bb136a8 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx @@ -14,11 +14,13 @@ import MultipleChoiceSummary from "./MultipleChoiceSummary"; import OpenTextSummary from "./OpenTextSummary"; export default function SummaryList({ environmentId, surveyId }) { - const { responses, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId); + const { responsesData, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId); const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId); const [confetti, setConfetti] = useState(false); const searchParams = useSearchParams(); + const responses = responsesData?.responses; + useEffect(() => { if (searchParams) { const newSurveyParam = searchParams.get("success"); diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryMetadata.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryMetadata.tsx index 938fb24c44..fe59f8e1dc 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryMetadata.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryMetadata.tsx @@ -17,10 +17,12 @@ import { useMemo } from "react"; import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown"; export default function SummaryMetadata({ surveyId, environmentId }) { - const { responses, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId); + const { responsesData, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId); const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId); const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId); + const responses = responsesData?.responses; + const completionRate = useMemo(() => { if (!responses) return 0; return (responses.filter((r) => r.finished).length / responses.length) * 100; diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/page.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/page.tsx index 1e5c6098ed..706fcfc250 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/page.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/page.tsx @@ -2,11 +2,13 @@ import ContentWrapper from "@/components/shared/ContentWrapper"; import SurveyResultsTabs from "../SurveyResultsTabs"; import SummaryList from "./SummaryList"; import SummaryMetadata from "./SummaryMetadata"; +import ResponsesLimitReachedBanner from "../ResponsesLimitReachedBanner"; export default function SummaryPage({ params }) { return ( <> + diff --git a/apps/web/lib/api/apiHelper.ts b/apps/web/lib/api/apiHelper.ts index d1f100b6e1..7bbde00a0e 100644 --- a/apps/web/lib/api/apiHelper.ts +++ b/apps/web/lib/api/apiHelper.ts @@ -53,6 +53,36 @@ export const hasUserEnvironmentAccess = async (user, environmentId) => { return false; }; +export const getPlan = async (req, res) => { + if (req.headers["x-api-key"]) { + const apiKey = req.headers["x-api-key"].toString(); + const apiKeyData = await prisma.apiKey.findUnique({ + where: { + hashedKey: hashApiKey(apiKey), + }, + select: { + environment: { + select: { + product: { + select: { + team: { + select: { + plan: true, + }, + }, + }, + }, + }, + }, + }, + }); + return apiKeyData?.environment.product.team.plan || "free"; + } else { + const user = await getSessionUser(req, res); + return user ? user.plan : "free"; + } +}; + export const hasApiEnvironmentAccess = async (apiKey, environmentId) => { // write function to check if the API Key has access to the environment const apiKeyData = await prisma.apiKey.findUnique({ diff --git a/apps/web/lib/responses/responses.ts b/apps/web/lib/responses/responses.ts index 22aa87feca..b5313aab99 100644 --- a/apps/web/lib/responses/responses.ts +++ b/apps/web/lib/responses/responses.ts @@ -8,7 +8,7 @@ export const useResponses = (environmentId: string, surveyId: string) => { ); return { - responses: data, + responsesData: data, isLoadingResponses: isLoading, isErrorResponses: error, mutateRespones: mutate, diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts index 17c7cb1cf4..b6a0245080 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts @@ -1,5 +1,6 @@ -import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; +import { getPlan, hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; +import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -33,7 +34,18 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) }, ], }); - return res.json(responses); + + if (process.env.NEXT_PUBLIC_IS_FORMBRICKS_CLOUD === "1") { + const plan = await getPlan(req, res); + if (plan === "free" && responses.length > RESPONSES_LIMIT_FREE) { + return res.json({ + count: responses.length, + responses: responses.slice(responses.length - RESPONSES_LIMIT_FREE, responses.length), // get last 30 from array + reachedLimit: true, + }); + } + } + return res.json({ count: responses.length, responses, reachedLimit: false }); } // Unknown HTTP Method diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts new file mode 100644 index 0000000000..5ac2b4704b --- /dev/null +++ b/packages/lib/constants.ts @@ -0,0 +1 @@ +export const RESPONSES_LIMIT_FREE = 30;