feat(license): show refetch payment status button to reconcile payments (#8551)

This commit is contained in:
Yunus M 2025-07-17 20:00:33 +05:30 committed by GitHub
parent ebb2f1fd63
commit 478d28eda1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 195 additions and 69 deletions

View File

@ -8,5 +8,6 @@
"actNow": "Act now to avoid any disruptions and continue where you left off.", "actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.", "contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue", "continueMyJourney": "Settle your bill to continue",
"somethingWentWrong": "Something went wrong" "somethingWentWrong": "Something went wrong",
"refreshPaymentStatus": "Refresh Status"
} }

View File

@ -8,5 +8,6 @@
"actNow": "Act now to avoid any disruptions and continue where you left off.", "actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.", "contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue", "continueMyJourney": "Settle your bill to continue",
"somethingWentWrong": "Something went wrong" "somethingWentWrong": "Something went wrong",
"refreshPaymentStatus": "Refresh Status"
} }

View File

@ -0,0 +1,24 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/licenses', {
key: props.key,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default apply;

View File

@ -2,15 +2,11 @@ import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply'; import { PayloadProps } from 'types/api/licenses/apply';
const apply = async ( const apply = async (): Promise<SuccessResponseV2<PayloadProps>> => {
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try { try {
const response = await axios.post<PayloadProps>('/licenses', { const response = await axios.put<PayloadProps>('/licenses');
key: props.key,
});
return { return {
httpStatusCode: response.status, httpStatusCode: response.status,

View File

@ -0,0 +1,56 @@
import { Button, Tooltip } from 'antd';
import refreshPaymentStatus from 'api/v3/licenses/put';
import cx from 'classnames';
import { RefreshCcw } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
function RefreshPaymentStatus({
btnShape,
type,
}: {
btnShape?: 'default' | 'round' | 'circle';
type?: 'button' | 'text' | 'tooltip';
}): JSX.Element {
const { t } = useTranslation(['failedPayment']);
const { activeLicenseRefetch } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
const handleRefreshPaymentStatus = async (): Promise<void> => {
setIsLoading(true);
try {
await refreshPaymentStatus();
await Promise.all([activeLicenseRefetch()]);
} catch (e) {
console.error(e);
}
setIsLoading(false);
};
return (
<span className="refresh-payment-status-btn-wrapper">
<Tooltip title={type === 'tooltip' ? t('refreshPaymentStatus') : ''}>
<Button
type={type === 'text' ? 'text' : 'default'}
shape={btnShape}
className={cx('periscope-btn', { text: type === 'text' })}
onClick={handleRefreshPaymentStatus}
icon={<RefreshCcw size={14} />}
loading={isLoading}
>
{type !== 'tooltip' ? t('refreshPaymentStatus') : ''}
</Button>
</Tooltip>
</span>
);
}
RefreshPaymentStatus.defaultProps = {
btnShape: 'default',
type: 'button',
};
export default RefreshPaymentStatus;

View File

@ -4,6 +4,21 @@
.app-banner-wrapper { .app-banner-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
.refresh-payment-status {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 4px;
.refresh-payment-status-btn-wrapper {
display: inline-block;
&:hover {
text-decoration: underline;
}
}
}
} }
.app-layout { .app-layout {
@ -12,24 +27,24 @@
width: 100%; width: 100%;
&.isWorkspaceRestricted { &.isWorkspaceRestricted {
height: calc(100% - 32px); height: calc(100% - 48px);
// same styles as its either trial expired or payment failed // same styles as its either trial expired or payment failed
&.isTrialExpired { &.isTrialExpired {
height: calc(100% - 64px); height: calc(100% - 96px);
} }
&.isPaymentFailed { &.isPaymentFailed {
height: calc(100% - 64px); height: calc(100% - 96px);
} }
} }
&.isTrialExpired { &.isTrialExpired {
height: calc(100% - 32px); height: calc(100% - 48px);
} }
&.isPaymentFailed { &.isPaymentFailed {
height: calc(100% - 32px); height: calc(100% - 48px);
} }
.app-content { .app-content {
@ -196,5 +211,5 @@
.workspace-restricted-banner, .workspace-restricted-banner,
.trial-expiry-banner, .trial-expiry-banner,
.payment-failed-banner { .payment-failed-banner {
height: 32px; height: 48px;
} }

View File

@ -16,6 +16,7 @@ import cx from 'classnames';
import ChangelogModal from 'components/ChangelogModal/ChangelogModal'; import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway'; import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import { Events } from 'constants/events'; import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
@ -181,11 +182,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
]); ]);
useEffect(() => { useEffect(() => {
// refetch the changelog only when the current tab becomes active + there isn't an active request + no changelog already available // refetch the changelog only when the current tab becomes active + there isn't an active request
if (!changelog && !getChangelogByVersionResponse.isLoading && isVisible) { if (!getChangelogByVersionResponse.isLoading && isVisible) {
getChangelogByVersionResponse.refetch(); getChangelogByVersionResponse.refetch();
} }
/* eslint-disable react-hooks/exhaustive-deps */ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible]); }, [isVisible]);
useEffect(() => { useEffect(() => {
@ -665,6 +666,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
upgrade upgrade
</a> </a>
to continue using SigNoz features. to continue using SigNoz features.
<span className="refresh-payment-status">
{' '}
| Already upgraded? <RefreshPaymentStatus type="text" />
</span>
</span> </span>
) : ( ) : (
'Please contact your administrator for upgrading to a paid plan.' 'Please contact your administrator for upgrading to a paid plan.'
@ -691,6 +696,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
pay the bill pay the bill
</a> </a>
to continue using SigNoz features. to continue using SigNoz features.
<span className="refresh-payment-status">
{' '}
| Already paid? <RefreshPaymentStatus type="text" />
</span>
</span> </span>
) : ( ) : (
' Please contact your administrator to pay the bill.' ' Please contact your administrator to pay the bill.'

View File

@ -20,6 +20,7 @@ import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create'; import updateCreditCardApi from 'api/v1/checkout/create';
import manageCreditCardApi from 'api/v1/portal/create'; import manageCreditCardApi from 'api/v1/portal/create';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@ -440,14 +441,15 @@ export default function BillingContainer(): JSX.Element {
</Typography.Text> </Typography.Text>
) : null} ) : null}
</Flex> </Flex>
<Flex gap={20}> <Flex gap={8}>
<Button <Button
type="dashed" type="default"
size="middle" size="middle"
loading={isLoadingBilling || isLoadingManageBilling} loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading || isFetchingBillingData} disabled={isLoading || isFetchingBillingData}
onClick={handleCsvDownload} onClick={handleCsvDownload}
icon={<CloudDownloadOutlined />} icon={<CloudDownloadOutlined />}
className="periscope-btn"
> >
Download CSV Download CSV
</Button> </Button>
@ -463,6 +465,8 @@ export default function BillingContainer(): JSX.Element {
? t('manage_billing') ? t('manage_billing')
: t('upgrade_plan')} : t('upgrade_plan')}
</Button> </Button>
<RefreshPaymentStatus type="tooltip" />
</Flex> </Flex>
</Flex> </Flex>

View File

@ -1,5 +1,5 @@
import { Button, Form, Input } from 'antd'; import { Button, Form, Input } from 'antd';
import apply from 'api/v3/licenses/put'; import apply from 'api/v3/licenses/post';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';

View File

@ -48,7 +48,7 @@ $dark-theme: 'darkMode';
&__actions { &__actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 8px;
.ant-btn-link { .ant-btn-link {
color: var(--text-vanilla-400); color: var(--text-vanilla-400);

View File

@ -24,9 +24,6 @@ describe('WorkspaceLocked', () => {
}); });
expect(workspaceLocked).toBeInTheDocument(); expect(workspaceLocked).toBeInTheDocument();
const gotQuestionText = await screen.findByText(/got question?/i);
expect(gotQuestionText).toBeInTheDocument();
const contactUsBtn = await screen.findByRole('button', { const contactUsBtn = await screen.findByRole('button', {
name: /Contact Us/i, name: /Contact Us/i,
}); });

View File

@ -18,6 +18,7 @@ import {
} from 'antd'; } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create'; import updateCreditCardApi from 'api/v1/checkout/create';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
@ -289,26 +290,28 @@ export default function WorkspaceBlocked(): JSX.Element {
</span> </span>
<span className="workspace-locked__modal__header__actions"> <span className="workspace-locked__modal__header__actions">
{isAdmin && ( {isAdmin && (
<Button <Flex gap={8} justify="center" align="center">
className="workspace-locked__modal__header__actions__billing" <Button
type="link" className="workspace-locked__modal__header__actions__billing"
size="small" type="link"
role="button" size="small"
onClick={handleViewBilling} role="button"
> onClick={handleViewBilling}
View Billing >
</Button> View Billing
</Button>
<RefreshPaymentStatus btnShape="round" />
</Flex>
)} )}
<Typography.Text className="workspace-locked__modal__title">
Got Questions?
</Typography.Text>
<Button <Button
type="default" type="default"
shape="round" shape="round"
size="middle" size="middle"
href="mailto:cloud-support@signoz.io" href="mailto:cloud-support@signoz.io"
role="button" role="button"
className="periscope-btn"
onClick={handleContactUsClick} onClick={handleContactUsClick}
> >
Contact Us Contact Us
@ -349,7 +352,7 @@ export default function WorkspaceBlocked(): JSX.Element {
justify="center" justify="center"
align="middle" align="middle"
className="workspace-locked__modal__cta" className="workspace-locked__modal__cta"
gutter={[16, 16]} gutter={[8, 8]}
> >
<Col> <Col>
<Alert <Alert
@ -360,34 +363,37 @@ export default function WorkspaceBlocked(): JSX.Element {
</Row> </Row>
)} )}
{isAdmin && ( {isAdmin && (
<Row <Flex gap={8} vertical justify="center" align="center">
justify="center" <Row
align="middle" justify="center"
className="workspace-locked__modal__cta" align="middle"
gutter={[16, 16]} className="workspace-locked__modal__cta"
> gutter={[8, 8]}
<Col> >
<Button <Col>
type="primary" <Button
shape="round" type="primary"
size="middle" shape="round"
loading={isLoading} size="middle"
onClick={handleUpdateCreditCard} loading={isLoading}
> onClick={handleUpdateCreditCard}
Continue my Journey >
</Button> Continue my Journey
</Col> </Button>
<Col> </Col>
<Button <Col>
type="default" <Button
shape="round" type="default"
size="middle" shape="round"
onClick={handleExtendTrial} size="middle"
> className="periscope-btn"
{t('needMoreTime')} onClick={handleExtendTrial}
</Button> >
</Col> {t('needMoreTime')}
</Row> </Button>
</Col>
</Row>
</Flex>
)} )}
<div className="workspace-locked__tabs"> <div className="workspace-locked__tabs">

View File

@ -4,6 +4,7 @@ import {
Alert, Alert,
Button, Button,
Col, Col,
Flex,
Modal, Modal,
Row, Row,
Skeleton, Skeleton,
@ -11,6 +12,7 @@ import {
Typography, Typography,
} from 'antd'; } from 'antd';
import manageCreditCardApi from 'api/v1/portal/create'; import manageCreditCardApi from 'api/v1/portal/create';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
@ -146,9 +148,9 @@ function WorkspaceSuspended(): JSX.Element {
justify="center" justify="center"
align="middle" align="middle"
className="workspace-suspended__modal__cta" className="workspace-suspended__modal__cta"
gutter={[16, 16]} gutter={[8, 8]}
> >
<Col> <Flex gap={8} justify="center" align="center">
<Button <Button
type="primary" type="primary"
shape="round" shape="round"
@ -158,7 +160,8 @@ function WorkspaceSuspended(): JSX.Element {
> >
{t('continueMyJourney')} {t('continueMyJourney')}
</Button> </Button>
</Col> <RefreshPaymentStatus btnShape="round" />
</Flex>
</Row> </Row>
)} )}
<div className="workspace-suspended__creative"> <div className="workspace-suspended__creative">

View File

@ -54,6 +54,20 @@
} }
} }
&.text {
color: var(--bg-vanilla-100) !important;
background-color: transparent !important;
border: none;
box-shadow: none;
box-shadow: none;
padding: 4px 4px;
&:hover {
color: var(--bg-vanilla-300) !important;
background-color: transparent !important;
}
}
&.success { &.success {
color: var(--bg-forest-400) !important; color: var(--bg-forest-400) !important;
border-radius: 2px; border-radius: 2px;