diff --git a/apps/desktop/src/renderer/components/Paywall/Paywall.tsx b/apps/desktop/src/renderer/components/Paywall/Paywall.tsx index 60be1481ca0..689a0cdac3d 100644 --- a/apps/desktop/src/renderer/components/Paywall/Paywall.tsx +++ b/apps/desktop/src/renderer/components/Paywall/Paywall.tsx @@ -1,9 +1,10 @@ -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 { useNavigate } from "@tanstack/react-router"; +import { useEffect, useRef, useState } from "react"; +import { posthog } from "../../lib/posthog"; +import { FeaturePreview } from "./components/FeaturePreview"; +import { FeatureSidebar } from "./components/FeatureSidebar"; import { FEATURE_ID_MAP, PRO_FEATURES } from "./constants"; import type { GatedFeature } from "./usePaywall"; @@ -15,10 +16,13 @@ type PaywallOptions = { let showPaywallFn: ((options: PaywallOptions) => void) | null = null; export const Paywall = () => { + const navigate = useNavigate(); const [paywallOptions, setPaywallOptions] = useState( null, ); const [isOpen, setIsOpen] = useState(false); + const openTimeRef = useRef(null); + const featuresViewedRef = useRef>(new Set()); showPaywallFn = (options: PaywallOptions) => { setPaywallOptions(options); @@ -31,14 +35,30 @@ export const Paywall = () => { }; }, []); + const triggerSource = paywallOptions?.feature; const initialFeatureId = - (paywallOptions?.feature && FEATURE_ID_MAP[paywallOptions.feature]) || + (triggerSource && FEATURE_ID_MAP[triggerSource]) || PRO_FEATURES[0]?.id || "team-collaboration"; const [selectedFeatureId, setSelectedFeatureId] = useState(initialFeatureId); + // Track paywall_opened when modal opens + useEffect(() => { + if (isOpen && paywallOptions) { + openTimeRef.current = Date.now(); + featuresViewedRef.current = new Set([initialFeatureId]); + + const feature = PRO_FEATURES.find((f) => f.id === initialFeatureId); + posthog.capture("paywall_opened", { + trigger_source: paywallOptions.feature, + feature_id: initialFeatureId, + feature_title: feature?.title, + }); + } + }, [isOpen, paywallOptions, initialFeatureId]); + useEffect(() => { if (paywallOptions?.feature && isOpen) { const mappedId = @@ -49,8 +69,31 @@ export const Paywall = () => { } }, [paywallOptions?.feature, isOpen]); + const handleSelectFeature = (featureId: string) => { + if (featureId !== selectedFeatureId) { + const feature = PRO_FEATURES.find((f) => f.id === featureId); + posthog.capture("paywall_feature_clicked", { + trigger_source: triggerSource, + feature_id: featureId, + feature_title: feature?.title, + previous_feature_id: selectedFeatureId, + }); + featuresViewedRef.current.add(featureId); + } + setSelectedFeatureId(featureId); + }; + const handleOpenChange = (open: boolean) => { if (!open) { + const timeSpent = openTimeRef.current + ? Date.now() - openTimeRef.current + : 0; + posthog.capture("paywall_cancelled", { + trigger_source: triggerSource, + feature_id: selectedFeatureId, + features_viewed_count: featuresViewedRef.current.size, + time_spent_ms: timeSpent, + }); setIsOpen(false); } }; @@ -63,103 +106,37 @@ export const Paywall = () => { } const handleUpgrade = () => { + const timeSpent = openTimeRef.current + ? Date.now() - openTimeRef.current + : 0; + posthog.capture("paywall_upgrade_clicked", { + trigger_source: triggerSource, + feature_id: selectedFeatureId, + feature_title: selectedFeature.title, + features_viewed_count: featuresViewedRef.current.size, + time_spent_ms: timeSpent, + }); setIsOpen(false); + navigate({ to: "/settings/billing/plans" }); }; 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} - -
-
+ +
- diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/FeaturePreview.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/FeaturePreview.tsx new file mode 100644 index 00000000000..3184d93e276 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/FeaturePreview.tsx @@ -0,0 +1,71 @@ +import { Badge } from "@superset/ui/badge"; +import { MeshGradient } from "@superset/ui/mesh-gradient"; +import { cn } from "@superset/ui/utils"; +import type { ComponentType } from "react"; +import type { ProFeature } from "../../constants"; +import { PRO_FEATURES } from "../../constants"; +import { CloudWorkspacesDemo } from "./components/CloudWorkspacesDemo"; +import { IntegrationsDemo } from "./components/IntegrationsDemo"; +import { MobileAppDemo } from "./components/MobileAppDemo"; +import { TasksDemo } from "./components/TasksDemo"; +import { TeamCollaborationDemo } from "./components/TeamCollaborationDemo"; + +const DEMO_COMPONENTS: Record = { + "team-collaboration": TeamCollaborationDemo, + integrations: IntegrationsDemo, + tasks: TasksDemo, + "cloud-workspaces": CloudWorkspacesDemo, + "mobile-app": MobileAppDemo, +}; + +interface FeaturePreviewProps { + selectedFeature: ProFeature; +} + +export function FeaturePreview({ selectedFeature }: FeaturePreviewProps) { + const DemoComponent = DEMO_COMPONENTS[selectedFeature.id]; + + return ( +
+
+ {PRO_FEATURES.map((proFeature) => ( +
+ +
+ ))} + +
+ {DemoComponent ? : null} +
+
+ +
+
+ + {selectedFeature.title} + + PRO + {selectedFeature.comingSoon && ( + + Coming Soon + + )} +
+ + {selectedFeature.description} + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/CloudWorkspacesDemo.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/CloudWorkspacesDemo.tsx new file mode 100644 index 00000000000..e81ca36f62a --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/CloudWorkspacesDemo.tsx @@ -0,0 +1,85 @@ +import { + HiArrowPath, + HiCloud, + HiComputerDesktop, + HiDeviceTablet, +} from "react-icons/hi2"; + +const WORKSPACES = [ + { id: "1", name: "superset-app", branch: "main", synced: true }, + { id: "2", name: "api-server", branch: "feature/auth", synced: true }, + { id: "3", name: "mobile-app", branch: "dev", synced: false }, +]; + +export function CloudWorkspacesDemo() { + return ( +
+
+ {/* Header */} +
+
+
+
+
+
+
+ Cloud +
+
+ + {/* Cloud sync visual */} +
+
+
+ + Desktop +
+
+
+
+ +
+
+
+
+
+ +
+ Cloud +
+
+
+
+ +
+
+
+
+ + Tablet +
+
+ + {/* Synced workspaces */} +
+ Synced Workspaces +
+
+ {WORKSPACES.map((ws) => ( +
+
+ {ws.name} + {ws.branch} +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/index.ts b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/index.ts new file mode 100644 index 00000000000..a9c31d55875 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/index.ts @@ -0,0 +1 @@ +export { CloudWorkspacesDemo } from "./CloudWorkspacesDemo"; diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx new file mode 100644 index 00000000000..0d32f993a52 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx @@ -0,0 +1,76 @@ +import { HiArrowPath, HiCheck } from "react-icons/hi2"; +import { SiGithub, SiLinear } from "react-icons/si"; + +const SYNCED_ITEMS = [ + { id: "1", type: "issue", name: "SUP-142: Fix auth flow", status: "synced" }, + { id: "2", type: "pr", name: "PR #89: Add workspace sync", status: "synced" }, + { + id: "3", + type: "issue", + name: "SUP-156: Mobile responsive", + status: "syncing", + }, +]; + +export function IntegrationsDemo() { + return ( +
+
+ {/* Header */} +
+
+
+
+
+
+
+ Integrations +
+
+ + {/* Connected services */} +
+
+
+
+ +
+ Linear +
+
+
+ +
+
+
+
+ +
+ GitHub +
+
+ + {/* Synced items */} +
+ Synced Items +
+
+ {SYNCED_ITEMS.map((item) => ( +
+ {item.status === "synced" ? ( + + ) : ( + + )} + {item.name} +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/index.ts b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/index.ts new file mode 100644 index 00000000000..1dd7a1aff1b --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/index.ts @@ -0,0 +1 @@ +export { IntegrationsDemo } from "./IntegrationsDemo"; diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/MobileAppDemo.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/MobileAppDemo.tsx new file mode 100644 index 00000000000..2276ade8252 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/MobileAppDemo.tsx @@ -0,0 +1,134 @@ +function SupersetIcon({ className }: { className?: string }) { + return ( + + ); +} + +const CHAT_MESSAGES = [ + { + id: "1", + role: "user", + content: "Can you add dark mode to the settings page?", + }, + { + id: "2", + role: "assistant", + content: + "I'll add a dark mode toggle to the settings. Let me update the theme context and add the UI switch.", + }, + { + id: "3", + role: "assistant", + content: + "Done! I've added:\n• Theme toggle in settings\n• Dark/light mode support\n• System preference detection", + isLatest: true, + }, +]; + +export function MobileAppDemo() { + return ( +
+ {/* Phone frame - large and cropped at bottom */} +
+ {/* Dynamic Island */} +
+ + {/* Screen content */} +
+ {/* Status bar */} +
+ 9:41 +
+
+
+
+
+
+
+
+
+
+
+
+ + {/* App header */} +
+
+
+ +
+
+
+ Superset Agent +
+
● Online
+
+
+
+ + {/* Chat messages */} +
+ {CHAT_MESSAGES.map((msg) => ( +
+
+

+ {msg.content} +

+
+
+ ))} +
+ + {/* Input bar */} +
+
+ Message... +
+ +
+
+
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/index.ts b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/index.ts new file mode 100644 index 00000000000..098183f6532 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/index.ts @@ -0,0 +1 @@ +export { MobileAppDemo } from "./MobileAppDemo"; diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TasksDemo/TasksDemo.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TasksDemo/TasksDemo.tsx new file mode 100644 index 00000000000..b0b6e60ec19 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TasksDemo/TasksDemo.tsx @@ -0,0 +1,121 @@ +import { HiCheck } from "react-icons/hi2"; + +const TASKS = [ + { + id: "1", + title: "Implement user authentication", + status: "done", + assignee: "SC", + }, + { + id: "2", + title: "Add workspace sync API", + status: "in-progress", + assignee: "AR", + }, + { + id: "3", + title: "Fix mobile responsive layout", + status: "in-progress", + assignee: "JL", + }, + { + id: "4", + title: "Update API documentation", + status: "todo", + assignee: "TK", + }, + { + id: "5", + title: "Write unit tests for auth", + status: "todo", + assignee: "SC", + }, +]; + +function SpinnerIcon({ className }: { className?: string }) { + return ( + + ); +} + +export function TasksDemo() { + return ( +
+
+ {/* Header */} +
+
+
+
+
+
+
+ My Tasks +
+ + {TASKS.length} tasks + +
+ + {/* Task list */} +
+ {TASKS.map((task) => ( +
+ {/* Status indicator */} + {task.status === "done" ? ( +
+ +
+ ) : task.status === "in-progress" ? ( + + ) : ( +
+ )} + + {/* Task content */} +
+ + {task.title} + +
+ + {/* Assignee */} +
+ {task.assignee} +
+
+ ))} +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TasksDemo/index.ts b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TasksDemo/index.ts new file mode 100644 index 00000000000..7a6b6d90a6c --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TasksDemo/index.ts @@ -0,0 +1 @@ +export { TasksDemo } from "./TasksDemo"; diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TeamCollaborationDemo/TeamCollaborationDemo.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TeamCollaborationDemo/TeamCollaborationDemo.tsx new file mode 100644 index 00000000000..30f443f84dd --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TeamCollaborationDemo/TeamCollaborationDemo.tsx @@ -0,0 +1,72 @@ +import { HiCheck } from "react-icons/hi2"; + +const TEAM_MEMBERS = [ + { name: "Sarah Chen", initials: "SC" }, + { name: "Alex Rivera", initials: "AR" }, + { name: "Jordan Lee", initials: "JL" }, + { name: "Taylor Kim", initials: "TK" }, +]; + +const ACTIVITY = [ + { id: "1", user: "Sarah", action: "merged PR #142", time: "2m" }, + { id: "2", user: "Alex", action: "started workspace", time: "5m" }, + { id: "3", user: "Jordan", action: "completed task", time: "12m" }, +]; + +export function TeamCollaborationDemo() { + return ( +
+
+ {/* Header */} +
+
+
+
+
+
+
+ Team +
+
+ + {/* Team members */} +
+
+ Online Now +
+
+ {TEAM_MEMBERS.map((member, index) => ( +
+ {member.initials} +
+ ))} +
+ +3 +
+
+ + {/* Activity feed */} +
+ Recent Activity +
+
+ {ACTIVITY.map((item) => ( +
+ + + {item.user}{" "} + {item.action} + + {item.time} +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TeamCollaborationDemo/index.ts b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TeamCollaborationDemo/index.ts new file mode 100644 index 00000000000..218d3ecc67a --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TeamCollaborationDemo/index.ts @@ -0,0 +1 @@ +export { TeamCollaborationDemo } from "./TeamCollaborationDemo"; diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/index.ts b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/index.ts new file mode 100644 index 00000000000..f3a2fd9a362 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/index.ts @@ -0,0 +1 @@ +export { FeaturePreview } from "./FeaturePreview"; diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeatureSidebar/FeatureSidebar.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeatureSidebar/FeatureSidebar.tsx new file mode 100644 index 00000000000..5755a88e0d8 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeatureSidebar/FeatureSidebar.tsx @@ -0,0 +1,95 @@ +import { cn } from "@superset/ui/utils"; +import { useMemo } from "react"; +import type { ProFeature } from "../../constants"; +import { PRO_FEATURES } from "../../constants"; + +interface FeatureSidebarProps { + selectedFeatureId: string; + highlightedFeatureId?: string; + onSelectFeature: (featureId: string) => void; +} + +export function FeatureSidebar({ + selectedFeatureId, + highlightedFeatureId, + onSelectFeature, +}: FeatureSidebarProps) { + const orderedFeatures = useMemo(() => { + if (!highlightedFeatureId) return PRO_FEATURES; + + const highlighted = PRO_FEATURES.find((f) => f.id === highlightedFeatureId); + if (!highlighted) return PRO_FEATURES; + + return [ + highlighted, + ...PRO_FEATURES.filter((f) => f.id !== highlightedFeatureId), + ]; + }, [highlightedFeatureId]); + + return ( +
+
+

Pro Features

+
+ +
+ {orderedFeatures.map((proFeature) => ( + onSelectFeature(proFeature.id)} + /> + ))} +
+
+ ); +} + +interface FeatureButtonProps { + feature: ProFeature; + isSelected: boolean; + onSelect: () => void; +} + +function FeatureButton({ feature, isSelected, onSelect }: FeatureButtonProps) { + const Icon = feature.icon; + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeatureSidebar/index.ts b/apps/desktop/src/renderer/components/Paywall/components/FeatureSidebar/index.ts new file mode 100644 index 00000000000..e67891eaae6 --- /dev/null +++ b/apps/desktop/src/renderer/components/Paywall/components/FeatureSidebar/index.ts @@ -0,0 +1 @@ +export { FeatureSidebar } from "./FeatureSidebar"; diff --git a/apps/desktop/src/renderer/components/Paywall/constants.ts b/apps/desktop/src/renderer/components/Paywall/constants.ts index 2beb0888945..bf51ac20efd 100644 --- a/apps/desktop/src/renderer/components/Paywall/constants.ts +++ b/apps/desktop/src/renderer/components/Paywall/constants.ts @@ -1,8 +1,11 @@ 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 { + HiCloud, + HiDevicePhoneMobile, + HiOutlineClipboardDocumentList, + HiOutlinePuzzlePiece, + HiUsers, +} from "react-icons/hi2"; import type { GatedFeature } from "./usePaywall"; import { GATED_FEATURES } from "./usePaywall"; @@ -13,6 +16,7 @@ export interface ProFeature { icon: IconType; iconColor: string; gradientColors: readonly [string, string, string, string]; + comingSoon?: boolean; } export const PRO_FEATURES: ProFeature[] = [ @@ -26,47 +30,50 @@ export const PRO_FEATURES: ProFeature[] = [ gradientColors: ["#1e40af", "#1e3a8a", "#172554", "#1a1a2e"], }, { - id: "ai-features", - title: "AI-Powered Features", + id: "integrations", + title: "Integrations", description: - "Enhanced AI agent capabilities with context-aware completions, automated workflow suggestions, and intelligent terminal assistance.", - icon: IoSparkles, + "Connect Linear, GitHub, and more to sync issues and PRs directly with your workspaces.", + icon: HiOutlinePuzzlePiece, iconColor: "text-purple-500", - gradientColors: ["#6b21a8", "#581c87", "#3b0764", "#1a1a2e"], + gradientColors: ["#7c3aed", "#6d28d9", "#4c1d95", "#1a1a2e"], }, { - id: "advanced-terminal", - title: "Advanced Terminal", + id: "tasks", + title: "Tasks", 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", + "Track and manage tasks synced from Linear. Stay on top of your work without leaving Superset.", + icon: HiOutlineClipboardDocumentList, + iconColor: "text-emerald-500", gradientColors: ["#047857", "#065f46", "#064e3b", "#1a1a2e"], }, { - id: "unlimited-workspaces", - title: "Unlimited Workspaces", + id: "cloud-workspaces", + title: "Cloud 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", + "Access your workspaces from anywhere with cloud-hosted environments.", + icon: HiCloud, + iconColor: "text-amber-500", gradientColors: ["#b45309", "#92400e", "#78350f", "#1a1a2e"], + comingSoon: true, }, { - id: "priority-support", - title: "Priority Support", + id: "mobile-app", + title: "Mobile App", description: - "Priority email support from the Superset team. Early access to new Pro features and beta releases.", - icon: RiRocketLine, + "Monitor workspaces and manage tasks on the go. Continue conversations from anywhere.", + icon: HiDevicePhoneMobile, iconColor: "text-red-500", gradientColors: ["#7f1d1d", "#991b1b", "#450a0a", "#1a1a2e"], + comingSoon: true, }, ]; // 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", + [GATED_FEATURES.INTEGRATIONS]: "integrations", + [GATED_FEATURES.TASKS]: "tasks", + [GATED_FEATURES.CLOUD_WORKSPACES]: "cloud-workspaces", + [GATED_FEATURES.MOBILE_APP]: "mobile-app", }; diff --git a/apps/desktop/src/renderer/components/Paywall/usePaywall.ts b/apps/desktop/src/renderer/components/Paywall/usePaywall.ts index 65bcfc5fec5..575bc8bd5cd 100644 --- a/apps/desktop/src/renderer/components/Paywall/usePaywall.ts +++ b/apps/desktop/src/renderer/components/Paywall/usePaywall.ts @@ -5,9 +5,10 @@ type UserPlan = "free" | "pro"; export const GATED_FEATURES = { INVITE_MEMBERS: "invite-members", - AI_COMPLETION: "ai-completion", - SPLIT_TERMINAL: "split-terminal", - CREATE_WORKSPACE: "create-workspace", + INTEGRATIONS: "integrations", + TASKS: "tasks", + CLOUD_WORKSPACES: "cloud-workspaces", + MOBILE_APP: "mobile-app", } as const; export type GatedFeature = (typeof GATED_FEATURES)[keyof typeof GATED_FEATURES]; @@ -15,7 +16,7 @@ export type GatedFeature = (typeof GATED_FEATURES)[keyof typeof GATED_FEATURES]; export function usePaywall() { const { data: session } = authClient.useSession(); - const userPlan: UserPlan = "free"; + const userPlan: UserPlan = (session?.session?.plan as UserPlan) ?? "free"; function hasAccess(feature: GatedFeature): boolean { void feature; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/PlansComparison.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/PlansComparison.tsx deleted file mode 100644 index aeb21c7f535..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/PlansComparison.tsx +++ /dev/null @@ -1,542 +0,0 @@ -import { Button } from "@superset/ui/button"; -import { toast } from "@superset/ui/sonner"; -import { Switch } from "@superset/ui/switch"; -import { cn } from "@superset/ui/utils"; -import { useLiveQuery } from "@tanstack/react-db"; -import { useQuery } from "@tanstack/react-query"; -import { Link } from "@tanstack/react-router"; -import { format } from "date-fns"; -import { Fragment, useState } from "react"; -import { HiArrowLeft, HiArrowUpRight, HiCheck } from "react-icons/hi2"; -import { env } from "renderer/env.renderer"; -import { authClient } from "renderer/lib/auth-client"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import type { PlanTier } from "../../constants"; - -type PlanCardAction = - | "current" - | "upgrade" - | "downgrade" - | "restore" - | "contact"; -type PlanCardData = { - id: "free" | "pro" | "enterprise"; - name: string; - price: { monthly: string; yearly: string } | string; - priceNote?: { monthly: string; yearly: string } | string; - billingText: { monthly: string; yearly: string } | string; - showBillingToggle?: boolean; - actions: Array<{ - label: string; - action: PlanCardAction; - variant: "default" | "secondary" | "outline"; - size?: "default" | "sm"; - fullWidth?: boolean; - align?: "center" | "start"; - }>; -}; - -type ComparisonValue = string | boolean | null; -type ComparisonRow = { - label: string; - values: ComparisonValue[]; -}; -type ComparisonSection = { - title: string; - rows: ComparisonRow[]; -}; - -const PLAN_CARDS: PlanCardData[] = [ - { - id: "free", - name: "Free", - price: "$0", - priceNote: "per user/month", - billingText: "Free for everyone", - actions: [ - { - label: "Current plan", - action: "current", - variant: "secondary", - }, - ], - }, - { - id: "pro", - name: "Pro", - price: { monthly: "$20", yearly: "$180" }, - priceNote: { monthly: "per user/month", yearly: "per user/year" }, - billingText: { monthly: "Billed monthly", yearly: "Billed yearly" }, - showBillingToggle: true, - actions: [ - { - label: "Upgrade", - action: "upgrade", - variant: "default", - }, - ], - }, - { - id: "enterprise", - name: "Enterprise", - price: "Custom pricing", - billingText: "Billed yearly", - actions: [ - { - label: "Request a trial", - action: "contact", - variant: "outline", - }, - ], - }, -]; - -const COMPARISON_SECTIONS: ComparisonSection[] = [ - { - title: "Usage", - rows: [ - { - label: "Team members", - values: ["1", "Unlimited", "Unlimited"], - }, - { - label: "Workspaces", - values: ["5", "Unlimited", "Unlimited"], - }, - { - label: "Projects", - values: ["3", "Unlimited", "Unlimited"], - }, - ], - }, - { - title: "Features", - rows: [ - { - label: "Desktop app", - values: [true, true, true], - }, - { - label: "Local workspaces", - values: [true, true, true], - }, - { - label: "GitHub integration", - values: [true, true, true], - }, - { - label: "Cloud workspaces", - values: [null, true, true], - }, - { - label: "Mobile app", - values: [null, true, true], - }, - { - label: "Linear integration", - values: [null, true, true], - }, - { - label: "Team collaboration", - values: [null, true, true], - }, - ], - }, - { - title: "Support", - rows: [ - { - label: "Priority support", - values: [null, true, true], - }, - { - label: "Uptime SLA", - values: [null, null, true], - }, - { - label: "Custom contracts", - values: [null, null, true], - }, - ], - }, - { - title: "Security", - rows: [ - { - label: "SSO/SAML", - values: [null, null, true], - }, - { - label: "IP restrictions", - values: [null, null, true], - }, - { - label: "SCIM provisioning", - values: [null, null, true], - }, - { - label: "Audit log", - values: [null, null, true], - }, - ], - }, -]; - -export function PlansComparison() { - const [isYearly, setIsYearly] = useState(true); - const [isUpgrading, setIsUpgrading] = useState(false); - const [isCanceling, setIsCanceling] = useState(false); - const [isRestoring, setIsRestoring] = useState(false); - const { data: session } = authClient.useSession(); - const collections = useCollections(); - - const activeOrgId = session?.session?.activeOrganizationId; - - const { data: subscriptionData, refetch: refetchSubscription } = useQuery({ - queryKey: ["subscription", activeOrgId], - queryFn: async () => { - if (!activeOrgId) return null; - const result = await authClient.subscription.list({ - query: { referenceId: activeOrgId }, - }); - return result.data?.find((s) => s.status === "active"); - }, - enabled: !!activeOrgId, - }); - - const currentPlan: PlanTier = (subscriptionData?.plan as PlanTier) ?? "free"; - const cancelAt = subscriptionData?.cancelAt; - - const { data: membersData } = useLiveQuery( - (q) => - q - .from({ members: collections.members }) - .select(({ members }) => ({ id: members.id })), - [collections], - ); - const memberCount = membersData?.length ?? 1; - - const currentPlanLabelByTier: Record = { - free: "Free", - pro: "Pro", - enterprise: "Enterprise", - }; - const currentPlanLabel = currentPlanLabelByTier[currentPlan]; - - const getValue = (value: T | { monthly: T; yearly: T }): T => { - if (typeof value === "object" && value !== null && "monthly" in value) { - return isYearly ? value.yearly : value.monthly; - } - return value as T; - }; - - const handlePlanAction = async (action: PlanCardAction) => { - if (action === "current") { - return; - } - - if (action === "contact") { - window.open("mailto:founders@superset.sh", "_blank"); - return; - } - - if (!activeOrgId) return; - - if (action === "downgrade") { - setIsCanceling(true); - try { - await authClient.subscription.cancel( - { - referenceId: activeOrgId, - returnUrl: env.NEXT_PUBLIC_WEB_URL, - }, - { - onSuccess: (ctx) => { - if (ctx.data?.url) { - window.open(ctx.data.url, "_blank"); - } - }, - }, - ); - await refetchSubscription(); - } finally { - setIsCanceling(false); - } - return; - } - - if (action === "restore") { - setIsRestoring(true); - try { - await authClient.subscription.restore({ - referenceId: activeOrgId, - }); - await refetchSubscription(); - toast.success("Plan restored"); - } finally { - setIsRestoring(false); - } - return; - } - - setIsUpgrading(true); - try { - await authClient.subscription.upgrade( - { - plan: "pro", - referenceId: activeOrgId, - annual: isYearly, - seats: memberCount, - successUrl: `${env.NEXT_PUBLIC_WEB_URL}/settings/billing?success=true`, - cancelUrl: env.NEXT_PUBLIC_WEB_URL, - disableRedirect: true, - }, - { - onSuccess: (ctx) => { - if (ctx.data?.url) { - window.open(ctx.data.url, "_blank"); - } - }, - }, - ); - } finally { - setIsUpgrading(false); - } - }; - - const renderComparisonValue = (value: ComparisonValue) => { - if (value === null || value === false) { - return Not included; - } - - if (value === true) { - return ; - } - - return ( - <> - - {value} - - ); - }; - - const highlightColumnIndex = 1; - const highlightColumnStart = highlightColumnIndex + 2; - const gridColumnsClass = "grid grid-cols-[180px_repeat(3,_1fr)]"; - - return ( -
-
- -
-

Plans

-

- You are on the{" "} - - {currentPlanLabel} plan - - . If you have any questions or would like further support with your - plan,{" "} - - contact us - - - . -

-
-
- -
-
-
-
-
-
- {(["plan", "billing", "cta"] as const).map((rowKey, rowIndex) => ( - -
- {PLAN_CARDS.map((plan) => { - const isCurrent = currentPlanLabel === plan.name; - const isDowngrade = - plan.id === "free" && currentPlan !== "free"; - - let planActions: typeof plan.actions; - if (isCurrent && cancelAt) { - planActions = [ - { - label: isRestoring ? "Restoring..." : "Restore plan", - action: "restore" as const, - variant: "default" as const, - }, - ]; - } else if (isCurrent) { - planActions = [ - { - label: "Current plan", - action: "current" as const, - variant: "secondary" as const, - }, - ]; - } else if (isDowngrade && cancelAt) { - planActions = [ - { - label: `Starts ${cancelAt ? format(new Date(cancelAt), "MMMM d, yyyy") : ""}`, - action: "current" as const, - variant: "outline" as const, - }, - ]; - } else if (isDowngrade) { - planActions = [ - { - label: isCanceling - ? "Downgrading..." - : "Downgrade to Free", - action: "downgrade" as const, - variant: "outline" as const, - }, - ]; - } else { - planActions = plan.actions; - } - - if (rowKey === "plan") { - return ( -
-
-
- {plan.name} -
-
- {getValue(plan.price)} -
- {plan.priceNote && ( -
- {getValue(plan.priceNote)} -
- )} -
-
- ); - } - - if (rowKey === "billing") { - return ( -
- {plan.showBillingToggle && ( - - )} - {getValue(plan.billingText)} -
- ); - } - - return ( -
-
- {planActions.map((action) => ( - - ))} -
-
- ); - })} - - {rowIndex < 2 && ( - <> -
-
- - )} - - ))} - - {COMPARISON_SECTIONS.map((section, sectionIndex) => ( - -
- {section.title} -
-
- - {section.rows.map((row, rowIndex) => { - const isLastRow = - sectionIndex === COMPARISON_SECTIONS.length - 1 && - rowIndex === section.rows.length - 1; - - return ( - -
- {row.label} -
- {row.values.map((value, valueIndex) => ( -
- {renderComparisonValue(value)} -
- ))} - {!isLastRow && ( -
- )} - - ); - })} - - ))} -
-
-
-
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/FeatureList/FeatureList.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/FeatureList/FeatureList.tsx deleted file mode 100644 index d9d8843cc5f..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/FeatureList/FeatureList.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { HiCheck } from "react-icons/hi2"; -import type { PlanFeature } from "../../../../constants"; - -interface FeatureListProps { - features: PlanFeature[]; -} - -export function FeatureList({ features }: FeatureListProps) { - return ( -
    - {features.map((feature) => ( -
  • - -
    - {feature.name} - {feature.limit && ( - - ({feature.limit}) - - )} -
    -
  • - ))} -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/FeatureList/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/FeatureList/index.ts deleted file mode 100644 index c95ecd135a7..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/FeatureList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { FeatureList } from "./FeatureList"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/PlanCard/PlanCard.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/PlanCard/PlanCard.tsx deleted file mode 100644 index bf6bf7a1940..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/PlanCard/PlanCard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Badge } from "@superset/ui/badge"; -import { Button } from "@superset/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@superset/ui/card"; -import { Separator } from "@superset/ui/separator"; -import { toast } from "@superset/ui/sonner"; -import { cn } from "@superset/ui/utils"; -import type { Plan, PlanTier } from "../../../../constants"; -import { FeatureList } from "../FeatureList"; - -interface PlanCardProps { - plan: Plan; - currentPlan: PlanTier; -} - -export function PlanCard({ plan, currentPlan }: PlanCardProps) { - const isCurrent = plan.id === currentPlan; - - const handleCTA = () => { - if (plan.cta.action === "current") { - return; - } - - if (plan.cta.action === "contact") { - window.open("mailto:founders@superset.sh", "_blank"); - } else if (plan.cta.action === "upgrade") { - toast.info("Stripe integration coming soon"); - } - }; - - return ( - - -
-
-
- {plan.name} - {isCurrent && Current} -
- {plan.description} -
-
-
- -
- {plan.price === null ? ( -
- {plan.id === "free" ? "Free" : "Contact sales"} -
- ) : ( -
-
- - ${plan.price.monthly / 100} - - per user/month -
- {plan.price.yearly && ( -
- or ${plan.price.yearly / 100}/year (~$ - {Math.round(plan.price.yearly / 12 / 100)} - /mo) -
- )} -
- )} -
- - -
- - - -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/PlanCard/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/PlanCard/index.ts deleted file mode 100644 index 2916c3c64ef..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/components/PlanCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PlanCard } from "./PlanCard"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/index.ts deleted file mode 100644 index c69fe2b2020..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PlansComparison } from "./PlansComparison"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts index b3031e874be..9af00ffa30c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts @@ -69,7 +69,7 @@ export const PLANS: Record = { included: true, limit: "$20/seat", }, - { id: "workspaces", name: "Unlimited workspaces", included: true }, + { id: "tasks", name: "Task management", included: true }, { id: "cloud", name: "Cloud workspaces", included: true }, { id: "mobile", name: "Mobile app access", included: true }, { id: "priority", name: "Priority support", included: true }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx index 9dc1679904f..18231f0dacc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx @@ -1,6 +1,4 @@ -import { FEATURE_FLAGS } from "@superset/shared/constants"; -import { createFileRoute, Navigate } from "@tanstack/react-router"; -import { useFeatureFlagEnabled } from "posthog-js/react"; +import { createFileRoute } from "@tanstack/react-router"; import { useMemo } from "react"; import { useSettingsSearchQuery } from "renderer/stores/settings-state"; import { getMatchingItemsForSection } from "../utils/settings-search"; @@ -12,7 +10,6 @@ export const Route = createFileRoute("/_authenticated/settings/billing/")({ function BillingPage() { const searchQuery = useSettingsSearchQuery(); - const billingEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.BILLING_ENABLED); const visibleItems = useMemo(() => { if (!searchQuery) return null; @@ -21,13 +18,5 @@ function BillingPage() { ); }, [searchQuery]); - if (billingEnabled === undefined) { - return null; - } - - if (billingEnabled === false) { - return ; - } - return ; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx index dc9697f8b0c..2cc19bb0043 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx @@ -1,7 +1,17 @@ -import { FEATURE_FLAGS } from "@superset/shared/constants"; -import { createFileRoute, Navigate } from "@tanstack/react-router"; -import { useFeatureFlagEnabled } from "posthog-js/react"; -import { PlansComparison } from "../components/PlansComparison"; +import { Button } from "@superset/ui/button"; +import { toast } from "@superset/ui/sonner"; +import { Switch } from "@superset/ui/switch"; +import { cn } from "@superset/ui/utils"; +import { useLiveQuery } from "@tanstack/react-db"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import { format } from "date-fns"; +import { Fragment, useState } from "react"; +import { HiArrowLeft, HiArrowUpRight, HiCheck } from "react-icons/hi2"; +import { env } from "renderer/env.renderer"; +import { authClient } from "renderer/lib/auth-client"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import type { PlanTier } from "../constants"; export const Route = createFileRoute("/_authenticated/settings/billing/plans/")( { @@ -9,16 +19,533 @@ export const Route = createFileRoute("/_authenticated/settings/billing/plans/")( }, ); +type PlanCardAction = + | "current" + | "upgrade" + | "downgrade" + | "restore" + | "contact"; + +type PlanCardData = { + id: "free" | "pro" | "enterprise"; + name: string; + price: { monthly: string; yearly: string } | string; + priceNote?: { monthly: string; yearly: string } | string; + billingText: { monthly: string; yearly: string } | string; + showBillingToggle?: boolean; + actions: Array<{ + label: string; + action: PlanCardAction; + variant: "default" | "secondary" | "outline"; + size?: "default" | "sm"; + fullWidth?: boolean; + align?: "center" | "start"; + }>; +}; + +type ComparisonValue = string | boolean | null; + +type ComparisonRow = { + label: string; + values: ComparisonValue[]; +}; + +type ComparisonSection = { + title: string; + rows: ComparisonRow[]; +}; + +const PLAN_CARDS: PlanCardData[] = [ + { + id: "free", + name: "Free", + price: "$0", + priceNote: "per user/month", + billingText: "Free for everyone", + actions: [ + { + label: "Current plan", + action: "current", + variant: "secondary", + }, + ], + }, + { + id: "pro", + name: "Pro", + price: { monthly: "$20", yearly: "$180" }, + priceNote: { monthly: "per user/month", yearly: "per user/year" }, + billingText: { monthly: "Billed monthly", yearly: "Billed yearly" }, + showBillingToggle: true, + actions: [ + { + label: "Upgrade", + action: "upgrade", + variant: "default", + }, + ], + }, + { + id: "enterprise", + name: "Enterprise", + price: "Custom pricing", + billingText: "Billed yearly", + actions: [ + { + label: "Request a trial", + action: "contact", + variant: "outline", + }, + ], + }, +]; + +const COMPARISON_SECTIONS: ComparisonSection[] = [ + { + title: "Usage", + rows: [ + { + label: "Team members", + values: ["1", "Unlimited", "Unlimited"], + }, + { + label: "Workspaces", + values: ["Unlimited", "Unlimited", "Unlimited"], + }, + { + label: "Projects", + values: ["Unlimited", "Unlimited", "Unlimited"], + }, + ], + }, + { + title: "Features", + rows: [ + { + label: "Desktop app", + values: [true, true, true], + }, + { + label: "Local workspaces", + values: [true, true, true], + }, + { + label: "GitHub integration", + values: [true, true, true], + }, + { + label: "Cloud workspaces", + values: [null, true, true], + }, + { + label: "Mobile app", + values: [null, true, true], + }, + { + label: "Linear integration", + values: [null, true, true], + }, + { + label: "Team collaboration", + values: [null, true, true], + }, + ], + }, + { + title: "Support", + rows: [ + { + label: "Priority support", + values: [null, true, true], + }, + { + label: "Uptime SLA", + values: [null, null, true], + }, + { + label: "Custom contracts", + values: [null, null, true], + }, + ], + }, + { + title: "Security", + rows: [ + { + label: "SSO/SAML", + values: [null, null, true], + }, + { + label: "IP restrictions", + values: [null, null, true], + }, + { + label: "SCIM provisioning", + values: [null, null, true], + }, + { + label: "Audit log", + values: [null, null, true], + }, + ], + }, +]; + function PlansPage() { - const billingEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.BILLING_ENABLED); + const [isYearly, setIsYearly] = useState(true); + const [isUpgrading, setIsUpgrading] = useState(false); + const [isCanceling, setIsCanceling] = useState(false); + const [isRestoring, setIsRestoring] = useState(false); + const { data: session } = authClient.useSession(); + const collections = useCollections(); + + const activeOrgId = session?.session?.activeOrganizationId; + + const { data: subscriptionData, refetch: refetchSubscription } = useQuery({ + queryKey: ["subscription", activeOrgId], + queryFn: async () => { + if (!activeOrgId) return null; + const result = await authClient.subscription.list({ + query: { referenceId: activeOrgId }, + }); + return result.data?.find((s) => s.status === "active"); + }, + enabled: !!activeOrgId, + }); + + const currentPlan: PlanTier = (subscriptionData?.plan as PlanTier) ?? "free"; + const cancelAt = subscriptionData?.cancelAt; + + const { data: membersData } = useLiveQuery( + (q) => + q + .from({ members: collections.members }) + .select(({ members }) => ({ id: members.id })), + [collections], + ); + const memberCount = membersData?.length ?? 1; + + const currentPlanLabelByTier: Record = { + free: "Free", + pro: "Pro", + enterprise: "Enterprise", + }; + const currentPlanLabel = currentPlanLabelByTier[currentPlan]; + + const getValue = (value: T | { monthly: T; yearly: T }): T => { + if (typeof value === "object" && value !== null && "monthly" in value) { + return isYearly ? value.yearly : value.monthly; + } + return value as T; + }; + + const handlePlanAction = async (action: PlanCardAction) => { + if (action === "current") { + return; + } + + if (action === "contact") { + window.open("mailto:founders@superset.sh", "_blank"); + return; + } + + if (!activeOrgId) return; + + if (action === "downgrade") { + setIsCanceling(true); + try { + await authClient.subscription.cancel( + { + referenceId: activeOrgId, + returnUrl: env.NEXT_PUBLIC_WEB_URL, + }, + { + onSuccess: (ctx) => { + if (ctx.data?.url) { + window.open(ctx.data.url, "_blank"); + } + }, + }, + ); + await refetchSubscription(); + } finally { + setIsCanceling(false); + } + return; + } + + if (action === "restore") { + setIsRestoring(true); + try { + await authClient.subscription.restore({ + referenceId: activeOrgId, + }); + await refetchSubscription(); + toast.success("Plan restored"); + } finally { + setIsRestoring(false); + } + return; + } + + setIsUpgrading(true); + try { + await authClient.subscription.upgrade( + { + plan: "pro", + referenceId: activeOrgId, + annual: isYearly, + seats: memberCount, + successUrl: `${env.NEXT_PUBLIC_WEB_URL}/settings/billing?success=true`, + cancelUrl: env.NEXT_PUBLIC_WEB_URL, + disableRedirect: true, + }, + { + onSuccess: (ctx) => { + if (ctx.data?.url) { + window.open(ctx.data.url, "_blank"); + } + }, + }, + ); + } finally { + setIsUpgrading(false); + } + }; + + const renderComparisonValue = (value: ComparisonValue) => { + if (value === null || value === false) { + return Not included; + } + + if (value === true) { + return ; + } + + return ( + <> + + {value} + + ); + }; + + const highlightColumnIndex = 1; + const highlightColumnStart = highlightColumnIndex + 2; + const gridColumnsClass = "grid grid-cols-[180px_repeat(3,_1fr)]"; + + return ( +
+
+ +
+

Plans

+

+ You are on the{" "} + + {currentPlanLabel} plan + + . If you have any questions or would like further support with your + plan,{" "} + + contact us + + + . +

+
+
+ +
+
+
+
+
+
+ {(["plan", "billing", "cta"] as const).map((rowKey, rowIndex) => ( + +
+ {PLAN_CARDS.map((plan) => { + const isCurrent = currentPlanLabel === plan.name; + const isDowngrade = + plan.id === "free" && currentPlan !== "free"; + + let planActions: typeof plan.actions; + if (isCurrent && cancelAt) { + planActions = [ + { + label: isRestoring ? "Restoring..." : "Restore plan", + action: "restore" as const, + variant: "default" as const, + }, + ]; + } else if (isCurrent) { + planActions = [ + { + label: "Current plan", + action: "current" as const, + variant: "secondary" as const, + }, + ]; + } else if (isDowngrade && cancelAt) { + planActions = [ + { + label: `Starts ${cancelAt ? format(new Date(cancelAt), "MMMM d, yyyy") : ""}`, + action: "current" as const, + variant: "outline" as const, + }, + ]; + } else if (isDowngrade) { + planActions = [ + { + label: isCanceling + ? "Downgrading..." + : "Downgrade to Free", + action: "downgrade" as const, + variant: "outline" as const, + }, + ]; + } else { + planActions = plan.actions; + } + + if (rowKey === "plan") { + return ( +
+
+
+ {plan.name} +
+
+ {getValue(plan.price)} +
+ {plan.priceNote && ( +
+ {getValue(plan.priceNote)} +
+ )} +
+
+ ); + } + + if (rowKey === "billing") { + return ( +
+ {plan.showBillingToggle && ( + + )} + {getValue(plan.billingText)} +
+ ); + } + + return ( +
+
+ {planActions.map((action) => ( + + ))} +
+
+ ); + })} + + {rowIndex < 2 && ( + <> +
+
+ + )} + + ))} - if (billingEnabled === undefined) { - return null; - } + {COMPARISON_SECTIONS.map((section, sectionIndex) => ( + +
+ {section.title} +
+
- if (billingEnabled === false) { - return ; - } + {section.rows.map((row, rowIndex) => { + const isLastRow = + sectionIndex === COMPARISON_SECTIONS.length - 1 && + rowIndex === section.rows.length - 1; - return ; + return ( + +
+ {row.label} +
+ {row.values.map((value, valueIndex) => ( +
+ {renderComparisonValue(value)} +
+ ))} + {!isLastRow && ( +
+ )} + + ); + })} + + ))} +
+
+
+
+ ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx index 68c5da62bdb..f0523525060 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx @@ -1,7 +1,5 @@ -import { FEATURE_FLAGS } from "@superset/shared/constants"; import { cn } from "@superset/ui/utils"; import { Link, useMatchRoute } from "@tanstack/react-router"; -import { useFeatureFlagEnabled } from "posthog-js/react"; import { HiOutlineBell, HiOutlineBuildingOffice2, @@ -110,20 +108,13 @@ const GENERAL_SECTIONS: { export function GeneralSettings({ matchCounts }: GeneralSettingsProps) { const matchRoute = useMatchRoute(); - const billingEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.BILLING_ENABLED); - - // Filter by feature flags first, then by search matches - const availableSections = GENERAL_SECTIONS.filter((section) => { - if (section.section === "billing" && !billingEnabled) return false; - return true; - }); // When searching, only show sections that have matches const filteredSections = matchCounts - ? availableSections.filter( + ? GENERAL_SECTIONS.filter( (section) => (matchCounts[section.section] ?? 0) > 0, ) - : availableSections; + : GENERAL_SECTIONS; if (filteredSections.length === 0) { return null; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx index 89ff46efb4b..57cbbe32899 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx @@ -14,6 +14,10 @@ import { useCallback, useEffect, useState } from "react"; import { FaGithub } from "react-icons/fa"; import { HiCheckCircle, HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; import { SiLinear } from "react-icons/si"; +import { + GATED_FEATURES, + usePaywall, +} from "renderer/components/Paywall/usePaywall"; import { env } from "renderer/env.renderer"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { authClient } from "renderer/lib/auth-client"; @@ -43,6 +47,7 @@ export function IntegrationsSettings({ const { data: session } = authClient.useSession(); const activeOrganizationId = session?.session?.activeOrganizationId; const collections = useCollections(); + const { gateFeature } = usePaywall(); const { data: integrations, isLoading: isLoadingIntegrations } = useLiveQuery( (q) => @@ -135,7 +140,11 @@ export function IntegrationsSettings({ isConnected={isLinearConnected} connectedOrgName={linearConnection?.externalOrgName} isLoading={isLoading} - onManage={() => handleOpenWeb("/integrations/linear")} + onManage={() => + gateFeature(GATED_FEATURES.INTEGRATIONS, () => + handleOpenWeb("/integrations/linear"), + ) + } /> )} @@ -147,7 +156,11 @@ export function IntegrationsSettings({ isConnected={isGithubConnected} connectedOrgName={githubInstallation?.accountLogin} isLoading={isLoading} - onManage={() => handleOpenWeb("/integrations/github")} + onManage={() => + gateFeature(GATED_FEATURES.INTEGRATIONS, () => + handleOpenWeb("/integrations/github"), + ) + } /> )}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx index 0e785e286eb..7978756a7f9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx @@ -6,6 +6,10 @@ import { alert } from "@superset/ui/atoms/Alert"; import { Button } from "@superset/ui/button"; import { useState } from "react"; import { HiOutlinePlus } from "react-icons/hi2"; +import { + GATED_FEATURES, + usePaywall, +} from "renderer/components/Paywall/usePaywall"; import { InviteMemberDialog } from "./components/InviteMemberDialog"; interface InviteMemberButtonProps { @@ -20,6 +24,7 @@ export function InviteMemberButton({ organizationName, }: InviteMemberButtonProps) { const [open, setOpen] = useState(false); + const { gateFeature } = usePaywall(); const invitableRoles = getInvitableRoles(currentUserRole); @@ -29,13 +34,15 @@ export function InviteMemberButton({ } const handleClick = () => { - alert({ - title: "This will affect your billing", - description: - "Adding members will increase your subscription cost, prorated to your billing cycle.", - confirmText: "Continue", - cancelText: "Cancel", - onConfirm: () => setOpen(true), + gateFeature(GATED_FEATURES.INVITE_MEMBERS, () => { + alert({ + title: "This will affect your billing", + description: + "Adding members will increase your subscription cost, prorated to your billing cycle.", + confirmText: "Continue", + cancelText: "Cancel", + onConfirm: () => setOpen(true), + }); }); }; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx index 10ffe4fdc02..7273f98e349 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx @@ -1,10 +1,12 @@ -import { FEATURE_FLAGS } from "@superset/shared/constants"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useMatchRoute, useNavigate } from "@tanstack/react-router"; -import { useFeatureFlagEnabled } from "posthog-js/react"; import { HiOutlineClipboardDocumentList } from "react-icons/hi2"; import { LuLayers } from "react-icons/lu"; +import { + GATED_FEATURES, + usePaywall, +} from "renderer/components/Paywall/usePaywall"; import { STROKE_WIDTH } from "../constants"; import { NewWorkspaceButton } from "./NewWorkspaceButton"; @@ -17,9 +19,7 @@ export function WorkspaceSidebarHeader({ }: WorkspaceSidebarHeaderProps) { const navigate = useNavigate(); const matchRoute = useMatchRoute(); - const hasTasksAccess = useFeatureFlagEnabled( - FEATURE_FLAGS.ELECTRIC_TASKS_ACCESS, - ); + const { gateFeature } = usePaywall(); // Derive active state from route const isWorkspacesListOpen = !!matchRoute({ to: "/workspaces" }); @@ -35,7 +35,9 @@ export function WorkspaceSidebarHeader({ }; const handleTasksClick = () => { - navigate({ to: "/tasks" }); + gateFeature(GATED_FEATURES.TASKS, () => { + navigate({ to: "/tasks" }); + }); }; if (isCollapsed) { @@ -59,28 +61,26 @@ export function WorkspaceSidebarHeader({ Workspaces - {hasTasksAccess && ( - - - - - Tasks - - )} + + + + + Tasks +
@@ -105,26 +105,24 @@ export function WorkspaceSidebarHeader({ Workspaces - {hasTasksAccess && ( - - )} +