Skip to content

feat(paywall): add visual demos and polish billing UI#1040

Merged
saddlepaddle merged 6 commits intomainfrom
satya-patel/deer
Jan 28, 2026
Merged

feat(paywall): add visual demos and polish billing UI#1040
saddlepaddle merged 6 commits intomainfrom
satya-patel/deer

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Jan 28, 2026

Summary

  • Add visual demo components for each paywall feature (Mobile App, Tasks, Team Collaboration, Integrations, Cloud Workspaces)
  • Add "Coming Soon" badges for Mobile App and Cloud Workspaces
  • Polish demo styling: same size (300px), centered, glassy aesthetic
  • Update billing page to replace "Unlimited workspaces" with "Task management"
  • Make Workspaces and Projects unlimited for all plans

Test plan

  • Open paywall modal and cycle through each feature - verify demos display correctly
  • Verify "Coming Soon" badge shows for Mobile App and Cloud Workspaces
  • Check billing page shows "Task management" instead of "Unlimited workspaces"
  • Verify plans page shows Unlimited for Workspaces/Projects on all plans

Summary by CodeRabbit

  • New Features

    • Redesigned paywall with interactive sidebar and rich feature previews (demos for Tasks, Integrations, Cloud Workspaces, Mobile App).
    • New Integrations and Mobile App entries (Mobile App marked coming soon).
  • Changes & Improvements

    • Added analytics for paywall interactions and time/view tracking.
    • Renamed features for clarity (Tasks, Cloud Workspaces) and updated gating: Tasks, Integrations, and member invites require Pro.
    • Rebuilt billing plans UI with improved pricing and comparison matrix.
  • Chores

    • Billing settings now always visible.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Refactors the paywall UI into FeatureSidebar and FeaturePreview with analytics and time-tracking, redefines pro features (integrations, tasks, cloud-workspaces, mobile-app), replaces PlansComparison with a data-driven Plans page, removes billing feature-flag gating, and adds paywall gating across several UI entry points.

Changes

Cohort / File(s) Summary
Paywall Core Restructuring
apps/desktop/src/renderer/components/Paywall/Paywall.tsx, apps/desktop/src/renderer/components/Paywall/constants.ts, apps/desktop/src/renderer/components/Paywall/usePaywall.ts
Replaced static UI with FeatureSidebar/FeaturePreview; added navigation and PostHog analytics (paywall_opened, paywall_feature_clicked, paywall_cancelled, paywall_upgrade_clicked); added open-time and features-viewed tracking; redefined PRO features and FEATURE_ID_MAP; added ProFeature.comingSoon; derive userPlan from session.
Paywall Feature Components & Demos
apps/desktop/src/renderer/components/Paywall/components/FeatureSidebar/*, apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/*, apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/*Demo/*
Added FeatureSidebar (feature list) and FeaturePreview (gradient + demo mapping); introduced demo components: CloudWorkspacesDemo, IntegrationsDemo, MobileAppDemo, TasksDemo, TeamCollaborationDemo with barrels.
Feature Gating Integration
apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx
Replaced several feature-flag checks with usePaywall gateFeature calls for INTEGRATIONS, INVITE_MEMBERS, TASKS before navigation or dialog actions.
Plans Page Rewrite
apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx
Removed PlansComparison usage and implemented a new data-driven Plans page: plan cards, monthly/yearly cadence, active subscription fetch, member count, and action handlers (upgrade/downgrade/restore/contact) with network calls and toasts.
PlansComparison Removal
apps/desktop/src/renderer/routes/_authenticated/settings/billing/components/PlansComparison/*
Deleted PlansComparison component and subcomponents (PlanCard, FeatureList) and their barrel exports.
Billing Feature-flag Removal & Config
apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts, apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx
Removed BILLING_ENABLED gating (billing now always shown); updated Pro plan features (replaced workspaces with tasks).
Barrel Exports / Index Updates
apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/index.ts, .../FeatureSidebar/index.ts, various demo index.ts files
Added/updated barrel exports to expose new components.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Paywall UI
    participant Analytics as PostHog
    participant Router as Navigator

    User->>UI: Open paywall
    UI->>Analytics: track paywall_opened (trigger_source, initialFeature)
    UI->>UI: start openTimeRef, featuresViewedRef
    User->>UI: Click feature item
    UI->>Analytics: track paywall_feature_clicked (prev_feature_id, title)
    UI->>UI: update selectedFeatureId, increment featuresViewedRef
    User->>UI: Click Upgrade
    UI->>Analytics: track paywall_upgrade_clicked (time_spent, selected_feature, features_viewed)
    UI->>Router: navigate to /settings/billing/plans
    User->>UI: Close paywall (cancel)
    UI->>Analytics: track paywall_cancelled (time_spent, features_viewed, current_feature)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
A rabbit hopped through feature trees,
Swapping flags for sidebars, previews, and breeze.
I tracked the time, I clicked each view,
Hopped to billing when upgrades flew,
Integrations bloom — a carrot-cheer for you!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description provided is comprehensive, covering changes, test plan, and context. However, it does not follow the template structure with required sections like 'Type of Change' and 'Related Issues'. Consider restructuring the description to match the template with explicit sections for Related Issues, Type of Change (e.g., New feature), and Additional Notes for better consistency.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: adding visual demo components to the paywall and refining the billing UI.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx`:
- Around line 254-313: The subscription action handlers
(authClient.subscription.cancel, .restore, .upgrade) currently only use
try/finally and swallow errors; update each block to catch errors, log them with
context (e.g., console.error or processLogger) including the action and
activeOrgId, and show user-facing feedback via toast.error (e.g., "Failed to
downgrade/restore/upgrade plan") before resetting the loading state
(setIsCanceling, setIsRestoring, setIsUpgrading); ensure refetchSubscription is
only awaited on success or handle errors appropriately so the UI reflects
failure.
🧹 Nitpick comments (6)
apps/desktop/src/renderer/components/Paywall/components/demos/MobileAppDemo/MobileAppDemo.tsx (1)

24-43: Consider removing unused isLatest field.

The isLatest property on line 41 is defined but never referenced in the rendering logic. Either remove it or utilize it (e.g., to highlight the latest message).

apps/desktop/src/renderer/components/Paywall/components/demos/IntegrationsDemo/IntegrationsDemo.tsx (1)

4-13: Consider removing unused type field or adding explicit typing.

The type property is defined but never used in the rendering logic. Either remove it or utilize it (e.g., to show different icons for issues vs PRs). Adding explicit TypeScript types would also improve maintainability.

🔧 Optional: Add explicit typing
+type SyncedItem = {
+	id: string;
+	name: string;
+	status: "synced" | "syncing";
+};
+
-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",
-	},
-];
+const SYNCED_ITEMS: SyncedItem[] = [
+	{ id: "1", name: "SUP-142: Fix auth flow", status: "synced" },
+	{ id: "2", name: "PR `#89`: Add workspace sync", status: "synced" },
+	{ id: "3", name: "SUP-156: Mobile responsive", status: "syncing" },
+];
apps/desktop/src/renderer/components/Paywall/components/index.ts (1)

1-1: Consider using explicit named exports to prevent potential circular dependencies.

The export * pattern can inadvertently create circular dependencies as the module grows. Using explicit named exports provides better control and makes the public API surface explicit.

♻️ Suggested explicit exports
-export * from "./demos";
+export {
+	CloudWorkspacesDemo,
+	IntegrationsDemo,
+	MobileAppDemo,
+	TasksDemo,
+	TeamCollaborationDemo,
+} from "./demos";

Based on learnings: "Avoid barrel file abuse with export * from that creates circular dependencies".

apps/desktop/src/renderer/components/Paywall/components/demos/TasksDemo/TasksDemo.tsx (1)

3-34: Consider adding a type for task status.

The status field uses string literals without type constraints. Adding a type would provide better IDE support and catch typos.

🔧 Suggested type definition
+type TaskStatus = "done" | "in-progress" | "todo";
+
+interface Task {
+	id: string;
+	title: string;
+	status: TaskStatus;
+	assignee: string;
+}
+
-const TASKS = [
+const TASKS: Task[] = [
 	{
 		id: "1",
 		title: "Implement user authentication",
 		status: "done",
 		assignee: "SC",
 	},
apps/desktop/src/renderer/components/Paywall/usePaywall.ts (1)

19-19: Type assertion on session plan could accept invalid values.

The cast as UserPlan will accept any string from the API. If the backend ever returns an unexpected value (e.g., "basic", "enterprise"), it would pass through silently. The fallback to "free" provides safety, but you may want to validate explicitly.

🔧 Optional: Add runtime validation
-	const userPlan: UserPlan = (session?.session?.plan as UserPlan) ?? "free";
+	const rawPlan = session?.session?.plan;
+	const userPlan: UserPlan = rawPlan === "pro" ? "pro" : "free";
apps/desktop/src/renderer/components/Paywall/Paywall.tsx (1)

169-172: Simplify IIFE to direct conditional rendering.

The IIFE adds unnecessary complexity. A simpler approach would be more readable.

🔧 Simplified rendering
 <div className="absolute inset-0 flex items-center justify-center">
-	{(() => {
-		const DemoComponent = DEMO_COMPONENTS[selectedFeature.id];
-		return DemoComponent ? <DemoComponent /> : null;
-	})()}
+	{DEMO_COMPONENTS[selectedFeature.id] && (
+		(() => {
+			const Demo = DEMO_COMPONENTS[selectedFeature.id];
+			return <Demo />;
+		})()
+	)}
 </div>

Or extract above the return:

const DemoComponent = DEMO_COMPONENTS[selectedFeature.id];

// ... in JSX:
<div className="absolute inset-0 flex items-center justify-center">
  {DemoComponent && <DemoComponent />}
</div>

Comment on lines +254 to +313
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);
}
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 | 🟠 Major

Add error handling for subscription actions.

The downgrade, restore, and upgrade actions lack user-facing error feedback. If any operation fails, the loading state resets but the user has no indication of failure.

Per coding guidelines: "Never swallow errors silently; at minimum log them with context."

🔧 Proposed fix with error handling
 		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();
+			} catch (error) {
+				console.error("[billing/downgrade] Failed to cancel subscription:", error);
+				toast.error("Failed to downgrade plan. Please try again.");
 			} finally {
 				setIsCanceling(false);
 			}
 			return;
 		}

 		if (action === "restore") {
 			setIsRestoring(true);
 			try {
 				await authClient.subscription.restore({
 					referenceId: activeOrgId,
 				});
 				await refetchSubscription();
 				toast.success("Plan restored");
+			} catch (error) {
+				console.error("[billing/restore] Failed to restore subscription:", error);
+				toast.error("Failed to restore plan. Please try again.");
 			} 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");
 						}
 					},
 				},
 			);
+		} catch (error) {
+			console.error("[billing/upgrade] Failed to upgrade subscription:", error);
+			toast.error("Failed to upgrade plan. Please try again.");
 		} finally {
 			setIsUpgrading(false);
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
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();
} catch (error) {
console.error("[billing/downgrade] Failed to cancel subscription:", error);
toast.error("Failed to downgrade plan. Please try again.");
} finally {
setIsCanceling(false);
}
return;
}
if (action === "restore") {
setIsRestoring(true);
try {
await authClient.subscription.restore({
referenceId: activeOrgId,
});
await refetchSubscription();
toast.success("Plan restored");
} catch (error) {
console.error("[billing/restore] Failed to restore subscription:", error);
toast.error("Failed to restore plan. Please try again.");
} 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");
}
},
},
);
} catch (error) {
console.error("[billing/upgrade] Failed to upgrade subscription:", error);
toast.error("Failed to upgrade plan. Please try again.");
} finally {
setIsUpgrading(false);
}
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx`
around lines 254 - 313, The subscription action handlers
(authClient.subscription.cancel, .restore, .upgrade) currently only use
try/finally and swallow errors; update each block to catch errors, log them with
context (e.g., console.error or processLogger) including the action and
activeOrgId, and show user-facing feedback via toast.error (e.g., "Failed to
downgrade/restore/upgrade plan") before resetting the loading state
(setIsCanceling, setIsRestoring, setIsUpgrading); ensure refetchSubscription is
only awaited on success or handle errors appropriately so the UI reflects
failure.

- Add visual demo components for each paywall feature:
  - MobileAppDemo: Phone mockup with Superset Agent chat
  - TasksDemo: Task list with status indicators
  - TeamCollaborationDemo: Avatar stack with activity feed
  - IntegrationsDemo: Linear + GitHub sync visualization
  - CloudWorkspacesDemo: Device sync diagram

- Add "Coming Soon" badges for Mobile App and Cloud Workspaces
- Use actual Superset {[]} bracket logo in mobile demo
- Make all demo cards same size (300px) and centered
- Remove min-height constraint on description text area
- Update billing:
  - Replace "Unlimited workspaces" with "Task management"
  - Make Workspaces and Projects unlimited for all plans
- Update Mobile App description to two lines
Track user engagement with the paywall modal:
- paywall_opened: when modal opens with trigger source
- paywall_feature_clicked: when user explores features
- paywall_cancelled: when user closes without upgrading
- paywall_upgrade_clicked: when user clicks upgrade button

All events include trigger_source, feature context, time_spent_ms,
and features_viewed_count for conversion funnel analysis.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx`:
- Around line 40-44: Replace the invalid Tailwind class used on the HiArrowPath
icon in IntegrationsDemo (the element with className containing
"animate-spin-slow") with a valid Tailwind animation utility: change the class
reference on HiArrowPath to use an arbitrary animation value like
"animate-[spin_3s_linear_infinite]" or alternatively define a reusable custom
animation in your Tailwind config and use that class name; ensure you update the
className on the HiArrowPath element accordingly.
🧹 Nitpick comments (7)
apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/MobileAppDemo.tsx (2)

1-22: Extract SupersetIcon into its own component file.
Keeps the one‑component‑per‑file structure and makes the icon reusable elsewhere.

As per coding guidelines: Use folder structure with one component per file: ComponentName/ComponentName.tsx with barrel export in index.ts.


47-55: Centralize demo sizing constants.
Hard‑coded values (e.g., 340×700 frame, 50px radius) are design tokens—consider module‑level constants to make updates consistent across demos.

As per coding guidelines: Avoid magic numbers by extracting them to named constants at module top.

apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TasksDemo/TasksDemo.tsx (2)

36-59: Extract SpinnerIcon into its own component file.
Helps maintain the one‑component‑per‑file structure and enables reuse across demos.

As per coding guidelines: Use folder structure with one component per file: ComponentName/ComponentName.tsx with barrel export in index.ts.


63-76: Pull fixed demo sizes into named constants.
The 300px card width and other fixed values will be easier to tune if centralized.

As per coding guidelines: Avoid magic numbers by extracting them to named constants at module top.

apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/FeaturePreview.tsx (1)

13-19: Consider stricter typing for the demo components map.

Using Record<string, ComponentType> allows any string key, but only specific feature IDs are valid. This could be tightened to catch mismatches at compile time.

💡 Suggested type-safe approach
+type FeatureId = ProFeature["id"];
+
-const DEMO_COMPONENTS: Record<string, ComponentType> = {
+const DEMO_COMPONENTS: Record<FeatureId, ComponentType> = {
 	"team-collaboration": TeamCollaborationDemo,
 	integrations: IntegrationsDemo,
 	tasks: TasksDemo,
 	"cloud-workspaces": CloudWorkspacesDemo,
 	"mobile-app": MobileAppDemo,
-};
+} satisfies Record<FeatureId, ComponentType>;

This ensures all feature IDs have a corresponding demo and prevents typos.

apps/desktop/src/renderer/components/Paywall/Paywall.tsx (2)

38-45: Consider memoizing initialFeatureId to avoid recalculation on every render.

initialFeatureId is recalculated on every render despite only depending on triggerSource. While functionally correct (the sync effect at line 62-70 handles updates), wrapping this in useMemo would be cleaner.

Suggested optimization
-	const triggerSource = paywallOptions?.feature;
-	const initialFeatureId =
-		(triggerSource && FEATURE_ID_MAP[triggerSource]) ||
-		PRO_FEATURES[0]?.id ||
-		"team-collaboration";
+	const triggerSource = paywallOptions?.feature;
+	const initialFeatureId = useMemo(
+		() =>
+			(triggerSource && FEATURE_ID_MAP[triggerSource]) ||
+			PRO_FEATURES[0]?.id ||
+			"team-collaboration",
+		[triggerSource],
+	);

149-160: Refactor to use a params object for the paywall function.

Per coding guidelines, functions with 2+ parameters should accept a single params object with named properties instead of positional arguments.

Suggested refactor
-export const paywall = (
-	feature: GatedFeature,
-	context?: Record<string, unknown>,
-) => {
+export const paywall = ({
+	feature,
+	context,
+}: {
+	feature: GatedFeature;
+	context?: Record<string, unknown>;
+}) => {
 	if (!showPaywallFn) {
 		console.error(
 			"[paywall] Paywall not mounted. Make sure to render <Paywall /> in your app",
 		);
 		return;
 	}
 	showPaywallFn({ feature, context });
 };

Note: This is a breaking change for existing call sites. Update callers from paywall(feature, context) to paywall({ feature, context }).

As per coding guidelines: "Functions with 2+ parameters should accept a single params object with named properties instead of positional arguments."

Comment on lines +40 to +44
<div className="flex items-center gap-1">
<div className="w-6 h-px bg-white/20" />
<HiArrowPath className="w-4 h-4 text-white/40 animate-spin-slow" />
<div className="w-6 h-px bg-white/20" />
</div>
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for animate-spin-slow definition in Tailwind config and CSS files
rg "animate-spin-slow|spin-slow" --type-add 'config:*.{js,ts,cjs,mjs,json,css}' -i

Repository: superset-sh/superset

Length of output: 258


🏁 Script executed:

#!/bin/bash
# Find tailwind config files
echo "=== Tailwind config files ==="
fd -e "config\\.ts$" -e "config\\.js$" | grep -i tailwind

# Search for any animation definitions in CSS or config files
echo "=== Searching for animation definitions ==="
rg "@keyframes.*spin|keyframes" --type css --type ts --type js -A 3

# Also check for tailwind theme extensions
echo "=== Checking tailwind theme ==="
rg "theme.*animation|animation.*theme" --type ts --type js -B 2 -A 5

Repository: superset-sh/superset

Length of output: 878


🏁 Script executed:

#!/bin/bash
# Find all config files in the repo
echo "=== All config files ==="
fd "tailwind|config" --type f | head -20

# Check specifically in apps/desktop for CSS and config
echo "=== CSS files in apps/desktop ==="
fd -e "css" apps/desktop/

# Search for spin animation in all files
echo "=== Search for spin in CSS files ==="
rg "spin" --type css -B 2 -A 2

Repository: superset-sh/superset

Length of output: 1440


🏁 Script executed:

#!/bin/bash
# Look for tailwind config at repo root and common locations
echo "=== Looking for tailwind config at root ==="
ls -la | grep tailwind

# Search for any tailwind config file in the entire repo
echo "=== Finding tailwind.config files ==="
find . -maxdepth 2 -name "tailwind.config.*" 2>/dev/null

# Check postcss config which might reference tailwind
echo "=== Postcss configs ==="
find . -name "postcss.config.*" -exec cat {} \;

# Look for animation definitions in all JS/TS files
echo "=== Search for animate-spin-slow or spin-slow in all files ==="
rg "animate-spin-slow|spin.*slow" -t ts -t js -t css

Repository: superset-sh/superset

Length of output: 691


🏁 Script executed:

#!/bin/bash
# Find all CSS files in apps/desktop recursively
echo "=== CSS files in desktop app ==="
find apps/desktop -name "*.css" -type f

# Search for any `@theme` or animation definitions in CSS
echo "=== Global/main CSS files in entire repo ==="
find . -name "global.css" -o -name "styles.css" -o -name "main.css" | head -10

# Check those files for animations
echo "=== Content of desktop-related CSS files ==="
find apps/desktop -name "*.css" -exec echo "FILE: {}" \; -exec cat {} \;

# Also check if animate-spin is used and where it's defined
echo "=== Looking for animate-spin usage ==="
rg "animate-spin" --type css --type ts --type tsx -B 2 -A 2 | head -50

Repository: superset-sh/superset

Length of output: 17884


🏁 Script executed:

#!/bin/bash
# Find tw-animate-css file
echo "=== Searching for tw-animate-css file ==="
find apps/desktop -name "*tw-animate*" -o -name "*animate*css*"

# Also check with rg for the import pattern
echo "=== Looking for animate-spin-slow definition ==="
rg "animate-spin-slow" --type css --type ts --type js -B 5 -A 5

Repository: superset-sh/superset

Length of output: 2001


🏁 Script executed:

#!/bin/bash
# Search more broadly for tw-animate-css
echo "=== Finding tw-animate-css across entire repo ==="
find . -name "*tw-animate*" 2>/dev/null

# Check what's in the same directory as globals.css
echo "=== Files in apps/desktop/src/renderer ==="
ls -la apps/desktop/src/renderer/ | head -20

# Check for any CSS import files
echo "=== CSS files in apps/desktop/src/renderer ==="
find apps/desktop/src/renderer -name "*.css" -type f

# Also try a direct path relative to globals.css
echo "=== Checking for the file at expected location ==="
cat apps/desktop/src/renderer/tw-animate-css.css 2>&1 || echo "File not found"

Repository: superset-sh/superset

Length of output: 1722


🏁 Script executed:

#!/bin/bash
# View the exact import in globals.css
echo "=== First few lines of globals.css ==="
head -10 apps/desktop/src/renderer/globals.css

# Check if tw-animate-css is a package
echo "=== Checking package.json files for tw-animate-css ==="
rg "tw-animate-css" --type json

# Look for any package containing animate
echo "=== Checking for animation-related packages ==="
rg "@tailwindcss|tailwind" --type json | grep -i animate

# Check if animate-spin is available in standard Tailwind
echo "=== Looking for usage of animate-spin ==="
rg "animate-spin" --type tsx --type ts -B 2 -A 2

Repository: superset-sh/superset

Length of output: 733


🌐 Web query:

tw-animate-css npm package animate-spin-slow

💡 Result:

tw-animate-css is a CSS-first replacement for tailwindcss-animate (Tailwind v4 compatible). You install it and import it in your global CSS, then use its utilities like animate-in, animate-out, fade-in, zoom-in, etc. [1][2]

Does it include animate-spin-slow?

No — tw-animate-css does not define a Tailwind class named animate-spin-slow. Its “spin” utilities are for enter/exit transform helpers (e.g., spin-in, spin-out, spin-in-*, spin-out-*), not a animate-spin-slow preset class. [1][2]

How to do a “slow spin” in Tailwind instead

Tailwind’s built-in animate-spin uses the CSS variable --animate-spin (defaults to spin 1s linear infinite). [3] To slow it down, override it per-element:

<div class="animate-spin [--animate-spin:spin_3s_linear_infinite]"></div>

Or use an arbitrary animation value:

<div class="animate-[spin_3s_linear_infinite]"></div>

Sources: npm / README for tw-animate-css [1][2], Tailwind animation docs (v4) [3].

References:
[1] https://www.npmjs.com/package/tw-animate-css
[2] https://github.com/Wombosvideo/tw-animate-css
[3] https://tailwindcss.com/docs/animation


Replace animate-spin-slow with a valid Tailwind animation utility.

The animate-spin-slow class is not defined in tw-animate-css or Tailwind. Use an arbitrary animation value instead:

<HiArrowPath className="w-4 h-4 text-white/40 animate-[spin_3s_linear_infinite]" />

Or create a custom animation in your Tailwind config if you need this reusable across the app.

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx`
around lines 40 - 44, Replace the invalid Tailwind class used on the HiArrowPath
icon in IntegrationsDemo (the element with className containing
"animate-spin-slow") with a valid Tailwind animation utility: change the class
reference on HiArrowPath to use an arbitrary animation value like
"animate-[spin_3s_linear_infinite]" or alternatively define a reusable custom
animation in your Tailwind config and use that class name; ensure you update the
className on the HiArrowPath element accordingly.

@saddlepaddle saddlepaddle merged commit 2d5a76b into main Jan 28, 2026
4 of 5 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/components/Paywall/Paywall.tsx (1)

27-30: Prevent a stale feature preview on open.

selectedFeatureId is only updated in an effect, so the modal can briefly render the previously selected feature. Consider setting it synchronously when opening.

💡 Suggested adjustment
 	showPaywallFn = (options: PaywallOptions) => {
+		const mappedId =
+			FEATURE_ID_MAP[options.feature] || PRO_FEATURES[0]?.id || "team-collaboration";
+		setSelectedFeatureId(mappedId);
 		setPaywallOptions(options);
 		setIsOpen(true);
 	};

Also applies to: 62-70

🤖 Fix all issues with AI agents
In `@apps/desktop/src/renderer/components/Paywall/Paywall.tsx`:
- Around line 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.).
🧹 Nitpick comments (5)
apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/CloudWorkspacesDemo.tsx (1)

16-29: Consider extracting a shared paywall demo card wrapper.
The container/header styles mirror other demo components (e.g., IntegrationsDemo). A small shared wrapper would reduce duplication and keep visual tweaks consistent.

apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx (1)

333-335: Derive the highlighted column from PLAN_CARDS to avoid drift.
A fixed index can fall out of sync if plan ordering changes; compute it from the plan id.

♻️ Suggested refactor
-	const highlightColumnIndex = 1;
+	const highlightColumnIndex = Math.max(
+		0,
+		PLAN_CARDS.findIndex((plan) => plan.id === "pro"),
+	);
 	const highlightColumnStart = highlightColumnIndex + 2;
apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/TeamCollaborationDemo/TeamCollaborationDemo.tsx (1)

37-49: Replace the hard-coded “+3” with a named constant (or derived value).

This number can drift if TEAM_MEMBERS changes. Consider a constant or derived count to keep it aligned.

♻️ Suggested tweak
+const EXTRA_MEMBER_COUNT = 3;
...
-						<div className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-medium text-white/60 bg-white/10 border-2 border-[`#1a1a1a`]">
-							+3
-						</div>
+						<div className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-medium text-white/60 bg-white/10 border-2 border-[`#1a1a1a`]">
+							+{EXTRA_MEMBER_COUNT}
+						</div>

As per coding guidelines: Avoid magic numbers by extracting them to named constants at module top.

apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/MobileAppDemo/MobileAppDemo.tsx (1)

49-55: Consider extracting phone-frame dimensions into named constants.

The frame sizing is currently hard-coded, which makes later tweaks across demos more error-prone.

♻️ Suggested tweak
+const PHONE_FRAME_CLASS = "w-[340px] h-[700px]";
...
-			<div className="absolute right-12 top-10 w-[340px] h-[700px] bg-[`#0a0a0a`] rounded-[50px] border-[8px] border-[`#2a2a2a`] shadow-2xl overflow-hidden">
+			<div className={`absolute right-12 top-10 ${PHONE_FRAME_CLASS} bg-[`#0a0a0a`] rounded-[50px] border-[8px] border-[`#2a2a2a`] shadow-2xl overflow-hidden`}>

As per coding guidelines: Avoid magic numbers by extracting them to named constants at module top.

apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/FeaturePreview.tsx (1)

29-31: Extract preview dimensions into named constants to avoid drift.

The hard-coded width/height are likely shared design tokens across demos.

♻️ Suggested tweak
+const PREVIEW_WIDTH_CLASS = "w-[495px]";
+const PREVIEW_HEIGHT_CLASS = "h-[346px]";
...
-		<div className="flex w-[495px] flex-col">
-			<div className="relative h-[346px] overflow-hidden">
+		<div className={`flex ${PREVIEW_WIDTH_CLASS} flex-col`}>
+			<div className={`relative ${PREVIEW_HEIGHT_CLASS} overflow-hidden`}>

As per coding guidelines: Avoid magic numbers by extracting them to named constants at module top.

Comment on lines +54 to +58
posthog.capture("paywall_opened", {
trigger_source: paywallOptions.feature,
feature_id: initialFeatureId,
feature_title: feature?.title,
});
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.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant