From 5d122e5d7a3b1c6abc1ab9f4f8df9a2b8c1c5911 Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Fri, 14 Apr 2023 00:15:38 +0300 Subject: [PATCH] [MM-51413] - Update Cloud pricing modal to remove mentions of Cloud Free (#22639) * [MM-51413] - Update Cloud pricing modal to remove mentions of Cloud Free * cleanup starter from pricing modal * rm unused import * enterprise button states * fix purchase modal * revert * revert e2e * revert * revert * revert * revert * revert * revert * add feature flag * default FF to false * revert * fix feature flag self hosted checks * add links * feedback impl * feedback impl * fix lint * feedback impl * fix translation * feedback impl * feedback impl * fix height * improve alignment --------- Co-authored-by: Mattermost Build --- .../pricing_modal/blank_card_image.svg.tsx | 523 ++++++++++++++++++ .../src/components/pricing_modal/card.tsx | 108 +++- .../src/components/pricing_modal/content.tsx | 206 ++++--- .../pricing_modal/pricing_modal.scss | 113 ++++ .../pricing_modal/self_hosted_content.tsx | 12 +- webapp/channels/src/i18n/en.json | 10 +- webapp/channels/src/utils/constants.tsx | 1 + 7 files changed, 872 insertions(+), 101 deletions(-) create mode 100644 webapp/channels/src/components/pricing_modal/blank_card_image.svg.tsx diff --git a/webapp/channels/src/components/pricing_modal/blank_card_image.svg.tsx b/webapp/channels/src/components/pricing_modal/blank_card_image.svg.tsx new file mode 100644 index 0000000000..af07eb2e6e --- /dev/null +++ b/webapp/channels/src/components/pricing_modal/blank_card_image.svg.tsx @@ -0,0 +1,523 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +const BlankCardImage = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default BlankCardImage; diff --git a/webapp/channels/src/components/pricing_modal/card.tsx b/webapp/channels/src/components/pricing_modal/card.tsx index e8786589ec..b28ea8adcb 100644 --- a/webapp/channels/src/components/pricing_modal/card.tsx +++ b/webapp/channels/src/components/pricing_modal/card.tsx @@ -4,9 +4,16 @@ import React, {ReactNode} from 'react'; import {useIntl} from 'react-intl'; import styled from 'styled-components'; +import classNames from 'classnames'; + +import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; +import ExternalLink from 'components/external_link'; + +import {HostedCustomerLinks} from 'utils/constants'; import BuildingSvg from './building.svg'; import TadaSvg from './tada.svg'; +import BlankCardImage from './blank_card_image.svg'; export enum ButtonCustomiserClasses { grayed = 'grayed', @@ -38,7 +45,7 @@ type CardProps = { topColor: string; planLabel?: JSX.Element; plan: string; - planSummary?: string; + planSummary?: ReactNode; price?: string; rate?: ReactNode; planExtraInformation?: JSX.Element; @@ -48,6 +55,8 @@ type CardProps = { briefing: PlanBriefing; planAddonsInfo?: PlanAddonsInfo; planTrialDisclaimer?: JSX.Element; + isCloud: boolean; + cloudFreeDeprecated: boolean; } type StyledProps = { @@ -58,28 +67,101 @@ const StyledDiv = styled.div` background-color: ${(props) => props.bgColor}; `; +export function BlankCard() { + const {formatMessage} = useIntl(); + const [, contactSalesLink] = useOpenSalesLink(); + + return ( +
+
+ +
+ +
+
+ + {formatMessage({id: 'pricing_modal.questions', defaultMessage: 'Questions?'})} + + + + {formatMessage({id: 'pricing_modal.contact_us', defaultMessage: 'Contact us'})} + + +
+
+ {formatMessage({id: 'pricing_modal.reach_out', defaultMessage: 'Reach out to us and we’ll help you decide which plan is right for you and your organization.'})} +
+
+
+
+ + {formatMessage({id: 'pricing_modal.interested_self_hosting', defaultMessage: 'Interested in self-hosting?'})} + + + + {formatMessage({id: 'pricing_modal.learn_more', defaultMessage: 'Learn more'})} + + +
+
+ ); +} + function Card(props: CardProps) { const {formatMessage} = useIntl(); + const bottomClassName = classNames('bottom', { + bottom__round: props.cloudFreeDeprecated && props.isCloud, + }); + + const contactSalesCTAClassName = classNames('contact_sales_cta', { + contact_sales_cta__reduced: props.cloudFreeDeprecated && props.isCloud, + }); + + const planBriefingContentClassName = classNames('plan_briefing_content', { + plan_briefing_content__reduced: props.cloudFreeDeprecated, + }); + + const planPriceRateSectionClassName = classNames('plan_price_rate_section', { + plan_price_rate_section__expanded: props.cloudFreeDeprecated, + }); + + const planLimitsCtaClassName = classNames('plan_limits_cta', { + plan_limits_cta__expanded: props.cloudFreeDeprecated, + }); + + const buildingImgClassName = classNames('building_img', { + building_img__expanded: props.cloudFreeDeprecated, + }); + return (
{props.planLabel} - -
+ {(!props.cloudFreeDeprecated || !props.isCloud) && ( + + )} + +
-
+

{props.plan}

{props.planSummary}

- {props.price ?

{props.price}

: } - {props.rate} + {props.price ?

{props.price}

:
} + {props.cloudFreeDeprecated ? ({props.rate}) : ({props.rate})}
-
+
{props.planExtraInformation}
@@ -96,7 +178,7 @@ function Card(props: CardProps) { )}
-
+
{props.contactSalesCTA && (

{formatMessage({id: 'pricing_modal.or', defaultMessage: 'or'})}

@@ -105,9 +187,9 @@ function Card(props: CardProps) {
-
+ {!props.cloudFreeDeprecated &&
} {props.planTrialDisclaimer} -
+
{props.briefing.title} {props.briefing.items?.map((i) => { return ( diff --git a/webapp/channels/src/components/pricing_modal/content.tsx b/webapp/channels/src/components/pricing_modal/content.tsx index b4712ec50f..3edabd2186 100644 --- a/webapp/channels/src/components/pricing_modal/content.tsx +++ b/webapp/channels/src/components/pricing_modal/content.tsx @@ -20,6 +20,7 @@ import { } from 'mattermost-redux/selectors/entities/cloud'; import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {DispatchFunc} from 'mattermost-redux/types/actions'; +import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences'; import {Feedback} from '@mattermost/types/cloud'; import useGetUsage from 'components/common/hooks/useGetUsage'; @@ -44,7 +45,7 @@ import DowngradeTeamRemovalModal from './downgrade_team_removal_modal'; import ContactSalesCTA from './contact_sales_cta'; import StarterDisclaimerCTA from './starter_disclaimer_cta'; import StartTrialCaution from './start_trial_caution'; -import Card, {ButtonCustomiserClasses} from './card'; +import Card, {BlankCard, ButtonCustomiserClasses} from './card'; import './content.scss'; @@ -68,6 +69,8 @@ function Content(props: ContentProps) { const subscription = useSelector(selectCloudSubscription); const currentProduct = useSelector(selectSubscriptionProduct); const products = useSelector(selectCloudProducts); + + const cloudFreeDeprecated = useSelector(deprecateCloudFree); const yearlyProducts = findOnlyYearlyProducts(products || {}); // pricing modal should now only show yearly products const currentSubscriptionIsMonthly = currentProduct?.recurring_interval === RecurringIntervals.MONTH; @@ -118,8 +121,20 @@ function Content(props: ContentProps) { trial_notification: isPreTrial, }); + const getAdminProfessionalBtnText = () => { + if (currentSubscriptionIsMonthlyProfessional) { + return formatMessage({id: 'pricing_modal.btn.switch_to_annual', defaultMessage: 'Switch to annual billing'}); + } + + if (cloudFreeDeprecated) { + return formatMessage({id: 'pricing_modal.btn.purchase', defaultMessage: 'Purchase'}); + } + + return formatMessage({id: 'pricing_modal.btn.upgrade', defaultMessage: 'Upgrade'}); + }; + 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 adminProfessionalTierText = getAdminProfessionalBtnText(); const [openContactSales] = useOpenSalesLink(); const [openContactSupport] = useOpenCloudZendeskSupportForm('Workspace downgrade', ''); @@ -209,7 +224,7 @@ function Content(props: ContentProps) { action: () => openPurchaseModal('click_pricing_modal_professional_card_upgrade_button'), text: adminProfessionalTierText, disabled: isProfessionalAnnual || (isEnterprise && !isEnterpriseTrial), - customClass: isPostTrial ? ButtonCustomiserClasses.special : ButtonCustomiserClasses.active, + customClass: (cloudFreeDeprecated || isPostTrial) ? ButtonCustomiserClasses.special : ButtonCustomiserClasses.active, }; } @@ -234,7 +249,7 @@ function Content(props: ContentProps) { }; const enterpriseBtnDetails = () => { - if (isPostTrial && isAdmin) { + if (cloudFreeDeprecated || (isPostTrial && isAdmin)) { return { action: () => { trackEvent(TELEMETRY_CATEGORIES.CLOUD_PRICING, 'click_enterprise_contact_sales'); @@ -270,7 +285,7 @@ function Content(props: ContentProps) { }; const enterpriseCustomBtnDetails = () => { - if (!isPostTrial && isAdmin) { + if (!isPostTrial && isAdmin && !cloudFreeDeprecated) { return ( -
-
-
- {formatMessage({id: 'pricing_modal.lookingToSelfHost', defaultMessage: 'Looking to self-host?'})} - - trackEvent( - TELEMETRY_CATEGORIES.CLOUD_PURCHASING, - 'click_looking_to_self_host', - ) - } - href={CloudLinks.DEPLOYMENT_OPTIONS} - location='pricing_modal_content' - >{formatMessage({id: 'pricing_modal.reviewDeploymentOptions', defaultMessage: 'Review deployment options'})} + {!cloudFreeDeprecated && ( +
+
+
+ {formatMessage({id: 'pricing_modal.lookingToSelfHost', defaultMessage: 'Looking to self-host?'})} + + trackEvent( + TELEMETRY_CATEGORIES.CLOUD_PURCHASING, + 'click_looking_to_self_host', + ) + } + href={CloudLinks.DEPLOYMENT_OPTIONS} + location='pricing_modal_content' + >{formatMessage({id: 'pricing_modal.reviewDeploymentOptions', defaultMessage: 'Review deployment options'})} +
-
+ )} -
- } - />) : undefined} - planExtraInformation={} - buttonDetails={{ - action: () => { - if (!isStarter && !currentSubscriptionIsMonthly) { - openContactSupport(); - return; - } +
+ {!cloudFreeDeprecated && ( + } + />) : undefined} + planExtraInformation={} + buttonDetails={{ + action: () => { + if (!isStarter && !currentSubscriptionIsMonthly) { + openContactSupport(); + return; + } - if (!starterProduct) { - return; - } + if (!starterProduct) { + return; + } - if (usage.teams.active > 1) { - dispatch( - openModal({ - modalId: ModalIdentifiers.CLOUD_DOWNGRADE_CHOOSE_TEAM, - dialogType: DowngradeTeamRemovalModal, - dialogProps: { - product_id: starterProduct?.id, - starterProduct, - }, - }), - ); - } else { - dispatch( - openModal({ - modalId: ModalIdentifiers.FEEDBACK, - dialogType: DowngradeFeedbackModal, - dialogProps: { - onSubmit: handleClickDowngrade, - }, - }), - ); - } - }, - text: freeTierText, - disabled: isStarter || isEnterprise || !isAdmin, - customClass: (isStarter || isEnterprise || !isAdmin) ? ButtonCustomiserClasses.grayed : ButtonCustomiserClasses.secondary, - }} - briefing={{ - title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}), - items: hasLimits ? starterBriefing : legacyStarterBriefing, - }} - /> + if (usage.teams.active > 1) { + dispatch( + openModal({ + modalId: ModalIdentifiers.CLOUD_DOWNGRADE_CHOOSE_TEAM, + dialogType: DowngradeTeamRemovalModal, + dialogProps: { + product_id: starterProduct?.id, + starterProduct, + }, + }), + ); + } else { + dispatch( + openModal({ + modalId: ModalIdentifiers.FEEDBACK, + dialogType: DowngradeFeedbackModal, + dialogProps: { + onSubmit: handleClickDowngrade, + }, + }), + ); + } + }, + text: freeTierText, + disabled: isStarter || isEnterprise || !isAdmin, + customClass: (isStarter || isEnterprise || !isAdmin) ? ButtonCustomiserClasses.grayed : ButtonCustomiserClasses.secondary, + }} + briefing={{ + title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}), + items: hasLimits ? starterBriefing : legacyStarterBriefing, + }} + /> + + ) + + } , + })} price={`$${professionalPrice}`} rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}(billed annually)'}, { br:
, b: (chunks: React.ReactNode | React.ReactNodeArray) => ( - - {chunks} + + { + cloudFreeDeprecated ? chunks : ({chunks}) + } ), })} + isCloud={true} + cloudFreeDeprecated={cloudFreeDeprecated} planLabel={isProfessional ? ( ) : undefined} buttonDetails={professionalBtnDetails()} briefing={{ - title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}), + title: cloudFreeDeprecated ? formatMessage({id: 'pricing_modal.briefing.title_no_limit', defaultMessage: 'No limits on your team’s usage'}) : formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}), items: [ formatMessage({id: 'pricing_modal.briefing.professional.messageBoardsIntegrationsCalls', defaultMessage: 'Unlimited access to messages and files'}), formatMessage({id: 'pricing_modal.briefing.professional.unLimitedTeams', defaultMessage: 'Unlimited teams'}), @@ -442,6 +475,8 @@ function Content(props: ContentProps) { topColor='#E07315' plan='Enterprise' planSummary={formatMessage({id: 'pricing_modal.planSummary.enterprise', defaultMessage: 'Administration, security, and compliance for large teams'})} + isCloud={true} + cloudFreeDeprecated={cloudFreeDeprecated} planLabel={ isEnterprise ? ( ) : undefined} buttonDetails={enterpriseBtnDetails()} customButtonDetails={enterpriseCustomBtnDetails()} - planTrialDisclaimer={(!isPostTrial && isAdmin) ? : undefined} - contactSalesCTA={(isPostTrial || !isAdmin) ? undefined : } + planTrialDisclaimer={(!isPostTrial && isAdmin && !cloudFreeDeprecated) ? : undefined} + contactSalesCTA={(isPostTrial || !isAdmin || cloudFreeDeprecated) ? undefined : } briefing={{ - title: formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}), + title: cloudFreeDeprecated ? formatMessage({id: 'pricing_modal.briefing.title_large_scale', defaultMessage: 'Large scale collaboration'}) : formatMessage({id: 'pricing_modal.briefing.title', defaultMessage: 'Top features'}), items: [ formatMessage({id: 'pricing_modal.briefing.enterprise.groupSync', defaultMessage: 'AD/LDAP group sync'}), formatMessage({id: 'pricing_modal.briefing.enterprise.rolesAndPermissions', defaultMessage: 'Advanced roles and permissions'}), @@ -480,6 +515,7 @@ function Content(props: ContentProps) { ], }} /> + {cloudFreeDeprecated && }
diff --git a/webapp/channels/src/components/pricing_modal/pricing_modal.scss b/webapp/channels/src/components/pricing_modal/pricing_modal.scss index 0a3dd434f1..2366782e89 100644 --- a/webapp/channels/src/components/pricing_modal/pricing_modal.scss +++ b/webapp/channels/src/components/pricing_modal/pricing_modal.scss @@ -128,6 +128,74 @@ margin-bottom: 111px; gap: 48px; + .BlankCard { + position: relative; + width: 280px; + + .image { + margin-top: 8px; + } + + hr { + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + } + + .description { + border-bottom: 1px solid rgba(var(--center-channel-color-rgb) 0.16); + text-align: left; + + .title { + margin-top: 24px; + margin-bottom: 8px; + font-family: 'Open Sans'; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; + + .questions { + margin-right: 2px; + color: var(--center-channel-color); + } + + .contact { + color: var(--denim-button-bg); + } + } + + .content { + margin-bottom: 25px; + color: rgba(var(--center-channel-color-rgb), 0.72); + font-family: 'Open Sans'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + } + } + + .self-hosted-interest { + padding: 8px; + margin-top: 24px; + background: rgba(var(--denim-button-bg-rgb), 0.08); + border-radius: 8px; + font-family: 'Open Sans'; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; + + .interested { + margin-right: 2px; + color: var(--center-channel-color); + } + + .learn { + color: var(--denim-button-bg); + } + } + } + .PlanCard { position: relative; width: 280px; @@ -180,6 +248,7 @@ text-align: center; h3 { + margin-top: 32px; color: var(--center-channel-color); font-family: 'Metropolis'; font-size: 24px; @@ -210,6 +279,32 @@ font-weight: 400; line-height: 24px; } + + .plan_rate { + margin-bottom: 0; + } + + .building_img { + padding-top: 0; + } + + .building_img__expanded { + padding-top: 14px; + } + + .billed_annually { + font-family: 'Open sans', sans-serif; + font-size: 16px; + } + } + + .plan_price_rate_section__expanded { + height: 244px; + + span, + p { + margin-bottom: 45px; + } } .contact_sales_cta { @@ -218,10 +313,19 @@ text-align: center; } + .contact_sales_cta__reduced { + height: 32px; + margin-top: 0; + } + .plan_limits_cta { height: 21px; } + .plan_limits_cta__expanded { + height: 32px; + } + .plan_buttons { height: auto; @@ -306,6 +410,10 @@ } } } + + .plan_briefing_content__reduced { + margin-top: 0; + } } } @@ -355,6 +463,11 @@ } } } + + .bottom__round { + border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); + border-radius: 8px; + } } } } 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 67c1f8e69a..6ac25580ac 100644 --- a/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx +++ b/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx @@ -186,6 +186,8 @@ function SelfHostedContent(props: ContentProps) { planSummary={formatMessage({id: 'pricing_modal.planSummary.free', defaultMessage: 'Increased productivity for small teams'})} price='$0' rate={formatMessage({id: 'pricing_modal.price.freeForever', defaultMessage: 'Free forever'})} + isCloud={false} + cloudFreeDeprecated={false} planLabel={ isStarter ? ( , + })} price={professionalPrice} rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}(billed annually)'}, { br:
, b: (chunks: React.ReactNode | React.ReactNodeArray) => ( - + {chunks} ), })} + isCloud={false} + cloudFreeDeprecated={false} planLabel={ isProfessional ? ( (billed annually)", + "pricing_modal.reach_out": "Reach out to us and we’ll help you decide which plan is right for you and your organization.", "pricing_modal.reviewDeploymentOptions": "Review deployment options", "pricing_modal.start_trial.disclaimer": "By selecting Try free for 30 days, I agree to the Mattermost Software and Services License Agreement, Privacy Policy, and receiving product emails.", "pricing_modal.subtitle": "Choose a plan to get started", diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 87fc6be03a..856cb071df 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -1080,6 +1080,7 @@ export const HostedCustomerLinks = { SELF_HOSTED_BILLING: 'https://docs.mattermost.com/manage/self-hosted-billing.html', TERMS_AND_CONDITIONS: 'https://mattermost.com/enterprise-edition-terms/', SECURITY_UPDATES: 'https://mattermost.com/security-updates/', + DOWNLOAD: 'https://mattermost.com/download', NEWSLETTER_UNSUBSCRIBE_LINK: 'https://forms.mattermost.com/UnsubscribePage.html', PRIVACY: 'https://mattermost.com/privacy-policy/', };