diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.test.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.test.tsx
new file mode 100644
index 00000000000..8e610d2d249
--- /dev/null
+++ b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.test.tsx
@@ -0,0 +1,97 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {screen} from '@testing-library/react';
+import {renderWithIntlAndStore} from 'tests/react_testing_utils';
+import * as cloudActions from 'mattermost-redux/actions/cloud';
+
+import {CloudProducts} from 'utils/constants';
+
+import PaymentAnnouncementBar from './';
+
+jest.mock('mattermost-redux/actions/cloud', () => {
+ const original = jest.requireActual('mattermost-redux/actions/cloud');
+ return {
+ ...original,
+ __esModule: true,
+
+ // just testing that it fired, not that the result updated or anything like that
+ getCloudCustomer: jest.fn(() => ({type: 'bogus'})),
+ };
+});
+
+describe('PaymentAnnouncementBar', () => {
+ const happyPathStore = {
+ entities: {
+ users: {
+ currentUserId: 'me',
+ profiles: {
+ me: {
+ roles: 'system_admin',
+ },
+ },
+ },
+ general: {
+ license: {
+ Cloud: 'true',
+ },
+ },
+ cloud: {
+ subscription: {
+ product_id: 'prod_something',
+ last_invoice: {
+ status: 'failed',
+ },
+ },
+ customer: {
+ payment_method: {
+ exp_month: 12,
+ exp_year: (new Date()).getFullYear() + 1,
+ },
+ },
+ products: {
+ prod_something: {
+ id: 'prod_something',
+ sku: CloudProducts.PROFESSIONAL,
+ },
+ },
+ },
+ },
+ views: {
+ announcementBar: {
+ announcementBarState: {
+ announcementBarCount: 1,
+ },
+ },
+ },
+ };
+
+ it('when most recent payment failed, shows that', () => {
+ renderWithIntlAndStore(, happyPathStore);
+ screen.getByText('Your most recent payment failed');
+ });
+
+ it('when card is expired, shows that', () => {
+ const store = JSON.parse(JSON.stringify(happyPathStore));
+ store.entities.cloud.customer.payment_method.exp_year = (new Date()).getFullYear() - 1;
+ store.entities.cloud.subscription.last_invoice.status = 'success';
+ renderWithIntlAndStore(, store);
+ screen.getByText('Your credit card has expired', {exact: false});
+ });
+
+ it('when needed, fetches, customer', () => {
+ const store = JSON.parse(JSON.stringify(happyPathStore));
+ store.entities.cloud.customer = null;
+ store.entities.cloud.subscription.last_invoice.status = 'success';
+ renderWithIntlAndStore(, store);
+ expect(cloudActions.getCloudCustomer).toHaveBeenCalled();
+ });
+
+ it('when not an admin, does not fetch customer', () => {
+ const store = JSON.parse(JSON.stringify(happyPathStore));
+ store.entities.users.profiles.me.roles = '';
+ renderWithIntlAndStore(, store);
+ expect(cloudActions.getCloudCustomer).not.toHaveBeenCalled();
+ });
+});
diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts
deleted file mode 100644
index 86e3bd5f055..00000000000
--- a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// 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 {getCloudSubscription, getCloudCustomer} from 'mattermost-redux/actions/cloud';
-
-import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
-import {
- getCloudSubscription as selectCloudSubscription,
- getCloudCustomer as selectCloudCustomer,
- getSubscriptionProduct,
-} from 'mattermost-redux/selectors/entities/cloud';
-import {CloudProducts} from 'utils/constants';
-
-import {openModal} from 'actions/views/modals';
-
-import {GlobalState} from 'types/store';
-
-import PaymentAnnouncementBar from './payment_announcement_bar';
-
-function mapStateToProps(state: GlobalState) {
- const subscription = selectCloudSubscription(state);
- const customer = selectCloudCustomer(state);
- const subscriptionProduct = getSubscriptionProduct(state);
- return {
- userIsAdmin: isCurrentUserSystemAdmin(state),
- isCloud: getLicense(state).Cloud === 'true',
- subscription,
- customer,
- isStarterFree: subscriptionProduct?.sku === CloudProducts.STARTER,
- };
-}
-
-function mapDispatchToProps(dispatch: Dispatch) {
- return {
- actions: bindActionCreators(
- {
- savePreferences,
- openModal,
- getCloudSubscription,
- getCloudCustomer,
- },
- dispatch,
- ),
- };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(PaymentAnnouncementBar);
diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx
new file mode 100644
index 00000000000..f14153ad8eb
--- /dev/null
+++ b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/index.tsx
@@ -0,0 +1,89 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useEffect, useState} from 'react';
+import {FormattedMessage} from 'react-intl';
+import {useSelector, useDispatch} from 'react-redux';
+import {isEmpty} from 'lodash';
+
+import {DispatchFunc} from 'mattermost-redux/types/actions';
+import {getCloudCustomer} from 'mattermost-redux/actions/cloud';
+import {getLicense} from 'mattermost-redux/selectors/entities/general';
+import {
+ getCloudSubscription as selectCloudSubscription,
+ getCloudCustomer as selectCloudCustomer,
+ getSubscriptionProduct,
+} from 'mattermost-redux/selectors/entities/cloud';
+import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
+
+import {getHistory} from 'utils/browser_history';
+import {isCustomerCardExpired} from 'utils/cloud_utils';
+import {AnnouncementBarTypes, CloudProducts, ConsolePages} from 'utils/constants';
+import {t} from 'utils/i18n';
+
+import AnnouncementBar from '../default_announcement_bar';
+
+export default function PaymentAnnouncementBar() {
+ const [requestedCustomer, setRequestedCustomer] = useState(false);
+ const dispatch = useDispatch();
+ const subscription = useSelector(selectCloudSubscription);
+ const customer = useSelector(selectCloudCustomer);
+ const isStarterFree = useSelector(getSubscriptionProduct)?.sku === CloudProducts.STARTER;
+ const userIsAdmin = useSelector(isCurrentUserSystemAdmin);
+ const isCloud = useSelector(getLicense).Cloud === 'true';
+
+ useEffect(() => {
+ if (isCloud && !isStarterFree && isEmpty(customer) && userIsAdmin && !requestedCustomer) {
+ setRequestedCustomer(true);
+ dispatch(getCloudCustomer());
+ }
+ },
+ [isCloud, isStarterFree, customer, userIsAdmin, requestedCustomer]);
+
+ const mostRecentPaymentFailed = subscription?.last_invoice?.status === 'failed';
+
+ if (
+ // Prevents banner flashes if the subscription hasn't been loaded yet
+ isEmpty(subscription) ||
+ isStarterFree ||
+ !isCloud ||
+ !userIsAdmin ||
+ isEmpty(customer) ||
+ (!isCustomerCardExpired(customer) && !mostRecentPaymentFailed)
+ ) {
+ return null;
+ }
+
+ const updatePaymentInfo = () => {
+ getHistory().push(ConsolePages.PAYMENT_INFO);
+ };
+
+ let message = (
+
+ );
+
+ if (mostRecentPaymentFailed) {
+ message = (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx b/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx
deleted file mode 100644
index 5fe7c7fa4b0..00000000000
--- a/webapp/channels/src/components/announcement_bar/payment_announcement_bar/payment_announcement_bar.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React from 'react';
-
-import {isEmpty} from 'lodash';
-
-import {CloudCustomer, Subscription} from '@mattermost/types/cloud';
-
-import {getHistory} 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;
- isStarterFree: boolean;
- actions: {
- getCloudSubscription: () => void;
- getCloudCustomer: () => void;
- };
-};
-
-class PaymentAnnouncementBar extends React.PureComponent {
- async componentDidMount() {
- 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 (this.props.isStarterFree) {
- return false;
- }
-
- if (!isCloud) {
- return false;
- }
-
- if (!userIsAdmin) {
- return false;
- }
-
- if (!isCustomerCardExpired(this.props.customer) && !this.isMostRecentPaymentFailed()) {
- return false;
- }
-
- return true;
- };
-
- updatePaymentInfo = () => {
- getHistory().push('/admin_console/billing/payment_info');
- };
-
- render() {
- if (isEmpty(this.props.customer) || isEmpty(this.props.subscription)) {
- return null;
- }
-
- if (!this.shouldShowBanner()) {
- return null;
- }
-
- return (
-
-
- );
- }
-}
-
-export default PaymentAnnouncementBar;
diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx
index 652e1025865..97ea8d65d06 100644
--- a/webapp/channels/src/utils/constants.tsx
+++ b/webapp/channels/src/utils/constants.tsx
@@ -2021,6 +2021,7 @@ export const ConsolePages = {
WEB_SERVER: '/admin_console/environment/web_server',
PUSH_NOTIFICATION_CENTER: '/admin_console/environment/push_notification_server',
SMTP: '/admin_console/environment/smtp',
+ PAYMENT_INFO: '/admin_console/billing/payment_info',
BILLING_HISTORY: '/admin_console/billing/billing_history',
};