diff --git a/components/billing/plan-badge.tsx b/components/billing/plan-badge.tsx index 0c8c455c4..0520d2d70 100644 --- a/components/billing/plan-badge.tsx +++ b/components/billing/plan-badge.tsx @@ -12,7 +12,7 @@ export default function PlanBadge({ return ( diff --git a/components/billing/upgrade-plan-modal.tsx b/components/billing/upgrade-plan-modal.tsx index 0b615ef86..4f376724a 100644 --- a/components/billing/upgrade-plan-modal.tsx +++ b/components/billing/upgrade-plan-modal.tsx @@ -11,6 +11,10 @@ import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan import { PLANS } from "@/ee/stripe/utils"; import { CheckIcon, Users2Icon } from "lucide-react"; +import { useAnalytics } from "@/lib/analytics"; +import { usePlan } from "@/lib/swr/use-billing"; +import { capitalize, cn } from "@/lib/utils"; + import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Switch } from "@/components/ui/switch"; @@ -21,10 +25,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; -import { useAnalytics } from "@/lib/analytics"; -import { usePlan } from "@/lib/swr/use-billing"; -import { capitalize, cn } from "@/lib/utils"; - // Feature rendering component const FeatureItem = ({ feature }: { feature: Feature }) => { const baseClasses = `flex items-center ${feature.isHighlighted ? "bg-orange-50 -mx-6 px-6 py-2 -my-1 font-bold rounded-md dark:bg-orange-900/20" : ""}`; @@ -103,12 +103,14 @@ export function UpgradePlanModal({ trigger, open, setOpen, + highlightItem, children, }: { clickedPlan: PlanEnum; trigger?: string; open?: boolean; setOpen?: React.Dispatch>; + highlightItem?: string[]; children?: React.ReactNode; }) { const router = useRouter(); @@ -261,7 +263,12 @@ export function UpgradePlanModal({ diff --git a/components/datarooms/dataroom-navigation.tsx b/components/datarooms/dataroom-navigation.tsx index 3dc855a30..59d47cfd1 100644 --- a/components/datarooms/dataroom-navigation.tsx +++ b/components/datarooms/dataroom-navigation.tsx @@ -29,7 +29,7 @@ export const DataroomNavigation = ({ dataroomId }: { dataroomId?: string }) => { label: "Q&A Conversations", href: `/datarooms/${dataroomId}/conversations`, segment: "conversations", - disabled: !limits?.conversationsInDataroom, + limited: !limits?.conversationsInDataroom, }, { label: "Branding", diff --git a/components/navigation-menu.tsx b/components/navigation-menu.tsx index 8f655c734..b904a6eef 100644 --- a/components/navigation-menu.tsx +++ b/components/navigation-menu.tsx @@ -5,10 +5,15 @@ import { useRouter } from "next/router"; import * as React from "react"; -import { Separator } from "@/components/ui/separator"; +import { PlanEnum } from "@/ee/stripe/constants"; +import { CrownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; +import { Separator } from "@/components/ui/separator"; + +import { UpgradePlanModal } from "./billing/upgrade-plan-modal"; + type Props = { navigation: { label: string; @@ -16,6 +21,7 @@ type Props = { segment: string | null; tag?: string; disabled?: boolean; + limited?: boolean; }[]; className?: string; }; @@ -30,16 +36,19 @@ export const NavMenu: React.FC> = ({ >
    - {navigation.map(({ label, href, segment, tag, disabled }) => ( - - ))} + {navigation.map( + ({ label, href, segment, tag, disabled, limited }) => ( + + ), + )}
@@ -53,6 +62,7 @@ const NavItem: React.FC = ({ segment, tag, disabled, + limited, }) => { const router = useRouter(); // active is true if the segment included in the pathname, but not if it's the root pathname. unless the segment is the root pathname. @@ -75,31 +85,45 @@ const NavItem: React.FC = ({ return (
  • - - {label} - {tag ? ( -
    - {tag} + {limited ? ( + +
    + {label} +
    - ) : null} - +
    + ) : ( + + {label} + {tag ? ( +
    + {tag} +
    + ) : null} + + )}
  • ); }; diff --git a/components/settings/settings-header.tsx b/components/settings/settings-header.tsx index 8d9110000..718472201 100644 --- a/components/settings/settings-header.tsx +++ b/components/settings/settings-header.tsx @@ -10,7 +10,6 @@ export function SettingsHeader() { const { data: features } = useSWR<{ tokens: boolean; incomingWebhooks: boolean; - webhooks: boolean; }>( teamInfo?.currentTeam?.id ? `/api/feature-flags?teamId=${teamInfo.currentTeam.id}` @@ -59,9 +58,9 @@ export function SettingsHeader() { segment: "tags", }, { - label: "Billing", - href: `/settings/billing`, - segment: "billing", + label: "Webhooks", + href: `/settings/webhooks`, + segment: "webhooks", }, { label: "Tokens", @@ -69,18 +68,17 @@ export function SettingsHeader() { segment: "tokens", disabled: !features?.tokens, }, - { - label: "Webhooks", - href: `/settings/webhooks`, - segment: "webhooks", - disabled: !features?.webhooks, - }, { label: "Incoming Webhooks", href: `/settings/incoming-webhooks`, segment: "incoming-webhooks", disabled: !features?.incomingWebhooks, }, + { + label: "Billing", + href: `/settings/billing`, + segment: "billing", + }, ]} /> diff --git a/components/sidebar/app-sidebar.tsx b/components/sidebar/app-sidebar.tsx index 9860f656e..6748c8fef 100644 --- a/components/sidebar/app-sidebar.tsx +++ b/components/sidebar/app-sidebar.tsx @@ -143,6 +143,11 @@ export function AppSidebar({ ...props }: React.ComponentProps) { url: "/settings/domains", current: router.pathname.includes("settings/domains"), }, + { + title: "Webhooks", + url: "/settings/webhooks", + current: router.pathname.includes("settings/webhooks"), + }, { title: "Billing", url: "/settings/billing", diff --git a/components/sidebar/nav-main.tsx b/components/sidebar/nav-main.tsx index bfa5e5bb7..57cdc5be9 100644 --- a/components/sidebar/nav-main.tsx +++ b/components/sidebar/nav-main.tsx @@ -5,6 +5,8 @@ import Link from "next/link"; import { PlanEnum } from "@/ee/stripe/constants"; import { ChevronRight, CrownIcon, type LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + import { Collapsible, CollapsibleContent, @@ -22,8 +24,6 @@ import { SidebarMenuSubItem, } from "@/components/ui/sidebar"; -import { cn } from "@/lib/utils"; - import { UpgradePlanModal } from "../billing/upgrade-plan-modal"; export interface NavItem { diff --git a/ee/limits/constants.ts b/ee/limits/constants.ts index 773cd493c..11100c753 100644 --- a/ee/limits/constants.ts +++ b/ee/limits/constants.ts @@ -64,5 +64,6 @@ export const DATAROOMS_PLUS_PLAN_LIMITS = { datarooms: 1000, customDomainOnPro: true, customDomainInDataroom: true, + conversationsInDataroom: true, advancedLinkControlsOnPro: false, }; diff --git a/pages/settings/webhooks/[id]/index.tsx b/pages/settings/webhooks/[id]/index.tsx index f1a34c1bf..2f8061ec0 100644 --- a/pages/settings/webhooks/[id]/index.tsx +++ b/pages/settings/webhooks/[id]/index.tsx @@ -4,11 +4,11 @@ import { useEffect, useState } from "react"; import { useTeam } from "@/context/team-context"; import { Webhook } from "@prisma/client"; -import { format } from "date-fns"; import { ArrowLeft, Check, Copy, WebhookIcon } from "lucide-react"; import { toast } from "sonner"; import useSWR from "swr"; +import { usePlan } from "@/lib/swr/use-billing"; import { cn, fetcher } from "@/lib/utils"; import AppLayout from "@/components/layouts/app"; @@ -32,23 +32,14 @@ export default function WebhookDetail() { const router = useRouter(); const { id } = router.query; const teamInfo = useTeam(); + const { isFree, isPro } = usePlan(); const teamId = teamInfo?.currentTeam?.id; const [isEditing, setIsEditing] = useState(false); const [isCopied, setIsCopied] = useState(false); - - // Feature flag check - const { data: features } = useSWR<{ webhooks: boolean }>( - teamId ? `/api/feature-flags?teamId=${teamId}` : null, - fetcher, - ); - - // Redirect if feature is not enabled - useEffect(() => { - if (features && !features.webhooks) { - router.push("/settings/general"); - toast.error("This feature is not available for your team"); - } - }, [features, router]); + const [formData, setFormData] = useState({ + name: "", + triggers: [], + }); const { data: webhook, mutate } = useSWR( teamId && id ? `/api/teams/${teamId}/webhooks/${id}` : null, @@ -63,11 +54,6 @@ export default function WebhookDetail() { }, ); - const [formData, setFormData] = useState({ - name: "", - triggers: [], - }); - useEffect(() => { if (webhook) { setFormData({ @@ -78,6 +64,11 @@ export default function WebhookDetail() { }, [webhook]); const handleUpdate = async () => { + if (isFree || isPro) { + toast.error("This feature is not available on your plan"); + return; + } + try { const response = await fetch(`/api/teams/${teamId}/webhooks/${id}`, { method: "PATCH", @@ -346,7 +337,15 @@ export default function WebhookDetail() {
    {isEditing ? (
    - +
    ) : ( - )} diff --git a/pages/settings/webhooks/index.tsx b/pages/settings/webhooks/index.tsx index e1ddcae86..447df4fa9 100644 --- a/pages/settings/webhooks/index.tsx +++ b/pages/settings/webhooks/index.tsx @@ -1,22 +1,18 @@ import Link from "next/link"; -import { useRouter } from "next/router"; - -import { useEffect } from "react"; import { useTeam } from "@/context/team-context"; -import { format } from "date-fns"; -import { CircleHelpIcon, CopyIcon, WebhookIcon } from "lucide-react"; -import { toast } from "sonner"; +import { CircleHelpIcon, WebhookIcon } from "lucide-react"; import useSWR from "swr"; +import { usePlan } from "@/lib/swr/use-billing"; +import { fetcher } from "@/lib/utils"; + +import PlanBadge from "@/components/billing/plan-badge"; import AppLayout from "@/components/layouts/app"; import { SettingsHeader } from "@/components/settings/settings-header"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { BadgeTooltip } from "@/components/ui/tooltip"; -import { fetcher } from "@/lib/utils"; - interface Webhook { id: string; name: string; @@ -25,24 +21,10 @@ interface Webhook { } export default function WebhookSettings() { - const router = useRouter(); const teamInfo = useTeam(); + const { isFree, isPro } = usePlan(); const teamId = teamInfo?.currentTeam?.id; - // Feature flag check - const { data: features } = useSWR<{ webhooks: boolean }>( - teamId ? `/api/feature-flags?teamId=${teamId}` : null, - fetcher, - ); - - // Redirect if feature is not enabled - useEffect(() => { - if (features && !features.webhooks) { - router.push("/settings/general"); - toast.error("This feature is not available for your team"); - } - }, [features, router]); - const { data: webhooks } = useSWR( teamId ? `/api/teams/${teamId}/webhooks` : null, fetcher, @@ -55,8 +37,9 @@ export default function WebhookSettings() {
    -

    - Webhooks +

    + Webhooks{" "} + {isFree || isPro ? : null}

    Send data to external services when events happen in Papermark @@ -65,9 +48,9 @@ export default function WebhookSettings() {

    - + + +
    {/* Webhooks List */} diff --git a/pages/settings/webhooks/new.tsx b/pages/settings/webhooks/new.tsx index 8b92fdc6b..00d23e383 100644 --- a/pages/settings/webhooks/new.tsx +++ b/pages/settings/webhooks/new.tsx @@ -5,13 +5,15 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { useTeam } from "@/context/team-context"; +import { PlanEnum } from "@/ee/stripe/constants"; import { ArrowLeft, Check } from "lucide-react"; import { toast } from "sonner"; -import useSWR from "swr"; +import { z } from "zod"; import { newId } from "@/lib/id-helper"; -import { fetcher } from "@/lib/utils"; +import { usePlan } from "@/lib/swr/use-billing"; +import { UpgradePlanModal } from "@/components/billing/upgrade-plan-modal"; import AppLayout from "@/components/layouts/app"; import { SettingsHeader } from "@/components/settings/settings-header"; import Copy from "@/components/shared/icons/copy"; @@ -60,9 +62,19 @@ export const linkEvents: WebhookEvent[] = [ { id: "link-downloaded", label: "Link Downloaded", value: "link.downloaded" }, ]; +const formSchema = z.object({ + name: z + .string() + .min(3, "Please provide a webhook name with at least 3 characters."), + url: z.string().url("Please enter a valid URL."), + secret: z.string(), + triggers: z.array(z.string()), +}); + export default function NewWebhook() { const router = useRouter(); const teamInfo = useTeam(); + const { isFree, isPro } = usePlan(); const teamId = teamInfo?.currentTeam?.id; const [isLoading, setIsLoading] = useState(false); @@ -79,23 +91,24 @@ export default function NewWebhook() { setFormData((prev) => ({ ...prev, secret: generatedSecret })); }, []); - // Feature flag check - const { data: features } = useSWR<{ webhooks: boolean }>( - teamId ? `/api/feature-flags?teamId=${teamId}` : null, - fetcher, - ); - - // Redirect if feature is not enabled - useEffect(() => { - if (features && !features.webhooks) { - router.push("/settings/general"); - toast.error("This feature is not available for your team"); + const createWebhook = async () => { + if (isFree || isPro) { + toast.error("This feature is not available on your plan"); + return; } - }, [features, router]); - const createWebhook = async () => { try { setIsLoading(true); + const result = formSchema.safeParse(formData); + if (!result.success) { + const errors = result.error.flatten().fieldErrors; + Object.values(errors).forEach((errorMessages) => { + if (errorMessages) { + toast.error(errorMessages[0]); + } + }); + return; + } const response = await fetch(`/api/teams/${teamId}/webhooks`, { method: "POST", headers: { @@ -136,6 +149,7 @@ export default function NewWebhook() {
    { e.preventDefault(); + e.stopPropagation(); createWebhook(); }} className="space-y-8" @@ -152,7 +166,6 @@ export default function NewWebhook() { onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value })) } - required data-1p-ignore autoComplete="off" autoFocus @@ -286,7 +299,6 @@ export default function NewWebhook() { onChange={(e) => setFormData((prev) => ({ ...prev, url: e.target.value })) } - required />
    @@ -326,9 +338,21 @@ export default function NewWebhook() {
    - + {isFree || isPro ? ( + + + + ) : ( + + )}