diff --git a/apps/web/components/icons/QuestionMarkIcon.tsx b/apps/web/components/icons/QuestionMarkIcon.tsx new file mode 100644 index 000000000..ca49b1ee5 --- /dev/null +++ b/apps/web/components/icons/QuestionMarkIcon.tsx @@ -0,0 +1,23 @@ +interface QuestionMarkIconProps { + className?: string; +} + +export const QuestionMarkIcon = ({ className }: QuestionMarkIconProps) => { + return ( + + + + + ); +}; diff --git a/apps/web/components/pages/HomePage/Pricing/CommercialArt.tsx b/apps/web/components/pages/HomePage/Pricing/CommercialArt.tsx index 1957b4210..5a1f427a1 100644 --- a/apps/web/components/pages/HomePage/Pricing/CommercialArt.tsx +++ b/apps/web/components/pages/HomePage/Pricing/CommercialArt.tsx @@ -1,4 +1,5 @@ import { Fit, Layout, useRive } from "@rive-app/react-canvas"; +import clsx from "clsx"; import { forwardRef, memo, useImperativeHandle } from "react"; export interface CommercialArtRef { @@ -6,8 +7,12 @@ export interface CommercialArtRef { playDefaultAnimation: () => void; } +interface Props { + className?: string; +} + export const CommercialArt = memo( - forwardRef((_, ref) => { + forwardRef((props, ref) => { const { rive, RiveComponent: CommercialRive } = useRive({ src: "/rive/pricing.riv", artboard: "commercial", @@ -34,7 +39,12 @@ export const CommercialArt = memo( })); return ( - + ); }), ); diff --git a/apps/web/components/pages/HomePage/Pricing/CommercialCard.tsx b/apps/web/components/pages/HomePage/Pricing/CommercialCard.tsx index fe2ad1374..f0e5b4ac6 100644 --- a/apps/web/components/pages/HomePage/Pricing/CommercialCard.tsx +++ b/apps/web/components/pages/HomePage/Pricing/CommercialCard.tsx @@ -1,11 +1,18 @@ import { Button, Switch } from "@cap/ui"; -import { faCheck, faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { + faBriefcase, + faDownload, + faMinus, + faPlus, + faVideo, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import NumberFlow from "@number-flow/react"; import clsx from "clsx"; import { useRef, useState } from "react"; import { toast } from "sonner"; import { homepageCopy } from "../../../../data/homepage-copy"; +import { QuestionMarkIcon } from "../../../icons/QuestionMarkIcon"; import { CommercialArt, type CommercialArtRef } from "./CommercialArt"; export const CommercialCard = () => { @@ -64,18 +71,18 @@ export const CommercialCard = () => { className="flex flex-col flex-1 justify-between p-8 rounded-2xl border shadow-lg bg-gray-1 border-gray-5" >
-
+
-

+

{homepageCopy.pricing.commercial.title}

-

+

{homepageCopy.pricing.commercial.description}

-
+
Learn more about the commercial license here @@ -83,15 +90,15 @@ export const CommercialCard = () => {
- + $ - + {" "} / {billingCycleText} {isYearly ? ( -

+

or, $ { one-time payment

) : ( -

+

or, $ {" "} / year @@ -163,21 +170,43 @@ export const CommercialCard = () => {

+
-
    - {homepageCopy.pricing.commercial.features.map((feature) => ( -
  • +
diff --git a/apps/web/components/pages/HomePage/Pricing/ProArt.tsx b/apps/web/components/pages/HomePage/Pricing/ProArt.tsx index a622cb9e5..c9588e377 100644 --- a/apps/web/components/pages/HomePage/Pricing/ProArt.tsx +++ b/apps/web/components/pages/HomePage/Pricing/ProArt.tsx @@ -40,10 +40,7 @@ export const ProArt = memo( return ( ); }), diff --git a/apps/web/components/pages/HomePage/Pricing/ProCard.tsx b/apps/web/components/pages/HomePage/Pricing/ProCard.tsx index 68846d399..41a1a869f 100644 --- a/apps/web/components/pages/HomePage/Pricing/ProCard.tsx +++ b/apps/web/components/pages/HomePage/Pricing/ProCard.tsx @@ -1,10 +1,16 @@ import { Button, Switch } from "@cap/ui"; import { getProPlanId } from "@cap/utils"; -import { faCheck, faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { + faCloud, + faLink, + faMagic, + faMinus, + faPlus, + faUsers, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import NumberFlow from "@number-flow/react"; import clsx from "clsx"; -import { useRouter } from "next/navigation"; import { useRef, useState } from "react"; import { toast } from "sonner"; import { homepageCopy } from "../../../../data/homepage-copy"; @@ -16,7 +22,6 @@ export const ProCard = () => { const [proLoading, setProLoading] = useState(false); const [guestLoading, setGuestLoading] = useState(false); const proArtRef = useRef(null); - const { push } = useRouter(); const CAP_PRO_ANNUAL_PRICE_PER_USER = homepageCopy.pricing.pro.pricing.annual; const CAP_PRO_MONTHLY_PRICE_PER_USER = @@ -109,30 +114,30 @@ export const ProCard = () => { {homepageCopy.pricing.pro.badge} -
+
-

+

{homepageCopy.pricing.pro.title}

-

+

{homepageCopy.pricing.pro.description}

- + $ - + {" "} {billingCycleTextPro} {isAnnually ? ( -

+

or,{" "} { ) : ( <> for{" "} - {" "} + {" "} users,{" "} )} billed monthly

) : ( -

+

or,{" "} { ) : ( <> for{" "} - {" "} + {" "} users,{" "} )} @@ -237,14 +242,48 @@ export const ProCard = () => {

+ -
    - {homepageCopy.pricing.pro.features.map((feature) => ( -
  • - - {feature} -
  • - ))} +
    +
      +
    • + + + Unlimited cloud storage & shareable links + +
    • +
    • + + + Automatic AI title, transcription, summary, and chapter generation + +
    • +
    • + + + Connect a custom domain, e.g. cap.yourdomain.com + +
    • +
    • + + Shared team spaces +
    diff --git a/apps/web/components/pages/PricingPage.tsx b/apps/web/components/pages/PricingPage.tsx index 5839ae8bf..88b2347c7 100644 --- a/apps/web/components/pages/PricingPage.tsx +++ b/apps/web/components/pages/PricingPage.tsx @@ -1,9 +1,18 @@ "use client"; -import { faHeart } from "@fortawesome/free-solid-svg-icons"; +import { Button } from "@cap/ui"; +import { + faDownload, + faHandshake, + faHeart, + faServer, + faShield, + faUsers, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { motion } from "framer-motion"; import { Testimonials } from "../ui/Testimonials"; +import ComparePlans from "./_components/ComparePlans"; import Faq from "./HomePage/Faq"; import { CommercialCard, ProCard } from "./HomePage/Pricing"; @@ -62,7 +71,7 @@ export const PricingPage = () => { return ( -
    +
    {
    + {/* 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 + +
    +
    +
    +
    - +
    + +
    + +
    - - - - Cap vs Competitors Table - +
    ); 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) => ( + + ))} + + + + {rows.map((row) => ( + + + + + + + ))} + +
    + {plans.map((plan) => ( + +
    +
    + +
    +
    +

    + {plan.name} +

    +

    + {plan.price} +

    +
    +
    +
    + . + +
    + +
    +
    + {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: