Mm 51788 non-admins do not trigger a request to fetch stripe customer in cloud instances (#22821)

* do not query for customer if not a cloud admin
This commit is contained in:
Nathaniel Allred 2023-04-21 12:31:27 -05:00 committed by GitHub
parent 67735be261
commit 54db770811
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 187 additions and 149 deletions

View file

@ -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(<PaymentAnnouncementBar/>, 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(<PaymentAnnouncementBar/>, 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(<PaymentAnnouncementBar/>, 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(<PaymentAnnouncementBar/>, store);
expect(cloudActions.getCloudCustomer).not.toHaveBeenCalled();
});
});

View file

@ -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<GenericAction>) {
return {
actions: bindActionCreators(
{
savePreferences,
openModal,
getCloudSubscription,
getCloudCustomer,
},
dispatch,
),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PaymentAnnouncementBar);

View file

@ -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<DispatchFunc>();
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 = (
<FormattedMessage
id='admin.billing.subscription.creditCardExpired'
defaultMessage='Your credit card has expired. Update your payment information to avoid disruption.'
/>
);
if (mostRecentPaymentFailed) {
message = (
<FormattedMessage
id='admin.billing.subscription.mostRecentPaymentFailed'
defaultMessage='Your most recent payment failed'
/>
);
}
return (
<AnnouncementBar
type={AnnouncementBarTypes.CRITICAL}
showCloseButton={false}
onButtonClick={updatePaymentInfo}
modalButtonText={t('admin.billing.subscription.updatePaymentInfo')}
modalButtonDefaultText={'Update payment info'}
message={message}
showLinkAsButton={true}
isTallBanner={true}
/>
);
}

View file

@ -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<Props> {
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 (
<AnnouncementBar
type={AnnouncementBarTypes.CRITICAL}
showCloseButton={false}
onButtonClick={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}
/>
);
}
}
export default PaymentAnnouncementBar;

View file

@ -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',
};