Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

Просмотреть файл

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/cloud_start_trial_btn/cloud_start_trial_btn should match snapshot 1`] = `
<ContextProvider
value={
Object {
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
},
}
}
>
<CloudStartTrialButton
message="Cloud Start trial"
onClick={[MockFunction]}
telemetryId="test_telemetry_id"
/>
</ContextProvider>
`;

Просмотреть файл

@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/components/cloud_start_trial/input_business_email should match snapshot 1`] = `
<ForwardRef
autoComplete="off"
autoFocus={true}
containerClassName="request-business-email-container"
customMessage={null}
inputClassName="request-business-email-input"
label="Enter business email"
name="request-business-email"
onChange={[MockFunction]}
placeholder="name@companyname.com"
required={true}
type="email"
value="foo@example.com"
/>
`;

Просмотреть файл

@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/request_business_email_modal/request_business_email_modal should match snapshot 1`] = `
<ContextProvider
value={
Object {
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
},
}
}
>
<RequestBusinessEmailModal
onExited={[MockFunction]}
/>
</ContextProvider>
`;

Просмотреть файл

@@ -0,0 +1,14 @@
.CloudStartTrialButton {
&:not(.style-link) {
width: fit-content;
padding: 13px 20px;
border: none;
font-size: 14px;
font-weight: 600;
line-height: 14px;
}
&.style-link {
padding-left: 0;
}
}

Просмотреть файл

@@ -0,0 +1,154 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {ReactWrapper, shallow} from 'enzyme';
import {Provider} from 'react-redux';
import {act} from 'react-dom/test-utils';
import * as cloudActions from 'actions/cloud';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {trackEvent} from 'actions/telemetry_actions.jsx';
import {TELEMETRY_CATEGORIES} from 'utils/constants';
import CloudStartTrialButton from './cloud_start_trial_btn';
jest.mock('actions/telemetry_actions.jsx', () => {
const original = jest.requireActual('actions/telemetry_actions.jsx');
return {
...original,
trackEvent: jest.fn(),
};
});
jest.mock('mattermost-redux/actions/general', () => ({
...jest.requireActual('mattermost-redux/actions/general'),
getLicenseConfig: () => ({type: 'adsf'}),
getClientConfig: () => ({type: 'adsf'}),
}));
jest.mock('mattermost-redux/actions/cloud', () => ({
...jest.requireActual('mattermost-redux/actions/cloud'),
getCloudSubscription: () => ({type: 'adsf'}),
getCloudProducts: () => ({type: 'adsf'}),
getCloudLimits: () => ({}),
}));
describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
const state = {
entities: {
admin: {},
general: {
license: {
IsLicensed: 'true',
Cloud: 'true',
},
},
cloud: {
subscription: {
is_free_trial: 'false',
trial_end_at: 0,
},
},
},
views: {
modals: {
modalState: {
learn_more_trial_modal: {
open: 'true',
},
},
},
},
};
const store = mockStore(state);
const props = {
onClick: jest.fn(),
message: 'Cloud Start trial',
telemetryId: 'test_telemetry_id',
};
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<CloudStartTrialButton {...props}/>
</Provider>,
);
expect(wrapper).toMatchSnapshot();
});
test('should handle on click and change button text on SUCCESSFUL trial request', async () => {
const mockOnClick = jest.fn();
const requestTrialFn: () => () => Promise<any> = () => () => Promise.resolve(true);
jest.spyOn(cloudActions, 'requestCloudTrial').mockImplementation(requestTrialFn);
let wrapper: ReactWrapper<any>;
// Mount the component
await act(async () => {
wrapper = mountWithIntl(
<Provider store={store}>
<CloudStartTrialButton
{...props}
onClick={mockOnClick}
email='fakeemail@topreventbusinessemailvalidation'
/>
</Provider>,
);
});
await act(async () => {
expect(wrapper.find('.CloudStartTrialButton').text().includes('Cloud Start trial')).toBe(true);
wrapper.find('.CloudStartTrialButton').simulate('click');
});
await act(async () => {
expect(wrapper.find('.CloudStartTrialButton').text().includes('Loaded!')).toBe(true);
});
expect(mockOnClick).toHaveBeenCalled();
expect(trackEvent).toHaveBeenCalledWith(TELEMETRY_CATEGORIES.CLOUD_START_TRIAL_BUTTON, 'test_telemetry_id');
});
test('should handle on click and change button text on FAILED trial request', async () => {
const mockOnClick = jest.fn();
const requestTrialFn: () => () => Promise<any> = () => () => Promise.resolve(true);
jest.spyOn(cloudActions, 'requestCloudTrial').mockImplementation(requestTrialFn);
let wrapper: ReactWrapper<any>;
// Mount the component
await act(async () => {
wrapper = mountWithIntl(
<Provider store={store}>
<CloudStartTrialButton
{...props}
onClick={mockOnClick}
/>
</Provider>,
);
});
await act(async () => {
expect(wrapper.find('.CloudStartTrialButton').text().includes('Cloud Start trial')).toBe(true);
});
await act(async () => {
wrapper.find('.CloudStartTrialButton').simulate('click');
});
await act(async () => {
expect(wrapper.find('.CloudStartTrialButton').text().includes('Failed')).toBe(true);
});
});
});

Просмотреть файл

@@ -0,0 +1,183 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch} from 'react-redux';
import {DispatchFunc} from 'mattermost-redux/types/actions';
import useGetSubscription from 'components/common/hooks/useGetSubscription';
import {requestCloudTrial, validateWorkspaceBusinessEmail, getCloudLimits} from 'actions/cloud';
import {trackEvent} from 'actions/telemetry_actions';
import {openModal, closeModal} from 'actions/views/modals';
import TrialBenefitsModal from 'components/trial_benefits_modal/trial_benefits_modal';
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
import RequestBusinessEmailModal from './request_business_email_modal';
import './cloud_start_trial_btn.scss';
export type CloudStartTrialBtnProps = {
message: string;
telemetryId: string;
onClick?: () => void;
extraClass?: string;
afterTrialRequest?: () => void;
email?: string;
disabled?: boolean;
};
enum TrialLoadStatus {
NotStarted = 'NOT_STARTED',
Started = 'STARTED',
Success = 'SUCCESS',
Failed = 'FAILED',
Embargoed = 'EMBARGOED',
}
const TIME_UNTIL_CACHE_PURGE_GUESS = 5000;
const CloudStartTrialButton = ({
message,
telemetryId,
extraClass,
onClick,
afterTrialRequest,
email,
disabled = false,
}: CloudStartTrialBtnProps) => {
const {formatMessage} = useIntl();
const dispatch = useDispatch<DispatchFunc>();
const subscription = useGetSubscription();
const [openBusinessEmailModal, setOpenBusinessEmailModal] = useState(false);
const [status, setLoadStatus] = useState(TrialLoadStatus.NotStarted);
const validateBusinessEmailOnLoad = async () => {
const isValidBusinessEmail = await validateWorkspaceBusinessEmail()();
if (!isValidBusinessEmail) {
setOpenBusinessEmailModal(true);
}
};
useEffect(() => {
validateBusinessEmailOnLoad();
}, []);
const requestStartTrial = async (): Promise<TrialLoadStatus> => {
setLoadStatus(TrialLoadStatus.Started);
// email is set ONLY from the instance of this component created in the requestBusinessEmail modal.
// So the flow is the following: If the email of the admin and the
// email of the CWS customer are not valid, the requestBusinessModal is shown and that component will
// create this StartCloudTrialBtn passing the email as Truthy, so the requetTrial flow continues normally
if (openBusinessEmailModal && !email) {
trackEvent(
TELEMETRY_CATEGORIES.CLOUD_START_TRIAL_BUTTON,
'trial_request_attempt_with_no_valid_business_email',
);
await dispatch(closeModal(ModalIdentifiers.LEARN_MORE_TRIAL_MODAL));
openRequestBusinessEmailModal();
setLoadStatus(TrialLoadStatus.Failed);
return TrialLoadStatus.Failed;
}
const subscriptionUpdated = await dispatch(requestCloudTrial('start_cloud_trial_btn', subscription?.id as string, (email || '')));
if (!subscriptionUpdated) {
setLoadStatus(TrialLoadStatus.Failed);
return TrialLoadStatus.Failed;
}
function ensureUpdatedData() {
// Depending on timing of pods rolling, the webhook may still not get sent.
// Re-request limits as a just-in-case, but only well after any
// pods still alive should have either purged cache,
// updated limits, or be brand new pods that won't be holding onto stale limits
// We don't need to re-request subscription: the updated value is sent in the
// request cloud trial response.
// We don't need to request license: its update process is independent
// from subscription/limit changes and always happens after pods roll.
dispatch(getCloudLimits());
}
setTimeout(ensureUpdatedData, TIME_UNTIL_CACHE_PURGE_GUESS);
if (afterTrialRequest) {
afterTrialRequest();
}
setLoadStatus(TrialLoadStatus.Success);
return TrialLoadStatus.Success;
};
const openTrialBenefitsModal = async (status: TrialLoadStatus) => {
// Only open the benefits modal if the trial request succeeded
if (status !== TrialLoadStatus.Success) {
return;
}
await dispatch(openModal({
modalId: ModalIdentifiers.TRIAL_BENEFITS_MODAL,
dialogType: TrialBenefitsModal,
dialogProps: {trialJustStarted: true},
}));
};
const openRequestBusinessEmailModal = () => {
dispatch(openModal({
modalId: ModalIdentifiers.REQUEST_BUSINESS_EMAIL_MODAL,
dialogType: RequestBusinessEmailModal,
}));
};
const btnText = (status: TrialLoadStatus): string => {
switch (status) {
case TrialLoadStatus.Started:
return formatMessage({id: 'start_cloud_trial.modal.gettingTrial', defaultMessage: 'Getting Trial...'});
case TrialLoadStatus.Success:
return formatMessage({id: 'start_cloud_trial.modal.loaded', defaultMessage: 'Loaded!'});
case TrialLoadStatus.Failed:
return formatMessage({id: 'start_cloud_trial.modal.failed', defaultMessage: 'Failed'});
case TrialLoadStatus.Embargoed:
return formatMessage({id: 'admin.license.trial-request.embargoed'});
default:
return message;
}
};
const startCloudTrial = async () => {
if (status !== TrialLoadStatus.NotStarted) {
return;
}
const updatedStatus = await requestStartTrial();
if (updatedStatus !== TrialLoadStatus.Success) {
return;
}
trackEvent(
TELEMETRY_CATEGORIES.CLOUD_START_TRIAL_BUTTON,
telemetryId,
);
// on click will execute whatever action is sent from the invoking place, if nothing is sent, open the trial benefits modal
if (onClick) {
onClick();
return;
}
await openTrialBenefitsModal(updatedStatus);
};
return (
<button
id='start_cloud_trial_btn'
className={`CloudStartTrialButton ${extraClass}`}
onClick={startCloudTrial}
disabled={disabled || status === TrialLoadStatus.Failed}
>
{btnText(status)}
</button>
);
};
export default CloudStartTrialButton;

Просмотреть файл

@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {mount, shallow} from 'enzyme';
import {ItemStatus} from 'utils/constants';
import InputBusinessEmail from './input_business_email';
describe('/components/cloud_start_trial/input_business_email', () => {
const handleEmailValuesMockFn = jest.fn();
const baseProps = {
handleEmailValues: handleEmailValuesMockFn,
email: 'foo@example.com',
customInputLabel: null,
};
test('should match snapshot', () => {
const wrapper = shallow(<InputBusinessEmail {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
test('test input business email displays the input element correctly', () => {
const wrapper = mount(
<InputBusinessEmail {...baseProps}/>,
);
const inputElement = wrapper.find('.Input');
expect(inputElement.length).toBe(1);
});
test('test input business email displays the SUCCESS custom message correctly', () => {
const wrapper = mount(
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.SUCCESS, value: 'success value'}}}/>,
);
const customMessageElement = wrapper.find('.Input___customMessage.Input___success');
expect(customMessageElement.length).toBe(1);
});
test('test input business email displays the WARNING custom message correctly', () => {
const wrapper = mount(
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.WARNING, value: 'warning value'}}}/>,
);
const customMessageElement = wrapper.find('.Input___customMessage.Input___warning');
expect(customMessageElement.length).toBe(1);
});
test('test input business email displays the ERROR custom message correctly', () => {
const wrapper = mount(
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.ERROR, value: 'error value'}}}/>,
);
const customMessageElement = wrapper.find('.Input___customMessage.Input___error');
expect(customMessageElement.length).toBe(1);
});
test('test input business email displays the INFO custom message correctly', () => {
const wrapper = mount(
<InputBusinessEmail {...{...baseProps, customInputLabel: {type: ItemStatus.INFO, value: 'info value'}}}/>,
);
const customMessageElement = wrapper.find('.Input___customMessage.Input___info');
expect(customMessageElement.length).toBe(1);
});
test('test the input element handles the onChange event correctly', () => {
const event = {
target: {value: 'email@domain.com'},
};
const wrapper = mount(
<InputBusinessEmail {...baseProps}/>,
);
const inputElement = wrapper.find('.Input');
inputElement.find('input').simulate('change', event);
expect(handleEmailValuesMockFn).toBeCalled();
});
});

Просмотреть файл

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import Input, {CustomMessageInputType} from 'components/widgets/inputs/input/input';
interface InputBusinessEmailProps {
email: string;
handleEmailValues: (e: React.ChangeEvent<HTMLInputElement>) => void;
customInputLabel: CustomMessageInputType;
}
const InputBusinessEmail = ({
email,
handleEmailValues,
customInputLabel,
}: InputBusinessEmailProps): JSX.Element => {
const {formatMessage} = useIntl();
return (
<Input
type='email'
autoComplete='off'
autoFocus={true}
required={true}
value={email}
name='request-business-email'
containerClassName='request-business-email-container'
inputClassName='request-business-email-input'
label={formatMessage({id: 'start_cloud_trial.modal.enter_trial_email.input.label', defaultMessage: 'Enter business email'})}
placeholder={formatMessage({id: 'start_cloud_trial.modal.enter_trial_email.input.placeholder', defaultMessage: 'name@companyname.com'})}
onChange={handleEmailValues}
customMessage={customInputLabel}
/>
);
};
export default InputBusinessEmail;

Просмотреть файл

@@ -0,0 +1,115 @@
@import 'utils/variables';
@import 'utils/mixins';
.RequestBusinessEmailModal {
height: 320px;
&.modal-dialog {
margin-top: calc(50vh - 350px) !important;
}
.modal-content {
padding: 0 !important;
border-color: rgba(var(--center-channel-color-rgb), 0.16);
background: var(--center-channel-bg);
border-radius: 8px;
color: var(--center-channel-color);
}
.modal-header {
.close {
&:hover,
&:active,
&:focus,
&:active:focus {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.72);
opacity: 1;
}
top: 6px;
right: 4px;
width: 4rem;
height: 4rem;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56) !important;
font-family:
'Open Sans',
sans-serif;
font-size: 32px;
font-weight: 400;
}
height: 38px;
padding: 0;
border: 0;
background: var(--center-channel-bg) !important;
border-radius: 8px;
color: var(--center-channel-color);
}
.modal-body {
display: flex;
overflow: hidden;
width: 100%;
height: calc(100% - 38px);
flex-direction: column;
padding: 0;
.GenericModal__body {
height: 100%;
padding: 0 24px 24px 24px;
.container-footer {
bottom: 0;
height: 36px;
}
}
.request-business-email-input {
height: 34px !important;
border: 0 !important;
border-radius: 0 !important;
}
.start-trial-email-title {
margin-bottom: 22px;
color: var(--center-channel-color);
font-size: 22px;
font-weight: 600;
line-height: 28px;
}
.start-trial-email-description {
margin-bottom: 16px;
color: var(--center-channel-color);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.start-trial-email-disclaimer {
margin-top: 56px;
}
.start-trial-button {
display: flex;
button {
@include primary-button;
margin-left: auto;
}
}
}
.modal-centered {
text-align: center;
}
.modal-footer {
padding: 12px 24px 24px;
border: none;
border-radius: 4px;
}
}

Просмотреть файл

@@ -0,0 +1,253 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {act} from 'react-dom/test-utils';
import {shallow} from 'enzyme';
import * as cloudActions from 'actions/cloud';
import GenericModal from 'components/generic_modal';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import RequestBusinessEmailModal from './request_business_email_modal';
jest.useFakeTimers();
jest.mock('lodash/debounce', () => jest.fn((fn) => fn));
describe('components/request_business_email_modal/request_business_email_modal', () => {
const state = {
entities: {
users: {
currentUserId: 'current_user_id',
},
admin: {},
general: {
license: {
IsLicensed: 'true',
Cloud: 'true',
},
config: {},
},
cloud: {
subscription: {id: 'subscriptionID'},
},
},
views: {
modals: {
modalState: {
request_business_email_modal: {
open: 'true',
},
},
},
},
};
const props = {
onExited: jest.fn(),
};
const store = mockStore(state);
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
expect(wrapper).toMatchSnapshot();
});
test('should show the Start Cloud Trial Button', async () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
await act(async () => {
const startTrialBtn = wrapper.find('CloudStartTrialButton');
expect(startTrialBtn).toHaveLength(1);
});
});
test('should call on close', async () => {
const mockOnClose = jest.fn();
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal
{...props}
onClose={mockOnClose}
/>
</Provider>,
);
await act(async () => {
wrapper.find(GenericModal).props().onExited();
expect(mockOnClose).toHaveBeenCalled();
});
});
test('should call on exited', async () => {
const mockOnExited = jest.fn();
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal
{...props}
onExited={mockOnExited}
/>
</Provider>,
);
await act(async () => {
wrapper.find(GenericModal).props().onExited();
expect(mockOnExited).toHaveBeenCalled();
});
});
test('should show the Input to enter the valid Business Email', async () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
await act(async () => {
expect(wrapper.find('InputBusinessEmail')).toHaveLength(1);
});
});
test('should start with Start Cloud Trial Button disabled', async () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
await act(async () => {
const startTrialBtn = wrapper.find('CloudStartTrialButton');
expect(startTrialBtn.props().disabled).toEqual(true);
});
});
test('should ENABLE the trial button if email is VALID', async () => {
// mock validation response to TRUE meaning the email is a valid email
const validateBusinessEmail = () => () => Promise.resolve(true);
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
const event = {
target: {value: 'valid-email@domain.com'},
};
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
await act(async () => {
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
const input = inputBusinessEmail.find('input');
input.find('input').at(0).simulate('change', event);
});
act(() => {
wrapper.update();
const startTrialBtn = wrapper.find('CloudStartTrialButton');
expect(startTrialBtn.props().disabled).toEqual(false);
});
});
test('should show the success custom message if the email is valid', async () => {
// mock validation response to TRUE meaning the email is a valid email
const validateBusinessEmail = () => () => Promise.resolve(true);
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
const event = {
target: {value: 'valid-email@domain.com'},
};
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
await act(async () => {
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
const input = inputBusinessEmail.find('input');
input.find('input').at(0).simulate('change', event);
});
act(() => {
wrapper.update();
const customMessageElement = wrapper.find('.Input___customMessage.Input___success');
expect(customMessageElement.length).toBe(1);
});
});
test('should DISABLE the trial button if email is INVALID', async () => {
// mock validation response to FALSE meaning the email is an invalid email
const validateBusinessEmail = () => () => Promise.resolve(false);
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
const event = {
target: {value: 'INvalid-email@domain.com'},
};
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
await act(async () => {
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
const input = inputBusinessEmail.find('input');
input.find('input').at(0).simulate('change', event);
});
act(() => {
wrapper.update();
const startTrialBtn = wrapper.find('CloudStartTrialButton');
expect(startTrialBtn.props().disabled).toEqual(true);
});
});
test('should show the error custom message if the email is invalid', async () => {
// mock validation response to FALSE meaning the email is an invalid email
const validateBusinessEmail = () => () => Promise.resolve(false);
jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail);
const event = {
target: {value: 'INvalid-email@domain.com'},
};
const wrapper = mountWithIntl(
<Provider store={store}>
<RequestBusinessEmailModal {...props}/>
</Provider>,
);
await act(async () => {
const inputBusinessEmail = wrapper.find('InputBusinessEmail');
const input = inputBusinessEmail.find('input');
input.find('input').at(0).simulate('change', event);
});
act(() => {
wrapper.update();
const customMessageElement = wrapper.find('.Input___customMessage.Input___error');
expect(customMessageElement.length).toBe(1);
});
});
});

Просмотреть файл

@@ -0,0 +1,171 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import {useDispatch} from 'react-redux';
import {FormattedMessage, useIntl} from 'react-intl';
import {debounce} from 'lodash';
import {DispatchFunc} from 'mattermost-redux/types/actions';
import {trackEvent} from 'actions/telemetry_actions';
import {closeModal} from 'actions/views/modals';
import {validateBusinessEmail} from 'actions/cloud';
import {ItemStatus, TELEMETRY_CATEGORIES, ModalIdentifiers, LicenseLinks, AboutLinks} from 'utils/constants';
import GenericModal from 'components/generic_modal';
import {CustomMessageInputType} from 'components/widgets/inputs/input/input';
import ExternalLink from 'components/external_link';
import {isEmail} from 'mattermost-redux/utils/helpers';
import StartCloudTrialBtn from './cloud_start_trial_btn';
import InputBusinessEmail from './input_business_email';
import './request_business_email_modal.scss';
type Props = {
onClose?: () => void;
onExited: () => void;
}
const RequestBusinessEmailModal = (
{
onClose,
onExited,
}: Props): JSX.Element | null => {
const {formatMessage} = useIntl();
const dispatch = useDispatch<DispatchFunc>();
const [email, setEmail] = useState<string>('');
const [customInputLabel, setCustomInputLabel] = useState<CustomMessageInputType>(null);
const [trialBtnDisabled, setTrialBtnDisabled] = useState<boolean>(true);
useEffect(() => {
trackEvent(
TELEMETRY_CATEGORIES.REQUEST_BUSINESS_EMAIL,
'request_business_email',
);
}, []);
const handleOnClose = useCallback(() => {
if (onClose) {
onClose();
}
onExited();
}, [onClose, onExited]);
const handleEmailValues = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const email = e.target.value;
setEmail(email.trim().toLowerCase());
validateEmail(email);
}, []);
const validateEmail = useCallback(debounce(async (email: string) => {
// no value set, no validation and clean the custom input label
if (!email) {
setTrialBtnDisabled(true);
setCustomInputLabel(null);
return;
}
// function isEmail aready handle empty / null value
if (!isEmail(email)) {
const errMsg = formatMessage({id: 'request_business_email_modal.invalidEmail', defaultMessage: 'This doesn\'t look like a valid email'});
setCustomInputLabel({type: ItemStatus.WARNING, value: errMsg});
setTrialBtnDisabled(true);
return;
}
// go and validate the email against the validateBusinessEmail endpoint
const isValidBusinessEmail = await validateBusinessEmail(email)();
if (!isValidBusinessEmail) {
const errMsg = formatMessage({id: 'request_business_email_modal.not_business_email', defaultMessage: 'This doesn\'t look like a business email'});
setCustomInputLabel({type: ItemStatus.ERROR, value: errMsg});
setTrialBtnDisabled(true);
return;
}
// if it is a valid business email, proceed, enable the start trial button and notify the user about the email is valid
const okMsg = formatMessage({id: 'request_business_email_modal.valid_business_email', defaultMessage: 'This is a valid email'});
setCustomInputLabel({type: ItemStatus.SUCCESS, value: okMsg});
setTrialBtnDisabled(false);
}, 250), []);
// this function will be executed after successfull trial request, closing this request business email modal
const closeMeAfterSuccessTrialReq = async () => {
await dispatch(closeModal(ModalIdentifiers.REQUEST_BUSINESS_EMAIL_MODAL));
};
return (
<GenericModal
className='RequestBusinessEmailModal'
id='RequestBusinessEmailModal'
onExited={handleOnClose}
>
<div className='start-trial-email-title'>
<FormattedMessage
id='start_cloud_trial.modal.enter_trial_email.title'
defaultMessage='Enter an email to start your trial'
/>
</div>
<div className='start-trial-email-description'>
<FormattedMessage
id='start_cloud_trial.modal.enter_trial_email.description'
defaultMessage='Start a trial and enter a business email to get started. '
/>
</div>
<div className='start-trial-email-input'>
<InputBusinessEmail
email={email}
handleEmailValues={handleEmailValues}
customInputLabel={customInputLabel}
/>
</div>
<div className='start-trial-email-disclaimer'>
<FormattedMessage
id='request_business_email.start_trial.modal.disclaimer'
defaultMessage='By selecting <highlight>“Start trial”</highlight>, I agree to the <linkEvaluation>Mattermost Software and Services License Agreement</linkEvaluation>, <linkPrivacy>privacy policy</linkPrivacy> and receiving product emails.'
values={{
highlight: (msg: React.ReactNode) => (
<strong>
{msg}
</strong>
),
linkEvaluation: (msg: React.ReactNode) => (
<ExternalLink
href={LicenseLinks.SOFTWARE_SERVICES_LICENSE_AGREEMENT}
location='request_business_email_modal'
>
{msg}
</ExternalLink>
),
linkPrivacy: (msg: React.ReactNode) => (
<ExternalLink
href={AboutLinks.PRIVACY_POLICY}
location='request_business_email_modal'
>
{msg}
</ExternalLink>
),
}}
/>
</div>
<div className='start-trial-button'>
<StartCloudTrialBtn
message={formatMessage({id: 'cloud.startTrial.modal.btn', defaultMessage: 'Start trial'})}
telemetryId='request_business_email_modal'
disabled={trialBtnDisabled}
email={email}
afterTrialRequest={closeMeAfterSuccessTrialReq}
/>
</div>
</GenericModal>
);
};
export default RequestBusinessEmailModal;