Skip to content

feat: plans page revamp#1534

Merged
rohanchkrabrty merged 8 commits intomainfrom
feat-plans-revamp
Apr 14, 2026
Merged

feat: plans page revamp#1534
rohanchkrabrty merged 8 commits intomainfrom
feat-plans-revamp

Conversation

@rohanchkrabrty
Copy link
Copy Markdown
Contributor

Summary

  • Migrate plans page from views/plans to views-new/plans using @raystack/apsara-v1 components, CSS Modules, and design tokens
  • Add plan comparison layout with plan cards (name, price, action button, interval toggle) and feature comparison table matching the Figma design
  • Add confirmation dialog (upgrade shows price, downgrade shows warning) that always opens on plan action button click before executing checkout or plan change
  • Wire into client-demo with route, settings nav item, and page wrapper
  • Reuse all existing business logic (usePlans, useFrontier, groupPlansPricingByInterval, listFeatures query) without modification

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Apr 14, 2026 8:15pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b7f9527-0078-4cfe-bf60-d55b5411ed19

📥 Commits

Reviewing files that changed from the base of the PR and between 4a7688d and 484b3a9.

📒 Files selected for processing (1)
  • web/sdk/react/views-new/billing/components/payment-method-card.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/sdk/react/views-new/billing/components/payment-method-card.tsx

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Billing and Plans pages to organization settings for managing subscriptions.
    • Introduced plan comparison table displaying features across available plans.
    • Added plan selection with pricing, interval toggles, and trial options.
    • Implemented plan change confirmation dialog with effective date information.
    • Enhanced subscription management flows including upgrades, downgrades, and cancellations.
  • Improvements

    • Reorganized billing settings layout for better usability.

Walkthrough

This PR adds billing and plans management functionality to the SDK, introducing new view components (PlansView, BillingView), plan selection UI with confirmation dialogs, feature comparison tables, and associated hooks for checkout and subscription management. It also wires these views into the client-demo app's organization settings router.

Changes

Cohort / File(s) Summary
Settings Routing & Navigation
web/apps/client-demo/src/Router.tsx, web/apps/client-demo/src/pages/Settings.tsx
Added routes and navigation items for billing and plans pages under organization settings (/:orgId/settings/billing, /:orgId/settings/plans).
Plans View & Components
web/sdk/react/views-new/plans/plans-view.tsx, web/sdk/react/views-new/plans/components/plan-card.tsx, web/sdk/react/views-new/plans/components/feature-table.tsx, web/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsx, web/sdk/react/views-new/plans/components/*.module.css, web/sdk/react/views-new/plans/index.ts
Introduced plans view with plan cards (including interval selection and trial handling), feature comparison table, and plan-change confirmation dialog. Includes CSS modules for styling.
Plans Logic & Helpers
web/sdk/react/views-new/plans/hooks/use-plans.tsx, web/sdk/react/views-new/plans/helpers/index.ts
Added usePlans hook for checkout, plan changes, subscription cancellation, and verification logic; added groupPlansPricingByInterval helper to organize plan data by pricing intervals.
SDK Exports
web/sdk/react/index.ts
Extended barrel exports to include BillingView and PlansView from the new views modules.
Client Demo Pages
web/apps/client-demo/src/pages/settings/Plans.tsx
Added Plans settings page component wrapping the SDK's PlansView.
Billing Refinements
web/sdk/react/views/billing/billing-page.tsx, web/sdk/react/views-new/billing/components/payment-method-card.tsx
Reorganized billing layout, added KYC status prop to billing details, refactored tooltip logic in payment method card to conditionally display only when disabled.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
  • rsbh

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.

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 13, 2026

Coverage Report for CI Build 24420743019

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage remained the same at 41.606%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 36442
Covered Lines: 15162
Line Coverage: 41.61%
Coverage Strength: 11.89 hits per line

💛 - Coveralls

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: 10

🧹 Nitpick comments (15)
web/sdk/react/views-new/billing/components/billing-details-card.tsx (1)

36-58: Tooltip logic is correct but could be clearer.

The disabled={!isButtonDisabled} on Tooltip.Trigger means the tooltip only activates when the button is disabled. This is correct behavior, but the double-negative makes it harder to read. Consider extracting to a named variable:

const showTooltip = isButtonDisabled;
// ...
<Tooltip.Trigger disabled={!showTooltip} ...>
web/sdk/react/views-new/billing/components/upcoming-plan-change-banner.tsx (2)

12-13: Consolidate duplicate imports from the same module.

The imports from @connectrpc/connect-query are split across two lines. Consolidate them for cleaner code.

♻️ Suggested fix
-import { useMutation } from '@connectrpc/connect-query';
-import { createConnectQueryKey, useTransport } from '@connectrpc/connect-query';
+import { useMutation, createConnectQueryKey, useTransport } from '@connectrpc/connect-query';

96-102: Unsafe type assertion on metadata.

Casting metadata as Record<string, number> assumes all values are numbers, but metadata is typically a flexible key-value store that may contain strings. If weightage is stored as a string, Number() will handle it, but consider using a safer approach or validating the type.

web/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsx (2)

177-187: Unnecessary loading state for synchronous operation.

The setIsNewPlanLoading(true/false) wraps a synchronous Array.find() operation. This will cause an unnecessary render cycle and the loading state will never be visible to users.

♻️ Suggested fix
   useEffect(() => {
     if (planId) {
-      setIsNewPlanLoading(true);
-      try {
-        const plan = isNewPlanBasePlan ? basePlan : currentPlan;
-        if (plan) setNewPlan(plan);
-      } finally {
-        setIsNewPlanLoading(false);
-      }
+      const plan = isNewPlanBasePlan ? basePlan : currentPlan;
+      if (plan) setNewPlan(plan);
     }
   }, [planId, isNewPlanBasePlan, basePlan, currentPlan]);

Then remove isNewPlanLoading from the isLoading computation on line 189.


102-102: Fragile string comparison for determining upgrade status.

Comparing planAction.btnLabel === 'Upgrade' couples business logic to UI text. If the label is localized or changed, this breaks.

Consider using a semantic property like planAction.isUpgrade or comparing weightage directly.

web/sdk/react/views-new/billing/components/confirm-cycle-switch-dialog.tsx (2)

176-209: onConfirm should be memoized with useCallback.

This async function is defined inside the component body without memoization, causing it to be recreated on every render. While not critical since it's only passed to an onClick handler, it's inconsistent with patterns in other dialogs.

♻️ Suggested fix
-  async function onConfirm() {
+  const onConfirm = useCallback(async () => {
     const nextPlanId = nextPlan?.id;
     if (!nextPlanId) return;
     // ... rest of function
-  }
+  }, [nextPlan?.id, isPaymentMethodRequired, checkoutPlan, changePlan, verifyPlanChange, handle, nextPlanIntervalName, dateFormat, isUpgrade]);

246-248: Limited currency symbol formatting.

Only USD gets the $ symbol. Other currencies will display as {amount} without any symbol. Consider using Intl.NumberFormat or the Amount component used elsewhere for consistent currency formatting.

web/sdk/react/views-new/billing/components/invoices.tsx (1)

29-95: Consider guarding against NaN when coercing amount.

On line 64, if getValue() returns undefined or null, Number() will produce NaN. The Amount component may not handle this gracefully.

🛡️ Suggested defensive check
   cell: ({ row, getValue }) => {
-    const value = Number(getValue());
+    const rawValue = getValue();
+    const value = rawValue != null ? Number(rawValue) : 0;
     return (
       <Text size="regular" variant="secondary">
         <Amount currency={row?.original?.currency} value={value} />
       </Text>
     );
   }
web/sdk/react/views-new/billing/billing-view.tsx (1)

27-29: Module-scope dialog handles are shared across component instances.

These handles are created at module scope, meaning all instances of BillingView share the same dialog state. This is typically fine for singleton views but could cause unexpected behavior if multiple BillingView instances are rendered simultaneously.

web/sdk/react/views-new/plans/plans-view.tsx (3)

52-60: Consider adding error handling for features query.

Unlike the billing view which handles invoice fetch errors with a toast, this component silently ignores errors from listFeatures. If the features query fails, the UI might show an incomplete feature table without user feedback.

🛡️ Suggested error handling
- const { data: featuresData } = useQuery(
+ const { data: featuresData, error: featuresError } = useQuery(
   FrontierServiceQueries.listFeatures,
   create(ListFeaturesRequestSchema, {})
 );
+
+ useEffect(() => {
+   if (featuresError) {
+     toastManager.add({
+       title: 'Failed to load plan features',
+       description: featuresError?.message,
+       type: 'error'
+     });
+   }
+ }, [featuresError]);

104-123: Potential stale closure: selectedIntervals read inside effect but not in deps.

The effect reads selectedIntervals[plan.slug] on line 109 to skip already-set intervals, but selectedIntervals is not in the dependency array. On subsequent runs, the effect sees a stale snapshot.

This may be intentional (only initialize defaults once per plan), but it can cause subtle bugs if the logic changes. Consider using a functional update or ref if this is intentional.

♻️ Using functional update to avoid stale closure
 useEffect(() => {
   if (groupedPlans.length === 0) return;

-  const defaults: Record<string, IntervalKeys> = {};
-  groupedPlans.forEach(plan => {
-    if (selectedIntervals[plan.slug]) return;
-    const planIntervals = Object.values(plan.intervals)
-      .sort((a, b) => a.weightage - b.weightage)
-      .map(i => i.interval);
-    const activePlanInterval = Object.values(plan.intervals).find(
-      p => p.planId === activeSubscription?.planId
-    );
-    defaults[plan.slug] =
-      activePlanInterval?.interval || planIntervals[0] || 'year';
-  });
-
-  if (Object.keys(defaults).length > 0) {
-    setSelectedIntervals(prev => ({ ...prev, ...defaults }));
-  }
+  setSelectedIntervals(prev => {
+    const defaults: Record<string, IntervalKeys> = {};
+    groupedPlans.forEach(plan => {
+      if (prev[plan.slug]) return;
+      const planIntervals = Object.values(plan.intervals)
+        .sort((a, b) => a.weightage - b.weightage)
+        .map(i => i.interval);
+      const activePlanInterval = Object.values(plan.intervals).find(
+        p => p.planId === activeSubscription?.planId
+      );
+      defaults[plan.slug] =
+        activePlanInterval?.interval || planIntervals[0] || 'year';
+    });
+    return Object.keys(defaults).length > 0 ? { ...prev, ...defaults } : prev;
+  });
 }, [groupedPlans, activeSubscription?.planId]);

125-132: Consider memoizing currentPlanPricing.

This nested loop runs on every render. While the dataset is likely small, memoizing it would be more consistent with the other derived values in this component.

♻️ Memoized version
- let currentPlanPricing: IntervalPricingWithPlan | undefined;
- groupedPlans.forEach(group => {
-   Object.values(group.intervals).forEach(plan => {
-     if (plan.planId === activeSubscription?.planId) {
-       currentPlanPricing = plan;
-     }
-   });
- });
+ const currentPlanPricing = useMemo(() => {
+   for (const group of groupedPlans) {
+     for (const plan of Object.values(group.intervals)) {
+       if (plan.planId === activeSubscription?.planId) {
+         return plan;
+       }
+     }
+   }
+   return undefined;
+ }, [groupedPlans, activeSubscription?.planId]);
web/sdk/react/views-new/billing/components/upcoming-billing-cycle.tsx (3)

70-100: Inconsistent org ID usage between queries.

The invoice query uses activeOrganization?.id (line 77), while the member count query uses billingAccount?.orgId (line 94). These should typically be the same, but if they ever differ, it could cause subtle bugs.

Consider using a consistent source for the organization ID.


134-142: Error toast may fire multiple times for different errors.

The error variable combines both memberCountError and invoiceError using ||. If both errors occur, only the first truthy one is displayed. Additionally, if one error clears and another appears, the effect will fire again.

Consider handling each error independently or using a more robust error aggregation strategy.

♻️ Handle errors independently
- const error = memberCountError || invoiceError;
- useEffect(() => {
-   if (error) {
-     toastManager.add({
-       title: 'Failed to get upcoming billing cycle details',
-       type: 'error'
-     });
-   }
- }, [error]);
+ useEffect(() => {
+   if (invoiceError) {
+     toastManager.add({
+       title: 'Failed to load upcoming invoice',
+       type: 'error'
+     });
+   }
+ }, [invoiceError]);
+
+ useEffect(() => {
+   if (memberCountError) {
+     toastManager.add({
+       title: 'Failed to load member count',
+       type: 'error'
+     });
+   }
+ }, [memberCountError]);

154-154: Consider specifying skeleton dimensions for better loading UX.

A bare <Skeleton /> may render with default dimensions. Specifying width and height would provide a more predictable placeholder that matches the final rendered content.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 100c21b4-2bf0-4923-b853-0c10014c94d8

📥 Commits

Reviewing files that changed from the base of the PR and between 73227f6 and bcf854a.

📒 Files selected for processing (28)
  • web/apps/admin/src/utils/helper.ts
  • web/apps/client-demo/src/Router.tsx
  • web/apps/client-demo/src/pages/Settings.tsx
  • web/apps/client-demo/src/pages/settings/Billing.tsx
  • web/apps/client-demo/src/pages/settings/Plans.tsx
  • web/sdk/admin/utils/helper.ts
  • web/sdk/admin/views/organizations/details/side-panel/billing-details-section.tsx
  • web/sdk/react/index.ts
  • web/sdk/react/utils/index.ts
  • web/sdk/react/views-new/billing/billing-view.module.css
  • web/sdk/react/views-new/billing/billing-view.tsx
  • web/sdk/react/views-new/billing/components/billing-details-card.tsx
  • web/sdk/react/views-new/billing/components/billing-details-dialog.tsx
  • web/sdk/react/views-new/billing/components/confirm-cycle-switch-dialog.tsx
  • web/sdk/react/views-new/billing/components/invoices.tsx
  • web/sdk/react/views-new/billing/components/payment-issue.tsx
  • web/sdk/react/views-new/billing/components/payment-method-card.tsx
  • web/sdk/react/views-new/billing/components/upcoming-billing-cycle.tsx
  • web/sdk/react/views-new/billing/components/upcoming-plan-change-banner.tsx
  • web/sdk/react/views-new/billing/index.ts
  • web/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsx
  • web/sdk/react/views-new/plans/components/feature-table.module.css
  • web/sdk/react/views-new/plans/components/feature-table.tsx
  • web/sdk/react/views-new/plans/components/plan-card.module.css
  • web/sdk/react/views-new/plans/components/plan-card.tsx
  • web/sdk/react/views-new/plans/index.ts
  • web/sdk/react/views-new/plans/plans-view.module.css
  • web/sdk/react/views-new/plans/plans-view.tsx

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: 2

🧹 Nitpick comments (6)
web/sdk/react/views-new/plans/helpers/index.ts (3)

11-24: Redundant metadata extraction.

metaData (line 11) and planMetadata (line 24) perform identical operations. Consider reusing the first variable.

♻️ Proposed fix
   const metaData = (plan?.metadata as Record<string, string>) || {};
   const slug = metaData?.plan_group_id || makePlanSlug(plan);
   plansMap[slug] = plansMap[slug] || {
     slug: slug,
     title: plan.title,
     description: plan?.description,
     weightage: 0,
     intervals: {},
     features: {}
   };
   const planInterval = (plan?.interval || '') as IntervalKeys;
   const productPrices = getPlanPrice(plan);

-  const planMetadata = (plan?.metadata as Record<string, string>) || {};
   plansMap[slug].intervals[planInterval] = {
     planId: plan?.id || '',
     planName: plan?.name || '',
     interval: planInterval,
-    weightage: planMetadata?.weightage ? Number(planMetadata?.weightage) : 0,
+    weightage: metaData?.weightage ? Number(metaData?.weightage) : 0,
     productNames: [],
     trialDays: plan?.trialDays ? String(plan.trialDays) : '',
     features: {},
     ...productPrices
   };

13-20: Remove unused features property from group-level initialization.

The features: {} property (line 19) is initialized at the group level but never populated—features are only stored at the interval level (line 32). Additionally, PlanIntervalPricing type doesn't include a root-level features property, so this creates an extra untyped property.

♻️ Proposed fix
   plansMap[slug] = plansMap[slug] || {
     slug: slug,
     title: plan.title,
     description: plan?.description,
     weightage: 0,
-    intervals: {},
-    features: {}
+    intervals: {}
   };

36-45: Consider using push() instead of spread for accumulation.

The spread pattern [...arr, newItem] creates a new array on each iteration. For better performance with many products, use push() directly.

♻️ Proposed fix
   plan?.products?.forEach(product => {
-    plansMap[slug].intervals[planInterval].productNames = [
-      ...plansMap[slug].intervals[planInterval].productNames,
-      product.name || ''
-    ];
+    plansMap[slug].intervals[planInterval].productNames.push(product.name || '');
     product.features?.forEach(feature => {
       plansMap[slug].intervals[planInterval].features[feature?.title || ''] =
         feature;
     });
   });
web/sdk/react/views-new/plans/hooks/use-plans.tsx (3)

71-74: Memoize planMap to avoid recalculation on every render.

planMap is rebuilt on each render. Since it only depends on allPlans, wrap it in useMemo.

♻️ Proposed fix
+import { useCallback, useState, useMemo } from 'react';
...
-  const planMap = allPlans.reduce((acc, p) => {
-    if (p.id) acc[p.id] = p;
-    return acc;
-  }, {} as Record<string, Plan>);
+  const planMap = useMemo(
+    () =>
+      allPlans.reduce((acc, p) => {
+        if (p.id) acc[p.id] = p;
+        return acc;
+      }, {} as Record<string, Plan>),
+    [allPlans]
+  );

76-78: Consider memoizing isCurrentlyTrialing.

This value is recalculated on every render. If subscriptions is stable, consider wrapping in useMemo.


245-249: getSubscribedPlans depends on unmemoized planMap.

Since planMap is recreated each render, the useCallback for getSubscribedPlans may not prevent unnecessary recalculations. This is resolved by memoizing planMap as suggested earlier.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b79db317-5725-459b-82db-0de691e3dde3

📥 Commits

Reviewing files that changed from the base of the PR and between bcf854a and 4a7688d.

📒 Files selected for processing (9)
  • web/sdk/react/views-new/billing/components/payment-method-card.tsx
  • web/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsx
  • web/sdk/react/views-new/plans/components/plan-card.module.css
  • web/sdk/react/views-new/plans/components/plan-card.tsx
  • web/sdk/react/views-new/plans/helpers/index.ts
  • web/sdk/react/views-new/plans/hooks/use-plans.tsx
  • web/sdk/react/views-new/plans/index.ts
  • web/sdk/react/views-new/plans/plans-view.tsx
  • web/sdk/react/views/billing/billing-page.tsx
✅ Files skipped from review due to trivial changes (2)
  • web/sdk/react/views-new/plans/components/plan-card.module.css
  • web/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • web/sdk/react/views-new/plans/index.ts
  • web/sdk/react/views-new/billing/components/payment-method-card.tsx
  • web/sdk/react/views-new/plans/components/plan-card.tsx
  • web/sdk/react/views-new/plans/plans-view.tsx

@rohanchkrabrty rohanchkrabrty enabled auto-merge (squash) April 14, 2026 20:15
@rohanchkrabrty rohanchkrabrty merged commit 6ff1ad5 into main Apr 14, 2026
8 checks passed
@rohanchkrabrty rohanchkrabrty deleted the feat-plans-revamp branch April 14, 2026 20:20
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.

3 participants