build free limit into formbricks cloud

This commit is contained in:
Matthias Nannt
2023-04-16 13:45:41 +02:00
parent d470b6576f
commit 1ec4dcb02d
10 changed files with 94 additions and 6 deletions

View File

@@ -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 && (
<div className="bg-brand-light relative isolate flex items-center gap-x-6 overflow-hidden px-6 py-2.5 sm:px-3.5 sm:before:flex-1">
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<p className="text-sm leading-6 text-gray-900">
<strong className="font-semibold">Free limit reached</strong>
<svg viewBox="0 0 2 2" className="mx-2 inline h-0.5 w-0.5 fill-current" aria-hidden="true">
<circle cx={1} cy={1} r={1} />
</svg>
You can only see {RESPONSES_LIMIT_FREE} of the {count} responses you received.
</p>
<Link
href={`/environments/${environmentId}/settings/billing`}
className="flex-none rounded-full bg-white/50 px-3.5 py-1 text-sm font-semibold text-slate-900 shadow-sm hover:bg-white/40 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900">
Upgrade now <span aria-hidden="true">&rarr;</span>
</Link>
</div>
<div className="flex flex-1"></div>
</div>
)}
</>
);
}

View File

@@ -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

View File

@@ -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}
/>
<ResponsesLimitReachedBanner environmentId={params.environmentId} surveyId={params.surveyId} />
<ContentWrapper>
<ResponseTimeline environmentId={params.environmentId} surveyId={params.surveyId} />
</ContentWrapper>

View File

@@ -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");

View File

@@ -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;

View File

@@ -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 (
<>
<SurveyResultsTabs activeId="summary" environmentId={params.environmentId} surveyId={params.surveyId} />
<ResponsesLimitReachedBanner environmentId={params.environmentId} surveyId={params.surveyId} />
<ContentWrapper>
<SummaryMetadata surveyId={params.surveyId} environmentId={params.environmentId} />
<SummaryList environmentId={params.environmentId} surveyId={params.surveyId} />

View File

@@ -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({

View File

@@ -8,7 +8,7 @@ export const useResponses = (environmentId: string, surveyId: string) => {
);
return {
responses: data,
responsesData: data,
isLoadingResponses: isLoading,
isErrorResponses: error,
mutateRespones: mutate,

View File

@@ -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

View File

@@ -0,0 +1 @@
export const RESPONSES_LIMIT_FREE = 30;