From 1308f0f15fcfe1c17db4284f36fe56d2871ce738 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 14 Aug 2024 20:50:35 +0530 Subject: [PATCH] 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 --- frontend/src/AppRoutes/index.tsx | 10 +- .../ChatSupportGateway/ChatSupportGateway.tsx | 136 +++++++++++++ .../LaunchChatSupport.styles.scss} | 0 .../LaunchChatSupport/LaunchChatSupport.tsx | 184 ++++++++++++++++++ .../util.ts | 0 .../ResizeTable/DynamicColumnTable.tsx | 4 +- frontend/src/components/ResizeTable/types.ts | 4 +- .../facingIssueBtn/FacingIssueBtn.tsx | 70 ------- frontend/src/constants/features.ts | 1 + .../container/AppLayout/AppLayout.styles.scss | 65 +++++++ frontend/src/container/AppLayout/index.tsx | 17 +- .../src/container/FormAlertRules/index.tsx | 6 +- .../container/ListAlertRules/ListAlert.tsx | 2 +- .../ListOfDashboard/DashboardsList.tsx | 6 +- .../DashboardDescription/index.tsx | 6 +- .../LeftContainer/QuerySection/index.tsx | 4 +- frontend/src/pages/Integrations/Header.tsx | 6 +- .../IntegrationDetailPage.tsx | 6 +- frontend/src/pages/Support/Support.tsx | 122 +++++++++++- 19 files changed, 549 insertions(+), 100 deletions(-) create mode 100644 frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx rename frontend/src/components/{facingIssueBtn/FacingIssueBtn.style.scss => LaunchChatSupport/LaunchChatSupport.styles.scss} (100%) create mode 100644 frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx rename frontend/src/components/{facingIssueBtn => LaunchChatSupport}/util.ts (100%) delete mode 100644 frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index ac225db25a..23e7ea9644 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -66,6 +66,14 @@ function App(): JSX.Element { allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active || false; + const isPremiumSupportEnabled = + allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active || + false; + + const showAddCreditCardModal = + !isPremiumSupportEnabled && + !licenseData?.payload?.trialConvertedToSubscription; + dispatch({ type: UPDATE_FEATURE_FLAG_RESPONSE, payload: { @@ -82,7 +90,7 @@ function App(): JSX.Element { setRoutes(newRoutes); } - if (isLoggedInState && isChatSupportEnabled) { + if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.Intercom('boot', { diff --git a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx new file mode 100644 index 0000000000..b361f725c3 --- /dev/null +++ b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx @@ -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(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, + ): 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 ( + <> +
+ +
+ + {/* Add Credit Card Modal */} + Add Credit Card for Chat Support} + open={isAddCreditCardModalOpen} + closable + onCancel={(): void => setIsAddCreditCardModalOpen(false)} + destroyOnClose + footer={[ + , + , + ]} + > + + You're currently on Trial plan + . Add a credit card to access SigNoz chat support to your workspace. + + + + ); +} diff --git a/frontend/src/components/facingIssueBtn/FacingIssueBtn.style.scss b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.styles.scss similarity index 100% rename from frontend/src/components/facingIssueBtn/FacingIssueBtn.style.scss rename to frontend/src/components/LaunchChatSupport/LaunchChatSupport.styles.scss diff --git a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx new file mode 100644 index 0000000000..c568a7e82b --- /dev/null +++ b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx @@ -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; + 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(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, + ): 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 +
+ + + + + {/* Add Credit Card Modal */} + Add Credit Card for Chat Support} + open={isAddCreditCardModalOpen} + closable + onCancel={(): void => setIsAddCreditCardModalOpen(false)} + destroyOnClose + footer={[ + , + , + ]} + > + + You're currently on Trial plan + . Add a credit card to access SigNoz chat support to your workspace. + + +
+ ) : null; +} + +LaunchChatSupport.defaultProps = { + message: '', + buttonText: '', + className: '', + onHoverText: '', + intercomMessageDisabled: false, +}; + +export default LaunchChatSupport; diff --git a/frontend/src/components/facingIssueBtn/util.ts b/frontend/src/components/LaunchChatSupport/util.ts similarity index 100% rename from frontend/src/components/facingIssueBtn/util.ts rename to frontend/src/components/LaunchChatSupport/util.ts diff --git a/frontend/src/components/ResizeTable/DynamicColumnTable.tsx b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx index 5a3dfd39dd..53cccbe546 100644 --- a/frontend/src/components/ResizeTable/DynamicColumnTable.tsx +++ b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx @@ -5,7 +5,7 @@ import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd'; import { ColumnGroupType, ColumnType } from 'antd/es/table'; import { ColumnsType } from 'antd/lib/table'; import logEvent from 'api/common/logEvent'; -import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; +import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport'; import { SlidersHorizontal } from 'lucide-react'; import { memo, useEffect, useState } from 'react'; import { popupContainer } from 'utils/selectPopupContainer'; @@ -96,7 +96,7 @@ function DynamicColumnTable({ return (
- {facingIssueBtn && } + {facingIssueBtn && } {dynamicColumns && ( { tablesource: typeof TableDataSource[keyof typeof TableDataSource]; dynamicColumns: TableProps['columns']; onDragColumn?: (fromIndex: number, toIndex: number) => void; - facingIssueBtn?: FacingIssueBtnProps; + facingIssueBtn?: LaunchChatSupportProps; shouldSendAlertsLogEvent?: boolean; } diff --git a/frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx b/frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx deleted file mode 100644 index 093c2b8f02..0000000000 --- a/frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx +++ /dev/null @@ -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; - 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 -
- - - -
- ) : null; -} - -FacingIssueBtn.defaultProps = { - message: '', - buttonText: '', - className: '', - onHoverText: '', - intercomMessageDisabled: false, -}; - -export default FacingIssueBtn; diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index f0b170545a..bdacdb057b 100644 --- a/frontend/src/constants/features.ts +++ b/frontend/src/constants/features.ts @@ -20,4 +20,5 @@ export enum FeatureKeys { ONBOARDING = 'ONBOARDING', CHAT_SUPPORT = 'CHAT_SUPPORT', GATEWAY = 'GATEWAY', + PREMIUM_SUPPORT = 'PREMIUM_SUPPORT', } diff --git a/frontend/src/container/AppLayout/AppLayout.styles.scss b/frontend/src/container/AppLayout/AppLayout.styles.scss index c789ba5e0f..2ae1531c79 100644 --- a/frontend/src/container/AppLayout/AppLayout.styles.scss +++ b/frontend/src/container/AppLayout/AppLayout.styles.scss @@ -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 { .app-layout { .app-content { diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index ba64e9af45..0f267976d2 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -9,12 +9,15 @@ import getLocalStorageKey from 'api/browser/localstorage/get'; import getUserLatestVersion from 'api/user/getLatestVersion'; import getUserVersion from 'api/user/getVersion'; import cx from 'classnames'; +import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import { IS_SIDEBAR_COLLAPSED } from 'constants/app'; +import { FeatureKeys } from 'constants/features'; import ROUTES from 'constants/routes'; import SideNav from 'container/SideNav'; import TopNav from 'container/TopNav'; import { useIsDarkMode } from 'hooks/useDarkMode'; +import useFeatureFlags from 'hooks/useFeatureFlag'; import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; @@ -49,6 +52,7 @@ import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { ChildrenContainer, Layout, LayoutContent } from './styles'; import { getRouteKey } from './utils'; +// eslint-disable-next-line sonarjs/cognitive-complexity function AppLayout(props: AppLayoutProps): JSX.Element { const { isLoggedIn, user, role } = useSelector( (state) => state.app, @@ -58,10 +62,19 @@ function AppLayout(props: AppLayoutProps): JSX.Element { getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true', ); + const { notifications } = useNotifications(); + const isDarkMode = useIsDarkMode(); const { data: licenseData, isFetching } = useLicense(); + const isPremiumChatSupportEnabled = + useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; + + const showAddCreditCardModal = + !isPremiumChatSupportEnabled && + !licenseData?.payload?.trialConvertedToSubscription; + const { pathname } = useLocation(); const { t } = useTranslation(['titles']); @@ -95,8 +108,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const latestCurrentCounter = useRef(0); const latestVersionCounter = useRef(0); - const { notifications } = useNotifications(); - const onCollapse = useCallback(() => { setCollapsed((collapsed) => !collapsed); }, []); @@ -331,6 +342,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
+ + {showAddCreditCardModal && } ); } diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 4b383eb272..965b21aa5a 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -13,8 +13,8 @@ import { import saveAlertApi from 'api/alerts/save'; import testAlertApi from 'api/alerts/testAlert'; import logEvent from 'api/common/logEvent'; -import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; -import { alertHelpMessage } from 'components/facingIssueBtn/util'; +import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport'; +import { alertHelpMessage } from 'components/LaunchChatSupport/util'; import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts'; import { FeatureKeys } from 'constants/features'; import { QueryParams } from 'constants/query'; @@ -712,7 +712,7 @@ function FormAlertRules({ > Check an example alert - Create and manage dashboards for your workspace. - }
- - Manage Integrations for this workspace - All Integrations - (null); + const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState( + false, + ); const handleChannelWithRedirects = (url: string): void => { window.open(url, '_blank'); @@ -117,10 +135,67 @@ export default function Support(): JSX.Element { 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, + ): 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 => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (window.Intercom) { + if (showAddCreditCardModal) { + setIsAddCreditCardModalOpen(true); + } else if (window.Intercom) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.Intercom('show'); @@ -183,6 +258,43 @@ export default function Support(): JSX.Element { ), )}
+ + {/* Add Credit Card Modal */} + Add Credit Card for Chat Support} + open={isAddCreditCardModalOpen} + closable + onCancel={(): void => setIsAddCreditCardModalOpen(false)} + destroyOnClose + footer={[ + , + , + ]} + > + + You're currently on Trial plan + . Add a credit card to access SigNoz chat support to your workspace. + + ); }