Skip to content

Commit

Permalink
chore: add banner for payment failure
Browse files Browse the repository at this point in the history
  • Loading branch information
rsbh committed Jan 31, 2024
1 parent f0e97f8 commit e244c5c
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 57 deletions.
5 changes: 5 additions & 0 deletions sdks/js/packages/core/react/assets/exclamation-triangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,30 @@
color: var(--foreground-base);
font-weight: 400;
}

.flex1 {
flex: 1;
}

.paymentIssueBox {
padding: var(--pd-8);
border-radius: var(--br-4);
box-shadow: var(--shadow-xs);
border: 0.5px solid var(--border-danger);
background: var(--background-danger);
}

.paymentIssueText {
color: var(--foreground-danger);
}

.retryPaymentBtn {
background: var(--background-danger);
color: var(--foreground-base);
}

.retryPaymentBtn:focus,
.retryPaymentBtn:hover {
outline: none;
font-weight: 500;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,39 @@ import { styles } from '../styles';
import { useFrontier } from '~/react/contexts/FrontierContext';
import { useCallback, useEffect, useState } from 'react';
import billingStyles from './billing.module.css';
import { V1Beta1BillingAccount, V1Beta1PaymentMethod } from '~/src';
import {
V1Beta1BillingAccount,
V1Beta1Invoice,
V1Beta1PaymentMethod
} from '~/src';
import * as _ from 'lodash';
import { toast } from 'sonner';
import Skeleton from 'react-loading-skeleton';
import { converBillingAddressToString } from '~/react/utils';
import Invoices from './invoices';

import { UpcomingBillingCycle } from './upcoming-billing-cycle';
import { PaymentIssue } from './payment-issue';

interface BillingHeaderProps {
billingSupportEmail?: string;
isLoading?: boolean;
}

const BillingHeader = ({ billingSupportEmail }: BillingHeaderProps) => {
const BillingHeader = ({
billingSupportEmail,
isLoading
}: BillingHeaderProps) => {
return (
<Flex direction="row" justify="between" align="center">
<Flex direction="column" gap="small">
<Flex direction="column" gap="small">
{isLoading ? (
<Skeleton containerClassName={billingStyles.flex1} />
) : (
<Text size={6}>Billing</Text>
)}
{isLoading ? (
<Skeleton containerClassName={billingStyles.flex1} />
) : (
<Text size={4} style={{ color: 'var(--foreground-muted)' }}>
Oversee your billing and invoices.
{billingSupportEmail ? (
Expand All @@ -38,7 +53,7 @@ const BillingHeader = ({ billingSupportEmail }: BillingHeaderProps) => {
</>
) : null}
</Text>
</Flex>
)}
</Flex>
);
};
Expand Down Expand Up @@ -128,12 +143,35 @@ export default function Billing() {
const {
billingAccount: activeBillingAccount,
client,
config
config,
activeSubscription,
isActiveSubscriptionLoading
} = useFrontier();
const navigate = useNavigate({ from: '/billing' });
const [billingAccount, setBillingAccount] = useState<V1Beta1BillingAccount>();
const [paymentMethod, setPaymentMethod] = useState<V1Beta1PaymentMethod>();
const [isBillingAccountLoading, setBillingAccountLoading] = useState(false);
const [invoices, setInvoices] = useState<V1Beta1Invoice[]>([]);
const [isInvoicesLoading, setIsInvoicesLoading] = useState(false);

const fetchInvoices = useCallback(
async (organizationId: string, billingId: string) => {
setIsInvoicesLoading(true);
try {
const resp = await client?.frontierServiceListInvoices(
organizationId,
billingId
);
const newInvoices = resp?.data?.invoices || [];
setInvoices(newInvoices);
} catch (err) {
console.error(err);
} finally {
setIsInvoicesLoading(false);
}
},
[client]
);

useEffect(() => {
async function getPaymentMethod(orgId: string, billingId: string) {
Expand Down Expand Up @@ -161,8 +199,14 @@ export default function Billing() {

if (activeBillingAccount?.id && activeBillingAccount?.org_id) {
getPaymentMethod(activeBillingAccount?.org_id, activeBillingAccount?.id);
fetchInvoices(activeBillingAccount?.org_id, activeBillingAccount?.id);
}
}, [activeBillingAccount?.id, activeBillingAccount?.org_id, client]);
}, [
activeBillingAccount?.id,
activeBillingAccount?.org_id,
client,
fetchInvoices
]);

const onAddDetailsClick = useCallback(() => {
if (billingAccount?.id) {
Expand All @@ -173,31 +217,39 @@ export default function Billing() {
}
}, [billingAccount?.id, navigate]);

const isLoading =
isBillingAccountLoading || isActiveSubscriptionLoading || isInvoicesLoading;

return (
<Flex direction="column" style={{ width: '100%' }}>
<Flex style={styles.header}>
<Text size={6}>Billing</Text>
</Flex>
<Flex direction="column" gap="large" style={styles.container}>
<Flex direction="column" style={{ gap: '24px' }}>
<BillingHeader billingSupportEmail={config.billing?.supportEmail} />
<BillingHeader
isLoading={isLoading}
billingSupportEmail={config.billing?.supportEmail}
/>
<PaymentIssue
isLoading={isLoading}
subscription={activeSubscription}
invoices={invoices}
/>

<Flex style={{ gap: '24px' }}>
<PaymentMethod
paymentMethod={paymentMethod}
isLoading={isBillingAccountLoading}
isLoading={isLoading}
/>
<BillingDetails
billingAccount={activeBillingAccount}
onAddDetailsClick={onAddDetailsClick}
isLoading={isBillingAccountLoading}
isLoading={isLoading}
/>
</Flex>
<UpcomingBillingCycle />
<Invoices
organizationId={activeBillingAccount?.org_id || ''}
billingId={activeBillingAccount?.id || ''}
isLoading={isBillingAccountLoading}
/>
<Invoices invoices={invoices} isLoading={isLoading} />
</Flex>
</Flex>
<Outlet />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import { V1Beta1Invoice } from '~/src';
import { capitalize } from '~/utils';

interface InvoicesProps {
organizationId: string;
billingId: string;
isLoading: boolean;
invoices: V1Beta1Invoice[];
}

interface getColumnsOptions {
Expand Down Expand Up @@ -112,38 +111,11 @@ const noDataChildren = (
</EmptyState>
);

export default function Invoices({
organizationId,
billingId,
isLoading
}: InvoicesProps) {
const { client, config } = useFrontier();
const [invoices, setInvoices] = useState<V1Beta1Invoice[]>([]);
const [isInvoicesLoading, setIsInvoicesLoading] = useState(false);

const fetchInvoices = useCallback(
async (organizationId: string, billingId: string) => {
setIsInvoicesLoading(true);
try {
const resp = await client?.frontierServiceListInvoices(
organizationId,
billingId
);
const newInvoices = resp?.data?.invoices || [];
setInvoices(newInvoices);
} catch (err) {
console.error(err);
} finally {
setIsInvoicesLoading(false);
}
},
[client]
);

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

const columns = getColumns({
isLoading: showLoader,
isLoading: isLoading,
dateFormat: config?.dateFormat || DEFAULT_DATE_FORMAT
});
const tableStyle = useMemo(
Expand All @@ -162,11 +134,6 @@ export default function Invoices({
);
}, [invoices, isLoading]);

useEffect(() => {
if (billingId && organizationId) {
fetchInvoices(organizationId, billingId);
}
}, [billingId, fetchInvoices, organizationId]);
return (
<div>
<DataTable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Flex, Image, Button, Text } from '@raystack/apsara';
import Skeleton from 'react-loading-skeleton';
import { INVOICE_STATES, SUBSCRIPTION_STATES } from '~/react/utils/constants';
import { V1Beta1Invoice, V1Beta1Subscription } from '~/src';
import billingStyles from './billing.module.css';
import exclamationTriangle from '~/react/assets/exclamation-triangle.svg';
import dayjs from 'dayjs';
import { useCallback } from 'react';

interface PaymentIssueProps {
isLoading?: boolean;
subscription?: V1Beta1Subscription;
invoices: V1Beta1Invoice[];
}

export function PaymentIssue({
isLoading,
subscription,
invoices
}: PaymentIssueProps) {
const isPastDue = subscription?.state === SUBSCRIPTION_STATES.PAST_DUE;
const openInvoices = invoices
.filter(inv => inv.state === INVOICE_STATES.OPEN)
.sort((a, b) => (dayjs(a.due_date).isAfter(b.due_date) ? -1 : 1));

const onRetryPayment = useCallback(() => {
window.location.href = openInvoices[0]?.hosted_url || '';
}, [openInvoices]);

return isLoading ? (
<Skeleton />
) : isPastDue ? (
<Flex className={billingStyles.paymentIssueBox} justify={'between'}>
<Flex gap="small" className={billingStyles.flex1}>
{/* @ts-ignore */}
<Image src={exclamationTriangle} alt="Exclamation Triangle" />
<Text className={billingStyles.paymentIssueText}>
Your Payment is due. Please try again
</Text>
</Flex>
<Flex className={billingStyles.flex1} justify={'end'}>
<Button
className={billingStyles.retryPaymentBtn}
onClick={onRetryPayment}
>
Retry
</Button>
</Flex>
</Flex>
) : null;
}
11 changes: 11 additions & 0 deletions sdks/js/packages/core/react/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
export const DEFAULT_DATE_FORMAT = 'DD MMM YYYY';

export const SUBSCRIPTION_STATES = {
ACTIVE: 'active',
PAST_DUE: 'past_due'
} as const;

export const INVOICE_STATES = {
OPEN: 'open',
PAID: 'paid',
DRAFT: 'draft'
} as const;
11 changes: 6 additions & 5 deletions sdks/js/packages/core/react/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import dayjs from 'dayjs';
import { V1Beta1Subscription, BillingAccountAddress } from '~/src';
import { IntervalPricingWithPlan } from '~/src/types';
import { SUBSCRIPTION_STATES } from './constants';

export const AuthTooltipMessage =
'You don’t have access to perform this action';

export const SUBSCRIPTION_STATES = {
ACTIVE: 'active'
};

export const converBillingAddressToString = (
address?: BillingAccountAddress
) => {
Expand All @@ -21,7 +18,11 @@ export const converBillingAddressToString = (

export const getActiveSubscription = (subscriptions: V1Beta1Subscription[]) => {
const activeSubscriptions = subscriptions
.filter(sub => sub.state === SUBSCRIPTION_STATES.ACTIVE)
.filter(
sub =>
sub.state === SUBSCRIPTION_STATES.ACTIVE ||
sub.state === SUBSCRIPTION_STATES.PAST_DUE
)
.sort((a, b) => (dayjs(a.updated_at).isAfter(b.updated_at) ? -1 : 1));

return activeSubscriptions[0];
Expand Down

0 comments on commit e244c5c

Please sign in to comment.