feat(paywall): add visual demos and polish billing UI#1040
feat(paywall): add visual demos and polish billing UI#1040saddlepaddle merged 6 commits intomainfrom
Conversation
📝 WalkthroughWalkthroughRefactors 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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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 unusedisLatestfield.The
isLatestproperty 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 unusedtypefield or adding explicit typing.The
typeproperty 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 * fromthat 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 UserPlanwill 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>
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
00d1f77 to
846225a
Compare
There was a problem hiding this comment.
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: ExtractSupersetIconinto 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.tsxwith barrel export inindex.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: ExtractSpinnerIconinto 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.tsxwith barrel export inindex.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 memoizinginitialFeatureIdto avoid recalculation on every render.
initialFeatureIdis recalculated on every render despite only depending ontriggerSource. While functionally correct (the sync effect at line 62-70 handles updates), wrapping this inuseMemowould 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 thepaywallfunction.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)topaywall({ feature, context }).As per coding guidelines: "Functions with 2+ parameters should accept a single params object with named properties instead of positional arguments."
| <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> |
There was a problem hiding this comment.
🧩 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}' -iRepository: 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 5Repository: 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 2Repository: 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 cssRepository: 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 -50Repository: 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 5Repository: 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 2Repository: 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.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
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.
selectedFeatureIdis 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 fromPLAN_CARDSto 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.
| posthog.capture("paywall_opened", { | ||
| trigger_source: paywallOptions.feature, | ||
| feature_id: initialFeatureId, | ||
| feature_title: feature?.title, | ||
| }); |
There was a problem hiding this comment.
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.).
Summary
Test plan
Summary by CodeRabbit
New Features
Changes & Improvements
Chores
✏️ Tip: You can customize this high-level summary in your review settings.