[MM-51236] - Add opt-in checkbox in self-hosted account creation page (#22709)
* [MM-51236] - Add opt-in checkbox in self-hosted account creation page * add server impl * fix lint and translations * add tests * improvents * fix model * fix mocks * feedback impl * update text color * handle error * fix tests
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
bbadbfde45
Коммит
ed36e8bd64
@@ -93,6 +93,28 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="newsletter"
|
||||
>
|
||||
<span
|
||||
className="interested"
|
||||
>
|
||||
Interested in receiving Mattermost security updates via newsletter?
|
||||
</span>
|
||||
<span
|
||||
className="link"
|
||||
>
|
||||
Sign up at
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/security-updates/"
|
||||
key=".1"
|
||||
location="signup"
|
||||
>
|
||||
https://mattermost.com/security-updates/
|
||||
</ExternalLink>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<SaveButton
|
||||
btnClass="btn-primary"
|
||||
defaultMessage="Create account"
|
||||
@@ -244,6 +266,28 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="newsletter"
|
||||
>
|
||||
<span
|
||||
className="interested"
|
||||
>
|
||||
Interested in receiving Mattermost security updates via newsletter?
|
||||
</span>
|
||||
<span
|
||||
className="link"
|
||||
>
|
||||
Sign up at
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/security-updates/"
|
||||
key=".1"
|
||||
location="signup"
|
||||
>
|
||||
https://mattermost.com/security-updates/
|
||||
</ExternalLink>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<SaveButton
|
||||
btnClass="btn-primary"
|
||||
defaultMessage="Create account"
|
||||
|
||||
@@ -166,6 +166,22 @@
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.newsletter {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 32px;
|
||||
color: var(--center-channel-color-rgb);
|
||||
font-family: 'Open Sans';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
.interested {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.signup-body-card-form-button-submit {
|
||||
@include primary-button;
|
||||
@include button-large;
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import {shallow, ReactWrapper} from 'enzyme';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import {act} from '@testing-library/react';
|
||||
import {act, screen} from '@testing-library/react';
|
||||
|
||||
import * as global_actions from 'actions/global_actions';
|
||||
|
||||
@@ -15,16 +15,18 @@ import Signup from 'components/signup/signup';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
||||
import SaveButton from 'components/save_button';
|
||||
import * as useCWSAvailabilityCheckAll from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {ClientConfig} from '@mattermost/types/config';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {WindowSizes} from 'utils/constants';
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
|
||||
let mockState: GlobalState;
|
||||
let mockLocation = {pathname: '', search: '', hash: ''};
|
||||
const mockHistoryPush = jest.fn();
|
||||
let mockLicense = {IsLicensed: 'true'};
|
||||
let mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
let mockConfig: Partial<ClientConfig>;
|
||||
let mockDispatch = jest.fn();
|
||||
|
||||
@@ -96,7 +98,7 @@ describe('components/signup/Signup', () => {
|
||||
beforeEach(() => {
|
||||
mockLocation = {pathname: '', search: '', hash: ''};
|
||||
|
||||
mockLicense = {IsLicensed: 'true'};
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
mockState = {
|
||||
entities: {
|
||||
@@ -178,7 +180,7 @@ describe('components/signup/Signup', () => {
|
||||
});
|
||||
|
||||
it('should match snapshot for all signup options enabled with isLicensed disabled', () => {
|
||||
mockLicense = {IsLicensed: 'false'};
|
||||
mockLicense = {IsLicensed: 'false', Cloud: 'false'};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Signup/>,
|
||||
@@ -295,4 +297,45 @@ describe('components/signup/Signup', () => {
|
||||
expect(wrapper.find('.content-layout-column-title').text()).toEqual('This invite link is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show newsletter check box opt-in for self-hosted non airgapped workspaces', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
const {container: signupContainer} = renderWithIntlAndStore(
|
||||
<BrowserRouter>
|
||||
<Signup/>
|
||||
</BrowserRouter>, {});
|
||||
|
||||
screen.getByTestId('signup-body-card-form-check-newsletter');
|
||||
const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter');
|
||||
expect(checkInput).toHaveAttribute('type', 'checkbox');
|
||||
|
||||
expect(signupContainer).toHaveTextContent(/I would like to receive Mattermost security updates via newsletter. Data Terms and Conditions apply/);
|
||||
});
|
||||
|
||||
it('should NOT show newsletter check box opt-in for self-hosted AND airgapped workspaces', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => false);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
const {container: signupContainer} = renderWithIntlAndStore(
|
||||
<BrowserRouter>
|
||||
<Signup/>
|
||||
</BrowserRouter>, {});
|
||||
|
||||
expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow();
|
||||
expect(signupContainer).toHaveTextContent('Interested in receiving Mattermost security updates via newsletter?Sign up at https://mattermost.com/security-updates/.');
|
||||
});
|
||||
|
||||
it('should not show any newsletter related opt-in or text for cloud', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'true'};
|
||||
|
||||
renderWithIntlAndStore(
|
||||
<BrowserRouter>
|
||||
<Signup/>
|
||||
</BrowserRouter>, {});
|
||||
|
||||
expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,9 +48,12 @@ import LoginOpenIDIcon from 'components/widgets/icons/login_openid_icon';
|
||||
import LoginOffice365Icon from 'components/widgets/icons/login_office_365_icon';
|
||||
import Input, {CustomMessageInputType, SIZE} from 'components/widgets/inputs/input/input';
|
||||
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
||||
import CheckInput from 'components/widgets/inputs/check';
|
||||
import SaveButton from 'components/save_button';
|
||||
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import {Constants, ItemStatus, ValidationErrors} from 'utils/constants';
|
||||
import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants';
|
||||
import {isValidUsername, isValidPassword, getPasswordConfig, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils';
|
||||
|
||||
import './signup.scss';
|
||||
@@ -99,7 +102,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
TermsOfServiceLink,
|
||||
PrivacyPolicyLink,
|
||||
} = config;
|
||||
const {IsLicensed} = useSelector(getLicense);
|
||||
const {IsLicensed, Cloud} = useSelector(getLicense);
|
||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||
const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined));
|
||||
@@ -110,6 +113,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const passwordInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isLicensed = IsLicensed === 'true';
|
||||
const isCloud = Cloud === 'true';
|
||||
const enableOpenServer = EnableOpenServer === 'true';
|
||||
const noAccounts = NoAccounts === 'true';
|
||||
const enableSignUpWithEmail = EnableSignUpWithEmail === 'true';
|
||||
@@ -136,12 +140,24 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const [teamName, setTeamName] = useState(parsedTeamName ?? '');
|
||||
const [alertBanner, setAlertBanner] = useState<AlertBannerProps | null>(null);
|
||||
const [isMobileView, setIsMobileView] = useState(false);
|
||||
const [subscribeToSecurityNewsletter, setSubscribeToSecurityNewsletter] = useState(false);
|
||||
|
||||
const canReachCWS = useCWSAvailabilityCheck();
|
||||
|
||||
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML;
|
||||
const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner);
|
||||
const canSubmit = Boolean(email && name && password) && !hasError && !loading;
|
||||
const {error: passwordInfo} = isValidPassword('', getPasswordConfig(config), intl);
|
||||
|
||||
const subscribeToSecurityNewsletterFunc = () => {
|
||||
try {
|
||||
Client4.subscribeToNewsletter({email, subscribed_content: 'security_newsletter'});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getExternalSignupOptions = () => {
|
||||
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
||||
|
||||
@@ -564,6 +580,9 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
}
|
||||
|
||||
await handleSignupSuccess(user, data as UserProfile);
|
||||
if (subscribeToSecurityNewsletter) {
|
||||
subscribeToSecurityNewsletterFunc();
|
||||
}
|
||||
} else {
|
||||
setIsWaiting(false);
|
||||
}
|
||||
@@ -571,6 +590,60 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
|
||||
const handleReturnButtonOnClick = () => history.replace('/');
|
||||
|
||||
const getNewsletterCheck = () => {
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canReachCWS) {
|
||||
return (
|
||||
<CheckInput
|
||||
id='signup-body-card-form-check-newsletter'
|
||||
name='newsletter'
|
||||
onChange={() => setSubscribeToSecurityNewsletter(!subscribeToSecurityNewsletter)}
|
||||
text={
|
||||
formatMessage(
|
||||
{id: 'newsletter_optin.checkmark.text', defaultMessage: 'I would like to receive Mattermost security updates via newsletter. Data <a>Terms and Conditions</a> apply'},
|
||||
{
|
||||
a: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<ExternalLink
|
||||
location='signup-newsletter-checkmark'
|
||||
href={HostedCustomerLinks.SECURITY_UPDATES}
|
||||
>
|
||||
{chunks}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
)}
|
||||
checked={subscribeToSecurityNewsletter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className='newsletter'>
|
||||
<span className='interested'>
|
||||
{formatMessage({id: 'newsletter_optin.title', defaultMessage: 'Interested in receiving Mattermost security updates via newsletter?'})}
|
||||
</span>
|
||||
<span className='link'>
|
||||
{formatMessage(
|
||||
{id: 'newsletter_optin.desc', defaultMessage: 'Sign up at <a>{link}</a>.'},
|
||||
{
|
||||
link: HostedCustomerLinks.SECURITY_UPDATES,
|
||||
a: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<ExternalLink
|
||||
location='signup'
|
||||
href={HostedCustomerLinks.SECURITY_UPDATES}
|
||||
>
|
||||
{chunks}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnBlur = (e: FocusEvent<HTMLInputElement>, inputId: string) => {
|
||||
const text = e.target.value;
|
||||
if (!text) {
|
||||
@@ -736,6 +809,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
error={passwordError}
|
||||
onBlur={(e) => handleOnBlur(e, 'password')}
|
||||
/>
|
||||
{getNewsletterCheck()}
|
||||
<SaveButton
|
||||
extraClasses='signup-body-card-form-button-submit large'
|
||||
saving={isWaiting}
|
||||
|
||||
19
webapp/channels/src/components/widgets/inputs/check/check.scss
Обычный файл
19
webapp/channels/src/components/widgets/inputs/check/check.scss
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
.check-input {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.text {
|
||||
margin-left: 8px;
|
||||
color: var(--center-channel-color-rgb);
|
||||
font-family: 'Open Sans';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
28
webapp/channels/src/components/widgets/inputs/check/index.tsx
Обычный файл
28
webapp/channels/src/components/widgets/inputs/check/index.tsx
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import './check.scss';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
name: string;
|
||||
text: ReactNode;
|
||||
onChange: () => void;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
function CheckInput(props: Props) {
|
||||
return (
|
||||
<div className='check-input'>
|
||||
<input
|
||||
{...props}
|
||||
data-testid={props.id}
|
||||
type='checkbox'
|
||||
/>
|
||||
<span className='text'>{props.text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckInput;
|
||||
@@ -4254,6 +4254,9 @@
|
||||
"navbar.viewPinnedPosts": "View Pinned Posts",
|
||||
"newChannelWithBoard.tutorialTip.description": "The board you just created can be quickly accessed by clicking on the Boards icon in the App bar. You can view the boards that are linked to this channel in the right-hand sidebar and open one in full view.",
|
||||
"newChannelWithBoard.tutorialTip.title": "Access linked boards from the App Bar",
|
||||
"newsletter_optin.checkmark.text": "I would like to receive Mattermost security updates via newsletter. Data <a>Terms and Conditions</a> apply",
|
||||
"newsletter_optin.desc": "Sign up at <a>{link}</a>.",
|
||||
"newsletter_optin.title": "Interested in receiving Mattermost security updates via newsletter?",
|
||||
"next_steps_view.welcomeToMattermost": "Welcome to Mattermost",
|
||||
"no_results.channel_files_filtered.subtitle": "This channel doesn't contains any file with the selected file format.",
|
||||
"no_results.channel_files_filtered.title": "No files found",
|
||||
|
||||
@@ -1077,6 +1077,7 @@ export const HostedCustomerLinks = {
|
||||
BILLING_DOCS: 'https://mattermost.com/pl/how-self-hosted-billing-works',
|
||||
SELF_HOSTED_BILLING: 'https://docs.mattermost.com/manage/self-hosted-billing.html',
|
||||
TERMS_AND_CONDITIONS: 'https://mattermost.com/enterprise-edition-terms/',
|
||||
SECURITY_UPDATES: 'https://mattermost.com/security-updates/',
|
||||
};
|
||||
|
||||
export const DocLinks = {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
CreateSubscriptionRequest,
|
||||
Feedback,
|
||||
WorkspaceDeletionRequest,
|
||||
NewsletterRequestBody,
|
||||
} from '@mattermost/types/cloud';
|
||||
import {
|
||||
SelfHostedSignupForm,
|
||||
@@ -3893,6 +3894,13 @@ export default class Client4 {
|
||||
);
|
||||
};
|
||||
|
||||
subscribeToNewsletter = (newletterRequestBody: NewsletterRequestBody) => {
|
||||
return this.doFetch<StatusOK>(
|
||||
`${this.getHostedCustomerRoute()}/subscribe-newsletter`,
|
||||
{method: 'post', body: JSON.stringify(newletterRequestBody)},
|
||||
);
|
||||
};
|
||||
|
||||
createPaymentMethod = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getCloudRoute()}/payment`,
|
||||
|
||||
@@ -220,6 +220,11 @@ export interface CreateSubscriptionRequest {
|
||||
internal_purchase_order?: string;
|
||||
}
|
||||
|
||||
export interface NewsletterRequestBody {
|
||||
email: string;
|
||||
subscribed_content: string;
|
||||
}
|
||||
|
||||
export const areShippingDetailsValid = (address: Address | null | undefined): boolean => {
|
||||
if (!address) {
|
||||
return false;
|
||||
|
||||
Ссылка в новой задаче
Block a user