Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 67 additions & 90 deletions apps/desktop/src/renderer/components/Paywall/Paywall.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -15,10 +16,13 @@ type PaywallOptions = {
let showPaywallFn: ((options: PaywallOptions) => void) | null = null;

export const Paywall = () => {
const navigate = useNavigate();
const [paywallOptions, setPaywallOptions] = useState<PaywallOptions | null>(
null,
);
const [isOpen, setIsOpen] = useState(false);
const openTimeRef = useRef<number | null>(null);
const featuresViewedRef = useRef<Set<string>>(new Set());

showPaywallFn = (options: PaywallOptions) => {
setPaywallOptions(options);
Expand All @@ -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<string>(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,
});
Comment on lines +54 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add context to PostHog payloads to meet analytics requirements.

PaywallOptions.context is defined but never sent. If the intent is to capture feature context, it should be included in all paywall events.

✅ Example change (apply to all captures)
 			posthog.capture("paywall_opened", {
 				trigger_source: paywallOptions.feature,
 				feature_id: initialFeatureId,
 				feature_title: feature?.title,
+				context: paywallOptions.context,
 			});

Also applies to: 75-80, 91-96, 112-118

🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/components/Paywall/Paywall.tsx` around lines 54 -
58, The PostHog payloads in Paywall.tsx are missing the PaywallOptions.context
property; update every posthog.capture call (e.g., the "paywall_opened" capture
and the other captures around the existing calls) to include context:
paywallOptions.context (or feature_context if that naming is used) in the event
properties so the feature context is sent with each paywall event; locate calls
to posthog.capture in Paywall.tsx and add the context property to their property
objects (preserve existing keys like trigger_source, feature_id, feature_title,
etc.).

}
}, [isOpen, paywallOptions, initialFeatureId]);

useEffect(() => {
if (paywallOptions?.feature && isOpen) {
const mappedId =
Expand All @@ -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);
}
};
Expand All @@ -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 (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent
className="!w-[744px] !max-w-[744px] p-0 gap-0 overflow-hidden"
className="!w-[744px] !max-w-[744px] p-0 gap-0 overflow-hidden !rounded-none"
showCloseButton={false}
>
<div className="flex">
<div className="flex flex-col border-r bg-neutral-900">
<div className="px-5 py-2.5">
<h1 className="mb-0 mt-1.5 text-lg font-bold text-foreground">
Pro Features
</h1>
</div>

<div className="flex flex-col gap-2.5 px-5 py-2.5">
{PRO_FEATURES.map((proFeature) => {
const Icon = proFeature.icon;
const isSelected = selectedFeatureId === proFeature.id;

return (
<button
key={proFeature.id}
type="button"
onClick={() => setSelectedFeatureId(proFeature.id)}
className={cn(
"group flex w-[209px] h-16 items-center gap-3 px-4 py-3.5 transition-all duration-200 ease-out",
"cursor-pointer text-left",
isSelected
? "bg-muted text-foreground"
: "text-foreground/70 hover:text-foreground hover:bg-foreground/5",
)}
>
<Icon
className={cn(
"shrink-0 text-xl transition-all duration-200 ease-out",
isSelected
? proFeature.iconColor
: "text-foreground/40 group-hover:text-foreground/60",
)}
/>
<span
className={cn(
"text-sm font-semibold transition-all duration-200",
isSelected ? "text-foreground" : "",
)}
>
{proFeature.title}
</span>
</button>
);
})}
</div>
</div>

<div className="flex h-[487px] w-[495px] flex-col">
<div className="relative h-[346px] overflow-hidden">
{PRO_FEATURES.map((proFeature) => (
<div
key={`gradient-${proFeature.id}`}
className={cn(
"absolute inset-0 transition-opacity duration-1000 ease-in-out",
selectedFeature.id === proFeature.id
? "opacity-100"
: "opacity-0",
)}
>
<MeshGradient
colors={proFeature.gradientColors}
className="absolute inset-0 w-full h-full"
/>
</div>
))}

<div className="absolute inset-0 flex items-center justify-center">
<selectedFeature.icon className="text-white/20 text-[120px] select-none pointer-events-none" />
</div>
</div>

<div className="flex min-h-[141px] w-full flex-col border-t bg-background px-6 py-4 items-center justify-center">
<div className="mb-2 flex w-full items-center justify-center gap-2">
<span className="text-lg font-semibold text-foreground">
{selectedFeature.title}
</span>
<Badge variant="default">PRO</Badge>
</div>
<span className="text-center text-sm font-normal text-muted-foreground">
{selectedFeature.description}
</span>
</div>
</div>
<FeatureSidebar
selectedFeatureId={selectedFeatureId}
highlightedFeatureId={initialFeatureId}
onSelectFeature={handleSelectFeature}
/>
<FeaturePreview selectedFeature={selectedFeature} />
</div>

<div className="box-border flex items-center justify-between border-t bg-background px-5 py-4">
<Button variant="outline" onClick={() => setIsOpen(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleUpgrade}>Get Superset Pro</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, ComponentType> = {
"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 (
<div className="flex w-[495px] flex-col">
<div className="relative h-[346px] overflow-hidden">
{PRO_FEATURES.map((proFeature) => (
<div
key={`gradient-${proFeature.id}`}
className={cn(
"absolute inset-0 transition-opacity duration-1000 ease-in-out",
selectedFeature.id === proFeature.id
? "opacity-100"
: "opacity-0",
)}
>
<MeshGradient
colors={proFeature.gradientColors}
className="absolute inset-0 w-full h-full"
/>
</div>
))}

<div className="absolute inset-0 flex items-center justify-center">
{DemoComponent ? <DemoComponent /> : null}
</div>
</div>

<div className="flex w-full flex-col border-t bg-background px-6 py-4 items-center justify-center">
<div className="mb-2 flex w-full items-center justify-center gap-2">
<span className="text-lg font-semibold text-foreground">
{selectedFeature.title}
</span>
<Badge variant="default">PRO</Badge>
{selectedFeature.comingSoon && (
<Badge variant="secondary" className="text-[10px]">
Coming Soon
</Badge>
)}
</div>
<span className="text-center text-sm font-normal text-muted-foreground">
{selectedFeature.description}
</span>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full h-full flex items-center justify-center">
<div className="w-[300px] bg-[#1a1a1a]/90 backdrop-blur-sm rounded-lg border border-white/10 shadow-2xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 bg-[#2a2a2a]/80 border-b border-white/5">
<div className="flex items-center gap-2">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-[#ff5f57]" />
<div className="w-2.5 h-2.5 rounded-full bg-[#febc2e]" />
<div className="w-2.5 h-2.5 rounded-full bg-[#28c840]" />
</div>
<span className="text-xs text-white/60 ml-1">Cloud</span>
</div>
</div>

{/* Cloud sync visual */}
<div className="p-4">
<div className="flex items-center justify-center gap-3 mb-4 py-3">
<div className="flex flex-col items-center gap-1">
<HiComputerDesktop className="w-6 h-6 text-white/50" />
<span className="text-[9px] text-white/40">Desktop</span>
</div>
<div className="flex flex-col items-center">
<div className="flex items-center gap-1">
<div className="w-4 h-px bg-white/20" />
<HiArrowPath className="w-3 h-3 text-white/30" />
<div className="w-4 h-px bg-white/20" />
</div>
</div>
<div className="flex flex-col items-center gap-1">
<div className="w-10 h-10 rounded-full bg-amber-500/20 flex items-center justify-center">
<HiCloud className="w-5 h-5 text-amber-400" />
</div>
<span className="text-[9px] text-white/40">Cloud</span>
</div>
<div className="flex flex-col items-center">
<div className="flex items-center gap-1">
<div className="w-4 h-px bg-white/20" />
<HiArrowPath className="w-3 h-3 text-white/30" />
<div className="w-4 h-px bg-white/20" />
</div>
</div>
<div className="flex flex-col items-center gap-1">
<HiDeviceTablet className="w-6 h-6 text-white/50" />
<span className="text-[9px] text-white/40">Tablet</span>
</div>
</div>

{/* Synced workspaces */}
<div className="text-[10px] uppercase text-white/40 font-medium tracking-wider mb-2">
Synced Workspaces
</div>
<div className="space-y-1.5">
{WORKSPACES.map((ws) => (
<div
key={ws.id}
className="flex items-center gap-2 px-2 py-1.5 rounded bg-white/5 text-xs"
>
<div
className={`w-1.5 h-1.5 rounded-full ${ws.synced ? "bg-emerald-400" : "bg-amber-400"}`}
/>
<span className="text-white/70 truncate flex-1">{ws.name}</span>
<span className="text-white/30 text-[10px]">{ws.branch}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CloudWorkspacesDemo } from "./CloudWorkspacesDemo";
Loading
Loading