Skip to content

feat: plans page revamp#1534

Open
rohanchkrabrty wants to merge 5 commits intomainfrom
feat-plans-revamp
Open

feat: plans page revamp#1534
rohanchkrabrty wants to merge 5 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 13, 2026 4:36am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Billing page with invoice history, payment method management, and billing cycle details.
    • Added Plans page with feature comparison table and plan selection interface.
    • Added Billing and Plans navigation items to organization settings.
  • Bug Fixes

    • Fixed helper function naming inconsistency.

Walkthrough

This PR introduces comprehensive billing and plans management features to the admin and client SDKs. It adds a BillingView component with billing details, payment methods, and invoice management; a PlansView with plan selection and interval pricing; supporting dialogs and forms; and integrates these views into the client-demo app's organization settings routing. It also corrects a typo in the billing address helper function name.

Changes

Cohort / File(s) Summary
Helper function rename and normalization
web/apps/admin/src/utils/helper.ts, web/sdk/admin/utils/helper.ts, web/sdk/admin/views/organizations/.../billing-details-section.tsx, web/sdk/react/utils/index.ts
Renamed converBillingAddressToString to convertBillingAddressToString (typo fix) across helper modules and consumers; normalized string literals from double to single quotes; adjusted trailing commas and formatting.
Client-demo routing and navigation
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
Added billing and plans routes under /:orgId/settings; created minimal Billing and Plans page wrappers that render BillingView and PlansView; updated settings sidebar navigation with billing and plans menu items.
SDK exports
web/sdk/react/index.ts
Added public re-exports for BillingView and PlansView components from views-new modules.
Billing view and styling
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/index.ts
Implemented BillingView component with invoice fetching, billing state aggregation, and layout rendering (header, payment issue banner, payment method card, billing details card, upcoming billing cycle); added CSS styling for billing containers and link columns.
Billing detail components
web/sdk/react/views-new/billing/components/billing-details-card.tsx, web/sdk/react/views-new/billing/components/billing-details-dialog.tsx
Added BillingDetailsCard (renders current billing info with update/add button); implemented BillingDetailsDialog with form validation via Yup/react-hook-form, mutation handling for updating billing account, and toast notifications.
Billing cycle and payment components
web/sdk/react/views-new/billing/components/payment-method-card.tsx, web/sdk/react/views-new/billing/components/payment-issue.tsx, web/sdk/react/views-new/billing/components/upcoming-billing-cycle.tsx
Added PaymentMethodCard (displays and updates payment method via checkout); PaymentIssue (shows past-due alerts with retry); UpcomingBillingCycle (fetches upcoming invoice/user count, displays plan change options).
Billing dialogs and invoices
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/upcoming-plan-change-banner.tsx
Implemented ConfirmCycleSwitchDialog (confirms plan cycle switches with savings calculations and payment handling); Invoices component (renders invoice DataTable with date/status/amount/actions); UpcomingPlanChangeBanner (displays and cancels scheduled plan changes).
Plans view and styling
web/sdk/react/views-new/plans/plans-view.module.css, web/sdk/react/views-new/plans/plans-view.tsx, web/sdk/react/views-new/plans/index.ts
Implemented PlansView with feature fetching, plan grouping by interval, interval selection state management, and rendering of plan cards and feature comparison table; added CSS for sticky header and flex layout.
Plan card and feature components
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/components/feature-table.module.css, web/sdk/react/views-new/plans/components/feature-table.tsx
Added PlanCard (renders plan pricing/intervals with action button and interval tabs); FeatureTable (renders feature-to-plan comparison matrix with icons and values); added CSS modules for card styling and table layout.
Plan change dialog
web/sdk/react/views-new/plans/components/confirm-plan-change-dialog.tsx
Implemented ConfirmPlanChangeDialog with plan change action logic, checkout routing, subscription verification, downgrade/upgrade messaging, and effective-date calculations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #1397: Introduces converBillingAddressToString helper in admin utils and billing consumers; this PR corrects the typo to convertBillingAddressToString and updates all references.
  • PR #1486: Extends client SDK exports and client-demo routing with new view components; this PR adds billing and plans views to those export/routing surfaces.
  • PR #1483: Adds General/Preferences settings routes and sidebar items under /:orgId/settings; this PR adds Billing/Plans routes to the same settings area.

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

Coverage Report for CI Build 24325869432

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

Comment on lines +113 to +128
taxId: billingAccount.taxData?.[0]?.id || ''
});
}
}, [billingAccount, reset]);

async function onSubmit(data: BillingDetailsFormData) {
if (!billingAccount?.id) return;
try {
const taxData = data.taxId
? [
create(BillingAccount_TaxSchema, {
id: data.taxId,
type: billingAccount.taxData?.[0]?.type || ''
})
]
: [];
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

Potential data loss with multiple tax entries.

The form only handles the first tax entry (taxData?.[0]). If billingAccount.taxData contains multiple entries, only the first one is preserved and re-submitted, silently discarding the rest.

If the API supports multiple tax IDs, consider handling them as an array field. If only one is ever expected, this is fine but worth documenting.

Comment on lines +183 to +185
onSuccess: data => {
window.location.href = data?.checkoutUrl as string;
}
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

Potential navigation to undefined URL.

Same issue as in confirm-plan-change-dialog.tsx - if data?.checkoutUrl is undefined, this will navigate to the string "undefined".

🛡️ Suggested fix
         onSuccess: data => {
-          window.location.href = data?.checkoutUrl as string;
+          if (data?.checkoutUrl) {
+            window.location.href = data.checkoutUrl;
+          }
         }
📝 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
onSuccess: data => {
window.location.href = data?.checkoutUrl as string;
}
onSuccess: data => {
if (data?.checkoutUrl) {
window.location.href = data.checkoutUrl;
}
}

Comment on lines +32 to +34
const onRetryPayment = useCallback(() => {
window.location.href = openInvoices[0]?.hostedUrl || '';
}, [openInvoices]);
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

Guard retry navigation when no hosted invoice URL is available.

At Line 33, falling back to '' can trigger an unintended reload instead of a payment retry path.

Proposed fix
+  const retryUrl = openInvoices[0]?.hostedUrl;
+
   const onRetryPayment = useCallback(() => {
-    window.location.href = openInvoices[0]?.hostedUrl || '';
-  }, [openInvoices]);
+    if (!retryUrl) return;
+    window.location.href = retryUrl;
+  }, [retryUrl]);
...
       <Button
         variant="text"
         color="neutral"
         size="small"
         onClick={onRetryPayment}
+        disabled={!retryUrl}
         data-test-id="frontier-sdk-retry-payment-btn"
       >

Also applies to: 47-53

Comment on lines +58 to +84
async function updatePaymentMethod() {
const query = qs.stringify(
{
details: btoa(
qs.stringify({
organization_id: activeOrganization?.id,
type: 'billing'
})
),
checkout_id: '{{.CheckoutID}}'
},
{ encode: false }
);
const cancel_url = `${config?.billing?.cancelUrl}?${query}`;
const success_url = `${config?.billing?.successUrl}?${query}`;

const resp = await createCheckoutMutation(
create(CreateCheckoutRequestSchema, {
orgId: activeOrganization?.id || '',
cancelUrl: cancel_url,
successUrl: success_url,
setupBody: {
paymentMethod: true,
customerPortal: 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

Validate checkout prerequisites before firing mutation.

At Line 76, an empty orgId and/or missing billing URLs can still reach createCheckout, causing avoidable mutation failures and noisy error toasts.

Proposed fix
   async function updatePaymentMethod() {
+    if (
+      !activeOrganization?.id ||
+      !config?.billing?.cancelUrl ||
+      !config?.billing?.successUrl
+    ) {
+      toastManager.add({
+        title: 'Billing configuration missing',
+        description: 'Unable to start checkout. Please contact support.',
+        type: 'error'
+      });
+      return;
+    }
+
     const query = qs.stringify(
       {
         details: btoa(
           qs.stringify({
             organization_id: activeOrganization?.id,
             type: 'billing'
           })
         ),
         checkout_id: '{{.CheckoutID}}'
       },
       { encode: false }
     );

Also applies to: 91-91

Comment on lines +99 to +117
{isAllowed ? (
<Tooltip>
<Tooltip.Trigger render={<span />}>
<Button
variant="outline"
color="neutral"
size="small"
onClick={updatePaymentMethod}
disabled={isBtnDisabled}
data-test-id="frontier-sdk-update-payment-method-btn"
>
{isPaymentMethodAvailable ? 'Update' : 'Add method'}
</Button>
</Tooltip.Trigger>
<Tooltip.Content>
{AuthTooltipMessage}
</Tooltip.Content>
</Tooltip>
) : null}
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

Authorization tooltip is shown in the allowed branch.

At Line 99, isAllowed currently renders a tooltip whose content says the user lacks access, which is contradictory and confusing.

Proposed fix
-        {isAllowed ? (
-          <Tooltip>
-            <Tooltip.Trigger render={<span />}>
-              <Button
-                variant="outline"
-                color="neutral"
-                size="small"
-                onClick={updatePaymentMethod}
-                disabled={isBtnDisabled}
-                data-test-id="frontier-sdk-update-payment-method-btn"
-              >
-                {isPaymentMethodAvailable ? 'Update' : 'Add method'}
-              </Button>
-            </Tooltip.Trigger>
-            <Tooltip.Content>
-              {AuthTooltipMessage}
-            </Tooltip.Content>
-          </Tooltip>
-        ) : null}
+        {isAllowed ? (
+          <Button
+            variant="outline"
+            color="neutral"
+            size="small"
+            onClick={updatePaymentMethod}
+            disabled={isBtnDisabled}
+            data-test-id="frontier-sdk-update-payment-method-btn"
+          >
+            {isPaymentMethodAvailable ? 'Update' : 'Add method'}
+          </Button>
+        ) : (
+          <Tooltip>
+            <Tooltip.Trigger render={<span />}>
+              <Button variant="outline" color="neutral" size="small" disabled>
+                {isPaymentMethodAvailable ? 'Update' : 'Add method'}
+              </Button>
+            </Tooltip.Trigger>
+            <Tooltip.Content>{AuthTooltipMessage}</Tooltip.Content>
+          </Tooltip>
+        )}

Comment on lines +206 to +214
<Tooltip>
<Tooltip.Trigger render={<span />}>
<InfoCircledIcon />
</Tooltip.Trigger>
<Tooltip.Content>
{NEGATIVE_BALANCE_TOOLTIP_MESSAGE}
</Tooltip.Content>
</Tooltip>
) : null}
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

🧩 Analysis chain

🌐 Web query:

@raystack/apsara-v1 Tooltip component API render prop usage

💡 Result:

The Tooltip component in @raystack/apsara-v1 uses a render prop on the Tooltip.Trigger subcomponent to specify the trigger element. ## Basic Usage import { Tooltip } from '@raystack/apsara'; <Tooltip.Trigger render={Hover me} /> <Tooltip.Content>Helpful information</Tooltip.Content> ## API for Tooltip.Trigger - render: The element that triggers the tooltip. Must be a renderable element (ReactElement). The render prop allows passing any React element as the trigger, such as buttons, icons, or custom components. This provides flexibility for custom trigger rendering while maintaining tooltip behavior on hover/focus. ## Full Structure - Tooltip (root): Manages state (open, onOpenChange, delay) - Tooltip.Trigger: Uses render prop for the trigger element; optional delay override - Tooltip.Content: Displays content (showArrow, side, align, sideOffset, className, style) - Tooltip.Provider: Shared config for multiple tooltips (delay) Examples include controlled tooltips, positioned tooltips, and provider usage, all consistently using Tooltip.Trigger render={...}.

Citations:


Fix Tooltip.Trigger usage: pass the icon via render prop, not as a child.

The Tooltip.Trigger component uses a render prop to specify the trigger element itself. The current pattern combines render={<span />} with a nested child <InfoCircledIcon />, which is incorrect. The render prop should contain the complete trigger element. Use either render={<InfoCircledIcon />} or render={<span><InfoCircledIcon /></span>} instead.

Comment on lines +143 to +146
Your {currentPlanName} will be{' '}
{planAction?.btnDoneLabel.toLowerCase()} to {upcomingPlanName} from{' '}
{expiryDate}.
</Text>
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

Potential runtime error if planAction is undefined.

planAction?.btnDoneLabel.toLowerCase() will throw if btnDoneLabel is undefined or if planAction is undefined and the optional chain doesn't protect the entire expression.

Consider adding a fallback:

🛡️ Suggested fix
 <Text size="small">
   Your {currentPlanName} will be{' '}
-  {planAction?.btnDoneLabel.toLowerCase()} to {upcomingPlanName} from{' '}
+  {planAction?.btnDoneLabel?.toLowerCase() ?? 'changed'} to {upcomingPlanName} from{' '}
   {expiryDate}.
 </Text>
📝 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
Your {currentPlanName} will be{' '}
{planAction?.btnDoneLabel.toLowerCase()} to {upcomingPlanName} from{' '}
{expiryDate}.
</Text>
Your {currentPlanName} will be{' '}
{planAction?.btnDoneLabel?.toLowerCase() ?? 'changed'} to {upcomingPlanName} from{' '}
{expiryDate}.
</Text>

Comment on lines +156 to +163
} else {
checkoutPlan({
planId,
isTrial: false,
onSuccess: data => {
window.location.href = data?.checkoutUrl as string;
}
});
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

Potential navigation to undefined URL.

If checkoutPlan succeeds but data?.checkoutUrl is undefined, window.location.href will be set to "undefined" (string), causing a broken navigation.

🛡️ Suggested fix
       checkoutPlan({
         planId,
         isTrial: false,
         onSuccess: data => {
-          window.location.href = data?.checkoutUrl as string;
+          if (data?.checkoutUrl) {
+            window.location.href = data.checkoutUrl;
+          }
         }
       });

Comment on lines +24 to +26
const value = productMetaDataFeatureValues?.[0] || '-';
const isAvailable = value?.toLowerCase() === 'true';

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Feature type definitions =="
rg -n -C3 'interface Feature|type Feature|metadata' web/sdk/src/types.ts || true

echo
echo "== Plans feature metadata read/write patterns =="
rg -n -C2 'productNames|features|metadata' web/sdk/react/views-new/plans web/sdk/src || true

Repository: raystack/frontier

Length of output: 7619


🏁 Script executed:

rg -n -B5 -A10 'const value = productMetaDataFeatureValues' web/sdk/react/views-new/plans/components/feature-table.tsx

Repository: raystack/frontier

Length of output: 747


🏁 Script executed:

rg -n -B2 -A5 'metadata:' web/sdk/src/types.ts

Repository: raystack/frontier

Length of output: 380


🏁 Script executed:

fd -e ts -e tsx web/sdk/react/views-new/plans/components/feature-table.tsx -x cat -n

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

rg -n 'metadata\s*=' web/sdk/src --type ts --type tsx -A2 -B2

Repository: raystack/frontier

Length of output: 88


🏁 Script executed:

rg -n 'productNames' web/sdk/react/views-new/plans --type ts -B5 -A5

Repository: raystack/frontier

Length of output: 1184


🏁 Script executed:

rg -n 'planFeature\|features\[' web/sdk/react/views-new/plans/components/feature-table.tsx -B3 -A3

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

rg -n 'interface.*Feature|type.*Feature' web/sdk/src/types.ts -B1 -A8

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

cat -n web/sdk/react/views-new/plans/components/feature-table.tsx

Repository: raystack/frontier

Length of output: 3151


🏁 Script executed:

cat -n web/sdk/src/types.ts | head -100

Repository: raystack/frontier

Length of output: 2446


🏁 Script executed:

rg -n 'IntervalPricingWithPlan|interface.*Plan' web/sdk/src/types.ts -A10

Repository: raystack/frontier

Length of output: 858


🏁 Script executed:

rg -n 'Feature.*metadata|get.*planFeature' web/sdk/react/views-new/plans -B2 -A5

Repository: raystack/frontier

Length of output: 1097


🏁 Script executed:

rg -n '@raystack/proton' web/sdk/src/types.ts

Repository: raystack/frontier

Length of output: 121


🏁 Script executed:

rg -n 'Record<string, Feature>' web/sdk/src/types.ts -B3 -A3

Repository: raystack/frontier

Length of output: 234


🏁 Script executed:

rg -n 'export.*Feature\|interface.*Feature' web/sdk/react --type ts -B2 -A8

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

find web/sdk -name "*.ts" -o -name "*.tsx" | xargs grep -l "Feature.*metadata\|metadata.*Record" | head -5

Repository: raystack/frontier

Length of output: 379


🏁 Script executed:

rg -n 'import.*Feature' web/sdk/react/views-new/plans/components/feature-table.tsx -B2 -A2

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

rg -n 'Feature' web/sdk/src/types.ts | grep -i import

Repository: raystack/frontier

Length of output: 121


🏁 Script executed:

rg -n 'as Record<string, number>' web/sdk/react/views-new/plans -B3 -A3

Repository: raystack/frontier

Length of output: 1803


Normalize metadata values to strings before calling .toLowerCase() to prevent runtime errors with numeric metadata.

At Line 25, value?.toLowerCase() assumes a string, but metadata values can be numeric (as evidenced by type assertions elsewhere in plans-view.tsx and confirm-plan-change-dialog.tsx casting metadata as Record<string, number>). Additionally, Line 24 uses || which incorrectly replaces valid falsy values like 0 and false with '-'.

Replace the || operator with nullish coalescing (??) and explicitly convert to string:

-  const value = productMetaDataFeatureValues?.[0] || '-';
-  const isAvailable = value?.toLowerCase() === 'true';
+  const rawValue = productMetaDataFeatureValues?.[0] ?? '-';
+  const displayValue = rawValue == null ? '-' : String(rawValue);
+  const isAvailable = displayValue.toLowerCase() === 'true';

Update the render at Line 37:

-          {value}
+          {displayValue}

Comment on lines +103 to +109
const onPlanActionClick = useCallback(() => {
onConfirmPlanChange?.({
planId: selectedIntervalPricing?.planId,
amount: selectedIntervalPricing?.amount,
currency: selectedIntervalPricing?.currency
});
}, [onConfirmPlanChange, selectedIntervalPricing]);
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

planId could be undefined when invoking callback.

If selectedIntervalPricing is undefined, planId will be undefined. The ConfirmPlanChangeDialog expects a valid planId string.

Consider guarding the callback invocation:

🛡️ Suggested fix
   const onPlanActionClick = useCallback(() => {
+    if (!selectedIntervalPricing?.planId) return;
     onConfirmPlanChange?.({
       planId: selectedIntervalPricing?.planId,
       amount: selectedIntervalPricing?.amount,
       currency: selectedIntervalPricing?.currency
     });
   }, [onConfirmPlanChange, selectedIntervalPricing]);
📝 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
const onPlanActionClick = useCallback(() => {
onConfirmPlanChange?.({
planId: selectedIntervalPricing?.planId,
amount: selectedIntervalPricing?.amount,
currency: selectedIntervalPricing?.currency
});
}, [onConfirmPlanChange, selectedIntervalPricing]);
const onPlanActionClick = useCallback(() => {
if (!selectedIntervalPricing?.planId) return;
onConfirmPlanChange?.({
planId: selectedIntervalPricing?.planId,
amount: selectedIntervalPricing?.amount,
currency: selectedIntervalPricing?.currency
});
}, [onConfirmPlanChange, selectedIntervalPricing]);

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.

2 participants