Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
MM-36731 - ux changes to cloud starter flat fee (#8341)
Browse files Browse the repository at this point in the history
* MM-36731 - ux changes to cloud starter flat fee

* add ux pr suggested changes

* prevent purchase modal to fail on first render

Co-authored-by: Pablo Velez Vidal <[email protected]>
  • Loading branch information
pvev and pvev committed Jul 8, 2021
1 parent bc47db6 commit 3647ca9
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import {FormattedDate, FormattedMessage, FormattedNumber} from 'react-intl';
import {Tooltip} from 'react-bootstrap';

import {CloudLinks, TrialPeriodDays} from 'utils/constants';
import {BillingSchemes, CloudLinks, TrialPeriodDays} from 'utils/constants';

import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import BlockableLink from 'components/admin_console/blockable_link';
Expand Down Expand Up @@ -201,17 +201,25 @@ export const lastInvoiceInfo = (invoice: any, product: any, fullCharges: any, pa
className='BillingSummary__lastInvoice-charge'
>
<div className='BillingSummary__lastInvoice-chargeDescription'>
<FormattedNumber
value={(charge.price_per_unit / 100.0)}
// eslint-disable-next-line react/style-prop-object
style='currency'
currency='USD'
/>
<FormattedMarkdownMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.userCount'
defaultMessage=' x {users} users'
values={{users: charge.quantity}}
/>
{product.billing_schema === BillingSchemes.FLAT_FEE ?
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.monthlyFlatFee'
defaultMessage='Monthly Flat Fee'
/> :
<>
<FormattedNumber
value={(charge.price_per_unit / 100.0)}
// eslint-disable-next-line react/style-prop-object
style='currency'
currency='USD'
/>
<FormattedMarkdownMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.userCount'
defaultMessage=' x {users} users'
values={{users: charge.quantity}}
/>
</>
}
</div>
<div className='BillingSummary__lastInvoice-chargeAmount'>
<FormattedNumber
Expand Down
17 changes: 12 additions & 5 deletions components/admin_console/billing/plan_details/plan_details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {trackEvent} from 'actions/telemetry_actions';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import OverlayTrigger from 'components/overlay_trigger';
import {getMonthLong} from 'utils/i18n';
import {CloudLinks, CloudProducts} from 'utils/constants';
import {BillingSchemes, CloudLinks, CloudProducts} from 'utils/constants';
import {localizeMessage} from 'utils/utils';

import Badge from 'components/widgets/badges/badge';
Expand Down Expand Up @@ -240,10 +240,17 @@ export const getPlanDetailElements = (
<div className='PlanDetails__plan'>
<div className='PlanDetails_paidTier__planName'>
{`$${product.price_per_seat.toFixed(2)}`}
<FormattedMessage
id='admin.billing.subscription.planDetails.perUserPerMonth'
defaultMessage='/user/month. '
/>
{product.billing_schema === BillingSchemes.FLAT_FEE ?
<FormattedMessage
id='admin.billing.subscription.planDetails.flatFeePerMonth'
defaultMessage='/month (Unlimited Users). '
/> :
<FormattedMessage
id='admin.billing.subscription.planDetails.perUserPerMonth'
defaultMessage='/user/month. '
/>
}

{howBillingWorksLink}
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions components/common/radio_group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type RadioGroupProps = {
values: Array<{ key: string; value: string}>;
value: string;
badge?: {matchVal: string; text: ReactNode};
sideLegend?: {matchVal: string; text: ReactNode};
isDisabled?: (id: string) => boolean | boolean;
onChange(e: React.ChangeEvent<HTMLInputElement>): void;
}
Expand All @@ -20,6 +21,7 @@ const RadioButtonGroup: React.FC<RadioGroupProps> = ({
values,
value,
badge,
sideLegend,
}: RadioGroupProps) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e);
Expand All @@ -43,6 +45,11 @@ const RadioButtonGroup: React.FC<RadioGroupProps> = ({
disabled={disabled}
/>
{key}
{(sideLegend && val === sideLegend?.matchVal) &&
<span className='side-legend'>
{sideLegend.text}
</span>
}
</label>
{(badge && val === badge?.matchVal) &&
<Badge className='radio-badge'>
Expand Down
62 changes: 49 additions & 13 deletions components/purchase_modal/purchase.scss
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
.select-plan {
box-sizing: border-box;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
margin-bottom: 20px;
margin-bottom: 16px;

.title {
display: flex;
Expand All @@ -143,7 +143,7 @@
margin-top: 16px;

label {
color: rgba(var(--sys-center-channel-color-rgb), 0.64);
color: var(--sys-center-channel-color-rgb);
font-family: Open Sans;
font-size: 12px;
font-style: normal;
Expand Down Expand Up @@ -213,18 +213,18 @@
margin-top: 10px !important;
margin-bottom: 10px;

.Badge {
.side-legend {
margin-top: -5px;
}
margin-left: 5px;
color: rgba(var(--sys-center-channel-color-rgb), 0.64);

&::before {
content: "(";
}

.radio-badge {
background-color: var(--sidebar-team-background);
border-radius: 4px;
color: var(--button-color);
font-size: 10px;
font-weight: 600;
line-height: 16px;
text-transform: uppercase;
&::after {
content: ")";
}
}
}
}
Expand All @@ -240,14 +240,50 @@
}
}

.Badge {
margin: 0 !important;

.unlimited-users-badge {
background-color: var(--sidebar-text-active-border);
border-radius: 4px;
color: var(--button-color);
font-size: 10px;
font-weight: 600;
line-height: 16px;
text-transform: uppercase;
}
}

.price-text {
padding: 16px 0;
display: flex;
align-items: baseline;
padding: 8px 0;
font-size: 32px;
font-weight: 600;
line-height: 1;

.price-decimals {
align-self: end;
font-size: 16px;

&::before {
content: '.';
}
}
}

.payment-note {
margin-top: 5px;
margin-bottom: 10px;
color: rgba(var(--sys-center-channel-color-rgb), 0.72);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}

.monthly-text {
align-self: center;
margin-left: 5px;
font-size: 14px;
font-weight: normal;
}
Expand Down
128 changes: 100 additions & 28 deletions components/purchase_modal/purchase_modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';

import {Stripe, StripeCardElementChangeEvent} from '@stripe/stripe-js';
Expand All @@ -18,13 +18,14 @@ import blueDots from 'images/cloud/blue.svg';
import LowerBlueDots from 'images/cloud/blue-lower.svg';
import cloudLogo from 'images/cloud/mattermost-cloud.svg';
import {trackEvent, pageVisited} from 'actions/telemetry_actions';
import {TELEMETRY_CATEGORIES, CloudLinks, CloudProducts} from 'utils/constants';
import {TELEMETRY_CATEGORIES, CloudLinks, CloudProducts, BillingSchemes} from 'utils/constants';

import PaymentDetails from 'components/admin_console/billing/payment_details';
import {STRIPE_CSS_SRC, STRIPE_PUBLIC_KEY} from 'components/payment_form/stripe';
import RootPortal from 'components/root_portal';
import FullScreenModal from 'components/widgets/modals/full_screen_modal';
import RadioButtonGroup from 'components/common/radio_group';
import Badge from 'components/widgets/badges/badge';

import {areBillingDetailsValid, BillingDetails} from 'types/cloud/sku';

Expand Down Expand Up @@ -191,12 +192,21 @@ export default class PurchaseModal extends React.PureComponent<Props, State> {

listPlans = () => {
const products = this.props.products!;
const options = Object.keys(products).map((key: string) => {
return {key: products[key].name, value: products[key].id, price: products[key].price_per_seat};
}).sort((a, b) => a.price - b.price);
const badgeTitle = (
const flatFeeProducts: any = [];
const userBasedProducts: any = [];
Object.keys(products).forEach((key: string) => {
const tempEl = {key: products[key].name, value: products[key].id, price: products[key].price_per_seat};
if (products[key].billing_scheme === BillingSchemes.FLAT_FEE) {
flatFeeProducts.push(tempEl);
} else {
userBasedProducts.push(tempEl);
}
});

const options = [...flatFeeProducts.sort((a: any, b: any) => a.price - b.price), ...userBasedProducts.sort((a: any, b: any) => a.price - b.price)];
const sideLegendTitle = (
<FormattedMessage
defaultMessage={'Current Plan'}
defaultMessage={'(Current Plan)'}
id={'admin.billing.subscription.purchaseModal.currentPlan'}
/>
);
Expand All @@ -207,13 +217,52 @@ export default class PurchaseModal extends React.PureComponent<Props, State> {
id='list-plans-radio-buttons'
values={options!}
value={this.state.selectedProduct?.id as string}
badge={{matchVal: this.state.currentProduct?.id as string, text: badgeTitle}}
sideLegend={{matchVal: this.state.currentProduct?.id as string, text: sideLegendTitle}}
onChange={(e: any) => this.onPlanSelected(e)}
/>
</div>
);
}

displayDecimals = () => {
const price = this.state.selectedProduct?.price_per_seat.toFixed(2);
if (!price) {
return null;
}
let decimals = null;
const [, decimalPart] = price?.toString().split('.') as string[];
if (this.state.selectedProduct?.billing_scheme === BillingSchemes.FLAT_FEE && decimalPart) {
decimals = decimalPart;
}
if (decimals === null) {
return null;
}
return (
<span className='price-decimals'>
{decimals}
</span>
);
}

contactSalesLink = (text: ReactNode) => {
return (
<a
className='footer-text'
onClick={() => {
trackEvent(
TELEMETRY_CATEGORIES.CLOUD_PURCHASING,
'click_contact_sales',
);
}}
href={this.props.contactSalesLink}
target='_new'
rel='noopener noreferrer'
>
{text}
</a>
);
}

editPaymentInfoHandler = () => {
this.setState((prevState: State) => {
return {
Expand Down Expand Up @@ -363,15 +412,50 @@ export default class PurchaseModal extends React.PureComponent<Props, State> {
<div className='bold-text'>
{this.state.selectedProduct?.name || ''}
</div>
<div className='price-text'>
{`$${this.state.selectedProduct?.price_per_seat || 0}`}
<span className='monthly-text'>
{this.state.selectedProduct?.billing_scheme === BillingSchemes.FLAT_FEE &&
<Badge className='unlimited-users-badge'>
<FormattedMessage
defaultMessage={' /user/month'}
id={'admin.billing.subscription.perUserPerMonth'}
defaultMessage={'Unlimited Users'}
id={'admin.billing.subscription.unlimitedUsers'}
/>
</Badge>
}
<div className='price-text'>
{`$${this.state.selectedProduct?.price_per_seat.toFixed(0) || 0}`}
{this.displayDecimals()}
<span className='monthly-text'>
{this.state.selectedProduct?.billing_scheme === BillingSchemes.FLAT_FEE ?
<FormattedMessage
defaultMessage={' /month'}
id={'admin.billing.subscription.perMonth'}
/> :
<FormattedMessage
defaultMessage={' /user/month'}
id={'admin.billing.subscription.perUserPerMonth'}
/>
}
</span>
</div>
{this.state.selectedProduct?.billing_scheme === BillingSchemes.FLAT_FEE &&
<div className='payment-note'>
<FormattedMessage
defaultMessage={'If your Mattermost Cloud trial started on or before September 15, please '}
id={'admin.billing.subscription.paymentNoteStart'}
/>
{this.contactSalesLink(
<FormattedMessage
defaultMessage={'contact our Sales team'}
id={
'admin.billing.subscription.privateCloudCard.contactOurSalesTeam'
}
/>,
)}
<FormattedMessage
defaultMessage={' for assistance'}
id={'admin.billing.subscription.paymentNoteEnd'}
/>
</div>
}
<div className='footer-text'>
<FormattedMessage
defaultMessage={'Payment begins: {beginDate}'}
Expand Down Expand Up @@ -410,26 +494,14 @@ export default class PurchaseModal extends React.PureComponent<Props, State> {
id={'admin.billing.subscription.otherBillingOption'}
/>
</div>
<a
className='footer-text'
onClick={() => {
trackEvent(
TELEMETRY_CATEGORIES.CLOUD_PURCHASING,
'click_contact_sales',
);
}}
href={this.props.contactSalesLink}
target='_new'
rel='noopener noreferrer'
>
{this.contactSalesLink(
<FormattedMessage
defaultMessage={'Contact Sales'}
id={
'admin.billing.subscription.privateCloudCard.contactSales'
}
/>
</a>

/>,
)}
<div className='logo'>
<img src={cloudLogo}/>
</div>
Expand Down
Loading

0 comments on commit 3647ca9

Please sign in to comment.