Skip to content

feat: billing page revamp#1518

Open
rohanchkrabrty wants to merge 1 commit intomainfrom
feat-billing-revamp
Open

feat: billing page revamp#1518
rohanchkrabrty wants to merge 1 commit intomainfrom
feat-billing-revamp

Conversation

@rohanchkrabrty
Copy link
Copy Markdown
Contributor

Summary

  • Migrate billing page from views/billing to views-new/billing using apsara-v1 components, CSS Modules with design tokens, and the ViewContainer/ViewHeader pattern
  • Add new BillingDetailsDialog with inline form (Name, Email, Address, Pincode, City, State, Country, Tax ID) that calls updateBillingAccount API directly, replacing the previous Stripe customer portal redirect
  • Update ConfirmCycleSwitchDialog to match Figma design with inline bold text layout and a new savings line ("You can save $X with Y cycle")
  • Wire billing route, nav item, and page wrapper in client-demo app
  • Use Dialog.createHandle pattern with handles scoped in the parent view and passed as props to avoid non-portable DTS build errors

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 8, 2026

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Apr 8, 2026 4:28am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Summary by CodeRabbit

New Features

  • Added a new Billing section under Settings with comprehensive billing management capabilities
  • Users can now view, update payment methods, and manage billing account details
  • Switch between different billing cycles and view scheduled plan changes
  • Display invoices and notifications for any payment issues requiring attention

Walkthrough

This pull request introduces a comprehensive billing management feature by adding a new billing page to the client-demo application and exposing a complete BillingView component from the React SDK. The changes include routing and navigation updates for the settings section, a billing page wrapper, and eight new billing-related components (billing view, details card/dialog, payment method card, cycle switching, invoices display, payment issue handling, upcoming billing cycle, and plan change banner).

Changes

Cohort / File(s) Summary
Settings Routing & Navigation (client-demo)
web/apps/client-demo/src/Router.tsx, web/apps/client-demo/src/pages/Settings.tsx
Added "billing" sub-route under /:orgId/settings and a "Billing" sidebar navigation item in the Settings page.
Billing Page Wrapper (client-demo)
web/apps/client-demo/src/pages/settings/Billing.tsx
New component that imports and renders BillingView from the React SDK.
SDK Barrel Exports
web/sdk/react/index.ts, web/sdk/react/views-new/billing/index.ts
Added public exports for BillingView and BillingViewProps to expose the billing feature from the SDK.
Billing View Component
web/sdk/react/views-new/billing/billing-view.tsx, web/sdk/react/views-new/billing/billing-view.module.css
Core billing page component managing state, queries, dialog handles, and layout; includes CSS styling for containers, boxes, separators, and form body.
Billing Sub-Components
web/sdk/react/views-new/billing/components/billing-details-card.tsx, billing-details-dialog.tsx, confirm-cycle-switch-dialog.tsx, invoices.tsx, payment-issue.tsx, payment-method-card.tsx, upcoming-billing-cycle.tsx, upcoming-plan-change-banner.tsx
Eight components implementing specific billing UI sections: account details display/editing, cycle switching with plan comparison, invoice listing, payment issue handling, payment method management, upcoming billing cycle info, and scheduled plan change cancellation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
  • rsbh
  • paanSinghCoder

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.

@rohanchkrabrty rohanchkrabrty changed the title feat(billing): revamp billing page with views-new pattern feat: billing page revamp Apr 8, 2026
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 24117711088

Coverage remained the same at 41.146%

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: 36298
Covered Lines: 14935
Line Coverage: 41.15%
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: 4

🧹 Nitpick comments (10)
web/sdk/react/views-new/billing/billing-view.module.css (2)

4-4: Consider using 1px instead of 0.5px for border width.

Sub-pixel border values like 0.5px may render inconsistently across browsers and display densities—some browsers round to 0px or 1px depending on the device pixel ratio.


48-51: Consider using a CSS custom property or viewport-relative unit for max-height.

The hardcoded 424px value may not adapt well to different viewport sizes or when the dialog content varies. Consider using a design token or a viewport-relative unit like max-height: min(424px, 70vh).

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

36-36: Skeleton lacks dimensions.

The <Skeleton /> element renders without explicit width/height, which may cause layout shifts or render as a minimal placeholder. Consider adding dimensions to match the expected banner size.

♻️ Proposed fix
- if (isLoading) return <Skeleton />;
+ if (isLoading) return <Skeleton width="100%" height={48} />;
web/sdk/react/views-new/billing/components/invoices.tsx (1)

107-109: Consider memoizing columns to avoid recreation on every render.

getColumns is called on every render. While the performance impact is minimal, memoizing with useMemo would be more idiomatic when the columns depend on config values.

♻️ Proposed fix
+import { useMemo } from 'react';

export function Invoices({ isLoading, invoices }: InvoicesProps) {
  const { config } = useFrontier();

- const columns = getColumns({
-   dateFormat: config?.dateFormat || DEFAULT_DATE_FORMAT
- });
+ const columns = useMemo(
+   () => getColumns({ dateFormat: config?.dateFormat || DEFAULT_DATE_FORMAT }),
+   [config?.dateFormat]
+ );
web/sdk/react/views-new/billing/components/payment-method-card.tsx (2)

100-113: Tooltip wrapper is missing content.

The Tooltip component wraps the button but never renders Tooltip.Content, so it serves no purpose. Either remove the Tooltip wrapper or add appropriate content (e.g., a disabled state message similar to BillingDetailsCard).

Proposed fix: Remove unnecessary Tooltip wrapper
       {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>
+          <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>
        ) : null}

85-88: No feedback when checkout URL is missing.

If the mutation succeeds but checkoutUrl is empty or undefined, the user receives no feedback. Consider adding a fallback toast notification.

Proposed fix
     const checkoutUrl = resp?.checkoutSession?.checkoutUrl;
     if (checkoutUrl) {
       window.location.href = checkoutUrl;
+    } else {
+      toastManager.add({
+        title: 'Unable to start checkout',
+        description: 'No checkout URL was returned. Please try again.',
+        type: 'error'
+      });
     }
web/sdk/react/views-new/billing/billing-view.tsx (1)

27-29: Module-scoped dialog handles create singleton state.

These handles are created at module scope, meaning all instances of BillingView would share the same dialog state. If this component is ever rendered multiple times (e.g., in different routes that remain mounted), opening a dialog from one instance would affect all instances. Consider moving the handle creation inside the component or using useMemo.

Proposed fix: Move handles inside component
-const cycleSwitchDialogHandle =
-  Dialog.createHandle<ConfirmCycleSwitchPayload>();
-const billingDetailsDialogHandle = Dialog.createHandle();
-
 export interface BillingViewProps {
   onNavigateToPlans?: () => void;
 }

 export function BillingView({ onNavigateToPlans }: BillingViewProps) {
+  const cycleSwitchDialogHandle = useMemo(
+    () => Dialog.createHandle<ConfirmCycleSwitchPayload>(),
+    []
+  );
+  const billingDetailsDialogHandle = useMemo(
+    () => Dialog.createHandle(),
+    []
+  );
+
   const {
web/sdk/react/views-new/billing/components/upcoming-billing-cycle.tsx (1)

134-142: Error toast loses specificity.

Both memberCountError and invoiceError trigger the same generic toast message. Consider differentiating or including the error message for debugging.

Proposed fix
   const error = memberCountError || invoiceError;
   useEffect(() => {
     if (error) {
       toastManager.add({
         title: 'Failed to get upcoming billing cycle details',
+        description: error instanceof Error ? error.message : undefined,
         type: 'error'
       });
     }
   }, [error]);
web/sdk/react/views-new/billing/components/confirm-cycle-switch-dialog.tsx (2)

17-17: Import only needed lodash function.

Importing the entire lodash library (import * as _) for a single isEmpty call increases bundle size. Import only what's needed.

Proposed fix
-import * as _ from 'lodash';
+import isEmpty from 'lodash/isEmpty';

Then update line 121:

-    _.isEmpty(paymentMethod) && nextPlanPrice.amount > 0;
+    isEmpty(paymentMethod) && nextPlanPrice.amount > 0;

243-250: Currency symbol handling is limited.

Only USD gets a symbol ($). Other currencies will display just the amount without a symbol. Consider using a more robust currency formatter or the Amount component used elsewhere in the billing views.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a8871bb8-8cdd-414c-b14a-40acbff38821

📥 Commits

Reviewing files that changed from the base of the PR and between 862e133 and 0420c0e.

📒 Files selected for processing (15)
  • 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/sdk/react/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

Comment on lines +183 to +186
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

Unsafe cast of potentially undefined checkoutUrl.

If data?.checkoutUrl is undefined, casting it as string will pass the type check but window.location.href will receive undefined, causing navigation to a literal "undefined" URL. Add a guard.

Proposed fix
       checkoutPlan({
         planId: nextPlanId,
         isTrial: false,
         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 +190 to +208
onSuccess: async () => {
const planPhase = await verifyPlanChange({
planId: nextPlanId
});
if (planPhase) {
handle.close();
const changeDate = timestampToDayjs(
planPhase?.effectiveAt
)?.format(dateFormat);
toastManager.add({
title: 'Plan cycle switch successful',
description: `Your plan cycle will be switched to ${nextPlanIntervalName} on ${changeDate}`,
type: 'success'
});
}
},
immediate: isUpgrade
});
}
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

No user feedback if verifyPlanChange returns undefined.

Per the codebase, verifyPlanChange performs a single fetch (not polling). If the subscription phase isn't immediately available after changePlan completes, planPhase will be undefined, and the user receives no feedback—the dialog remains open with no indication of success or failure.

Consider adding an else branch or implementing retry logic.

Proposed fix: Add fallback feedback
           const planPhase = await verifyPlanChange({
             planId: nextPlanId
           });
           if (planPhase) {
             handle.close();
             const changeDate = timestampToDayjs(
               planPhase?.effectiveAt
             )?.format(dateFormat);
             toastManager.add({
               title: 'Plan cycle switch successful',
               description: `Your plan cycle will be switched to ${nextPlanIntervalName} on ${changeDate}`,
               type: 'success'
             });
+          } else {
+            handle.close();
+            toastManager.add({
+              title: 'Plan cycle switch initiated',
+              description: 'Your plan change is being processed.',
+              type: 'success'
+            });
           }
📝 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: async () => {
const planPhase = await verifyPlanChange({
planId: nextPlanId
});
if (planPhase) {
handle.close();
const changeDate = timestampToDayjs(
planPhase?.effectiveAt
)?.format(dateFormat);
toastManager.add({
title: 'Plan cycle switch successful',
description: `Your plan cycle will be switched to ${nextPlanIntervalName} on ${changeDate}`,
type: 'success'
});
}
},
immediate: isUpgrade
});
}
onSuccess: async () => {
const planPhase = await verifyPlanChange({
planId: nextPlanId
});
if (planPhase) {
handle.close();
const changeDate = timestampToDayjs(
planPhase?.effectiveAt
)?.format(dateFormat);
toastManager.add({
title: 'Plan cycle switch successful',
description: `Your plan cycle will be switched to ${nextPlanIntervalName} on ${changeDate}`,
type: 'success'
});
} else {
handle.close();
toastManager.add({
title: 'Plan cycle switch initiated',
description: 'Your plan change is being processed.',
type: 'success'
});
}
},
immediate: isUpgrade
});
}

Comment on lines +32 to +34
const onRetryPayment = useCallback(() => {
window.location.href = openInvoices[0]?.hostedUrl || '';
}, [openInvoices]);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against empty or missing hostedUrl before navigation.

If openInvoices is empty or hostedUrl is undefined, setting window.location.href = '' can cause unexpected navigation (typically reloads the current page or navigates to the root). Consider disabling the retry button or showing a fallback message when no valid invoice URL is available.

🛡️ Proposed fix
+ const retryUrl = openInvoices[0]?.hostedUrl;
+
  const onRetryPayment = useCallback(() => {
-   window.location.href = openInvoices[0]?.hostedUrl || '';
-  }, [openInvoices]);
+   if (retryUrl) {
+     window.location.href = retryUrl;
+   }
+  }, [retryUrl]);

  if (isLoading) return <Skeleton />;
  if (!isPastDue) return null;
+ if (!retryUrl) return null; // Or show a message to contact support

  return (
📝 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 onRetryPayment = useCallback(() => {
window.location.href = openInvoices[0]?.hostedUrl || '';
}, [openInvoices]);
const retryUrl = openInvoices[0]?.hostedUrl;
const onRetryPayment = useCallback(() => {
if (retryUrl) {
window.location.href = retryUrl;
}
}, [retryUrl]);

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.

@rohilsurana @rsbh What should be the fallback here? All invoices page? Empty string as fallback can cause issues.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +142 to +146
<Text size="small">
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 undefined access on planAction.

If getPlanChangeAction returns undefined (e.g., when weightages are equal), planAction?.btnDoneLabel will be undefined, and calling .toLowerCase() on it will throw. The optional chaining only prevents accessing btnDoneLabel on undefined, but .toLowerCase() is still called on the result.

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

} from '@raystack/proton/frontier';
import { useMutation } from '@connectrpc/connect-query';
import { create } from '@bufbuild/protobuf';
import { AuthTooltipMessage } from '../../../utils';
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.

Please check if this import is unused.

import { DEFAULT_DATE_FORMAT } from '../../../utils/constants';
import { timestampToDayjs } from '../../../../utils/timestamp';
import { usePlans } from '../../../views/plans/hooks/usePlans';
import * as _ from 'lodash';
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.

Lets avoid * import. Please check if this can be a named import. If it's a big effort lets pick this later.

I think we are only using isEmpty from lodash.

</Text>
{isAllowed ? (
<Tooltip>
<Tooltip.Trigger render={<span />}>
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.

Tooltip doesn't have Content. Please add.


import { Button, Skeleton, Text, Flex, Tooltip } from '@raystack/apsara-v1';
import type { BillingAccount } from '@raystack/proton/frontier';
import { converBillingAddressToString } from '../../../utils';
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.

converBillingAddressToString lets fix the typo and all reference. conver -> convert

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