Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion web/apps/client-demo/src/pages/settings/Billing.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { BillingView } from '@raystack/frontier/react';
import { useNavigate, useParams } from 'react-router-dom';

export default function Billing() {
return <BillingView />;
const { orgId } = useParams<{ orgId: string }>();
const navigate = useNavigate();
return <BillingView onNavigateToPlans={() => navigate(`/${orgId}/settings/plans`)} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
background-color: var(--rs-color-background-base-primary);
width: 100%;
height: 100%;
padding: var(--rs-space-9) var(--rs-space-11);
overflow: auto;
}

.content {
width: 100%;
padding: var(--rs-space-9) var(--rs-space-11);
box-sizing: border-box;
}
13 changes: 11 additions & 2 deletions web/sdk/react/hooks/useBillingPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const useBillingPermission = () => {
{
permission: PERMISSIONS.DeletePermission,
resource
},
{
permission: PERMISSIONS.UpdatePermission,
resource
}
],
[resource]
Expand All @@ -22,17 +26,22 @@ export const useBillingPermission = () => {
!!activeOrganization?.id
);

const { isAllowed } = useMemo(() => {
const { isAllowed, canSeeBilling } = useMemo(() => {
return {
isAllowed: shouldShowComponent(
permissions,
`${PERMISSIONS.DeletePermission}::${resource}`
),
canSeeBilling: shouldShowComponent(
permissions,
`${PERMISSIONS.UpdatePermission}::${resource}`
)
};
}, [permissions, resource]);

return {
isFetching,
isAllowed
isAllowed,
canSeeBilling
};
};
8 changes: 1 addition & 7 deletions web/sdk/react/views-new/billing/billing-view.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@
text-align: right;
}

.linkColumn > a {
.viewInvoiceBtn {
color: var(--rs-color-foreground-accent-primary);
text-decoration: none;
}

.dialogFormBody {
max-height: 424px;
overflow-y: auto;
}
169 changes: 119 additions & 50 deletions web/sdk/react/views-new/billing/billing-view.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
'use client';

import { useEffect, useMemo } from 'react';
import { Flex, Dialog, toastManager } from '@raystack/apsara-v1';
import { useCallback, useEffect, useMemo } from 'react';
import qs from 'query-string';
import { Flex, Dialog, EmptyState, toastManager } from '@raystack/apsara-v1';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import {
CreateCheckoutRequestSchema,
ListInvoicesRequestSchema,
FrontierServiceQueries
} from '@raystack/proton/frontier';
import { useQuery as useConnectQuery } from '@connectrpc/connect-query';
import {
useMutation,
useQuery as useConnectQuery
} from '@connectrpc/connect-query';
import { create } from '@bufbuild/protobuf';
import { useFrontier } from '../../contexts/FrontierContext';
import { useBillingPermission } from '../../hooks/useBillingPermission';
Expand All @@ -22,11 +28,9 @@ import {
ConfirmCycleSwitchDialog,
type ConfirmCycleSwitchPayload
} from './components/confirm-cycle-switch-dialog';
import { BillingDetailsDialog } from './components/billing-details-dialog';

const cycleSwitchDialogHandle =
Dialog.createHandle<ConfirmCycleSwitchPayload>();
const billingDetailsDialogHandle = Dialog.createHandle();

export interface BillingViewProps {
onNavigateToPlans?: () => void;
Expand All @@ -45,7 +49,10 @@ export function BillingView({ onNavigateToPlans }: BillingViewProps) {
activeOrganization
} = useFrontier();

const { isAllowed, isFetching } = useBillingPermission();
const { isAllowed, canSeeBilling, isFetching } = useBillingPermission();

const isPermissionsLoading = !activeOrganization?.id || isFetching;
const hasNoAccess = !canSeeBilling && !isPermissionsLoading;

const {
data: invoicesData,
Expand All @@ -58,7 +65,7 @@ export function BillingView({ onNavigateToPlans }: BillingViewProps) {
nonzeroAmountOnly: true
}),
{
enabled: !!activeOrganization?.id
enabled: !!activeOrganization?.id && canSeeBilling
}
);

Expand All @@ -74,7 +81,7 @@ export function BillingView({ onNavigateToPlans }: BillingViewProps) {
}
}, [invoicesError]);

const isLoading =
const isLoading = !activeOrganization?.id ||
isBillingAccountLoading ||
isActiveSubscriptionLoading ||
isInvoicesLoading ||
Expand All @@ -93,54 +100,116 @@ export function BillingView({ onNavigateToPlans }: BillingViewProps) {
cycleSwitchDialogHandle.openWithPayload({ planId });
}

function handleBillingDetailsUpdateClick() {
billingDetailsDialogHandle.open(null);
}
const { mutateAsync: createCheckoutMutation, isPending: isCheckoutPending } =
useMutation(FrontierServiceQueries.createCheckout, {
onError: (err: Error) => {
toastManager.add({
title: 'Something went wrong',
description: err?.message,
type: 'error'
});
}
});

const handleBillingDetailsUpdateClick = useCallback(async () => {
const orgId = activeOrganization?.id || '';
if (!orgId) return;

try {
const query = qs.stringify(
{
details: btoa(
qs.stringify({
organization_id: orgId,
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,
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);
}
}, [
activeOrganization?.id,
createCheckoutMutation,
config?.billing?.cancelUrl,
config?.billing?.successUrl
]);

return (
<ViewContainer>
<ViewHeader title="Billing" description={description} />

<Flex direction="column" gap={7}>
<PaymentIssue
isLoading={isLoading}
subscription={activeSubscription}
invoices={invoices}
/>

<UpcomingPlanChangeBanner
isLoading={isLoading}
subscription={activeSubscription}
isAllowed={isAllowed}
/>

<Flex gap={7}>
<PaymentMethodCard
paymentMethod={paymentMethod}
isLoading={isLoading}
isAllowed={isAllowed}
/>
<BillingDetailsCard
billingAccount={billingAccount}
isLoading={isLoading}
isAllowed={isAllowed}
disabled={isOrganizationKycCompleted}
onUpdateClick={handleBillingDetailsUpdateClick}
/>
</Flex>

<UpcomingBillingCycle
isAllowed={isAllowed}
isPermissionLoading={isFetching}
onCycleSwitchClick={handleCycleSwitchClick}
onNavigateToPlans={onNavigateToPlans}
{hasNoAccess ? (
<EmptyState
icon={<ExclamationTriangleIcon />}
heading="Restricted Access"
subHeading="Admin access required, please reach out to your admin to view billing."
/>

<Invoices />
</Flex>

<ConfirmCycleSwitchDialog handle={cycleSwitchDialogHandle} />
<BillingDetailsDialog handle={billingDetailsDialogHandle} />
) : (
<>
<Flex direction="column" gap={7}>
<PaymentIssue
isLoading={isLoading}
subscription={activeSubscription}
invoices={invoices}
/>

<UpcomingPlanChangeBanner
isLoading={isLoading}
subscription={activeSubscription}
isAllowed={isAllowed}
/>

<Flex gap={7}>
<PaymentMethodCard
paymentMethod={paymentMethod}
isLoading={isLoading}
isAllowed={isAllowed}
/>
<BillingDetailsCard
billingAccount={billingAccount}
isLoading={isLoading}
isAllowed={isAllowed}
disabled={isOrganizationKycCompleted}
isActionLoading={isCheckoutPending}
onUpdateClick={handleBillingDetailsUpdateClick}
/>
</Flex>

<UpcomingBillingCycle
isAllowed={isAllowed}
isPermissionLoading={isPermissionsLoading}
onCycleSwitchClick={handleCycleSwitchClick}
onNavigateToPlans={onNavigateToPlans}
/>

<Invoices />
</Flex>

<ConfirmCycleSwitchDialog handle={cycleSwitchDialogHandle} />
</>
)}
</ViewContainer>
);
}
50 changes: 32 additions & 18 deletions web/sdk/react/views-new/billing/components/billing-details-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface BillingDetailsCardProps {
isLoading: boolean;
isAllowed: boolean;
disabled?: boolean;
isActionLoading?: boolean;
onUpdateClick?: () => void;
}

Expand All @@ -18,11 +19,12 @@ export function BillingDetailsCard({
isLoading,
isAllowed,
disabled = false,
isActionLoading = false,
onUpdateClick
}: BillingDetailsCardProps) {
const btnText =
billingAccount?.email || billingAccount?.name ? 'Update' : 'Add details';
const isButtonDisabled = isLoading || disabled;
const isButtonDisabled = isLoading || disabled || isActionLoading;

const address = convertBillingAddressToString(billingAccount?.address);

Expand All @@ -32,7 +34,7 @@ export function BillingDetailsCard({
<Text size="regular" weight="medium">
Billing details
</Text>
{isAllowed ? (
{!isLoading && isAllowed ? (
<Tooltip>
<Tooltip.Trigger
disabled={!isButtonDisabled}
Expand All @@ -44,6 +46,8 @@ export function BillingDetailsCard({
size="small"
onClick={onUpdateClick}
disabled={isButtonDisabled}
loading={isActionLoading}
loaderText={btnText}
data-test-id="frontier-sdk-billing-details-update-button"
>
{btnText}
Expand All @@ -57,22 +61,32 @@ export function BillingDetailsCard({
</Tooltip>
) : null}
</Flex>
<Flex direction="column" gap={2}>
<Text size="mini" weight="medium" variant="secondary">
Name
</Text>
<Text size="regular">
{isLoading ? <Skeleton /> : billingAccount?.name || 'N/A'}
</Text>
</Flex>
<Flex direction="column" gap={2}>
<Text size="mini" weight="medium" variant="secondary">
Address
</Text>
<Text size="regular">
{isLoading ? <Skeleton /> : address || 'N/A'}
</Text>
</Flex>
{isLoading ? (
<Flex direction="column" gap={2}>
<Skeleton height={16} width={60} />
<Skeleton height={20} width={250} />
</Flex>
) : (
<Flex direction="column" gap={2}>
<Text size="mini" weight="medium" variant="secondary">
Name
</Text>
<Text size="regular">{billingAccount?.name || 'N/A'}</Text>
</Flex>
)}
{isLoading ? (
<Flex direction="column" gap={2}>
<Skeleton height={16} width={80} />
<Skeleton height={20} width={300} />
</Flex>
) : (
<Flex direction="column" gap={2}>
<Text size="mini" weight="medium" variant="secondary">
Address
</Text>
<Text size="regular">{address || 'N/A'}</Text>
</Flex>
)}
</div>
);
}
Loading
Loading