- Oversee your billing and invoices.{' '}
- For more details, contact{' '}
-
- {billingSupportEmail}
-
- >
- ) : (
- 'Oversee your billing and invoices.'
- )
- }
- />
- );
-};
-
-
-interface BillingDetailsProps {
- billingAccount?: BillingAccount;
- onAddDetailsClick?: () => void;
- isLoading: boolean;
- isAllowed: boolean;
- disabled?: boolean;
-}
-
-const BillingDetails = ({
- billingAccount,
- onAddDetailsClick = () => {},
- isLoading,
- isAllowed,
- disabled = false
-}: BillingDetailsProps) => {
- const btnText =
- billingAccount?.email || billingAccount?.name ? 'Update' : 'Add details';
- const isButtonDisabled = isLoading || disabled;
- return (
-
-
- Billing Details
- {isAllowed ? (
-
-
-
- ) : null}
-
-
- Name
-
- {isLoading ? : billingAccount?.name || 'N/A'}
-
-
-
- Email
-
- {isLoading ? : billingAccount?.email || 'N/A'}
-
-
-
- );
-};
+import { useNavigate } from '@tanstack/react-router';
+import { BillingPage } from '~/react/views/billing';
export default function Billing() {
- const {
- billingAccount,
- isBillingAccountLoading,
- config,
- activeSubscription,
- isActiveSubscriptionLoading,
- paymentMethod,
- organizationKyc,
- isOrganizationKycLoading,
- activeOrganization
- } = useFrontier();
-
- const { isAllowed, isFetching } = useBillingPermission();
-
- const {
- data: invoicesData,
- isLoading: isInvoicesLoading,
- error: invoicesError
- } = useConnectQuery(
- FrontierServiceQueries.listInvoices,
- create(ListInvoicesRequestSchema, {
- orgId: activeOrganization?.id || '',
- nonzeroAmountOnly: true
- }),
- {
- enabled: !!activeOrganization?.id
- }
- );
-
- const invoices = useMemo(() => invoicesData?.invoices || [], [invoicesData]);
-
- useEffect(() => {
- if (invoicesError) {
- toast.error('Failed to load invoices', {
- description: invoicesError?.message
- });
- }
- }, [invoicesError]);
-
- const { mutateAsync: createCheckoutMutation } = useMutation(
- FrontierServiceQueries.createCheckout,
- {
- onError: (err: Error) => {
- console.error(err);
- toast.error('Something went wrong', {
- description: err?.message
- });
- }
- }
- );
-
- const onAddDetailsClick = useCallback(async () => {
- const orgId = activeOrganization?.id || '';
- if (!orgId) return;
-
- try {
- 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: false,
- customerPortal: true
- }
- })
- );
- const checkoutUrl = resp?.checkoutSession?.checkoutUrl;
- if (checkoutUrl) {
- window.location.href = checkoutUrl;
- }
- } catch (err) {
- console.error(err);
- toast.error('Something went wrong');
- }
- }, [
- activeOrganization?.id,
- createCheckoutMutation,
- config?.billing?.cancelUrl,
- config?.billing?.successUrl
- ]);
-
- const isLoading =
- isBillingAccountLoading ||
- isActiveSubscriptionLoading ||
- isInvoicesLoading ||
- isFetching ||
- isOrganizationKycLoading;
-
- const isOrganizationKycCompleted = organizationKyc?.status === true;
+ const navigate = useNavigate({ from: '/billing' });
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ navigate({ to: '/plans' })}
+ />
);
}
diff --git a/web/sdk/react/components/organization/routes.tsx b/web/sdk/react/components/organization/routes.tsx
index ef6d0eb19..6ce6e2ad4 100644
--- a/web/sdk/react/components/organization/routes.tsx
+++ b/web/sdk/react/components/organization/routes.tsx
@@ -28,7 +28,6 @@ import { DeleteDomain } from './domain/delete';
import Billing from './billing';
import Tokens from './tokens';
import { AddTokens } from './tokens/add-tokens';
-import { ConfirmCycleSwitch } from './billing/cycle-switch';
import Plans from './plans';
import APIKeys from './api-keys';
import { AddServiceAccount } from './api-keys/add';
@@ -216,12 +215,6 @@ const billingRoute = createRoute({
component: Billing
});
-const switchBillingCycleModalRoute = createRoute({
- getParentRoute: () => billingRoute,
- path: '/cycle-switch/$planId',
- component: ConfirmCycleSwitch
-});
-
const plansRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/plans',
@@ -313,7 +306,7 @@ export function getRootTree({ customScreens = [] }: getRootTreeOptions) {
projectPageRoute,
profileRoute,
preferencesRoute,
- billingRoute.addChildren([switchBillingCycleModalRoute]),
+ billingRoute,
plansRoute,
tokensRoute.addChildren([addTokensRoute]),
apiKeysRoute.addChildren([
diff --git a/web/sdk/react/views/billing/billing-page.tsx b/web/sdk/react/views/billing/billing-page.tsx
new file mode 100644
index 000000000..b7f85fd0c
--- /dev/null
+++ b/web/sdk/react/views/billing/billing-page.tsx
@@ -0,0 +1,317 @@
+import {
+ Button,
+ Skeleton,
+ Text,
+ Flex,
+ toast,
+ Tooltip,
+ Link
+} from '@raystack/apsara';
+import { PageHeader } from '~/react/components/common/page-header';
+import { useFrontier } from '~/react/contexts/FrontierContext';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ BillingAccount,
+ ListInvoicesRequestSchema,
+ FrontierServiceQueries,
+ CreateCheckoutRequestSchema
+} from '@raystack/proton/frontier';
+import { useQuery as useConnectQuery } from '@connectrpc/connect-query';
+import { create } from '@bufbuild/protobuf';
+import { useMutation } from '~hooks';
+import Invoices from './invoices';
+import qs from 'query-string';
+import sharedStyles from '../../components/organization/styles.module.css';
+import billingStyles from './billing.module.css';
+
+import { UpcomingBillingCycle } from './upcoming-billing-cycle';
+import { PaymentIssue } from './payment-issue';
+import { UpcomingPlanChangeBanner } from '~/react/components/common/upcoming-plan-change-banner';
+import { PaymentMethod } from './payment-method';
+import { useBillingPermission } from '~/react/hooks/useBillingPermission';
+import { ConfirmCycleSwitchDialog } from './confirm-cycle-switch-dialog';
+
+interface BillingHeaderProps {
+ billingSupportEmail?: string;
+ isLoading?: boolean;
+}
+
+const BillingHeader = ({
+ billingSupportEmail,
+ isLoading
+}: BillingHeaderProps) => {
+ if (isLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+ Oversee your billing and invoices.{' '}
+ For more details, contact{' '}
+
+ {billingSupportEmail}
+
+ >
+ ) : (
+ 'Oversee your billing and invoices.'
+ )
+ }
+ />
+ );
+};
+
+
+interface BillingDetailsProps {
+ billingAccount?: BillingAccount;
+ onAddDetailsClick?: () => void;
+ isLoading: boolean;
+ isAllowed: boolean;
+ disabled?: boolean;
+}
+
+const BillingDetails = ({
+ billingAccount,
+ onAddDetailsClick = () => {},
+ isLoading,
+ isAllowed,
+ disabled = false
+}: BillingDetailsProps) => {
+ const btnText =
+ billingAccount?.email || billingAccount?.name ? 'Update' : 'Add details';
+ const isButtonDisabled = isLoading || disabled;
+ return (
+
+
+ Billing Details
+ {isAllowed ? (
+
+
+
+ ) : null}
+
+
+ Name
+
+ {isLoading ? : billingAccount?.name || 'N/A'}
+
+
+
+ Email
+
+ {isLoading ? : billingAccount?.email || 'N/A'}
+
+
+
+ );
+};
+
+export interface BillingPageProps {
+ onNavigateToPlans?: () => void;
+}
+
+export default function BillingPage({ onNavigateToPlans }: BillingPageProps) {
+ const {
+ billingAccount,
+ isBillingAccountLoading,
+ config,
+ activeSubscription,
+ isActiveSubscriptionLoading,
+ paymentMethod,
+ organizationKyc,
+ isOrganizationKycLoading,
+ activeOrganization
+ } = useFrontier();
+
+ const { isAllowed, isFetching } = useBillingPermission();
+
+ const {
+ data: invoicesData,
+ isLoading: isInvoicesLoading,
+ error: invoicesError
+ } = useConnectQuery(
+ FrontierServiceQueries.listInvoices,
+ create(ListInvoicesRequestSchema, {
+ orgId: activeOrganization?.id || '',
+ nonzeroAmountOnly: true
+ }),
+ {
+ enabled: !!activeOrganization?.id
+ }
+ );
+
+ const invoices = useMemo(() => invoicesData?.invoices || [], [invoicesData]);
+
+ useEffect(() => {
+ if (invoicesError) {
+ toast.error('Failed to load invoices', {
+ description: invoicesError?.message
+ });
+ }
+ }, [invoicesError]);
+
+ const { mutateAsync: createCheckoutMutation } = useMutation(
+ FrontierServiceQueries.createCheckout,
+ {
+ onError: (err: Error) => {
+ console.error(err);
+ toast.error('Something went wrong', {
+ description: err?.message
+ });
+ }
+ }
+ );
+
+ const onAddDetailsClick = useCallback(async () => {
+ const orgId = activeOrganization?.id || '';
+ if (!orgId) return;
+
+ try {
+ 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: false,
+ customerPortal: true
+ }
+ })
+ );
+ const checkoutUrl = resp?.checkoutSession?.checkoutUrl;
+ if (checkoutUrl) {
+ window.location.href = checkoutUrl;
+ }
+ } catch (err) {
+ console.error(err);
+ toast.error('Something went wrong');
+ }
+ }, [
+ activeOrganization?.id,
+ createCheckoutMutation,
+ config?.billing?.cancelUrl,
+ config?.billing?.successUrl
+ ]);
+
+ const isLoading =
+ isBillingAccountLoading ||
+ isActiveSubscriptionLoading ||
+ isInvoicesLoading ||
+ isFetching ||
+ isOrganizationKycLoading;
+
+ const isOrganizationKycCompleted = organizationKyc?.status === true;
+
+ const [cycleSwitchState, setCycleSwitchState] = useState({
+ open: false,
+ planId: ''
+ });
+
+ const handleCycleSwitchOpenChange = (value: boolean) => {
+ if (!value) {
+ setCycleSwitchState({ open: false, planId: '' });
+ } else {
+ setCycleSwitchState(prev => ({ ...prev, open: value }));
+ }
+ };
+
+ const handleCycleSwitchClick = (planId: string) => {
+ setCycleSwitchState({ open: true, planId });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/sdk/react/components/organization/billing/billing.module.css b/web/sdk/react/views/billing/billing.module.css
similarity index 100%
rename from web/sdk/react/components/organization/billing/billing.module.css
rename to web/sdk/react/views/billing/billing.module.css
diff --git a/web/sdk/react/components/organization/billing/cycle-switch/index.tsx b/web/sdk/react/views/billing/confirm-cycle-switch-dialog.tsx
similarity index 89%
rename from web/sdk/react/components/organization/billing/cycle-switch/index.tsx
rename to web/sdk/react/views/billing/confirm-cycle-switch-dialog.tsx
index 1c1252966..8488bbd6d 100644
--- a/web/sdk/react/components/organization/billing/cycle-switch/index.tsx
+++ b/web/sdk/react/views/billing/confirm-cycle-switch-dialog.tsx
@@ -8,17 +8,26 @@ import {
Flex,
Dialog
} from '@raystack/apsara';
-import { useNavigate, useParams } from '@tanstack/react-router';
import { useFrontier } from '~/react/contexts/FrontierContext';
import { getPlanIntervalName, getPlanPrice } from '~/react/utils';
import * as _ from 'lodash';
import { DEFAULT_DATE_FORMAT } from '~/react/utils/constants';
import cross from '~/react/assets/cross.svg';
-import styles from '../../organization.module.css';
+import orgStyles from '../../components/organization/organization.module.css';
import { timestampToDayjs } from '~/utils/timestamp';
import { usePlans } from '~/react/views/plans/hooks/usePlans';
-export function ConfirmCycleSwitch() {
+export interface ConfirmCycleSwitchDialogProps {
+ open: boolean;
+ onOpenChange?: (value: boolean) => void;
+ planId: string;
+}
+
+export function ConfirmCycleSwitchDialog({
+ open,
+ onOpenChange,
+ planId
+}: ConfirmCycleSwitchDialogProps) {
const {
activePlan,
paymentMethod,
@@ -27,13 +36,11 @@ export function ConfirmCycleSwitch() {
allPlans,
isAllPlansLoading
} = useFrontier();
- const navigate = useNavigate({ from: '/billing/cycle-switch/$planId' });
- const { planId } = useParams({ from: '/billing/cycle-switch/$planId' });
const dateFormat = config?.dateFormat || DEFAULT_DATE_FORMAT;
- const closeModal = useCallback(
- () => navigate({ to: '/billing' }),
- [navigate]
+ const handleClose = useCallback(
+ () => onOpenChange?.(false),
+ [onOpenChange]
);
const {
@@ -85,7 +92,7 @@ export function ConfirmCycleSwitch() {
planId: nextPlanId
});
if (planPhase) {
- closeModal();
+ handleClose();
const changeDate = timestampToDayjs(planPhase?.effectiveAt)?.format(
dateFormat
);
@@ -105,9 +112,9 @@ export function ConfirmCycleSwitch() {
: 'the next billing cycle';
return (
-