SagarRajput-7 ad1b01f225
feat: [SIG-566]: Added message to alert user about their past due - subscription status (#4724)
* feat: [SIG-566]: Added message to alert user about their past due - subscription status

* feat: [SIG-566]: Added message string to billings.json

* feat: [SIG-566]: Added strings to billings.json

* feat: [SIG-566]: updated test cases

* feat: [SIG-566]: updated message text

* feat: [SIG-566]: code fix

* feat: [SIG-566]: code fix
2024-03-27 10:23:57 +05:30

513 lines
13 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-loop-func */
import './BillingContainer.styles.scss';
import { CheckCircleOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import {
Alert,
Button,
Card,
Col,
Flex,
Row,
Skeleton,
Table,
Tag,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { pick } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
interface DataType {
key: string;
name: string;
unit: string;
dataIngested: string;
pricePerUnit: string;
cost: string;
}
enum SubscriptionStatus {
PastDue = 'past_due',
Active = 'active',
}
const renderSkeletonInput = (): JSX.Element => (
<Skeleton.Input
style={{ marginTop: '10px', height: '40px', width: '100%' }}
active
/>
);
const dummyData: DataType[] = [
{
key: '1',
name: 'Logs',
unit: '',
dataIngested: '',
pricePerUnit: '',
cost: '',
},
{
key: '2',
name: 'Traces',
unit: '',
dataIngested: '',
pricePerUnit: '',
cost: '',
},
{
key: '3',
name: 'Metrics',
unit: '',
dataIngested: '',
pricePerUnit: '',
cost: '',
},
];
const dummyColumns: ColumnsType<DataType> = [
{
title: '',
dataIndex: 'name',
key: 'name',
render: renderSkeletonInput,
},
{
title: 'Unit',
dataIndex: 'unit',
key: 'unit',
render: renderSkeletonInput,
},
{
title: 'Data Ingested',
dataIndex: 'dataIngested',
key: 'dataIngested',
render: renderSkeletonInput,
},
{
title: 'Price per Unit',
dataIndex: 'pricePerUnit',
key: 'pricePerUnit',
render: renderSkeletonInput,
},
{
title: 'Cost (Billing period to date)',
dataIndex: 'cost',
key: 'cost',
render: renderSkeletonInput,
},
];
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function BillingContainer(): JSX.Element {
const { t } = useTranslation(['billings']);
const daysRemainingStr = t('days_remaining');
const [headerText, setHeaderText] = useState('');
const [billAmount, setBillAmount] = useState(0);
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<any[]>([]);
const [apiResponse, setApiResponse] = useState<
Partial<UsageResponsePayloadProps>
>({});
const { trackEvent } = useAnalytics();
const { isFetching, data: licensesData, error: licenseError } = useLicense();
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
const { notifications } = useNotifications();
const handleError = useAxiosError();
const isCloudUserVal = isCloudUser();
const processUsageData = useCallback(
(data: any): void => {
const {
details: { breakdown = [], billTotal },
billingPeriodStart,
billingPeriodEnd,
} = data?.payload || {};
const formattedUsageData: any[] = [];
if (breakdown && Array.isArray(breakdown)) {
for (let index = 0; index < breakdown.length; index += 1) {
const element = breakdown[index];
element?.tiers.forEach(
(
tier: { quantity: number; unitPrice: number; tierCost: number },
i: number,
) => {
formattedUsageData.push({
key: `${index}${i}`,
name: i === 0 ? element?.type : '',
dataIngested: `${tier.quantity} ${element?.unit}`,
pricePerUnit: tier.unitPrice,
cost: `$ ${tier.tierCost}`,
});
},
);
}
}
setData(formattedUsageData);
if (!licensesData?.payload?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
setHeaderText(
`Your current billing period is from ${getFormattedDate(
billingPeriodStart,
)} to ${getFormattedDate(billingPeriodEnd)}`,
);
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setBillAmount(billTotal);
}
setApiResponse(data?.payload || {});
},
[licensesData?.payload?.onTrial],
);
const isSubscriptionPastDue =
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
{
queryFn: () => getUsage(activeLicense?.key || ''),
onError: handleError,
enabled: activeLicense !== null,
onSuccess: processUsageData,
},
);
useEffect(() => {
const activeValidLicense =
licensesData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
if (!isFetching && licensesData?.payload?.onTrial && !licenseError) {
const remainingDays = getRemainingDays(licensesData?.payload?.trialEnd);
setIsFreeTrial(true);
setBillAmount(0);
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setHeaderText(
`You are in free trial period. Your free trial will end on ${getFormattedDate(
licensesData?.payload?.trialEnd,
)}`,
);
}
}, [isFetching, licensesData?.payload, licenseError]);
const columns: ColumnsType<DataType> = [
{
title: '',
dataIndex: 'name',
key: 'name',
render: (text): JSX.Element => <div>{text}</div>,
},
{
title: 'Data Ingested',
dataIndex: 'dataIngested',
key: 'dataIngested',
},
{
title: 'Price per Unit',
dataIndex: 'pricePerUnit',
key: 'pricePerUnit',
},
{
title: 'Cost (Billing period to date)',
dataIndex: 'cost',
key: 'cost',
},
];
const renderTableSkeleton = (): JSX.Element => (
<Table
dataSource={dummyData}
pagination={false}
columns={dummyColumns}
locale={{
emptyText: dummyData.map((u) => (
<Skeleton.Input
key={u.key}
style={{ marginTop: '10px', height: '50px', width: '100%' }}
active
/>
)),
}}
/>
);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
if (data?.payload?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const {
mutate: manageCreditCard,
isLoading: isLoadingManageBilling,
} = useMutation(manageCreditCardApi, {
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
});
const handleBilling = useCallback(async () => {
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
trackEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']),
org,
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
} else {
trackEvent('Billing : Manage Billing', {
user: pick(user, ['email', 'userId', 'name']),
org,
});
manageCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
activeLicense?.key,
isFreeTrial,
licensesData?.payload?.trialConvertedToSubscription,
manageCreditCard,
updateCreditCard,
]);
const BillingUsageGraphCallback = useCallback(
() =>
!isLoading && !isFetchingBillingData ? (
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
) : (
<Card className="empty-graph-card" bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />
</Card>
),
[apiResponse, billAmount, isLoading, isFetchingBillingData],
);
const { Text } = Typography;
const subscriptionPastDueMessage = (): JSX.Element => (
<Typography>
{`We were not able to process payments for your account. Please update your card details `}
<Text type="danger" onClick={handleBilling} style={{ cursor: 'pointer' }}>
{t('here')}
</Text>
{` if your payment information has changed. Email us at `}
<Text type="secondary">cloud-support@signoz.io</Text>
{` otherwise. Be sure to provide this information immediately to avoid interruption to your service.`}
</Typography>
);
return (
<div className="billing-container">
<Flex vertical style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
{t('billing')}
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_400}>
{t('manage_billing_and_costs')}
</Typography.Text>
</Flex>
<Card
bordered={false}
style={{ minHeight: 150, marginBottom: 16 }}
className="page-info"
>
<Flex justify="space-between" align="center">
<Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? t('enterprise_cloud') : t('enterprise')}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading && !isFetchingBillingData ? (
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
{daysRemaining} {daysRemainingStr}
</Typography.Text>
) : null}
</Flex>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? t('upgrade_plan')
: t('manage_billing')}
</Button>
</Flex>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Text
ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
>
{t('card_details_recieved_and_billing_info')}
</Typography.Text>
)}
{!isLoading && !isFetchingBillingData ? (
<Alert
message={headerText}
type="info"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
)}
{isSubscriptionPastDue &&
(!isLoading && !isFetchingBillingData ? (
<Alert
message={subscriptionPastDueMessage()}
type="error"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
))}
</Card>
<BillingUsageGraphCallback />
<div className="billing-details">
{!isLoading && !isFetchingBillingData && (
<Table
columns={columns}
dataSource={data}
pagination={false}
bordered={false}
/>
)}
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
<div className="upgrade-plan-benefits">
<Row
justify="space-between"
align="middle"
style={{
margin: 0,
}}
gutter={[16, 16]}
>
<Col span={20} className="plan-benefits">
<Typography.Text className="plan-benefit">
<CheckCircleOutlined />
{t('upgrade_now_text')}
</Typography.Text>
<Typography.Text className="plan-benefit">
<CheckCircleOutlined />
{t('Your billing will start only after the trial period')}
</Typography.Text>
<Typography.Text className="plan-benefit">
<CheckCircleOutlined />
<span>
{t('checkout_plans')} &nbsp;
<a
href="https://signoz.io/pricing/"
style={{
color: '#f99781',
}}
target="_blank"
rel="noreferrer"
>
{t('here')}
</a>
</span>
</Typography.Text>
</Col>
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
onClick={handleBilling}
>
{t('upgrade_plan')}
</Button>
</Col>
</Row>
</div>
)}
</div>
);
}