Skip to content

Commit

Permalink
[MM-28222][MM-28215] Payment Warnings (mattermost#6908)
Browse files Browse the repository at this point in the history
* [MM-28222][MM-28215] Payment Warnings

* PR feedback and a small bit of refactoring

Co-authored-by: Mattermod <[email protected]>
  • Loading branch information
devinbinnie and mattermod committed Oct 28, 2020
1 parent 9fba9a9 commit 986ec23
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 7 deletions.
4 changes: 4 additions & 0 deletions components/admin_console/billing/billing_subscriptions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
.BillingSubscriptions__topWrapper {
display: flex;
}

.AlertBanner {
margin-bottom: 20px;
}
}

.BillingSubscriptions__tooltip.tooltip {
Expand Down
77 changes: 72 additions & 5 deletions components/admin_console/billing/billing_subscriptions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useEffect} from 'react';
import React, {useEffect, useState} from 'react';
import {useDispatch, useStore, useSelector} from 'react-redux';
import {FormattedMessage, useIntl} from 'react-intl';

Expand All @@ -17,6 +17,7 @@ import {PreferenceType} from 'mattermost-redux/types/preferences';
import {pageVisited, trackEvent} from 'actions/telemetry_actions';
import {openModal} from 'actions/views/modals';
import AlertBanner from 'components/alert_banner';
import BlockableLink from 'components/admin_console/blockable_link';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import PurchaseModal from 'components/purchase_modal';
import FormattedAdminHeader from 'components/widgets/admin_console/formatted_admin_header';
Expand All @@ -28,6 +29,7 @@ import {
ModalIdentifiers,
TELEMETRY_CATEGORIES,
} from 'utils/constants';
import {isCustomerCardExpired} from 'utils/cloud_utils';

import privateCloudImage from 'images/private-cloud-image.svg';
import upgradeMattermostCloudImage from 'images/upgrade-mattermost-cloud-image.svg';
Expand All @@ -51,12 +53,15 @@ const BillingSubscriptions: React.FC<Props> = () => {
const currentUser = useSelector((state: GlobalState) => getCurrentUser(state));
const isCloud = useSelector((state: GlobalState) => getLicense(state).Cloud === 'true');
const subscription = useSelector((state: GlobalState) => state.entities.cloud.subscription);
const customer = useSelector((state: GlobalState) => state.entities.cloud.customer);
const products = useSelector((state: GlobalState) => state.entities.cloud.products);
const getCategory = makeGetCategory();
const preferences = useSelector<GlobalState, PreferenceType[]>((state) => getCategory(state, Preferences.ADMIN_CLOUD_UPGRADE_PANEL));

const contactSalesLink = useSelector((state: GlobalState) => getCloudContactUsLink(state, InquiryType.Sales));

const [showCreditCardBanner, setShowCreditCardBanner] = useState(isCustomerCardExpired(customer));

const onUpgradeMattermostCloud = () => {
trackEvent('cloud_admin', 'click_upgrade_mattermost_cloud');

Expand Down Expand Up @@ -95,6 +100,10 @@ const BillingSubscriptions: React.FC<Props> = () => {
return false;
};

const shouldShowPaymentFailedBanner = () => {
return subscription?.last_invoice?.status === 'failed';
};

const handleHide = async () => {
trackEvent(
TELEMETRY_CATEGORIES.CLOUD_ADMIN,
Expand Down Expand Up @@ -186,6 +195,35 @@ const BillingSubscriptions: React.FC<Props> = () => {
/>
<div className='admin-console__wrapper'>
<div className='admin-console__content'>
{shouldShowPaymentFailedBanner() && (
<AlertBanner
mode='danger'
title={formatMessage({
id: 'billing.subscription.info.mostRecentPaymentFailed',
defaultMessage: 'Your most recent payment failed',
})}
message={
<>
<FormattedMessage
id='billing.subscription.info.mostRecentPaymentFailed.description.mostRecentPaymentFailed'
defaultMessage='It looks your most recent payment failed because the credit card on your account has expired. Please '
/>
<BlockableLink
to='/admin_console/billing/payment_info'
>
<FormattedMessage
id='billing.subscription.info.mostRecentPaymentFailed.description.updatePaymentInformation'
defaultMessage='update your payment information'
/>
</BlockableLink>
<FormattedMessage
id='billing.subscription.info.mostRecentPaymentFailed.description.avoidAnyDisruption'
defaultMessage=' to avoid any disruption.'
/>
</>
}
/>
)}
{shouldShowInfoBanner() && (
<AlertBanner
mode='info'
Expand All @@ -201,10 +239,39 @@ const BillingSubscriptions: React.FC<Props> = () => {
onDismiss={() => handleHide()}
/>
)}
<div
className='BillingSubscriptions__topWrapper'
style={{marginTop: '20px'}}
>
{showCreditCardBanner && (
<AlertBanner
mode='danger'
title={
<FormattedMessage
id='admin.billing.subscription.creditCardHasExpired'
defaultMessage='Your credit card has expired'
/>
}
message={
<>
<FormattedMessage
id='admin.billing.subscription.creditCardHasExpired.please'
defaultMessage='Please '
/>
<BlockableLink
to='/admin_console/billing/payment_info'
>
<FormattedMessage
id='admin.billing.subscription.creditCardHasExpired.description.updatePaymentInformation'
defaultMessage='update your payment information'
/>
</BlockableLink>
<FormattedMessage
id='admin.billing.subscription.creditCardHasExpired.description.avoidAnyDisruption'
defaultMessage=' to avoid any disruption.'
/>
</>
}
onDismiss={() => setShowCreditCardBanner(false)}
/>
)}
<div className='BillingSubscriptions__topWrapper'>
<PlanDetails/>
{subscription?.is_paid_tier === 'true' ? <BillingSummary/> : upgradeMattermostCloud()}
</div>
Expand Down
3 changes: 3 additions & 0 deletions components/admin_console/billing/payment_info.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.PaymentInfo .AlertBanner {
margin-bottom: 20px;
}
46 changes: 44 additions & 2 deletions components/admin_console/billing/payment_info.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useEffect} from 'react';
import {useDispatch} from 'react-redux';
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';

import {DispatchFunc} from 'mattermost-redux/types/actions';
import {getCloudCustomer} from 'mattermost-redux/actions/cloud';
import {GlobalState} from 'mattermost-redux/types/store';

import {pageVisited} from 'actions/telemetry_actions';
import FormattedAdminHeader from 'components/widgets/admin_console/formatted_admin_header';
import AlertBanner from 'components/alert_banner';

import PaymentInfoDisplay from './payment_info_display';

import './payment_info.scss';

type Props = {

};

const PaymentInfo: React.FC<Props> = () => {
const dispatch = useDispatch<DispatchFunc>();
const customer = useSelector((state: GlobalState) => state.entities.cloud.customer);

const isCreditCardAboutToExpire = () => {
if (!customer) {
return false;
}

// Will developers ever learn? :D
const expiryYear = customer.payment_method.exp_year + 2000;

// This works because we store the expiry month as the actual 1-12 base month, but Date uses a 0-11 base month
// But credit cards expire at the end of their expiry month, so we can just use that number.
const lastExpiryDate = new Date(expiryYear, customer.payment_method.exp_month, 1);
const currentDatePlus10Days = new Date();
currentDatePlus10Days.setDate(currentDatePlus10Days.getDate() + 10);
return lastExpiryDate <= currentDatePlus10Days;
};

const [showCreditCardBanner, setShowCreditCardBanner] = useState(isCreditCardAboutToExpire());

useEffect(() => {
dispatch(getCloudCustomer());
Expand All @@ -33,6 +57,24 @@ const PaymentInfo: React.FC<Props> = () => {
/>
<div className='admin-console__wrapper'>
<div className='admin-console__content'>
{showCreditCardBanner && (
<AlertBanner
mode='info'
title={
<FormattedMessage
id='admin.billing.payment_info.creditCardAboutToExpire'
defaultMessage='Your credit card is about to expire'
/>
}
message={
<FormattedMessage
id='admin.billing.payment_info.creditCardAboutToExpire.description'
defaultMessage='Please update your payment information to avoid any disruption.'
/>
}
onDismiss={() => setShowCreditCardBanner(false)}
/>
)}
<PaymentInfoDisplay/>
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions components/announcement_bar/announcement_bar_controller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import TextDismissableBar from './text_dismissable_bar.jsx';
import AnnouncementBar from './default_announcement_bar';

import CloudAnnouncementBar from './cloud_announcement_bar';
import PaymentAnnouncementBar from './payment_announcement_bar';

export default class AnnouncementBarController extends React.PureComponent {
static propTypes = {
Expand Down Expand Up @@ -53,17 +54,22 @@ export default class AnnouncementBarController extends React.PureComponent {
);
}
let cloudAnnouncementBar = null;
let paymentAnnouncementBar = null;
if (this.props.license.Cloud === 'true') {
cloudAnnouncementBar = (
<CloudAnnouncementBar/>
);
paymentAnnouncementBar = (
<PaymentAnnouncementBar/>
);
}

return (
<React.Fragment>
{adminConfiguredAnnouncementBar}
{errorBar}
{cloudAnnouncementBar}
{paymentAnnouncementBar}
<VersionBar/>
<ConfigurationAnnouncementBar
config={this.props.config}
Expand Down
45 changes: 45 additions & 0 deletions components/announcement_bar/payment_announcement_bar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';

import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {GenericAction} from 'mattermost-redux/types/actions';
import {getStandardAnalytics} from 'mattermost-redux/actions/admin';
import {getCloudSubscription, getCloudCustomer} from 'mattermost-redux/actions/cloud';

import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';

import {openModal} from 'actions/views/modals';

import {GlobalState} from 'types/store';

import PaymentAnnouncementBar from './payment_announcement_bar';

function mapStateToProps(state: GlobalState) {
return {
userIsAdmin: isCurrentUserSystemAdmin(state),
isCloud: getLicense(state).Cloud === 'true',
subscription: state.entities.cloud.subscription,
customer: state.entities.cloud.customer,
};
}

function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return {
actions: bindActionCreators(
{
savePreferences,
getStandardAnalytics,
openModal,
getCloudSubscription,
getCloudCustomer,
},
dispatch,
),
};
}

export default connect(mapStateToProps, mapDispatchToProps)(PaymentAnnouncementBar);
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';

import {CloudCustomer, Subscription} from 'mattermost-redux/types/cloud';
import {isEmpty} from 'lodash';

import {browserHistory} from 'utils/browser_history';
import {isCustomerCardExpired} from 'utils/cloud_utils';
import {AnnouncementBarTypes} from 'utils/constants';
import {t} from 'utils/i18n';

import AnnouncementBar from '../default_announcement_bar';

type Props = {
userIsAdmin: boolean;
isCloud: boolean;
subscription?: Subscription;
customer?: CloudCustomer;
actions: {
getCloudSubscription: () => void;
getCloudCustomer: () => void;
};
};

export default class PaymentAnnouncementBar extends React.PureComponent<Props> {
async componentDidMount() {
if (isEmpty(this.props.subscription)) {
await this.props.actions.getCloudSubscription();
}

if (isEmpty(this.props.customer)) {
await this.props.actions.getCloudCustomer();
}
}

isMostRecentPaymentFailed = () => {
return this.props.subscription?.last_invoice?.status === 'failed';
}

shouldShowBanner = () => {
const {userIsAdmin, isCloud, subscription} = this.props;

// Prevents banner flashes if the subscription hasn't been loaded yet
if (subscription === null) {
return false;
}

if (subscription?.is_paid_tier !== 'true') {
return false;
}

if (!isCloud) {
return false;
}

if (!userIsAdmin) {
return false;
}

if (!isCustomerCardExpired(this.props.customer) && !this.isMostRecentPaymentFailed()) {
return false;
}

return true;
}

updatePaymentInfo = () => {
browserHistory.push('/admin_console/billing/payment_info');
}

render() {
if (isEmpty(this.props.customer) || isEmpty(this.props.subscription)) {
return null;
}

if (!this.shouldShowBanner()) {
return null;
}

return (
<AnnouncementBar
type={AnnouncementBarTypes.CRITICAL_LIGHT}
showCloseButton={false}
showModal={this.updatePaymentInfo}
modalButtonText={t('admin.billing.subscription.updatePaymentInfo')}
modalButtonDefaultText={'Update payment info'}
message={this.isMostRecentPaymentFailed() ? t('admin.billing.subscription.mostRecentPaymentFailed') : t('admin.billing.subscription.creditCardExpired')}
showLinkAsButton={true}
isTallBanner={true}
/>

);
}
}
Loading

0 comments on commit 986ec23

Please sign in to comment.