mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-19 13:29:08 -06:00
chore: limit remote action storing to userTargeting plan (#2521)
This commit is contained in:
25
apps/demo/components/SurveySwitch.tsx
Normal file
25
apps/demo/components/SurveySwitch.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@formbricks/ui"],
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"lucide-react": "^0.368.0",
|
||||
"next": "14.2.1",
|
||||
"react": "18.2.0",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user