mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-26 08:04:26 +08:00
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
This commit is contained in:
parent
e1679790f7
commit
ad1b01f225
14
frontend/public/locales/en/billings.json
Normal file
14
frontend/public/locales/en/billings.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"days_remaining": "days remaining in your billing period.",
|
||||||
|
"billing": "Billing",
|
||||||
|
"manage_billing_and_costs": "Manage your billing information, invoices, and monitor costs.",
|
||||||
|
"enterprise_cloud": "Enterprise Cloud",
|
||||||
|
"enterprise": "Enterprise",
|
||||||
|
"card_details_recieved_and_billing_info": "We have received your card details, your billing will only start after the end of your free trial period.",
|
||||||
|
"upgrade_plan": "Upgrade Plan",
|
||||||
|
"manage_billing": "Manage Billing",
|
||||||
|
"upgrade_now_text": "Upgrade now to have uninterrupted access",
|
||||||
|
"billing_start_info": "Your billing will start only after the trial period",
|
||||||
|
"checkout_plans": "Check out features in paid plans",
|
||||||
|
"here": "here"
|
||||||
|
}
|
@ -13,6 +13,7 @@ export interface UsageResponsePayloadProps {
|
|||||||
billTotal: number;
|
billTotal: number;
|
||||||
};
|
};
|
||||||
discount: number;
|
discount: number;
|
||||||
|
subscriptionStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsage = async (
|
const getUsage = async (
|
||||||
|
@ -56,14 +56,14 @@ describe('BillingContainer', () => {
|
|||||||
expect(cost).toBeInTheDocument();
|
expect(cost).toBeInTheDocument();
|
||||||
|
|
||||||
const manageBilling = screen.getByRole('button', {
|
const manageBilling = screen.getByRole('button', {
|
||||||
name: /manage billing/i,
|
name: 'manage_billing',
|
||||||
});
|
});
|
||||||
expect(manageBilling).toBeInTheDocument();
|
expect(manageBilling).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar = screen.getByText(/\$0/i);
|
const dollar = screen.getByText(/\$0/i);
|
||||||
expect(dollar).toBeInTheDocument();
|
expect(dollar).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByText('Billing');
|
const currentBill = screen.getByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ describe('BillingContainer', () => {
|
|||||||
const freeTrailText = await screen.findByText('Free Trial');
|
const freeTrailText = await screen.findByText('Free Trial');
|
||||||
expect(freeTrailText).toBeInTheDocument();
|
expect(freeTrailText).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByText('Billing');
|
const currentBill = screen.getByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$0/i);
|
const dollar0 = await screen.findByText(/\$0/i);
|
||||||
@ -85,18 +85,14 @@ describe('BillingContainer', () => {
|
|||||||
);
|
);
|
||||||
expect(onTrail).toBeInTheDocument();
|
expect(onTrail).toBeInTheDocument();
|
||||||
|
|
||||||
const numberOfDayRemaining = await screen.findByText(
|
const numberOfDayRemaining = await screen.findByText(/1 days_remaining/i);
|
||||||
/1 days remaining in your billing period./i,
|
|
||||||
);
|
|
||||||
expect(numberOfDayRemaining).toBeInTheDocument();
|
expect(numberOfDayRemaining).toBeInTheDocument();
|
||||||
const upgradeButton = await screen.findAllByRole('button', {
|
const upgradeButton = await screen.findAllByRole('button', {
|
||||||
name: /upgrade/i,
|
name: /upgrade_plan/i,
|
||||||
});
|
});
|
||||||
expect(upgradeButton[1]).toBeInTheDocument();
|
expect(upgradeButton[1]).toBeInTheDocument();
|
||||||
expect(upgradeButton.length).toBe(2);
|
expect(upgradeButton.length).toBe(2);
|
||||||
const checkPaidPlan = await screen.findByText(
|
const checkPaidPlan = await screen.findByText(/checkout_plans/i);
|
||||||
/Check out features in paid plans/i,
|
|
||||||
);
|
|
||||||
expect(checkPaidPlan).toBeInTheDocument();
|
expect(checkPaidPlan).toBeInTheDocument();
|
||||||
|
|
||||||
const link = screen.getByRole('link', { name: /here/i });
|
const link = screen.getByRole('link', { name: /here/i });
|
||||||
@ -114,7 +110,7 @@ describe('BillingContainer', () => {
|
|||||||
render(<BillingContainer />);
|
render(<BillingContainer />);
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentBill = screen.getByText('Billing');
|
const currentBill = screen.getByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$0/i);
|
const dollar0 = await screen.findByText(/\$0/i);
|
||||||
@ -126,17 +122,17 @@ describe('BillingContainer', () => {
|
|||||||
expect(onTrail).toBeInTheDocument();
|
expect(onTrail).toBeInTheDocument();
|
||||||
|
|
||||||
const receivedCardDetails = await screen.findByText(
|
const receivedCardDetails = await screen.findByText(
|
||||||
/We have received your card details, your billing will only start after the end of your free trial period./i,
|
/card_details_recieved_and_billing_info/i,
|
||||||
);
|
);
|
||||||
expect(receivedCardDetails).toBeInTheDocument();
|
expect(receivedCardDetails).toBeInTheDocument();
|
||||||
|
|
||||||
const manageBillingButton = await screen.findByRole('button', {
|
const manageBillingButton = await screen.findByRole('button', {
|
||||||
name: /manage billing/i,
|
name: /manage_billing/i,
|
||||||
});
|
});
|
||||||
expect(manageBillingButton).toBeInTheDocument();
|
expect(manageBillingButton).toBeInTheDocument();
|
||||||
|
|
||||||
const dayRemainingInBillingPeriod = await screen.findByText(
|
const dayRemainingInBillingPeriod = await screen.findByText(
|
||||||
/1 days remaining in your billing period./i,
|
/1 days_remaining/i,
|
||||||
);
|
);
|
||||||
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -156,7 +152,7 @@ describe('BillingContainer', () => {
|
|||||||
const billingPeriod = await findByText(billingPeriodText);
|
const billingPeriod = await findByText(billingPeriodText);
|
||||||
expect(billingPeriod).toBeInTheDocument();
|
expect(billingPeriod).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByText('Billing');
|
const currentBill = screen.getByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$1,278.3/i);
|
const dollar0 = await screen.findByText(/\$1,278.3/i);
|
||||||
@ -181,7 +177,7 @@ describe('BillingContainer', () => {
|
|||||||
);
|
);
|
||||||
render(<BillingContainer />);
|
render(<BillingContainer />);
|
||||||
const dayRemainingInBillingPeriod = await screen.findByText(
|
const dayRemainingInBillingPeriod = await screen.findByText(
|
||||||
/11 days remaining in your billing period./i,
|
/11 days_remaining/i,
|
||||||
);
|
);
|
||||||
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import updateCreditCardApi from 'api/billing/checkout';
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
import getUsage from 'api/billing/getUsage';
|
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||||
import manageCreditCardApi from 'api/billing/manage';
|
import manageCreditCardApi from 'api/billing/manage';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
@ -28,6 +28,7 @@ import useLicense from 'hooks/useLicense';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { pick } from 'lodash-es';
|
import { pick } from 'lodash-es';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation, useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -49,6 +50,11 @@ interface DataType {
|
|||||||
cost: string;
|
cost: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SubscriptionStatus {
|
||||||
|
PastDue = 'past_due',
|
||||||
|
Active = 'active',
|
||||||
|
}
|
||||||
|
|
||||||
const renderSkeletonInput = (): JSX.Element => (
|
const renderSkeletonInput = (): JSX.Element => (
|
||||||
<Skeleton.Input
|
<Skeleton.Input
|
||||||
style={{ marginTop: '10px', height: '40px', width: '100%' }}
|
style={{ marginTop: '10px', height: '40px', width: '100%' }}
|
||||||
@ -116,15 +122,19 @@ const dummyColumns: ColumnsType<DataType> = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export default function BillingContainer(): JSX.Element {
|
export default function BillingContainer(): JSX.Element {
|
||||||
const daysRemainingStr = 'days remaining in your billing period.';
|
const { t } = useTranslation(['billings']);
|
||||||
|
const daysRemainingStr = t('days_remaining');
|
||||||
const [headerText, setHeaderText] = useState('');
|
const [headerText, setHeaderText] = useState('');
|
||||||
const [billAmount, setBillAmount] = useState(0);
|
const [billAmount, setBillAmount] = useState(0);
|
||||||
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||||
const [daysRemaining, setDaysRemaining] = useState(0);
|
const [daysRemaining, setDaysRemaining] = useState(0);
|
||||||
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [apiResponse, setApiResponse] = useState<any>({});
|
const [apiResponse, setApiResponse] = useState<
|
||||||
|
Partial<UsageResponsePayloadProps>
|
||||||
|
>({});
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
const { trackEvent } = useAnalytics();
|
||||||
|
|
||||||
@ -186,6 +196,9 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
[licensesData?.payload?.onTrial],
|
[licensesData?.payload?.onTrial],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSubscriptionPastDue =
|
||||||
|
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
|
||||||
|
|
||||||
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
|
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
|
||||||
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
|
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
|
||||||
{
|
{
|
||||||
@ -342,14 +355,27 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
[apiResponse, billAmount, isLoading, isFetchingBillingData],
|
[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 (
|
return (
|
||||||
<div className="billing-container">
|
<div className="billing-container">
|
||||||
<Flex vertical style={{ marginBottom: 16 }}>
|
<Flex vertical style={{ marginBottom: 16 }}>
|
||||||
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
|
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
|
||||||
Billing
|
{t('billing')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text color={Color.BG_VANILLA_400}>
|
<Typography.Text color={Color.BG_VANILLA_400}>
|
||||||
Manage your billing information, invoices, and monitor costs.
|
{t('manage_billing_and_costs')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@ -361,7 +387,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
|
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
|
||||||
{isCloudUserVal ? 'Enterprise Cloud' : 'Enterprise'}{' '}
|
{isCloudUserVal ? t('enterprise_cloud') : t('enterprise')}{' '}
|
||||||
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
|
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
{!isLoading && !isFetchingBillingData ? (
|
{!isLoading && !isFetchingBillingData ? (
|
||||||
@ -378,8 +404,8 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
onClick={handleBilling}
|
onClick={handleBilling}
|
||||||
>
|
>
|
||||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
||||||
? 'Upgrade Plan'
|
? t('upgrade_plan')
|
||||||
: 'Manage Billing'}
|
: t('manage_billing')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@ -389,8 +415,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
ellipsis
|
ellipsis
|
||||||
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
||||||
>
|
>
|
||||||
We have received your card details, your billing will only start after
|
{t('card_details_recieved_and_billing_info')}
|
||||||
the end of your free trial period.
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -404,6 +429,18 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
) : (
|
) : (
|
||||||
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
|
<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>
|
</Card>
|
||||||
|
|
||||||
<BillingUsageGraphCallback />
|
<BillingUsageGraphCallback />
|
||||||
@ -434,16 +471,16 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
<Col span={20} className="plan-benefits">
|
<Col span={20} className="plan-benefits">
|
||||||
<Typography.Text className="plan-benefit">
|
<Typography.Text className="plan-benefit">
|
||||||
<CheckCircleOutlined />
|
<CheckCircleOutlined />
|
||||||
Upgrade now to have uninterrupted access
|
{t('upgrade_now_text')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text className="plan-benefit">
|
<Typography.Text className="plan-benefit">
|
||||||
<CheckCircleOutlined />
|
<CheckCircleOutlined />
|
||||||
Your billing will start only after the trial period
|
{t('Your billing will start only after the trial period')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text className="plan-benefit">
|
<Typography.Text className="plan-benefit">
|
||||||
<CheckCircleOutlined />
|
<CheckCircleOutlined />
|
||||||
<span>
|
<span>
|
||||||
Check out features in paid plans
|
{t('checkout_plans')}
|
||||||
<a
|
<a
|
||||||
href="https://signoz.io/pricing/"
|
href="https://signoz.io/pricing/"
|
||||||
style={{
|
style={{
|
||||||
@ -452,7 +489,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here
|
{t('here')}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -464,7 +501,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
loading={isLoadingBilling || isLoadingManageBilling}
|
loading={isLoadingBilling || isLoadingManageBilling}
|
||||||
onClick={handleBilling}
|
onClick={handleBilling}
|
||||||
>
|
>
|
||||||
Upgrade Plan
|
{t('upgrade_plan')}
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user