Paywalling

This commit is contained in:
Owen
2026-04-17 15:14:01 -07:00
parent 408eaf55f6
commit f74791111e
9 changed files with 418 additions and 83 deletions

View File

@@ -4,7 +4,9 @@ import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGra
import { apiResponseToFormValues } from "@app/lib/alertRuleForm";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
@@ -21,6 +23,8 @@ export default function EditAlertRulePage() {
const alertRuleId = parseInt(ruleIdParam, 10);
const api = createApiClient(useEnvContext());
const { isPaidUser } = usePaidStatus();
const isPaid = isPaidUser(tierMatrix.alertingRules);
const [formValues, setFormValues] = useState<AlertRuleFormValues | null | undefined>(undefined);
@@ -73,6 +77,7 @@ export default function EditAlertRulePage() {
alertRuleId={alertRuleId}
initialValues={formValues}
isNew={false}
disabled={!isPaid}
/>
);
}

View File

@@ -2,17 +2,22 @@
import AlertRuleGraphEditor from "@app/components/alert-rule-editor/AlertRuleGraphEditor";
import { defaultFormValues } from "@app/lib/alertRuleForm";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { useParams } from "next/navigation";
export default function NewAlertRulePage() {
const params = useParams();
const orgId = params.orgId as string;
const { isPaidUser } = usePaidStatus();
const isPaid = isPaidUser(tierMatrix.alertingRules);
return (
<AlertRuleGraphEditor
orgId={orgId}
initialValues={defaultFormValues()}
isNew
disabled={!isPaid}
/>
);
}

View File

@@ -22,7 +22,8 @@ import {
} from "@app/components/Credenza";
import { Button } from "@app/components/ui/button";
import { Switch } from "@app/components/ui/switch";
import { Globe, MoreHorizontal, Plus } from "lucide-react";
import { Globe, MoreHorizontal, Plus, ExternalLink, KeyRound } from "lucide-react";
import Link from "next/link";
import { AxiosResponse } from "axios";
import { build } from "@server/build";
import Image from "next/image";
@@ -181,6 +182,65 @@ interface DestinationTypePickerProps {
isPaywalled?: boolean;
}
const BOOK_A_DEMO_URL = "https://click.fossorial.io/ep922";
const CONTACT_URL = "https://pangolin.net/contact";
function ContactSalesDialog({
open,
onOpenChange
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const t = useTranslations();
return (
<Credenza open={open} onOpenChange={onOpenChange}>
<CredenzaContent className="sm:max-w-md">
<CredenzaHeader>
<CredenzaTitle>{t("streamingAddDestination")}</CredenzaTitle>
</CredenzaHeader>
<CredenzaBody>
<div className="mb-2 rounded-md border border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
<div className="py-3 px-4">
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
<KeyRound className="size-4 shrink-0 text-black-500" />
<span>
Contact sales to enable this feature.{" "}
<Link
href={BOOK_A_DEMO_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-black-600 underline"
>
Book a demo
<ExternalLink className="size-3.5 shrink-0" />
</Link>
{" or "}
<Link
href={CONTACT_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-black-600 underline"
>
contact us
<ExternalLink className="size-3.5 shrink-0" />
</Link>
.
</span>
</div>
</div>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("cancel")}</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
}
function DestinationTypePicker({
open,
onOpenChange,
@@ -189,6 +249,17 @@ function DestinationTypePicker({
}: DestinationTypePickerProps) {
const t = useTranslations();
const [selected, setSelected] = useState<DestinationType>("http");
const [contactSalesOpen, setContactSalesOpen] = useState(false);
const ENTERPRISE_ONLY_TYPES: DestinationType[] = ["s3", "datadog"];
function handleOptionSelect(type: DestinationType) {
if (ENTERPRISE_ONLY_TYPES.includes(type)) {
setContactSalesOpen(true);
} else {
onSelect(type);
}
}
const destinationTypeOptions: ReadonlyArray<
StrategyOption<DestinationType>
@@ -203,7 +274,6 @@ function DestinationTypePicker({
id: "s3",
title: t("streamingS3Title"),
description: t("streamingS3Description"),
disabled: true,
icon: (
<Image
src="/third-party/s3.png"
@@ -218,7 +288,6 @@ function DestinationTypePicker({
id: "datadog",
title: t("streamingDatadogTitle"),
description: t("streamingDatadogDescription"),
disabled: true,
icon: (
<Image
src="/third-party/dd.png"
@@ -236,6 +305,11 @@ function DestinationTypePicker({
}, [open]);
return (
<>
<ContactSalesDialog
open={contactSalesOpen}
onOpenChange={setContactSalesOpen}
/>
<Credenza open={open} onOpenChange={onOpenChange}>
<CredenzaContent className="sm:max-w-lg">
<CredenzaHeader>
@@ -255,7 +329,12 @@ function DestinationTypePicker({
<StrategySelect
options={destinationTypeOptions}
value={selected}
onChange={setSelected}
onChange={(type) => {
setSelected(type);
if (ENTERPRISE_ONLY_TYPES.includes(type)) {
setContactSalesOpen(true);
}
}}
cols={1}
/>
</div>
@@ -265,7 +344,7 @@ function DestinationTypePicker({
<Button variant="outline">{t("cancel")}</Button>
</CredenzaClose>
<Button
onClick={() => onSelect(selected)}
onClick={() => handleOptionSelect(selected)}
disabled={isPaywalled}
>
{t("continue")}
@@ -273,6 +352,7 @@ function DestinationTypePicker({
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}