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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
67735be261
Коммит
54db770811
@@ -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',
|
WEB_SERVER: '/admin_console/environment/web_server',
|
||||||
PUSH_NOTIFICATION_CENTER: '/admin_console/environment/push_notification_server',
|
PUSH_NOTIFICATION_CENTER: '/admin_console/environment/push_notification_server',
|
||||||
SMTP: '/admin_console/environment/smtp',
|
SMTP: '/admin_console/environment/smtp',
|
||||||
|
PAYMENT_INFO: '/admin_console/billing/payment_info',
|
||||||
BILLING_HISTORY: '/admin_console/billing/billing_history',
|
BILLING_HISTORY: '/admin_console/billing/billing_history',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user