+
{
+ {/* Comparison Table (Cap Pro vs Desktop License) */}
+
+
+
+
+ {/* Enterprise Card */}
-
+
+
+
+
+
+ Cap for Enterprise
+
+
+ Deploy Cap across your organization with enterprise-grade
+ features, dedicated support, and custom integrations.
+
+
+
+
+
+
+
+
+
+
+ SLAs & Priority Support
+
+
+
+ Loom Video Importer
+
+
+
+ Bulk Discounts
+
+
+
+ Self-hosting Support
+
+
+
+ SAML SSO Login
+
+
+
+
+ Advanced Security Controls
+
+
+
+
+
-
+
+
+
+
+
-
-
-
-
-
+
);
diff --git a/apps/web/components/pages/_components/ComparePlans.tsx b/apps/web/components/pages/_components/ComparePlans.tsx
new file mode 100644
index 000000000..51e741a09
--- /dev/null
+++ b/apps/web/components/pages/_components/ComparePlans.tsx
@@ -0,0 +1,412 @@
+"use client";
+
+import { Button } from "@cap/ui";
+import { getProPlanId, userIsPro } from "@cap/utils";
+import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { clsx } from "clsx";
+import { use, useMemo, useRef, useState } from "react";
+import { toast } from "sonner";
+import { useAuthContext } from "@/app/Layout/AuthContext";
+import {
+ CommercialArt,
+ CommercialArtRef,
+} from "../HomePage/Pricing/CommercialArt";
+import { ProArt, ProArtRef } from "../HomePage/Pricing/ProArt";
+
+const COLUMN_WIDTH = "min-w-[200px]";
+
+type Plan = {
+ name: string;
+ price: string;
+ href?: string;
+ disabled?: boolean;
+};
+
+const getButtonText = (planName: string): string => {
+ switch (planName) {
+ case "Free":
+ return "Download for free";
+ case "Desktop License":
+ return "Get Desktop License";
+ case "Cap Pro":
+ return "Get started";
+ default:
+ return "Get started";
+ }
+};
+
+const getButtonVariant = (planName: string) => {
+ switch (planName) {
+ case "Free":
+ return "gray";
+ case "Desktop License":
+ return "dark";
+ case "Cap Pro":
+ return "blue";
+ default:
+ return "gray";
+ }
+};
+
+// Icon component renderer
+const PlanIcon = ({
+ planName,
+ commercialArtRef,
+ proArtRef,
+}: {
+ planName: string;
+ commercialArtRef: React.RefObject
;
+ proArtRef: React.RefObject;
+}) => {
+ if (planName === "Desktop License") {
+ return (
+ commercialArtRef.current?.playHoverAnimation()}
+ onMouseLeave={() => commercialArtRef.current?.playDefaultAnimation()}
+ className="w-[90px] h-[80px]"
+ >
+
+
+ );
+ }
+
+ if (planName === "Cap Pro") {
+ return (
+ proArtRef.current?.playHoverAnimation()}
+ onMouseLeave={() => proArtRef.current?.playDefaultAnimation()}
+ className="w-[90px] ml-2 h-[80px]"
+ >
+
+
+ );
+ }
+
+ return null;
+};
+
+export const ComparePlans = () => {
+ const commercialArtRef = useRef(null);
+ const proArtRef = useRef(null);
+ const auth = use(useAuthContext().user);
+ const [proLoading, setProLoading] = useState(false);
+ const [guestLoading, setGuestLoading] = useState(false);
+ const [commercialLoading, setCommercialLoading] = useState(false);
+
+ // Check if user is already pro or any loading state is active
+ const isDisabled = useMemo(
+ () =>
+ (auth?.email && userIsPro(auth)) ||
+ proLoading ||
+ guestLoading ||
+ commercialLoading,
+ [auth, proLoading, guestLoading, commercialLoading],
+ );
+
+ const plans: Plan[] = useMemo(
+ () => [
+ {
+ name: "Free",
+ price: "$0 forever",
+ href: "/login",
+ disabled: isDisabled,
+ },
+ {
+ name: "Desktop License",
+ price: "$58 /lifetime or $29 /year",
+ disabled: isDisabled,
+ },
+ {
+ name: "Cap Pro",
+ price:
+ "$8.16 /mo per user, billed annually or $12 /mo per user, billed monthly",
+ disabled: isDisabled,
+ },
+ ],
+ [isDisabled],
+ );
+ // Feature comparison data
+ const rows = useMemo(
+ () => [
+ { label: "Unlimited recordings", free: false, desktop: true, pro: true },
+ { label: "Commercial usage", free: false, desktop: true, pro: true },
+ {
+ label: "Studio Mode with full editor",
+ free: true,
+ desktop: true,
+ pro: true,
+ },
+ { label: "Export to any format", free: true, desktop: true, pro: true },
+ { label: "4K export", free: true, desktop: true, pro: true },
+ {
+ label: "Cloud storage & bandwidth",
+ free: false,
+ desktop: false,
+ pro: "Unlimited",
+ },
+ {
+ label: "Auto-generated titles & summaries",
+ free: false,
+ desktop: false,
+ pro: true,
+ },
+ {
+ label: "Clickable chapters & transcriptions",
+ free: false,
+ desktop: false,
+ pro: true,
+ },
+ {
+ label: "Custom domain (cap.yourdomain.com)",
+ free: false,
+ desktop: false,
+ pro: true,
+ },
+ {
+ label: "Password protected shares",
+ free: false,
+ desktop: false,
+ pro: true,
+ },
+ {
+ label: "Viewer analytics & engagement",
+ free: false,
+ desktop: false,
+ pro: true,
+ },
+ { label: "Team workspaces", free: true, desktop: true, pro: true },
+ {
+ label: "Custom S3 bucket support",
+ free: false,
+ desktop: false,
+ pro: true,
+ },
+ { label: "Priority support", free: false, desktop: false, pro: true },
+ {
+ label: "Early features access",
+ free: false,
+ desktop: false,
+ pro: true,
+ },
+ { label: "Community support", free: true, desktop: true, pro: true },
+ {
+ label: "License type",
+ free: "Free",
+ desktop: "Perpetual (single device)",
+ pro: "Subscription",
+ },
+ ],
+ [],
+ );
+
+ // Generic checkout handler with error handling
+ const handleCheckout = async (
+ url: string,
+ body: Record,
+ setLoading: (loading: boolean) => void,
+ errorMessage: string,
+ ) => {
+ setLoading(true);
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+
+ const data = await response.json();
+
+ if (response.ok && data.url) {
+ window.location.href = data.url;
+ } else {
+ throw new Error(data.message || errorMessage);
+ }
+ } catch (error) {
+ console.error("Checkout error:", error);
+ toast.error(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const guestCheckout = (planId: string) =>
+ handleCheckout(
+ "/api/settings/billing/guest-checkout",
+ { priceId: planId, quantity: 1 },
+ setGuestLoading,
+ "Failed to create checkout session",
+ );
+
+ const openCommercialCheckout = () =>
+ handleCheckout(
+ "/api/commercial/checkout",
+ { type: "lifetime", quantity: 1 },
+ setCommercialLoading,
+ "Failed to start checkout process",
+ );
+
+ const planCheckout = async (planId?: string) => {
+ const finalPlanId = planId || getProPlanId("yearly");
+ setProLoading(true);
+
+ try {
+ const response = await fetch("/api/settings/billing/subscribe", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ priceId: finalPlanId, quantity: 1 }),
+ });
+
+ const data = await response.json();
+
+ if (data.auth === false) {
+ // User not authenticated, do guest checkout
+ setProLoading(false);
+ await guestCheckout(finalPlanId);
+ return;
+ }
+
+ if (data.subscription === true) {
+ toast.success("You are already on the Cap Pro plan");
+ return;
+ }
+
+ if (data.url) {
+ window.location.href = data.url;
+ }
+ } catch (error) {
+ console.error("Plan checkout error:", error);
+ toast.error("Failed to start subscription process");
+ } finally {
+ setProLoading(false);
+ }
+ };
+
+ const renderCell = (value: boolean | string) => {
+ if (typeof value === "boolean") {
+ return value ? (
+
+ ) : (
+ —
+ );
+ }
+ return {value};
+ };
+ return (
+
+
Compare plans
+
+
+
+
+
+ |
+ {plans.map((plan) => (
+
+
+
+
+
+ {plan.name}
+
+
+ {plan.price}
+
+
+
+ |
+ ))}
+
+
+
+ .
+ |
+ {plans.map((plan) => (
+
+
+
+
+ |
+ ))}
+
+
+
+ {rows.map((row) => (
+
+
+ {row.label}
+ |
+
+ {renderCell(row.free)}
+ |
+
+ {renderCell(row.desktop)}
+ |
+
+ {renderCell(row.pro)}
+ |
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default ComparePlans;
diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx
index 5bf6c7938..04eb65b15 100644
--- a/packages/ui/src/components/Button.tsx
+++ b/packages/ui/src/components/Button.tsx
@@ -15,7 +15,7 @@ const buttonVariants = cva(
variant: {
primary:
"bg-gray-12 dark-button-shadow text-gray-1 disabled:bg-gray-6 disabled:text-gray-9",
- blue: "bg-blue-600 text-white border border-blue-800 shadow-[0_1.50px_0_0_rgba(255,255,255,0.20)_inset] hover:bg-blue-700 disabled:bg-gray-6 disabled:text-gray-9",
+ blue: "bg-blue-600 text-white disabled:border-gray-8 border border-blue-800 shadow-[0_1.50px_0_0_rgba(255,255,255,0.20)_inset] hover:bg-blue-700 disabled:bg-gray-7 disabled:text-gray-10",
destructive:
"bg-red-500 text-white hover:bg-red-600 disabled:bg-red-200",
outline: