mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
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:
parent
67735be261
commit
54db770811
5 changed files with 187 additions and 149 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue