chore: limit remote action storing to userTargeting plan (#2521)

This commit is contained in:
Matti Nannt
2024-04-24 14:56:05 +02:00
committed by GitHub
parent 68c6dad26b
commit 4ae38546f0
14 changed files with 188 additions and 105 deletions

View File

@@ -0,0 +1,25 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
interface SurveySwitchProps {
value: "website" | "app";
formbricks: any;
}
export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
return (
<Select
value={value}
onValueChange={(v) => {
formbricks.logout();
window.location.href = `/${v}`;
}}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="website">Website Surveys</SelectItem>
<SelectItem value="app">App Surveys</SelectItem>
</SelectContent>
</Select>
);
};

View File

@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@formbricks/ui"],
async redirects() {
return [
{

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "^0.368.0",
"next": "14.2.1",
"react": "18.2.0",

View File

@@ -1,10 +1,10 @@
import { EarthIcon } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricksApp from "@formbricks/js/app";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
declare const window: any;
@@ -57,27 +57,11 @@ export default function AppPage({}) {
}
});
const removeFormbricksContainer = () => {
document.getElementById("formbricks-modal-container")?.remove();
document.getElementById("formbricks-app-container")?.remove();
localStorage.removeItem("formbricks-js-app");
};
return (
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col justify-between md:flex-row">
<div className="flex items-center gap-2">
<button
className="rounded-lg bg-[#038178] p-2 text-white focus:outline-none focus:ring-2 focus:ring-slate-900 focus:ring-offset-1"
onClick={() => {
removeFormbricksContainer();
window.location.href = "/website";
}}>
<div className="flex items-center gap-2">
<EarthIcon className="h-10 w-10" />
<span>Website Demo</span>
</div>
</button>
<SurveySwitch value="app" formbricks={formbricksApp} />
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks In-product Survey Demo App

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import formbricksWebsite from "@formbricks/js/website";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
declare const window: any;
@@ -57,27 +58,11 @@ export default function AppPage({}) {
}
});
const removeFormbricksContainer = () => {
document.getElementById("formbricks-modal-container")?.remove();
document.getElementById("formbricks-website-container")?.remove();
localStorage.removeItem("formbricks-js-website");
};
return (
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col items-center justify-between md:flex-row">
<div className="flex items-center gap-2">
<button
className="rounded-lg bg-[#038178] p-2 text-white focus:outline-none focus:ring-2 focus:ring-slate-900 focus:ring-offset-1"
onClick={() => {
removeFormbricksContainer();
window.location.href = "/app";
}}>
<div className="flex items-center gap-2">
<MonitorIcon className="h-10 w-10" />
<span>App Demo</span>
</div>
</button>
<SurveySwitch value="website" formbricks={formbricksWebsite} />
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks Website Survey Demo App

View File

@@ -20,9 +20,14 @@ import {
interface ActivityTabProps {
actionClass: TActionClass;
environmentId: string;
isUserTargetingEnabled: boolean;
}
export default function EventActivityTab({ actionClass, environmentId }: ActivityTabProps) {
export default function EventActivityTab({
actionClass,
environmentId,
isUserTargetingEnabled,
}: ActivityTabProps) {
// const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClass.id);
const [numEventsLastHour, setNumEventsLastHour] = useState<number | undefined>();
@@ -46,9 +51,9 @@ export default function EventActivityTab({ actionClass, environmentId }: Activit
numEventsLast7DaysData,
activeInactiveSurveys,
] = await Promise.all([
getActionCountInLastHourAction(actionClass.id, environmentId),
getActionCountInLast24HoursAction(actionClass.id, environmentId),
getActionCountInLast7DaysAction(actionClass.id, environmentId),
isUserTargetingEnabled ? getActionCountInLastHourAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast24HoursAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast7DaysAction(actionClass.id, environmentId) : 0,
getActiveInactiveSurveysAction(actionClass.id, environmentId),
]);
setNumEventsLastHour(numEventsLastHourData);
@@ -62,7 +67,7 @@ export default function EventActivityTab({ actionClass, environmentId }: Activit
setLoading(false);
}
}
}, [actionClass.id, environmentId]);
}, [actionClass.id, environmentId, isUserTargetingEnabled]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorComponent />;
@@ -70,23 +75,25 @@ export default function EventActivityTab({ actionClass, environmentId }: Activit
return (
<div className="grid grid-cols-3 pb-2">
<div className="col-span-2 space-y-4 pr-6">
<div>
<Label className="text-slate-500">Ocurrances</Label>
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLastHour}</p>
<p className="text-xs text-slate-500">last hour</p>
</div>
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast24Hours}</p>
<p className="text-xs text-slate-500">last 24 hours</p>
</div>
<div className="px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast7Days}</p>
<p className="text-xs text-slate-500">last week</p>
{isUserTargetingEnabled && (
<div>
<Label className="text-slate-500">Ocurrances</Label>
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLastHour}</p>
<p className="text-xs text-slate-500">last hour</p>
</div>
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast24Hours}</p>
<p className="text-xs text-slate-500">last 24 hours</p>
</div>
<div className="px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast7Days}</p>
<p className="text-xs text-slate-500">last week</p>
</div>
</div>
</div>
</div>
)}
<div>
<Label className="text-slate-500">Active surveys</Label>

View File

@@ -12,15 +12,19 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import ActionDetailModal from "./ActionDetailModal";
import AddNoCodeActionModal from "./AddActionModal";
interface ActionClassesTableProps {
environmentId: string;
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
isUserTargetingEnabled: boolean;
}
export default function ActionClassesTable({
environmentId,
actionClasses,
children: [TableHeading, actionRows],
}: {
environmentId: string;
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
}) {
isUserTargetingEnabled,
}: ActionClassesTableProps) {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
const { membershipRole, isLoading, error } = useMembershipRole(environmentId);
@@ -83,6 +87,7 @@ export default function ActionClassesTable({
setOpen={setActionDetailModalOpen}
actionClass={activeActionClass}
membershipRole={membershipRole}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
<AddNoCodeActionModal
environmentId={environmentId}

View File

@@ -13,6 +13,7 @@ interface ActionDetailModalProps {
setOpen: (v: boolean) => void;
actionClass: TActionClass;
membershipRole?: TMembershipRole;
isUserTargetingEnabled: boolean;
}
export default function ActionDetailModal({
@@ -21,11 +22,18 @@ export default function ActionDetailModal({
setOpen,
actionClass,
membershipRole,
isUserTargetingEnabled,
}: ActionDetailModalProps) {
const tabs = [
{
title: "Activity",
children: <EventActivityTab actionClass={actionClass} environmentId={environmentId} />,
children: (
<EventActivityTab
actionClass={actionClass}
environmentId={environmentId}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
),
},
{
title: "Settings",

View File

@@ -4,16 +4,34 @@ import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(action
import { Metadata } from "next";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export const metadata: Metadata = {
title: "Actions",
};
export default async function ActionClassesComponent({ params }) {
let actionClasses = await getActionClasses(params.environmentId);
const [actionClasses, team] = await Promise.all([
getActionClasses(params.environmentId),
getTeamByEnvironmentId(params.environmentId),
]);
if (!team) {
throw new Error("Team not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
: true;
return (
<>
<ActionClassesTable environmentId={params.environmentId} actionClasses={actionClasses}>
<ActionClassesTable
environmentId={params.environmentId}
actionClasses={actionClasses}
isUserTargetingEnabled={isUserTargetingEnabled}>
<ActionTableHeading />
{actionClasses.map((actionClass) => (
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} />

View File

@@ -1,7 +1,9 @@
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityTimeline";
import { getActionsByPersonId } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export default async function ActivitySection({
environmentId,
@@ -10,9 +12,20 @@ export default async function ActivitySection({
environmentId: string;
personId: string;
}) {
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
: true;
const [environment, actions] = await Promise.all([
getEnvironment(environmentId),
getActionsByPersonId(personId, 1),
isUserTargetingEnabled ? getActionsByPersonId(personId, 1) : [],
]);
if (!environment) {
@@ -21,7 +34,11 @@ export default async function ActivitySection({
return (
<div className="md:col-span-1">
<ActivityTimeline environment={environment} actions={actions.slice(0, 10)} />
<ActivityTimeline
environment={environment}
actions={actions.slice(0, 10)}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
</div>
);
}

View File

@@ -1,58 +1,71 @@
import { TAction } from "@formbricks/types/actions";
import { TEnvironment } from "@formbricks/types/environment";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
import { ActivityItemContent, ActivityItemIcon, ActivityItemPopover } from "./ActivityItemComponents";
interface IActivityTimelineProps {
environment: TEnvironment;
actions: TAction[];
isUserTargetingEnabled: boolean;
}
export default function ActivityTimeline({
environment,
actions,
}: {
environment: TEnvironment;
actions: TAction[];
}) {
isUserTargetingEnabled,
}: IActivityTimelineProps) {
return (
<>
<div className="flex items-center justify-between pb-6">
<h2 className="text-lg font-bold text-slate-700">Actions Timeline</h2>
</div>
<div className="relative">
{actions.length === 0 ? (
<EmptySpaceFiller type={"event"} environment={environment} />
) : (
<div>
{actions.map(
(actionItem, index) =>
actionItem && (
<li key={actionItem.id} className="list-none">
<div className="relative pb-12">
{index !== actions.length - 1 && (
<span
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
aria-hidden="true"
/>
)}
<div className="relative">
<ActivityItemPopover actionItem={actionItem}>
<div className="flex space-x-3 text-left">
<ActivityItemIcon actionItem={actionItem} />
<ActivityItemContent actionItem={actionItem} />
</div>
</ActivityItemPopover>
{!isUserTargetingEnabled ? (
<UpgradePlanNotice
message="Upgrade to the User Targeting plan to store action history."
textForUrl="Upgrade now."
url={`/environments/${environment.id}/settings/billing`}
/>
) : (
<div className="relative">
{actions.length === 0 ? (
<EmptySpaceFiller type={"event"} environment={environment} />
) : (
<div>
{actions.map(
(actionItem, index) =>
actionItem && (
<li key={actionItem.id} className="list-none">
<div className="relative pb-12">
{index !== actions.length - 1 && (
<span
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
aria-hidden="true"
/>
)}
<div className="relative">
<ActivityItemPopover actionItem={actionItem}>
<div className="flex space-x-3 text-left">
<ActivityItemIcon actionItem={actionItem} />
<ActivityItemContent actionItem={actionItem} />
</div>
</ActivityItemPopover>
</div>
</div>
</div>
</li>
)
)}
<div className="relative">
{actions.length === 10 && (
<div className="absolute bottom-0 flex h-56 w-full items-end justify-center bg-gradient-to-t from-slate-50 to-transparent"></div>
</li>
)
)}
<div className="relative">
{actions.length === 10 && (
<div className="absolute bottom-0 flex h-56 w-full items-end justify-center bg-gradient-to-t from-slate-50 to-transparent"></div>
)}
</div>
</div>
</div>
)}
</div>
)}
</div>
)}
</>
);
}

View File

@@ -8,15 +8,19 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
export default async function PersonPage({ params }) {
const environment = await getEnvironment(params.environmentId);
const environmentTags = await getTagsByEnvironmentId(params.environmentId);
const product = await getProductByEnvironmentId(params.environmentId);
const [environment, environmentTags, product] = await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);
if (!product) {
throw new Error("Product not found");
}
if (!environment) {
throw new Error("Environment not found");
}
return (
<div>
<main className="mx-auto px-4 sm:px-6 lg:px-8">

View File

@@ -2,6 +2,8 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createAction } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ZActionInput } from "@formbricks/types/actions";
interface Context {
@@ -32,6 +34,16 @@ export async function POST(req: Request, context: Context): Promise<Response> {
);
}
// Formbricks Cloud: Make sure environment is part of a paid plan
if (IS_FORMBRICKS_CLOUD) {
const team = await getTeamByEnvironmentId(context.params.environmentId);
if (!team || team.billing.features.userTargeting.status !== "active") {
// temporary return status code 200 to avoid CORS issues; will be changed to 400 in the future
return responses.successResponse({}, true);
//return responses.badRequestResponse("Storing actions is only possible in a paid plan", {}, true);
}
}
await createAction(inputValidation.data);
return responses.successResponse({}, true);

3
pnpm-lock.yaml generated
View File

@@ -42,6 +42,9 @@ importers:
'@formbricks/js':
specifier: workspace:*
version: link:../../packages/js
'@formbricks/ui':
specifier: workspace:*
version: link:../../packages/ui
lucide-react:
specifier: ^0.368.0
version: 0.368.0(react@18.2.0)