feat: move chat support behind paywall (#5673)

* feat: move chat support behind paywall

* feat: wire up chat support paywall

* feat: move chat support code from app layout to separate component

* feat: add log events
This commit is contained in:
Yunus M 2024-08-14 20:50:35 +05:30 committed by GitHub
parent 6c634b99d0
commit 1308f0f15f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 549 additions and 100 deletions

View File

@ -66,6 +66,14 @@ function App(): JSX.Element {
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active || allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
false; false;
const isPremiumSupportEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
false;
const showAddCreditCardModal =
!isPremiumSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
dispatch({ dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE, type: UPDATE_FEATURE_FLAG_RESPONSE,
payload: { payload: {
@ -82,7 +90,7 @@ function App(): JSX.Element {
setRoutes(newRoutes); setRoutes(newRoutes);
} }
if (isLoggedInState && isChatSupportEnabled) { if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.Intercom('boot', { window.Intercom('boot', {

View File

@ -0,0 +1,136 @@
import { Button, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { data: licenseData, isFetching } = useLicense();
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
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 handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `intercom icon`,
page: '',
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return (
<>
<div className="chat-support-gateway">
<Button
className="chat-support-gateway-btn"
onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', {
source: `intercom icon`,
page: '',
});
setIsAddCreditCardModalOpen(true);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 32"
className="chat-support-gateway-btn-icon"
>
<path d="M28 32s-4.714-1.855-8.527-3.34H3.437C1.54 28.66 0 27.026 0 25.013V3.644C0 1.633 1.54 0 3.437 0h21.125c1.898 0 3.437 1.632 3.437 3.645v18.404H28V32zm-4.139-11.982a.88.88 0 00-1.292-.105c-.03.026-3.015 2.681-8.57 2.681-5.486 0-8.517-2.636-8.571-2.684a.88.88 0 00-1.29.107 1.01 1.01 0 00-.219.708.992.992 0 00.318.664c.142.128 3.537 3.15 9.762 3.15 6.226 0 9.621-3.022 9.763-3.15a.992.992 0 00.317-.664 1.01 1.01 0 00-.218-.707z" />
</svg>
</Button>
</div>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</>
);
}

View File

@ -0,0 +1,184 @@
import './LaunchChatSupport.styles.scss';
import { Button, Modal, Tooltip, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import { isCloudUser } from 'utils/app';
export interface LaunchChatSupportProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function LaunchChatSupport({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
const { notifications } = useNotifications();
const { data: licenseData, isFetching } = useLicense();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) {
setIsAddCreditCardModalOpen(true);
} else {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
}
};
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 handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `facing issues button`,
page: '',
...attributes,
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip
title={onHoverText}
autoAdjustOverflow
style={{ padding: 8 }}
overlayClassName="tooltip-overlay"
>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</div>
) : null;
}
LaunchChatSupport.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default LaunchChatSupport;

View File

@ -5,7 +5,7 @@ import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnGroupType, ColumnType } from 'antd/es/table'; import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { SlidersHorizontal } from 'lucide-react'; import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react'; import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer'; import { popupContainer } from 'utils/selectPopupContainer';
@ -96,7 +96,7 @@ function DynamicColumnTable({
return ( return (
<div className="DynamicColumnTable"> <div className="DynamicColumnTable">
<Flex justify="flex-end" align="center" gap={8}> <Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />} {facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{dynamicColumns && ( {dynamicColumns && (
<Dropdown <Dropdown
getPopupContainer={popupContainer} getPopupContainer={popupContainer}

View File

@ -2,7 +2,7 @@
import { TableProps } from 'antd'; import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table'; import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn'; import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
import { TableDataSource } from './contants'; import { TableDataSource } from './contants';
@ -13,7 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource]; tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns']; dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void; onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: FacingIssueBtnProps; facingIssueBtn?: LaunchChatSupportProps;
shouldSendAlertsLogEvent?: boolean; shouldSendAlertsLogEvent?: boolean;
} }

View File

@ -1,70 +0,0 @@
import './FacingIssueBtn.style.scss';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { defaultTo } from 'lodash-es';
import { HelpCircle } from 'lucide-react';
import { isCloudUser } from 'utils/app';
export interface FacingIssueBtnProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
function FacingIssueBtn({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: FacingIssueBtnProps): JSX.Element | null {
const handleFacingIssuesClick = (): void => {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
};
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip
title={onHoverText}
autoAdjustOverflow
style={{ padding: 8 }}
overlayClassName="tooltip-overlay"
>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
</div>
) : null;
}
FacingIssueBtn.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default FacingIssueBtn;

View File

@ -20,4 +20,5 @@ export enum FeatureKeys {
ONBOARDING = 'ONBOARDING', ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT', CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY', GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
} }

View File

@ -24,6 +24,71 @@
} }
} }
.chat-support-gateway {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
.chat-support-gateway-btn {
max-width: 48px;
width: 48px;
max-height: 48px;
height: 48px;
padding: 12px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background-color: #f25733;
border: none;
&:hover {
color: white !important;
border-color: white !important;
}
.chat-support-gateway-btn-icon {
fill: white;
}
}
}
.add-credit-card-btn,
.cancel-btn {
display: inline-flex;
justify-content: center;
align-items: center;
}
.highlight-text {
border-radius: 2px;
background: rgba(78, 116, 248, 0.1);
padding-right: 4px;
font-family: 'Geist Mono';
color: var(--bg-robin-500);
}
.add-credit-card-modal {
.ant-modal-footer {
display: flex;
justify-content: flex-end;
}
.cancel-btn {
border-radius: 2px;
border: none;
background: var(--bg-slate-500, #161922);
box-shadow: none;
}
.add-credit-card-btn {
box-shadow: none;
}
}
.isDarkMode { .isDarkMode {
.app-layout { .app-layout {
.app-content { .app-content {

View File

@ -9,12 +9,15 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import getUserLatestVersion from 'api/user/getLatestVersion'; import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion'; import getUserVersion from 'api/user/getVersion';
import cx from 'classnames'; import cx from 'classnames';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app'; import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav'; import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav'; import TopNav from 'container/TopNav';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense'; import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
@ -49,6 +52,7 @@ import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { ChildrenContainer, Layout, LayoutContent } from './styles'; import { ChildrenContainer, Layout, LayoutContent } from './styles';
import { getRouteKey } from './utils'; import { getRouteKey } from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function AppLayout(props: AppLayoutProps): JSX.Element { function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>( const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,
@ -58,10 +62,19 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true', getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
); );
const { notifications } = useNotifications();
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const { data: licenseData, isFetching } = useLicense(); const { data: licenseData, isFetching } = useLicense();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
const { pathname } = useLocation(); const { pathname } = useLocation();
const { t } = useTranslation(['titles']); const { t } = useTranslation(['titles']);
@ -95,8 +108,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const latestCurrentCounter = useRef(0); const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0); const latestVersionCounter = useRef(0);
const { notifications } = useNotifications();
const onCollapse = useCallback(() => { const onCollapse = useCallback(() => {
setCollapsed((collapsed) => !collapsed); setCollapsed((collapsed) => !collapsed);
}, []); }, []);
@ -331,6 +342,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</Sentry.ErrorBoundary> </Sentry.ErrorBoundary>
</div> </div>
</Flex> </Flex>
{showAddCreditCardModal && <ChatSupportGateway />}
</Layout> </Layout>
); );
} }

View File

@ -13,8 +13,8 @@ import {
import saveAlertApi from 'api/alerts/save'; import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert'; import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { alertHelpMessage } from 'components/facingIssueBtn/util'; import { alertHelpMessage } from 'components/LaunchChatSupport/util';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts'; import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -712,7 +712,7 @@ function FormAlertRules({
> >
Check an example alert Check an example alert
</Button> </Button>
<FacingIssueBtn <LaunchChatSupport
attributes={{ attributes={{
alert: alertDef?.alert, alert: alertDef?.alert,
alertType: alertDef?.alertType, alertType: alertDef?.alertType,

View File

@ -5,7 +5,7 @@ import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save'; import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import DropDown from 'components/DropDown/DropDown'; import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/facingIssueBtn/util'; import { listAlertMessage } from 'components/LaunchChatSupport/util';
import { import {
DynamicColumnsKey, DynamicColumnsKey,
TableDataSource, TableDataSource,

View File

@ -25,8 +25,8 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import cx from 'classnames'; import cx from 'classnames';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardListMessage } from 'components/facingIssueBtn/util'; import { dashboardListMessage } from 'components/LaunchChatSupport/util';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
@ -664,7 +664,7 @@ function DashboardsList(): JSX.Element {
<Typography.Text className="subtitle"> <Typography.Text className="subtitle">
Create and manage dashboards for your workspace. Create and manage dashboards for your workspace.
</Typography.Text> </Typography.Text>
<FacingIssueBtn <LaunchChatSupport
attributes={{ attributes={{
screen: 'Dashboard list page', screen: 'Dashboard list page',
}} }}

View File

@ -12,8 +12,8 @@ import {
Typography, Typography,
} from 'antd'; } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util'; import { dashboardHelpMessage } from 'components/LaunchChatSupport/util';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
@ -322,7 +322,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
{isDashboardLocked && <LockKeyhole size={14} />} {isDashboardLocked && <LockKeyhole size={14} />}
</div> </div>
<div className="right-section"> <div className="right-section">
<FacingIssueBtn <LaunchChatSupport
attributes={{ attributes={{
uuid: selectedDashboard?.uuid, uuid: selectedDashboard?.uuid,
title: updatedTitle, title: updatedTitle,

View File

@ -4,7 +4,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip, Typography } from 'antd'; import { Button, Tabs, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl'; import PromQLIcon from 'assets/Dashboard/PromQl';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts'; import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@ -237,7 +237,7 @@ function QuerySection({
onChange={handleQueryCategoryChange} onChange={handleQueryCategoryChange}
tabBarExtraContent={ tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<FacingIssueBtn <LaunchChatSupport
attributes={{ attributes={{
uuid: selectedDashboard?.uuid, uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title, title: selectedDashboard?.data.title,

View File

@ -2,8 +2,8 @@ import './Integrations.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Flex, Input, Typography } from 'antd'; import { Flex, Input, Typography } from 'antd';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { integrationsListMessage } from 'components/facingIssueBtn/util'; import { integrationsListMessage } from 'components/LaunchChatSupport/util';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
@ -25,7 +25,7 @@ function Header(props: HeaderProps): JSX.Element {
<Typography.Text className="subtitle"> <Typography.Text className="subtitle">
Manage Integrations for this workspace Manage Integrations for this workspace
</Typography.Text> </Typography.Text>
<FacingIssueBtn <LaunchChatSupport
attributes={{ screen: 'Integrations list page' }} attributes={{ screen: 'Integrations list page' }}
eventName="Integrations: Facing issues in integrations" eventName="Integrations: Facing issues in integrations"
buttonText="Facing issues with integrations" buttonText="Facing issues with integrations"

View File

@ -5,8 +5,8 @@ import './IntegrationDetailPage.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Skeleton, Typography } from 'antd'; import { Button, Flex, Skeleton, Typography } from 'antd';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { integrationDetailMessage } from 'components/facingIssueBtn/util'; import { integrationDetailMessage } from 'components/LaunchChatSupport/util';
import { useGetIntegration } from 'hooks/Integrations/useGetIntegration'; import { useGetIntegration } from 'hooks/Integrations/useGetIntegration';
import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus'; import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus';
import { defaultTo } from 'lodash-es'; import { defaultTo } from 'lodash-es';
@ -77,7 +77,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
> >
All Integrations All Integrations
</Button> </Button>
<FacingIssueBtn <LaunchChatSupport
attributes={{ attributes={{
screen: 'Integrations detail page', screen: 'Integrations detail page',
activeTab: activeDetailTab, activeTab: activeDetailTab,

View File

@ -1,17 +1,29 @@
import './Support.styles.scss'; import './Support.styles.scss';
import { Button, Card, Typography } from 'antd'; import { Button, Card, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { import {
Book, Book,
Cable, Cable,
Calendar, Calendar,
CreditCard,
Github, Github,
MessageSquare, MessageSquare,
Slack, Slack,
X,
} from 'lucide-react'; } from 'lucide-react';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -86,6 +98,12 @@ const supportChannels = [
export default function Support(): JSX.Element { export default function Support(): JSX.Element {
const history = useHistory(); const history = useHistory();
const { notifications } = useNotifications();
const { data: licenseData, isFetching } = useLicense();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const handleChannelWithRedirects = (url: string): void => { const handleChannelWithRedirects = (url: string): void => {
window.open(url, '_blank'); window.open(url, '_blank');
@ -117,10 +135,67 @@ export default function Support(): JSX.Element {
window.location.href = mailtoLink; window.location.href = mailtoLink;
}; };
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
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 handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `chat`,
page: 'support',
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
const handleChat = (): void => { const handleChat = (): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment if (showAddCreditCardModal) {
// @ts-ignore setIsAddCreditCardModalOpen(true);
if (window.Intercom) { } else if (window.Intercom) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.Intercom('show'); window.Intercom('show');
@ -183,6 +258,43 @@ export default function Support(): JSX.Element {
), ),
)} )}
</div> </div>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</div> </div>
); );
} }