[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
@@ -8,6 +8,12 @@ type BootstrapSelfHostedSignupRequest struct {
|
|||||||
Reset bool `json:"reset"`
|
Reset bool `json:"reset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubscribeNewsletterRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
ServerID string `json:"server_id"`
|
||||||
|
SubscribedContent string `json:"subscribed_content"`
|
||||||
|
}
|
||||||
|
|
||||||
type BootstrapSelfHostedSignupResponse struct {
|
type BootstrapSelfHostedSignupResponse struct {
|
||||||
Progress string `json:"progress"`
|
Progress string `json:"progress"`
|
||||||
// email listed on the JWT claim
|
// email listed on the JWT claim
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ func (api *API) InitHostedCustomer() {
|
|||||||
api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET")
|
api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET")
|
||||||
// GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf
|
// GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf
|
||||||
api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET")
|
api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET")
|
||||||
|
|
||||||
|
api.BaseRoutes.HostedCustomer.Handle("/subscribe-newsletter", api.APIHandler(handleSubscribeToNewsletter)).Methods(http.MethodPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureSelfHostedAdmin(c *Context, where string) {
|
func ensureSelfHostedAdmin(c *Context, where string) {
|
||||||
@@ -293,3 +295,33 @@ func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
r,
|
r,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSubscribeToNewsletter(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
const where = "Api4.handleSubscribeToNewsletter"
|
||||||
|
ensured := ensureCloudInterface(c, where)
|
||||||
|
if !ensured {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(model.SubscribeNewsletterRequest)
|
||||||
|
err = json.Unmarshal(bodyBytes, req)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ServerID = c.App.Srv().TelemetryId()
|
||||||
|
|
||||||
|
if err := c.App.Cloud().SubscribeToNewsletter("", req); err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.server.cws.subscribe_to_newsletter.app_error", nil, "CWS Server failed to subscribe to newsletter.", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,4 +47,5 @@ type CloudInterface interface {
|
|||||||
CheckCWSConnection(userId string) error
|
CheckCWSConnection(userId string) error
|
||||||
|
|
||||||
SelfServeDeleteWorkspace(userID string, deletionRequest *model.WorkspaceDeletionRequest) error
|
SelfServeDeleteWorkspace(userID string, deletionRequest *model.WorkspaceDeletionRequest) error
|
||||||
|
SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,6 +540,20 @@ func (_m *CloudInterface) SelfServeDeleteWorkspace(userID string, deletionReques
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscribeToNewsletter provides a mock function with given fields: userID, req
|
||||||
|
func (_m *CloudInterface) SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error {
|
||||||
|
ret := _m.Called(userID, req)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string, *model.SubscribeNewsletterRequest) error); ok {
|
||||||
|
r0 = rf(userID, req)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo
|
// UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo
|
||||||
func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) {
|
func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) {
|
||||||
ret := _m.Called(userID, customerInfo)
|
ret := _m.Called(userID, customerInfo)
|
||||||
|
|||||||
@@ -2591,6 +2591,10 @@
|
|||||||
"id": "api.server.cws.needs_enterprise_edition",
|
"id": "api.server.cws.needs_enterprise_edition",
|
||||||
"translation": "Service only available in Mattermost Enterprise edition"
|
"translation": "Service only available in Mattermost Enterprise edition"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.server.cws.subscribe_to_newsletter.app_error",
|
||||||
|
"translation": "CWS Server failed to subscribe to newsletter."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.server.hosted_signup_unavailable.error",
|
"id": "api.server.hosted_signup_unavailable.error",
|
||||||
"translation": "Portal unavailable for self-hosted signup."
|
"translation": "Portal unavailable for self-hosted signup."
|
||||||
|
|||||||
@@ -93,6 +93,28 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value=""
|
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
|
<SaveButton
|
||||||
btnClass="btn-primary"
|
btnClass="btn-primary"
|
||||||
defaultMessage="Create account"
|
defaultMessage="Create account"
|
||||||
@@ -244,6 +266,28 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
value=""
|
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
|
<SaveButton
|
||||||
btnClass="btn-primary"
|
btnClass="btn-primary"
|
||||||
defaultMessage="Create account"
|
defaultMessage="Create account"
|
||||||
|
|||||||
@@ -166,6 +166,22 @@
|
|||||||
margin-top: 22px;
|
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 {
|
.signup-body-card-form-button-submit {
|
||||||
@include primary-button;
|
@include primary-button;
|
||||||
@include button-large;
|
@include button-large;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React from 'react';
|
|||||||
import {shallow, ReactWrapper} from 'enzyme';
|
import {shallow, ReactWrapper} from 'enzyme';
|
||||||
import {IntlProvider} from 'react-intl';
|
import {IntlProvider} from 'react-intl';
|
||||||
import {BrowserRouter} from 'react-router-dom';
|
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';
|
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 Input from 'components/widgets/inputs/input/input';
|
||||||
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
||||||
import SaveButton from 'components/save_button';
|
import SaveButton from 'components/save_button';
|
||||||
|
import * as useCWSAvailabilityCheckAll from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||||
|
|
||||||
import {RequestStatus} from 'mattermost-redux/constants';
|
import {RequestStatus} from 'mattermost-redux/constants';
|
||||||
import {ClientConfig} from '@mattermost/types/config';
|
import {ClientConfig} from '@mattermost/types/config';
|
||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
import {WindowSizes} from 'utils/constants';
|
import {WindowSizes} from 'utils/constants';
|
||||||
|
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||||
|
|
||||||
let mockState: GlobalState;
|
let mockState: GlobalState;
|
||||||
let mockLocation = {pathname: '', search: '', hash: ''};
|
let mockLocation = {pathname: '', search: '', hash: ''};
|
||||||
const mockHistoryPush = jest.fn();
|
const mockHistoryPush = jest.fn();
|
||||||
let mockLicense = {IsLicensed: 'true'};
|
let mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||||
let mockConfig: Partial<ClientConfig>;
|
let mockConfig: Partial<ClientConfig>;
|
||||||
let mockDispatch = jest.fn();
|
let mockDispatch = jest.fn();
|
||||||
|
|
||||||
@@ -96,7 +98,7 @@ describe('components/signup/Signup', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockLocation = {pathname: '', search: '', hash: ''};
|
mockLocation = {pathname: '', search: '', hash: ''};
|
||||||
|
|
||||||
mockLicense = {IsLicensed: 'true'};
|
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||||
|
|
||||||
mockState = {
|
mockState = {
|
||||||
entities: {
|
entities: {
|
||||||
@@ -178,7 +180,7 @@ describe('components/signup/Signup', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should match snapshot for all signup options enabled with isLicensed disabled', () => {
|
it('should match snapshot for all signup options enabled with isLicensed disabled', () => {
|
||||||
mockLicense = {IsLicensed: 'false'};
|
mockLicense = {IsLicensed: 'false', Cloud: 'false'};
|
||||||
|
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<Signup/>,
|
<Signup/>,
|
||||||
@@ -295,4 +297,45 @@ describe('components/signup/Signup', () => {
|
|||||||
expect(wrapper.find('.content-layout-column-title').text()).toEqual('This invite link is invalid');
|
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 LoginOffice365Icon from 'components/widgets/icons/login_office_365_icon';
|
||||||
import Input, {CustomMessageInputType, SIZE} from 'components/widgets/inputs/input/input';
|
import Input, {CustomMessageInputType, SIZE} from 'components/widgets/inputs/input/input';
|
||||||
import PasswordInput from 'components/widgets/inputs/password_input/password_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 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 {isValidUsername, isValidPassword, getPasswordConfig, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils';
|
||||||
|
|
||||||
import './signup.scss';
|
import './signup.scss';
|
||||||
@@ -99,7 +102,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
TermsOfServiceLink,
|
TermsOfServiceLink,
|
||||||
PrivacyPolicyLink,
|
PrivacyPolicyLink,
|
||||||
} = config;
|
} = config;
|
||||||
const {IsLicensed} = useSelector(getLicense);
|
const {IsLicensed, Cloud} = useSelector(getLicense);
|
||||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||||
const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined));
|
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 passwordInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const isLicensed = IsLicensed === 'true';
|
const isLicensed = IsLicensed === 'true';
|
||||||
|
const isCloud = Cloud === 'true';
|
||||||
const enableOpenServer = EnableOpenServer === 'true';
|
const enableOpenServer = EnableOpenServer === 'true';
|
||||||
const noAccounts = NoAccounts === 'true';
|
const noAccounts = NoAccounts === 'true';
|
||||||
const enableSignUpWithEmail = EnableSignUpWithEmail === 'true';
|
const enableSignUpWithEmail = EnableSignUpWithEmail === 'true';
|
||||||
@@ -136,12 +140,24 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
const [teamName, setTeamName] = useState(parsedTeamName ?? '');
|
const [teamName, setTeamName] = useState(parsedTeamName ?? '');
|
||||||
const [alertBanner, setAlertBanner] = useState<AlertBannerProps | null>(null);
|
const [alertBanner, setAlertBanner] = useState<AlertBannerProps | null>(null);
|
||||||
const [isMobileView, setIsMobileView] = useState(false);
|
const [isMobileView, setIsMobileView] = useState(false);
|
||||||
|
const [subscribeToSecurityNewsletter, setSubscribeToSecurityNewsletter] = useState(false);
|
||||||
|
|
||||||
|
const canReachCWS = useCWSAvailabilityCheck();
|
||||||
|
|
||||||
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML;
|
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML;
|
||||||
const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner);
|
const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner);
|
||||||
const canSubmit = Boolean(email && name && password) && !hasError && !loading;
|
const canSubmit = Boolean(email && name && password) && !hasError && !loading;
|
||||||
const {error: passwordInfo} = isValidPassword('', getPasswordConfig(config), intl);
|
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 getExternalSignupOptions = () => {
|
||||||
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
||||||
|
|
||||||
@@ -564,6 +580,9 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await handleSignupSuccess(user, data as UserProfile);
|
await handleSignupSuccess(user, data as UserProfile);
|
||||||
|
if (subscribeToSecurityNewsletter) {
|
||||||
|
subscribeToSecurityNewsletterFunc();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setIsWaiting(false);
|
setIsWaiting(false);
|
||||||
}
|
}
|
||||||
@@ -571,6 +590,60 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
|
|
||||||
const handleReturnButtonOnClick = () => history.replace('/');
|
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 handleOnBlur = (e: FocusEvent<HTMLInputElement>, inputId: string) => {
|
||||||
const text = e.target.value;
|
const text = e.target.value;
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -736,6 +809,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
|||||||
error={passwordError}
|
error={passwordError}
|
||||||
onBlur={(e) => handleOnBlur(e, 'password')}
|
onBlur={(e) => handleOnBlur(e, 'password')}
|
||||||
/>
|
/>
|
||||||
|
{getNewsletterCheck()}
|
||||||
<SaveButton
|
<SaveButton
|
||||||
extraClasses='signup-body-card-form-button-submit large'
|
extraClasses='signup-body-card-form-button-submit large'
|
||||||
saving={isWaiting}
|
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",
|
"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.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",
|
"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",
|
"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.subtitle": "This channel doesn't contains any file with the selected file format.",
|
||||||
"no_results.channel_files_filtered.title": "No files found",
|
"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',
|
BILLING_DOCS: 'https://mattermost.com/pl/how-self-hosted-billing-works',
|
||||||
SELF_HOSTED_BILLING: 'https://docs.mattermost.com/manage/self-hosted-billing.html',
|
SELF_HOSTED_BILLING: 'https://docs.mattermost.com/manage/self-hosted-billing.html',
|
||||||
TERMS_AND_CONDITIONS: 'https://mattermost.com/enterprise-edition-terms/',
|
TERMS_AND_CONDITIONS: 'https://mattermost.com/enterprise-edition-terms/',
|
||||||
|
SECURITY_UPDATES: 'https://mattermost.com/security-updates/',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocLinks = {
|
export const DocLinks = {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
CreateSubscriptionRequest,
|
CreateSubscriptionRequest,
|
||||||
Feedback,
|
Feedback,
|
||||||
WorkspaceDeletionRequest,
|
WorkspaceDeletionRequest,
|
||||||
|
NewsletterRequestBody,
|
||||||
} from '@mattermost/types/cloud';
|
} from '@mattermost/types/cloud';
|
||||||
import {
|
import {
|
||||||
SelfHostedSignupForm,
|
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 () => {
|
createPaymentMethod = async () => {
|
||||||
return this.doFetch(
|
return this.doFetch(
|
||||||
`${this.getCloudRoute()}/payment`,
|
`${this.getCloudRoute()}/payment`,
|
||||||
|
|||||||
@@ -220,6 +220,11 @@ export interface CreateSubscriptionRequest {
|
|||||||
internal_purchase_order?: string;
|
internal_purchase_order?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NewsletterRequestBody {
|
||||||
|
email: string;
|
||||||
|
subscribed_content: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const areShippingDetailsValid = (address: Address | null | undefined): boolean => {
|
export const areShippingDetailsValid = (address: Address | null | undefined): boolean => {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user