diff --git a/apps/desktop/src/renderer/components/Paywall/Paywall.tsx b/apps/desktop/src/renderer/components/Paywall/Paywall.tsx new file mode 100644 index 00000000000..60be1481ca0 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/Paywall.tsx @@ -0,0 +1,183 @@ +import { Badge } from "@superset/ui/badge"; +import { Button } from "@superset/ui/button"; +import { Dialog, DialogContent } from "@superset/ui/dialog"; +import { MeshGradient } from "@superset/ui/mesh-gradient"; +import { cn } from "@superset/ui/utils"; +import { useEffect, useState } from "react"; +import { FEATURE_ID_MAP, PRO_FEATURES } from "./constants"; +import type { GatedFeature } from "./usePaywall"; + +type PaywallOptions = { + feature: GatedFeature; + context?: Record; +}; + +let showPaywallFn: ((options: PaywallOptions) => void) | null = null; + +export const Paywall = () => { + const [paywallOptions, setPaywallOptions] = useState( + null, + ); + const [isOpen, setIsOpen] = useState(false); + + showPaywallFn = (options: PaywallOptions) => { + setPaywallOptions(options); + setIsOpen(true); + }; + + useEffect(() => { + return () => { + showPaywallFn = null; + }; + }, []); + + const initialFeatureId = + (paywallOptions?.feature && FEATURE_ID_MAP[paywallOptions.feature]) || + PRO_FEATURES[0]?.id || + "team-collaboration"; + + const [selectedFeatureId, setSelectedFeatureId] = + useState(initialFeatureId); + + useEffect(() => { + if (paywallOptions?.feature && isOpen) { + const mappedId = + FEATURE_ID_MAP[paywallOptions.feature] || PRO_FEATURES[0]?.id; + if (mappedId) { + setSelectedFeatureId(mappedId); + } + } + }, [paywallOptions?.feature, isOpen]); + + const handleOpenChange = (open: boolean) => { + if (!open) { + setIsOpen(false); + } + }; + + const selectedFeature = + PRO_FEATURES.find((f) => f.id === selectedFeatureId) || PRO_FEATURES[0]; + + if (!selectedFeature) { + return null; + } + + const handleUpgrade = () => { + setIsOpen(false); + }; + + return ( + + +
+
+
+

+ Pro Features +

+
+ +
+ {PRO_FEATURES.map((proFeature) => { + const Icon = proFeature.icon; + const isSelected = selectedFeatureId === proFeature.id; + + return ( + + ); + })} +
+
+ +
+
+ {PRO_FEATURES.map((proFeature) => ( +
+ +
+ ))} + +
+ +
+
+ +
+
+ + {selectedFeature.title} + + PRO +
+ + {selectedFeature.description} + +
+
+
+ +
+ + +
+
+
+ ); +}; + +export const paywall = ( + feature: GatedFeature, + context?: Record, +) => { + if (!showPaywallFn) { + console.error( + "[paywall] Paywall not mounted. Make sure to render in your app", + ); + return; + } + showPaywallFn({ feature, context }); +}; diff --git a/apps/desktop/src/renderer/components/Paywall/constants.ts b/apps/desktop/src/renderer/components/Paywall/constants.ts new file mode 100644 index 00000000000..2beb0888945 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/constants.ts @@ -0,0 +1,72 @@ +import type { IconType } from "react-icons"; +import { HiUsers } from "react-icons/hi2"; +import { IoSparkles, IoTerminal } from "react-icons/io5"; +import { MdWorkspaces } from "react-icons/md"; +import { RiRocketLine } from "react-icons/ri"; +import type { GatedFeature } from "./usePaywall"; +import { GATED_FEATURES } from "./usePaywall"; + +export interface ProFeature { + id: string; + title: string; + description: string; + icon: IconType; + iconColor: string; + gradientColors: readonly [string, string, string, string]; +} + +export const PRO_FEATURES: ProFeature[] = [ + { + id: "team-collaboration", + title: "Team Collaboration", + description: + "Invite your team to shared workspaces. See real-time updates, sync configurations, and manage team access across agents.", + icon: HiUsers, + iconColor: "text-blue-500", + gradientColors: ["#1e40af", "#1e3a8a", "#172554", "#1a1a2e"], + }, + { + id: "ai-features", + title: "AI-Powered Features", + description: + "Enhanced AI agent capabilities with context-aware completions, automated workflow suggestions, and intelligent terminal assistance.", + icon: IoSparkles, + iconColor: "text-purple-500", + gradientColors: ["#6b21a8", "#581c87", "#3b0764", "#1a1a2e"], + }, + { + id: "advanced-terminal", + title: "Advanced Terminal", + description: + "Split your terminal into multiple panes for parallel execution. Session persistence, custom themes, and comprehensive command history search.", + icon: IoTerminal, + iconColor: "text-green-500", + gradientColors: ["#047857", "#065f46", "#064e3b", "#1a1a2e"], + }, + { + id: "unlimited-workspaces", + title: "Unlimited Workspaces", + description: + "Create as many workspaces and worktrees as you need. Organize complex multi-agent workflows without hitting limits.", + icon: MdWorkspaces, + iconColor: "text-orange-500", + gradientColors: ["#b45309", "#92400e", "#78350f", "#1a1a2e"], + }, + { + id: "priority-support", + title: "Priority Support", + description: + "Priority email support from the Superset team. Early access to new Pro features and beta releases.", + icon: RiRocketLine, + iconColor: "text-red-500", + gradientColors: ["#7f1d1d", "#991b1b", "#450a0a", "#1a1a2e"], + }, +]; + +// Map gated feature IDs to the feature to highlight in the paywall dialog +export const FEATURE_ID_MAP: Record = { + [GATED_FEATURES.INVITE_MEMBERS]: "team-collaboration", + [GATED_FEATURES.AI_COMPLETION]: "ai-features", + [GATED_FEATURES.SPLIT_TERMINAL]: "advanced-terminal", + [GATED_FEATURES.CREATE_WORKSPACE]: "unlimited-workspaces", +}; diff --git a/apps/desktop/src/renderer/components/Paywall/index.ts b/apps/desktop/src/renderer/components/Paywall/index.ts new file mode 100644 index 00000000000..519461e781e --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/index.ts @@ -0,0 +1,3 @@ +export { Paywall, paywall } from "./Paywall"; +export type { GatedFeature } from "./usePaywall"; +export { GATED_FEATURES, usePaywall } from "./usePaywall"; diff --git a/apps/desktop/src/renderer/components/Paywall/usePaywall.ts b/apps/desktop/src/renderer/components/Paywall/usePaywall.ts new file mode 100644 index 00000000000..65bcfc5fec5 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/usePaywall.ts @@ -0,0 +1,52 @@ +import { authClient } from "renderer/lib/auth-client"; +import { paywall } from "./Paywall"; + +type UserPlan = "free" | "pro"; + +export const GATED_FEATURES = { + INVITE_MEMBERS: "invite-members", + AI_COMPLETION: "ai-completion", + SPLIT_TERMINAL: "split-terminal", + CREATE_WORKSPACE: "create-workspace", +} as const; + +export type GatedFeature = (typeof GATED_FEATURES)[keyof typeof GATED_FEATURES]; + +export function usePaywall() { + const { data: session } = authClient.useSession(); + + const userPlan: UserPlan = "free"; + + function hasAccess(feature: GatedFeature): boolean { + void feature; + return userPlan === "pro"; + } + + function gateFeature( + feature: GatedFeature, + callback: () => void | Promise, + context?: Record, + ): void { + if (hasAccess(feature)) { + const result = callback(); + if (result instanceof Promise) { + result.catch((error) => { + console.error(`[paywall] Callback error for ${feature}:`, error); + }); + } + } else { + const trackingContext = { + organizationId: session?.session?.activeOrganizationId, + userPlan, + ...context, + }; + paywall(feature, trackingContext); + } + } + + return { + hasAccess, + gateFeature, + userPlan, + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx index 39de8599172..0e88ad9a26d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx @@ -6,6 +6,7 @@ import { } from "@tanstack/react-router"; import { DndProvider } from "react-dnd"; import { NewWorkspaceModal } from "renderer/components/NewWorkspaceModal"; +import { Paywall } from "renderer/components/Paywall"; import { useUpdateListener } from "renderer/components/UpdateToast"; import { authClient } from "renderer/lib/auth-client"; import { dragDropManager } from "renderer/lib/dnd"; @@ -72,6 +73,7 @@ function AuthenticatedLayout() { + ); diff --git a/apps/desktop/src/types/stripe-gradient.d.ts b/apps/desktop/src/types/stripe-gradient.d.ts new file mode 100644 index 00000000000..fb2a11254a4 --- /dev/null +++ b/apps/desktop/src/types/stripe-gradient.d.ts @@ -0,0 +1,18 @@ +declare module "stripe-gradient" { + export class Gradient { + constructor(); + initGradient(selector: string): void; + play(): void; + pause(): void; + disconnect(): void; + } + + export class MiniGl { + constructor( + canvas: HTMLCanvasElement, + width?: number, + height?: number, + debug?: boolean, + ); + } +}