From 865b3d75e7f69fa582a504f945226a41f442a950 Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Mon, 27 Mar 2023 17:42:07 +0300 Subject: [PATCH] [MM-50976] - Contact Support to redirect to Zendesk and pre-fill known information (#22640) --- .../cancel_subscription.tsx | 14 +-- .../cloud_trial_banner.tsx | 3 +- .../contact_sales_card.tsx | 10 +- .../billing/billing_subscriptions/index.tsx | 13 +-- .../limit_reached_banner.test.tsx | 2 +- .../limit_reached_banner.tsx | 4 +- .../billing/billing_subscriptions/limits.tsx | 4 +- .../to_yearly_nudge_banner.tsx | 3 +- .../billing/delete_workspace/result_modal.tsx | 13 +-- .../feature_discovery.test.tsx | 3 - .../feature_discovery/feature_discovery.tsx | 28 +++-- .../admin_console/feature_discovery/index.tsx | 8 +- .../enterprise_edition_right_panel.test.tsx | 93 ++++++++++++---- .../renew_license_card.test.tsx | 35 +++++- .../workspace-optimization/dashboard.data.tsx | 7 +- .../contact_sales/contact_us.tsx | 7 +- .../overage_users_banner/index.tsx | 6 +- .../overage_users_banner.test.tsx | 12 ++- .../renewal_link/renewal_link.test.tsx | 39 ++++++- .../renewal_link/renewal_link.tsx | 7 +- .../cloud_subscribe_result_modal/error.tsx | 8 +- .../common/hooks/useOpenSalesLink.ts | 34 +++++- .../common/hooks/useOpenZendeskForm.ts | 26 +++++ .../pricing_modal/contact_sales_cta.tsx | 12 +-- .../src/components/pricing_modal/content.tsx | 13 ++- .../pricing_modal/self_hosted_content.tsx | 6 +- .../src/components/purchase_modal/index.ts | 17 ++- .../purchase_modal/purchase_modal.tsx | 13 ++- .../contact_sales_link.tsx | 7 +- .../self_hosted_purchase_modal/error.tsx | 6 +- .../ad_ldap_upsell_banner.tsx | 16 +-- webapp/channels/src/selectors/cloud.ts | 41 ------- .../src/utils/contact_support_sales.ts | 100 ++++++++++++++++++ 33 files changed, 416 insertions(+), 194 deletions(-) create mode 100644 webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts create mode 100644 webapp/channels/src/utils/contact_support_sales.ts diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx index 68f5503d66..cf8560ed3b 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cancel_subscription.tsx @@ -5,16 +5,12 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {trackEvent} from 'actions/telemetry_actions'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import ExternalLink from 'components/external_link'; -type Props = { - cancelAccountLink: any; -} - -const CancelSubscription = (props: Props) => { - const { - cancelAccountLink, - } = props; +const CancelSubscription = () => { + const description = `I am requesting that workspace "${window.location.host}" be deleted`; + const [, contactSupportURL] = useOpenCloudZendeskSupportForm('Request workspace be deleted', description); return (
@@ -33,7 +29,7 @@ const CancelSubscription = (props: Props) => {
trackEvent('cloud_admin', 'click_contact_us')} > diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx index 204495ce90..a62edc1e54 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/cloud_trial_banner.tsx @@ -24,7 +24,6 @@ import AlertBanner from 'components/alert_banner'; import UpgradeLink from 'components/widgets/links/upgrade_link'; import './cloud_trial_banner.scss'; -import {SalesInquiryIssue} from 'selectors/cloud'; export interface Props { trialEndDate: number; @@ -34,7 +33,7 @@ const CloudTrialBanner = ({trialEndDate}: Props): JSX.Element | null => { const endDate = new Date(trialEndDate); const DISMISSED_DAYS = 10; const {formatMessage} = useIntl(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); + const [openSalesLink] = useOpenSalesLink(); const dispatch = useDispatch(); const user = useSelector(getCurrentUser); const storedDismissedEndDate = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_TRIAL_BANNER, CloudBanners.UPGRADE_FROM_TRIAL)); diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx index 0687b78857..3e5bec63e9 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/contact_sales_card.tsx @@ -10,21 +10,19 @@ import {CloudLinks, CloudProducts} from 'utils/constants'; import PrivateCloudSvg from 'components/common/svg_images_components/private_cloud_svg'; import CloudTrialSvg from 'components/common/svg_images_components/cloud_trial_svg'; import {TelemetryProps} from 'components/common/hooks/useOpenPricingModal'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import ExternalLink from 'components/external_link'; type Props = { - contactSalesLink: any; isFreeTrial: boolean; - trialQuestionsLink: any; subscriptionPlan: string | undefined; onUpgradeMattermostCloud: (telemetryProps?: TelemetryProps | undefined) => void; } const ContactSalesCard = (props: Props) => { + const [openSalesLink, contactSalesLink] = useOpenSalesLink(); const { - contactSalesLink, isFreeTrial, - trialQuestionsLink, subscriptionPlan, onUpgradeMattermostCloud, } = props; @@ -145,7 +143,7 @@ const ContactSalesCard = (props: Props) => { {(isFreeTrial || subscriptionPlan === CloudProducts.ENTERPRISE || isCloudLegacyPlan) && trackEvent('cloud_admin', 'click_contact_sales')} > @@ -163,7 +161,7 @@ const ContactSalesCard = (props: Props) => { if (subscriptionPlan === CloudProducts.STARTER) { onUpgradeMattermostCloud({trackingLocation: 'admin_console_subscription_card_upgrade_now_button'}); } else { - window.open(contactSalesLink, '_blank'); + openSalesLink(); } }} className='PrivateCloudCard__actionButton' diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx index f4f07ab0f9..ac6bdb6ef1 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/index.tsx @@ -13,7 +13,6 @@ import FormattedAdminHeader from 'components/widgets/admin_console/formatted_adm import CloudTrialBanner from 'components/admin_console/billing/billing_subscriptions/cloud_trial_banner'; import CloudFetchError from 'components/cloud_fetch_error'; -import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud'; import { getSubscriptionProduct, getCloudSubscription as selectCloudSubscription, @@ -63,9 +62,6 @@ const BillingSubscriptions = () => { const isCardExpired = isCustomerCardExpired(useSelector(selectCloudCustomer)); - const contactSalesLink = useSelector(getCloudContactUsLink)(InquiryType.Sales); - const cancelAccountLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.CancelAccount); - const trialQuestionsLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.TrialQuestions); const trialEndDate = subscription?.trial_end_at || 0; const [showCreditCardBanner, setShowCreditCardBanner] = useState(true); @@ -159,19 +155,12 @@ const BillingSubscriptions = () => { ) : ( )} - {isAnnualProfessionalOrEnterprise && !isFreeTrial ? - : - - } + {isAnnualProfessionalOrEnterprise && !isFreeTrial ? : } } diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx index 0aa08eddb7..66e4356434 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.test.tsx @@ -177,7 +177,7 @@ describe('limits_reached_banner', () => { const store = mockStore(state); const spies = makeSpies(); const mockOpenSalesLink = jest.fn(); - spies.useOpenSalesLink.mockReturnValue(mockOpenSalesLink); + spies.useOpenSalesLink.mockReturnValue([mockOpenSalesLink, '']); spies.useGetUsageDeltas.mockReturnValue(someLimitReached); renderWithIntl(); screen.getByText(titleFree); diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx index d006716bea..666adc3bd0 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limit_reached_banner.tsx @@ -5,8 +5,6 @@ import React from 'react'; import {useIntl, FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; -import {SalesInquiryIssue} from 'selectors/cloud'; - import {CloudProducts} from 'utils/constants'; import {anyUsageDeltaExceededLimit} from 'utils/limits'; @@ -33,7 +31,7 @@ const LimitReachedBanner = (props: Props) => { const hasDismissedBanner = useSelector(getHasDismissedSystemConsoleLimitReached); - const openSalesLink = useOpenSalesLink(props.product?.sku === CloudProducts.PROFESSIONAL ? SalesInquiryIssue.UpgradeEnterprise : undefined); + const [openSalesLink] = useOpenSalesLink(); const openPricingModal = useOpenPricingModal(); const saveBool = useSaveBool(); if (hasDismissedBanner || !someLimitExceeded || !props.product || (props.product.sku !== CloudProducts.STARTER)) { diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx index 2798c1374a..3457fbd201 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/limits.tsx @@ -11,8 +11,6 @@ import { getSubscriptionProduct, } from 'mattermost-redux/selectors/entities/cloud'; -import {SalesInquiryIssue} from 'selectors/cloud'; - import {CloudProducts} from 'utils/constants'; import {asGBString, fallbackStarterLimits, hasSomeLimits} from 'utils/limits'; @@ -32,7 +30,7 @@ const Limits = (): JSX.Element | null => { const subscriptionProduct = useSelector(getSubscriptionProduct); const [cloudLimits, limitsLoaded] = useGetLimits(); const usage = useGetUsage(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); + const [openSalesLink] = useOpenSalesLink(); const openPricingModal = useOpenPricingModal(); if (!subscriptionProduct || !limitsLoaded || !hasSomeLimits(cloudLimits)) { diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx index 1bbf9823b3..3cc1e8aa75 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx @@ -10,7 +10,6 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; -import {SalesInquiryIssue} from 'selectors/cloud'; import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud'; import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {savePreferences} from 'mattermost-redux/actions/preferences'; @@ -79,7 +78,7 @@ const ToYearlyNudgeBannerDismissable = () => { const ToYearlyNudgeBanner = () => { const {formatMessage} = useIntl(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.AboutPurchasing); + const [openSalesLink] = useOpenSalesLink(); const openPurchaseModal = useOpenCloudPurchaseModal({}); const product = useSelector(selectSubscriptionProduct); diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx index b84c72ee90..bc452d3c57 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx @@ -7,6 +7,7 @@ import {useDispatch, useSelector} from 'react-redux'; import IconMessage from 'components/purchase_modal/icon_message'; import FullScreenModal from 'components/widgets/modals/full_screen_modal'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import {closeModal} from 'actions/views/modals'; import {isModalOpen} from 'selectors/views/modals'; @@ -14,9 +15,6 @@ import {GlobalState} from 'types/store'; import './result_modal.scss'; -import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; -import {InquiryType} from 'selectors/cloud'; - type Props = { onHide?: () => void; icon: JSX.Element; @@ -33,7 +31,7 @@ type Props = { export default function ResultModal(props: Props) { const dispatch = useDispatch(); - const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical); + const [openContactSupport] = useOpenCloudZendeskSupportForm('Delete workspace', ''); const isResultModalOpen = useSelector((state: GlobalState) => isModalOpen(state, props.identifier), @@ -64,14 +62,13 @@ export default function ResultModal(props: Props) { buttonHandler={props.primaryButtonHandler} className={'success'} formattedTertiaryButonText={ - props.contactSupportButtonVisible ? + props.contactSupportButtonVisible ? ( : - undefined + />) : undefined } - tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactUs : undefined} + tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactSupport : undefined} /> diff --git a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx index 2b8e5d19b2..e8589a0c09 100644 --- a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx +++ b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx @@ -17,7 +17,6 @@ describe('components/feature_discovery', () => { { { }); } + contactSalesFunc = () => { + const {customer, isCloud} = this.props; + const customerEmail = customer?.email || ''; + const firstName = customer?.contact_first_name || ''; + const lastName = customer?.contact_last_name || ''; + const companyName = customer?.name || ''; + const utmMedium = isCloud ? 'in-product-cloud' : 'in-product'; + goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, 'mattermost', utmMedium); + } + renderPostTrialCta = () => { const { minimumSKURequiredForFeature, @@ -110,7 +122,7 @@ export default class FeatureDiscovery extends React.PureComponent data-testid='featureDiscovery_primaryCallToAction' onClick={() => { trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + this.contactSalesFunc(); }} > hadPrevCloudTrial, isPaidSubscription, minimumSKURequiredForFeature, - contactSalesLink, } = this.props; const canRequestCloudFreeTrial = isCloud && !isCloudTrial && !hadPrevCloudTrial && !isPaidSubscription; @@ -217,11 +228,10 @@ export default class FeatureDiscovery extends React.PureComponent onClick={() => { if (isCloud) { trackEvent(TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); - window.open(contactSalesLink, '_blank'); } else { trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); } + this.contactSalesFunc(); }} > { const license = { IsLicensed: 'true', @@ -28,8 +61,11 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris } as EnterpriseEditionProps; test('should render for no Gov no Trial no Enterprise', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Plan'); @@ -43,11 +79,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Gov no Trial no Enterprise', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Upgrade to the Enterprise Gov Plan'); @@ -61,11 +100,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Enterprise no Trial', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?'); @@ -73,11 +115,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for E20 no Trial', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Need to increase your headcount?'); @@ -85,11 +130,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Trial no Gov', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Plan'); @@ -97,11 +145,14 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris }); test('should render for Trial Gov', () => { + const store = mockStore(initialState); const wrapper = mountWithIntl( - , + + + , ); expect(wrapper.find('.upgrade-title').text()).toEqual('Purchase the Enterprise Gov Plan'); diff --git a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx index eb43a3cf0f..c52e8272d7 100644 --- a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx @@ -12,6 +12,37 @@ import mockStore from 'tests/test_store'; import RenewalLicenseCard from './renew_license_card'; +const initialState = { + views: { + announcementBar: { + announcementBarState: { + announcementBarCount: 1, + }, + }, + }, + entities: { + general: { + config: { + CWSURL: '', + }, + license: { + IsLicensed: 'true', + Cloud: 'true', + }, + }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_user'}, + }, + }, + preferences: { + myPreferences: {}, + }, + cloud: {}, + }, +}; + const actImmediate = (wrapper: ReactWrapper) => act( () => @@ -47,7 +78,7 @@ describe('components/RenewalLicenseCard', () => { }); }); getRenewalLinkSpy.mockImplementation(() => promise); - const store = mockStore({}); + const store = mockStore(initialState); const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update @@ -64,7 +95,7 @@ describe('components/RenewalLicenseCard', () => { reject(new Error('License cannot be renewed from portal')); }); getRenewalLinkSpy.mockImplementation(() => promise); - const store = mockStore({}); + const store = mockStore(initialState); const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update diff --git a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx index b9477f36de..dd5ee9b565 100644 --- a/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx +++ b/webapp/channels/src/components/admin_console/workspace-optimization/dashboard.data.tsx @@ -18,8 +18,9 @@ import {getLicense} from 'mattermost-redux/selectors/entities/general'; import {GlobalState} from '@mattermost/types/store'; -import {CloudLinks, ConsolePages, DocLinks, LicenseLinks} from 'utils/constants'; +import {CloudLinks, ConsolePages, DocLinks} from 'utils/constants'; import {daysToLicenseExpire, isEnterpriseOrE20License, getIsStarterLicense} from '../../../utils/license_utils'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; export type DataModel = { [key: string]: { @@ -84,8 +85,10 @@ const useMetricsData = () => { const isEnterpriseLicense = isEnterpriseOrE20License(license); const isStarterLicense = getIsStarterLicense(license); + const [, contactSalesLink] = useOpenSalesLink(); + const trialOrEnterpriseCtaConfig = { - configUrl: canStartTrial ? ConsolePages.LICENSE : LicenseLinks.CONTACT_SALES, + configUrl: canStartTrial ? ConsolePages.LICENSE : contactSalesLink, configText: canStartTrial ? formatMessage({id: 'admin.reporting.workspace_optimization.cta.startTrial', defaultMessage: 'Start trial'}) : formatMessage({id: 'admin.reporting.workspace_optimization.cta.upgradeLicense', defaultMessage: 'Contact sales'}), }; diff --git a/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx b/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx index 48abe4a4dd..74b576192d 100644 --- a/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx +++ b/webapp/channels/src/components/announcement_bar/contact_sales/contact_us.tsx @@ -6,8 +6,9 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {trackEvent} from 'actions/telemetry_actions'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; + import './contact_us.scss'; -import {LicenseLinks} from '../../../utils/constants'; export interface Props { buttonTextElement?: JSX.Element; @@ -16,10 +17,12 @@ export interface Props { } const ContactUsButton: React.FC = (props: Props) => { + const [openContactSales] = useOpenSalesLink(); + const handleContactUsLinkClick = async (e: React.MouseEvent) => { e.preventDefault(); trackEvent('admin', props.eventID || 'in_trial_contact_sales'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); }; return ( diff --git a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx index 176af1dbd2..d22fe6389f 100644 --- a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx +++ b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx @@ -15,7 +15,8 @@ import {savePreferences} from 'mattermost-redux/actions/preferences'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {PreferenceType} from '@mattermost/types/preferences'; import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck'; -import {LicenseLinks, StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import {StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants'; import './overage_users_banner.scss'; @@ -34,6 +35,7 @@ const adminHasDismissed = ({preferenceName, overagePreferences, isWarningBanner} }; const OverageUsersBanner = () => { + const [openContactSales] = useOpenSalesLink(); const dispatch = useDispatch(); const stats = useSelector((state: GlobalState) => state.entities.admin.analytics) || {}; const isAdmin = useSelector(isCurrentUserSystemAdmin); @@ -90,7 +92,7 @@ const OverageUsersBanner = () => { const handleContactSalesClick = (e: React.MouseEvent) => { e.preventDefault(); trackEventFn('Contact Sales'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); }; const handleClick = isExpandable ? handleUpdateSeatsSelfServeClick : handleContactSalesClick; diff --git a/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx b/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx index 212c9dadae..dedd30a7c9 100644 --- a/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx +++ b/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx @@ -7,7 +7,7 @@ import {fireEvent, screen} from '@testing-library/react'; import {DeepPartial} from '@mattermost/types/utilities'; import {GlobalState} from 'types/store'; import {General} from 'mattermost-redux/constants'; -import {LicenseLinks, OverActiveUserLimits, Preferences, StatTypes} from 'utils/constants'; +import {OverActiveUserLimits, Preferences, StatTypes} from 'utils/constants'; import {renderWithIntlAndStore} from 'tests/react_testing_utils'; import {savePreferences} from 'mattermost-redux/actions/preferences'; import {trackEvent} from 'actions/telemetry_actions'; @@ -249,7 +249,10 @@ describe('components/overage_users_banner', () => { fireEvent.click(screen.getByText(contactSalesTextLink)); expect(windowSpy).toBeCalledTimes(1); - expect(windowSpy).toBeCalledWith(LicenseLinks.CONTACT_SALES, '_blank'); + + // only the email is encoded and other params are empty. See logic for useOpenSalesLink hook + const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product'; + expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank'); expect(trackEvent).toBeCalledTimes(1); expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', { cta: 'Contact Sales', @@ -368,7 +371,10 @@ describe('components/overage_users_banner', () => { fireEvent.click(screen.getByText(contactSalesTextLink)); expect(windowSpy).toBeCalledTimes(1); - expect(windowSpy).toBeCalledWith(LicenseLinks.CONTACT_SALES, '_blank'); + + // only the email is encoded and other params are empty. See logic for useOpenSalesLink hook + const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product'; + expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank'); expect(trackEvent).toBeCalledTimes(1); expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', { cta: 'Contact Sales', diff --git a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx index 7582add168..9dae6a38d0 100644 --- a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx +++ b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx @@ -3,13 +3,46 @@ import React from 'react'; import {ReactWrapper} from 'enzyme'; +import {Provider} from 'react-redux'; import {act} from 'react-dom/test-utils'; import {Client4} from 'mattermost-redux/client'; import {mountWithIntl} from 'tests/helpers/intl-test-helper'; +import mockStore from 'tests/test_store'; import RenewalLink from './renewal_link'; +const initialState = { + views: { + announcementBar: { + announcementBarState: { + announcementBarCount: 1, + }, + }, + }, + entities: { + general: { + config: { + CWSURL: '', + }, + license: { + IsLicensed: 'true', + Cloud: 'true', + }, + }, + users: { + currentUserId: 'current_user_id', + profiles: { + current_user_id: {roles: 'system_user'}, + }, + }, + preferences: { + myPreferences: {}, + }, + cloud: {}, + }, +}; + const actImmediate = (wrapper: ReactWrapper) => act( () => @@ -40,7 +73,8 @@ describe('components/RenewalLink', () => { }); }); getRenewalLinkSpy.mockImplementation(() => promise); - const wrapper = mountWithIntl(); + const store = mockStore(initialState); + const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update await actImmediate(wrapper); @@ -54,7 +88,8 @@ describe('components/RenewalLink', () => { reject(new Error('License cannot be renewed from portal')); }); getRenewalLinkSpy.mockImplementation(() => promise); - const wrapper = mountWithIntl(); + const store = mockStore(initialState); + const wrapper = mountWithIntl(); // wait for the promise to resolve and component to update await actImmediate(wrapper); diff --git a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx index 3ca79b1e11..dfae9f3ea1 100644 --- a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx +++ b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx @@ -11,9 +11,9 @@ import {trackEvent} from 'actions/telemetry_actions'; import {ModalData} from 'types/actions'; import { - LicenseLinks, ModalIdentifiers, } from 'utils/constants'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import NoInternetConnection from '../no_internet_connection/no_internet_connection'; @@ -31,6 +31,9 @@ export interface RenewalLinkProps { const RenewalLink = (props: RenewalLinkProps) => { const [renewalLink, setRenewalLink] = useState(''); const [manualInterventionRequired, setManualInterventionRequired] = useState(false); + + const [openContactSales] = useOpenSalesLink(); + useEffect(() => { Client4.getRenewalLink().then(({renewal_link: renewalLinkParam}) => { try { @@ -55,7 +58,7 @@ const RenewalLink = (props: RenewalLinkProps) => { } window.open(renewalLink, '_blank'); } else if (manualInterventionRequired) { - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); } else { showConnectionErrorModal(); } diff --git a/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx b/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx index 0b61d3598e..2340430c02 100644 --- a/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx +++ b/webapp/channels/src/components/cloud_subscribe_result_modal/error.tsx @@ -13,9 +13,8 @@ import PaymentFailedSvg from 'components/common/svg_images_components/payment_fa import IconMessage from 'components/purchase_modal/icon_message'; import FullScreenModal from 'components/widgets/modals/full_screen_modal'; -import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; -import {InquiryType} from 'selectors/cloud'; import {closeModal} from 'actions/views/modals'; import {ModalIdentifiers} from 'utils/constants'; import {isModalOpen} from 'selectors/views/modals'; @@ -31,7 +30,8 @@ type Props = { function ErrorModal(props: Props) { const dispatch = useDispatch(); const subscriptionProduct = useSelector(getSubscriptionProduct); - const openContactUs = useOpenSalesLink(undefined, InquiryType.Technical); + + const [openContactSupport] = useOpenCloudZendeskSupportForm('Cloud Subscription', ''); const isSuccessModalOpen = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.ERROR_MODAL), @@ -97,7 +97,7 @@ function ErrorModal(props: Props) { } /> } - tertiaryButtonHandler={openContactUs} + tertiaryButtonHandler={openContactSupport} buttonHandler={onBackButtonPress} className={'success'} /> diff --git a/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts b/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts index aecb58844f..4dbe44d0ca 100644 --- a/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts +++ b/webapp/channels/src/components/common/hooks/useOpenSalesLink.ts @@ -3,11 +3,35 @@ import {useSelector} from 'react-redux'; -import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud'; +import {getCloudCustomer, isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {buildMMURL, goToMattermostContactSalesForm} from 'utils/contact_support_sales'; +import {LicenseLinks} from 'utils/constants'; -export default function useOpenSalesLink(issue?: SalesInquiryIssue, inquireType: InquiryType = InquiryType.Sales) { - const contactSalesLink = useSelector(getCloudContactUsLink)(inquireType, issue); +export default function useOpenSalesLink(): [() => void, string] { + const isCloud = useSelector(isCurrentLicenseCloud); + const customer = useSelector(getCloudCustomer); + const currentUser = useSelector(getCurrentUser); + let customerEmail = ''; + let firstName = ''; + let lastName = ''; + let companyName = ''; + const utmSource = 'mattermost'; + let utmMedium = 'in-product'; - return () => window.open(contactSalesLink, '_blank'); + if (isCloud && customer) { + customerEmail = customer.email || ''; + firstName = customer.contact_first_name || ''; + lastName = customer.contact_last_name || ''; + companyName = customer.name || ''; + utmMedium = 'in-product-cloud'; + } else { + customerEmail = currentUser.email || ''; + } + + const contactSalesLink = buildMMURL(LicenseLinks.CONTACT_SALES, firstName, lastName, companyName, customerEmail, utmSource, utmMedium); + const goToSalesLinkFunc = () => { + goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, utmSource, utmMedium); + }; + return [goToSalesLinkFunc, contactSalesLink]; } - diff --git a/webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts b/webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts new file mode 100644 index 0000000000..16df9d9a02 --- /dev/null +++ b/webapp/channels/src/components/common/hooks/useOpenZendeskForm.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useSelector} from 'react-redux'; + +import {getCloudCustomer} from 'mattermost-redux/selectors/entities/cloud'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getCloudSupportLink, getSelfHostedSupportLink, goToCloudSupportForm, goToSelfHostedSupportForm} from 'utils/contact_support_sales'; + +export function useOpenCloudZendeskSupportForm(subject: string, description: string): [() => void, string] { + const customer = useSelector(getCloudCustomer); + const customerEmail = customer?.email || ''; + + const url = getCloudSupportLink(customerEmail, subject, description, window.location.host); + + return [() => goToCloudSupportForm(customerEmail, subject, description, window.location.host), url]; +} + +export function useOpenSelfHostedZendeskSupportForm(subject: string): [() => void, string] { + const currentUser = useSelector(getCurrentUser); + const customerEmail = currentUser.email || ''; + + const url = getSelfHostedSupportLink(customerEmail, subject); + + return [() => goToSelfHostedSupportForm(customerEmail, subject), url]; +} diff --git a/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx b/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx index 1a0c47ed2a..66e05d0947 100644 --- a/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx +++ b/webapp/channels/src/components/pricing_modal/contact_sales_cta.tsx @@ -11,8 +11,7 @@ import {trackEvent} from 'actions/telemetry_actions'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; -import {LicenseLinks, TELEMETRY_CATEGORIES} from 'utils/constants'; -import {SalesInquiryIssue} from 'selectors/cloud'; +import {TELEMETRY_CATEGORIES} from 'utils/constants'; const StyledA = styled.a` color: var(--denim-button-bg); @@ -27,11 +26,7 @@ text-align: center; function ContactSalesCTA() { const {formatMessage} = useIntl(); - const openSalesLink = useOpenSalesLink(SalesInquiryIssue.UpgradeEnterprise); - - const openSelfHostedLink = () => { - window.open(LicenseLinks.CONTACT_SALES, '_blank'); - }; + const [openSalesLink] = useOpenSalesLink(); const isCloud = useSelector(isCurrentLicenseCloud); @@ -42,11 +37,10 @@ function ContactSalesCTA() { e.preventDefault(); if (isCloud) { trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales'); - openSalesLink(); } else { trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales'); - openSelfHostedLink(); } + openSalesLink(); }} > {formatMessage({id: 'pricing_modal.btn.contactSalesForQuote', defaultMessage: 'Contact Sales'})} diff --git a/webapp/channels/src/components/pricing_modal/content.tsx b/webapp/channels/src/components/pricing_modal/content.tsx index 7dc7d8dcfc..e0f55999fe 100644 --- a/webapp/channels/src/components/pricing_modal/content.tsx +++ b/webapp/channels/src/components/pricing_modal/content.tsx @@ -10,8 +10,6 @@ import {CloudLinks, CloudProducts, LicenseSkus, ModalIdentifiers, MattermostFeat import {fallbackStarterLimits, asGBString, hasSomeLimits} from 'utils/limits'; import {findOnlyYearlyProducts, findProductBySku} from 'utils/products'; -import {getCloudContactUsLink, InquiryType, SalesInquiryIssue} from 'selectors/cloud'; - import {trackEvent} from 'actions/telemetry_actions'; import {closeModal, openModal} from 'actions/views/modals'; import {subscribeCloudSubscription} from 'actions/cloud'; @@ -38,6 +36,8 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal'; import useOpenDowngradeModal from 'components/common/hooks/useOpenDowngradeModal'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import ExternalLink from 'components/external_link'; import DowngradeTeamRemovalModal from './downgrade_team_removal_modal'; @@ -64,15 +64,12 @@ function Content(props: ContentProps) { const openPricingModalBackAction = useOpenPricingModal(); const isAdmin = useSelector(isCurrentUserSystemAdmin); - const contactSalesLink = useSelector(getCloudContactUsLink)(InquiryType.Sales, SalesInquiryIssue.UpgradeEnterprise); const subscription = useSelector(selectCloudSubscription); const currentProduct = useSelector(selectSubscriptionProduct); const products = useSelector(selectCloudProducts); const yearlyProducts = findOnlyYearlyProducts(products || {}); // pricing modal should now only show yearly products - const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); - const currentSubscriptionIsMonthly = currentProduct?.recurring_interval === RecurringIntervals.MONTH; const isEnterprise = currentProduct?.sku === CloudProducts.ENTERPRISE; const isEnterpriseTrial = subscription?.is_free_trial === 'true'; @@ -124,6 +121,8 @@ function Content(props: ContentProps) { const freeTierText = (!isStarter && !currentSubscriptionIsMonthly) ? formatMessage({id: 'pricing_modal.btn.contactSupport', defaultMessage: 'Contact Support'}) : formatMessage({id: 'pricing_modal.btn.downgrade', defaultMessage: 'Downgrade'}); const adminProfessionalTierText = currentSubscriptionIsMonthlyProfessional ? formatMessage({id: 'pricing_modal.btn.switch_to_annual', defaultMessage: 'Switch to annual billing'}) : formatMessage({id: 'pricing_modal.btn.upgrade', defaultMessage: 'Upgrade'}); + const [openContactSales] = useOpenSalesLink(); + const [openContactSupport] = useOpenCloudZendeskSupportForm('Workspace downgrade', ''); const openCloudPurchaseModal = useOpenCloudPurchaseModal({}); const openCloudDelinquencyModal = useOpenCloudPurchaseModal({ isDelinquencyModal: true, @@ -239,7 +238,7 @@ function Content(props: ContentProps) { return { action: () => { trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales'); - window.open(contactSalesLink, '_blank'); + openContactSales(); }, text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}), customClass: ButtonCustomiserClasses.active, @@ -350,7 +349,7 @@ function Content(props: ContentProps) { buttonDetails={{ action: () => { if (!isStarter && !currentSubscriptionIsMonthly) { - window.open(contactSupportLink, '_blank'); + openContactSupport(); return; } diff --git a/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx b/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx index c573b10c4a..9edb86d824 100644 --- a/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx +++ b/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx @@ -6,7 +6,7 @@ import {Modal} from 'react-bootstrap'; import {useIntl} from 'react-intl'; import {useDispatch, useSelector} from 'react-redux'; -import {CloudLinks, LicenseLinks, ModalIdentifiers, SelfHostedProducts, LicenseSkus, TELEMETRY_CATEGORIES, RecurringIntervals} from 'utils/constants'; +import {CloudLinks, ModalIdentifiers, SelfHostedProducts, LicenseSkus, TELEMETRY_CATEGORIES, RecurringIntervals} from 'utils/constants'; import {findSelfHostedProductBySku} from 'utils/hosted_customer'; import {trackEvent} from 'actions/telemetry_actions'; @@ -27,6 +27,7 @@ import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn'; import ExternalLink from 'components/external_link'; import useCanSelfHostedSignup from 'components/common/hooks/useCanSelfHostedSignup'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import { useControlAirGappedSelfHostedPurchaseModal, @@ -89,6 +90,7 @@ function SelfHostedContent(props: ContentProps) { const isEnterprise = license.SkuShortName === LicenseSkus.Enterprise; const isPostSelfHostedEnterpriseTrial = prevSelfHostedTrialLicense.IsLicensed === 'true'; + const [openContactSales] = useOpenSalesLink(); const controlScreeningInProgressModal = useControlScreeningInProgressModal(); const controlAirgappedModal = useControlAirGappedSelfHostedPurchaseModal(); @@ -287,7 +289,7 @@ function SelfHostedContent(props: ContentProps) { buttonDetails={(isPostSelfHostedEnterpriseTrial || !isAdmin) ? { action: () => { trackEvent('self_hosted_pricing', 'click_enterprise_contact_sales'); - window.open(LicenseLinks.CONTACT_SALES, '_blank'); + openContactSales(); }, text: formatMessage({id: 'pricing_modal.btn.contactSales', defaultMessage: 'Contact Sales'}), customClass: ButtonCustomiserClasses.active, diff --git a/webapp/channels/src/components/purchase_modal/index.ts b/webapp/channels/src/components/purchase_modal/index.ts index be38d6167d..0285722adf 100644 --- a/webapp/channels/src/components/purchase_modal/index.ts +++ b/webapp/channels/src/components/purchase_modal/index.ts @@ -19,7 +19,7 @@ import {GlobalState} from 'types/store'; import {BillingDetails} from 'types/cloud/sku'; import {isModalOpen} from 'selectors/views/modals'; -import {getCloudContactUsLink, InquiryType, getCloudDelinquentInvoices, isCloudDelinquencyGreaterThan90Days} from 'selectors/cloud'; +import {getCloudDelinquentInvoices, isCloudDelinquencyGreaterThan90Days} from 'selectors/cloud'; import {isDevModeEnabled} from 'selectors/general'; import {ModalIdentifiers} from 'utils/constants'; @@ -29,6 +29,7 @@ import {completeStripeAddPaymentMethod, subscribeCloudSubscription} from 'action import {ModalData} from 'types/actions'; import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription'; import {findOnlyYearlyProducts} from 'utils/products'; +import {getCloudContactSalesLink, getCloudSupportLink} from 'utils/contact_support_sales'; const PurchaseModal = makeAsyncComponent('PurchaseModal', React.lazy(() => import('./purchase_modal'))); @@ -39,19 +40,27 @@ function mapStateToProps(state: GlobalState) { const products = state.entities.cloud!.products; const yearlyProducts = findOnlyYearlyProducts(products || {}); + const customer = state.entities.cloud.customer; + const customerEmail = customer?.email || ''; + const firstName = customer?.contact_first_name || ''; + const lastName = customer?.contact_last_name || ''; + const companyName = customer?.name || ''; + const contactSalesLink = getCloudContactSalesLink(firstName, lastName, companyName, customerEmail, 'mattermost', 'in-product-cloud'); + const contactSupportLink = getCloudSupportLink(customerEmail, 'Cloud purchase', '', window.location.host); + return { show: isModalOpen(state, ModalIdentifiers.CLOUD_PURCHASE), products, yearlyProducts, isDevMode: isDevModeEnabled(state), - contactSupportLink: getCloudContactUsLink(state)(InquiryType.Technical), + contactSupportLink, invoices: getCloudDelinquentInvoices(state), isCloudDelinquencyGreaterThan90Days: isCloudDelinquencyGreaterThan90Days(state), isFreeTrial: subscription?.is_free_trial === 'true', isComplianceBlocked: subscription?.compliance_blocked === 'true', - contactSalesLink: getCloudContactUsLink(state)(InquiryType.Sales), + contactSalesLink, productId: subscription?.product_id, - customer: state.entities.cloud.customer, + customer, currentTeam: getCurrentTeam(state), theme: getTheme(state), isDelinquencyModal, diff --git a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx index 36b39a3ab0..3fdfe6f319 100644 --- a/webapp/channels/src/components/purchase_modal/purchase_modal.tsx +++ b/webapp/channels/src/components/purchase_modal/purchase_modal.tsx @@ -18,7 +18,6 @@ import ComplianceScreenFailedSvg from 'components/common/svg_images_components/a import AddressForm from 'components/payment_form/address_form'; import {t} from 'utils/i18n'; -import {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud'; import {ActionResult} from 'mattermost-redux/types/actions'; import {localizeMessage, getNextBillingDate, getBlankAddressWithCountry} from 'utils/utils'; @@ -34,6 +33,7 @@ import { ModalIdentifiers, RecurringIntervals, } from 'utils/constants'; +import {goToMattermostContactSalesForm} from 'utils/contact_support_sales'; import PaymentDetails from 'components/admin_console/billing/payment_details'; import {STRIPE_CSS_SRC, STRIPE_PUBLIC_KEY} from 'components/payment_form/stripe'; @@ -54,6 +54,8 @@ import {ModalData} from 'types/actions'; import {Theme} from 'mattermost-redux/selectors/entities/preferences'; +import {Address, CloudCustomer, Product, Invoice, areShippingDetailsValid, Feedback} from '@mattermost/types/cloud'; + import {areBillingDetailsValid, BillingDetails} from '../../types/cloud/sku'; import {Team} from '@mattermost/types/teams'; @@ -463,6 +465,7 @@ class PurchaseModal extends React.PureComponent { } confirmSwitchToAnnual = () => { + const {customer} = this.props; this.props.actions.openModal({ modalId: ModalIdentifiers.CONFIRM_SWITCH_TO_YEARLY, dialogType: SwitchToYearlyPlanConfirmModal, @@ -476,7 +479,11 @@ class PurchaseModal extends React.PureComponent { TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'confirm_switch_to_annual_click_contact_sales', ); - window.open(this.props.contactSalesLink, '_blank'); + const customerEmail = customer?.email || ''; + const firstName = customer?.contact_first_name || ''; + const lastName = customer?.contact_last_name || ''; + const companyName = customer?.name || ''; + goToMattermostContactSalesForm(firstName, lastName, companyName, customerEmail, 'mattermost', 'in-product-cloud'); }, }, }); @@ -1013,7 +1020,7 @@ class PurchaseModal extends React.PureComponent { }); }} contactSupportLink={ - this.props.contactSalesLink + this.props.contactSupportLink } currentTeam={this.props.currentTeam} onSuccess={() => { diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx index 350e44cb9f..a34e11df60 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/contact_sales_link.tsx @@ -4,18 +4,17 @@ import React from 'react'; import {useIntl} from 'react-intl'; -import {useSelector} from 'react-redux'; import {trackEvent} from 'actions/telemetry_actions'; -import {getCloudContactUsLink, InquiryType} from 'selectors/cloud'; import { TELEMETRY_CATEGORIES, } from 'utils/constants'; +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import ExternalLink from 'components/external_link'; export default function ContactSalesLink() { - const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); + const [, contactSalesLink] = useOpenSalesLink(); const intl = useIntl(); return ( {intl.formatMessage({id: 'self_hosted_signup.contact_sales', defaultMessage: 'Contact Sales'})} diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx index 10da32432d..3813f9efda 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/error.tsx @@ -4,13 +4,11 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; -import {useSelector} from 'react-redux'; - -import {getCloudContactUsLink, InquiryType} from 'selectors/cloud'; import PaymentFailedSvg from 'components/common/svg_images_components/payment_failed_svg'; import AccessDeniedHappySvg from 'components/common/svg_images_components/access_denied_happy_svg'; import IconMessage from 'components/purchase_modal/icon_message'; +import {useOpenSelfHostedZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm'; import ExternalLink from 'components/external_link'; interface Props { @@ -20,7 +18,7 @@ interface Props { } export default function ErrorPage(props: Props) { - const contactSupportLink = useSelector(getCloudContactUsLink)(InquiryType.Technical); + const [, contactSupportLink] = useOpenSelfHostedZendeskSupportForm('Purchase error'); let formattedTitle = ( { dispatch(getPrevTrialLicense()); @@ -54,14 +54,6 @@ function ADLDAPUpsellBanner() { const currentLicenseEndDate = new Date(parseInt(currentLicense?.ExpiresAt, 10)); - const openLink = () => { - if (isCloud) { - openSalesLink(); - } else { - window.open(LicenseLinks.CONTACT_SALES, '_blank'); - } - }; - const confirmBanner = (
@@ -71,7 +63,7 @@ function ADLDAPUpsellBanner() {
@@ -122,7 +114,7 @@ function ADLDAPUpsellBanner() { btn = ( diff --git a/webapp/channels/src/selectors/cloud.ts b/webapp/channels/src/selectors/cloud.ts index 9786b1f5e0..684a4e6c25 100644 --- a/webapp/channels/src/selectors/cloud.ts +++ b/webapp/channels/src/selectors/cloud.ts @@ -4,51 +4,10 @@ import {Invoice, Subscription} from '@mattermost/types/cloud'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {createSelector} from 'reselect'; import {GlobalState} from 'types/store'; -export enum InquiryType { - Technical = 'technical', - Sales = 'sales', - Billing = 'billing', -} - -export enum TechnicalInquiryIssue { - AdminConsole = 'admin_console', - MattermostMessaging = 'mm_messaging', - DataExport = 'data_export', - Other = 'other', -} - -export enum SalesInquiryIssue { - AboutPurchasing = 'about_purchasing', - CancelAccount = 'cancel_account', - PurchaseNonprofit = 'purchase_nonprofit', - TrialQuestions = 'trial_questions', - UpgradeEnterprise = 'upgrade_enterprise', - SomethingElse = 'something_else', -} - -type Issue = SalesInquiryIssue | TechnicalInquiryIssue - -export const getCloudContactUsLink: (state: GlobalState) => (inquiry: InquiryType, inquiryIssue?: Issue) => string = createSelector( - 'getCloudContactUsLink', - getConfig, - getCurrentUser, - (config, user) => { - // cloud/contact-us with query params for name, email and inquiry - const cwsUrl = config.CWSURL; - const fullName = `${user.first_name} ${user.last_name}`; - return (inquiry: InquiryType, inquiryIssue?: Issue) => { - const inquiryIssueQuery = inquiryIssue ? `&inquiry-issue=${inquiryIssue}` : ''; - - return `${cwsUrl}/cloud/contact-us?email=${encodeURIComponent(user.email)}&name=${encodeURIComponent(fullName)}&inquiry=${inquiry}${inquiryIssueQuery}`; - }; - }, -); - export const getExpandSeatsLink: (state: GlobalState) => (licenseId: string) => string = createSelector( 'getExpandSeatsLink', getConfig, diff --git a/webapp/channels/src/utils/contact_support_sales.ts b/webapp/channels/src/utils/contact_support_sales.ts new file mode 100644 index 0000000000..1899d599a6 --- /dev/null +++ b/webapp/channels/src/utils/contact_support_sales.ts @@ -0,0 +1,100 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Buffer} from 'buffer'; + +import {LicenseLinks} from './constants'; + +const baseZendeskFormURL = 'https://support.mattermost.com/hc/en-us/requests/new'; + +export enum ZendeskSupportForm { + SELF_HOSTED_SUPPORT_FORM = '11184911962004', + CLOUD_SUPPORT_FORM = '11184929555092', +} + +export enum ZendeskFormFieldIDs { + CLOUD_WORKSPACE_URL = '5245314479252', + SELF_HOSTED_ENVIRONMENT = '360026980452', + BILLING_SALES_CATEGORY = '360031056451', + EMAIL = 'anonymous_requester_email', + SUBJECT = 'subject', + DESCRIPTION = 'description' +} + +export type PrefillFieldFormFieldIDs = { + id: ZendeskFormFieldIDs; + val: string; +} + +export const buildZendeskSupportForm = (form: ZendeskSupportForm, formFieldIDs: PrefillFieldFormFieldIDs[]): string => { + let formUrl = `${baseZendeskFormURL}?ticket_form_id=${form}`; + + formFieldIDs.forEach((formPrefill) => { + formUrl = formUrl.concat(`&tf_${formPrefill.id}=${formPrefill.val}`); + }); + + if (form === ZendeskSupportForm.SELF_HOSTED_SUPPORT_FORM) { + formUrl = formUrl.concat(`&tf_${ZendeskFormFieldIDs.SELF_HOSTED_ENVIRONMENT}=production`); + } + + return formUrl; +}; + +export const goToSelfHostedSupportForm = (email: string, subject: string) => { + const form = ZendeskSupportForm.SELF_HOSTED_SUPPORT_FORM; + const url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + ]); + window.open(url, '_blank'); +}; + +export const getSelfHostedSupportLink = (email: string, subject: string) => { + const form = ZendeskSupportForm.SELF_HOSTED_SUPPORT_FORM; + const url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + ]); + return url; +}; + +export const goToCloudSupportForm = (email: string, subject: string, description: string, workspaceURL: string) => { + const form = ZendeskSupportForm.CLOUD_SUPPORT_FORM; + let url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + {id: ZendeskFormFieldIDs.DESCRIPTION, val: description}, + ]); + url = url.concat(`&tf_${ZendeskFormFieldIDs.CLOUD_WORKSPACE_URL}=${workspaceURL}`); + window.open(url, '_blank'); +}; + +export const getCloudSupportLink = (email: string, subject: string, description: string, workspaceURL: string) => { + const form = ZendeskSupportForm.CLOUD_SUPPORT_FORM; + let url = buildZendeskSupportForm(form, [ + {id: ZendeskFormFieldIDs.EMAIL, val: email}, + {id: ZendeskFormFieldIDs.SUBJECT, val: subject}, + {id: ZendeskFormFieldIDs.DESCRIPTION, val: description}, + ]); + url = url.concat(`&tf_${ZendeskFormFieldIDs.CLOUD_WORKSPACE_URL}=${workspaceURL}`); + return url; +}; + +const encodeString = (s: string) => { + return Buffer.from(s).toString('base64'); +}; + +export const buildMMURL = (baseURL: string, firstName: string, lastName: string, companyName: string, businessEmail: string, source: string, medium: string) => { + const mmURL = `${baseURL}?qk=${encodeString(firstName)}&qp=${encodeString(lastName)}&qw=${encodeString(companyName)}&qx=${encodeString(businessEmail)}&utm_source=${source}&utm_medium=${medium}`; + return mmURL; +}; + +export const goToMattermostContactSalesForm = (firstName: string, lastName: string, companyName: string, businessEmail: string, source: string, medium: string) => { + const url = buildMMURL(LicenseLinks.CONTACT_SALES, firstName, lastName, companyName, businessEmail, source, medium); + window.open(url, '_blank'); +}; + +export const getCloudContactSalesLink = (firstName: string, lastName: string, companyName: string, businessEmail: string, source: string, medium: string) => { + const url = buildMMURL(LicenseLinks.CONTACT_SALES, firstName, lastName, companyName, businessEmail, source, medium); + return url; +};