[MM-67030] Remove newsletter signup and replace with terms/privacy agreement (#34801)

* remove newsletter signup and replace with terms/privacy agreement

* removed subscribeToSecurityNewsletter, made checkbox required

* update signup test to remove newsletter and ensure the terms checkbox is required

* update unit test and e2e test to reflect changes

* fix e2e test

* Removed susbcribe-newsletter endpoint in server

* Update signup.test.tsx

* remove unused css

* remove unused css

* fixed broken tests

* fixed linter issues

* Remove redundant IntlProvider and comments

* Remove usage of test IDs from Signup tests

* Remove usage of fireEvent

* Remove usage of mountWithIntl from Signup tests

* update e2e tests

* fix playwright test

* Fix Lint in signup.ts

---------

Co-authored-by: maria.nunez <maria.nunez@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
This commit is contained in:
Matthew Birtch 2026-01-23 13:24:27 -05:00 committed by GitHub
parent 3a394b25e4
commit 09c4a61fed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 316 additions and 488 deletions

View file

@ -61,6 +61,8 @@ describe('Authentication', () => {
cy.get('#input_name').clear().type(`test${getRandomId()}`);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Make sure account was created successfully and we are at the select team page
@ -113,6 +115,8 @@ describe('Authentication', () => {
cy.get('#input_name').clear().type(`test${getRandomId()}`);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Make sure account was not created successfully
@ -146,6 +150,8 @@ describe('Authentication', () => {
cy.get('#input_name').clear().type(username);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Make sure account was created successfully and we are on the team joining page

View file

@ -61,6 +61,8 @@ describe('Authentication', () => {
cy.get('#input_password-input').clear().type('less');
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Assert the error is what is expected;
@ -68,6 +70,8 @@ describe('Authentication', () => {
cy.get('#input_password-input').clear().type('greaterthan7');
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Assert that we are not shown an MFA screen and instead a Teams You Can join page
@ -112,6 +116,8 @@ describe('Authentication', () => {
cy.get('#input_name').clear().type(`BestUsernameInTheWorld${getRandomId()}`);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
['NOLOWERCASE123!', 'noupppercase123!', 'NoNumber!', 'NoSymbol123'].forEach((option) => {
cy.get('#input_password-input').clear().type(option);
cy.findByText('Create account').click();

View file

@ -149,6 +149,8 @@ describe('Authentication', () => {
cy.get('#input_password-input').type('Test123456!');
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
['1user', 'te', 'user#1', 'user!1'].forEach((option) => {
cy.get('#input_name').clear().type(option);
cy.findByText('Create account').click();
@ -183,6 +185,8 @@ describe('Authentication', () => {
cy.get('#input_name').clear().type(`Test${getRandomId()}`);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Make sure account was created successfully and we are on the team joining page
@ -245,6 +249,8 @@ describe('Authentication', () => {
cy.get('#input_name').clear().type(`Test${getRandomId()}`);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Make sure account was not created successfully

View file

@ -58,6 +58,8 @@ describe('Authentication', () => {
cy.get('#input_name').clear().type(`Test${getRandomId()}`);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
// * Make sure account was not created successfully

View file

@ -31,6 +31,9 @@ function signupWithEmail(name, pw) {
// # Type 'unique1pw' for password
cy.get('#input_password-input').type(pw);
// # Check the terms and privacy checkbox
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
// # Click on Create Account button
cy.findByText('Create account').click();
}

View file

@ -71,6 +71,7 @@ describe('Onboarding', () => {
cy.get('#input_email').should('be.focused').and('be.visible').type(email);
cy.get('#input_name').should('be.visible').type(username);
cy.get('#input_password-input').should('be.visible').type(password);
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
cy.findByText('Create account').click();
cy.findByText('Youre almost done!').should('be.visible');

View file

@ -71,6 +71,9 @@ describe('Onboarding', () => {
cy.get('#input_name').should('be.visible').type(username);
cy.get('#input_password-input').should('be.visible').type(password);
// # Check the terms and privacy checkbox
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
// # Attempt to create an account by clicking on the 'Create account' button
cy.findByText('Create account').click();

View file

@ -72,15 +72,22 @@ describe('Signup Email page', () => {
cy.get('#input_password-input').should('be.visible').and('have.attr', 'placeholder', 'Choose a Password');
cy.findByText('Your password must be 5-72 characters long.').should('be.visible');
cy.get('#saveSetting').scrollIntoView().should('be.visible');
cy.get('#saveSetting').should('contain', 'Create account');
// * Check terms and privacy checkbox
cy.get('#signup-body-card-form-check-terms-and-privacy').should('be.visible').and('not.be.checked');
cy.findByText(/I agree to the/i).should('be.visible');
// * Check newsletter subscription checkbox text and links
cy.findByText('I would like to receive Mattermost security updates via newsletter.').should('be.visible');
cy.findByText(/By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news\./).should('be.visible');
cy.findByText(/I have read the/).parent().within(() => {
cy.findByRole('link', {name: 'Privacy Policy'}).should('be.visible').and('have.attr', 'href').and('include', 'mattermost.com/pl/privacy-policy/');
cy.findByRole('link', {name: 'unsubscribe'}).should('be.visible').and('have.attr', 'href').and('include', 'forms.mattermost.com/UnsubscribePage.html');
// * Check that submit button is disabled without accepting terms
cy.get('#saveSetting').scrollIntoView().should('be.visible');
cy.get('#saveSetting').should('contain', 'Create account').and('be.disabled');
// * Check terms and privacy links (now part of checkbox label)
cy.get('label[for="signup-body-card-form-check-terms-and-privacy"]').within(() => {
cy.findByText('Acceptable Use Policy').should('be.visible').
and('have.attr', 'href').
and('include', config.SupportSettings.TermsOfServiceLink || TERMS_OF_SERVICE_LINK);
cy.findByText('Privacy Policy').should('be.visible').
and('have.attr', 'href').
and('include', config.SupportSettings.PrivacyPolicyLink || PRIVACY_POLICY_LINK);
});
});
@ -116,4 +123,23 @@ describe('Signup Email page', () => {
cy.get('.footer-copyright').should('contain', `© ${currentYear} Mattermost Inc.`);
});
});
it('should enable submit button when terms checkbox is checked', () => {
// # Fill in valid form data
cy.get('#input_email').type('test@example.com');
cy.get('#input_name').type('testuser');
cy.get('#input_password-input').type('validPassword123');
// * Verify submit button is disabled
cy.get('#saveSetting').should('be.disabled');
// # Check the terms and privacy checkbox
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
// * Verify checkbox is now checked
cy.get('#signup-body-card-form-check-terms-and-privacy').should('be.checked');
// * Verify submit button is now enabled
cy.get('#saveSetting').should('not.be.disabled');
});
});

View file

@ -114,6 +114,9 @@ describe('Team Settings', () => {
cy.wait(TIMEOUTS.HALF_SEC);
cy.get('#input_password-input').type(password);
// # Check the terms and privacy checkbox
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
// # Attempt to create an account by clicking on the 'Create Account' button
cy.findByText('Create account').click();

View file

@ -87,7 +87,10 @@ describe('Team Settings', () => {
cy.get('#input_name').type(username);
cy.get('#input_password-input').type(password);
// # Attempt to create an account by clicking on the 'Create account' button
// # Check the terms and privacy checkbox
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
// # Attempt to create an account by clicking on the 'Create Account' button
cy.findByText('Create account').click();
// * Assert that the expected error message from creating an account with an email not from the allowed email domain exists and is visible

View file

@ -16,11 +16,9 @@ export default class SignupPage {
readonly usernameInput;
readonly passwordInput;
readonly passwordToggleButton;
readonly newsLetterCheckBox;
readonly newsLetterPrivacyPolicyLink;
readonly newsLetterUnsubscribeLink;
readonly agreementTermsOfUseLink;
readonly agreementPrivacyPolicyLink;
readonly termsAndPrivacyCheckBox;
readonly termsAndPrivacyAcceptableUsePolicyLink;
readonly termsAndPrivacyPrivacyPolicyLink;
readonly createAccountButton;
readonly loginLink;
readonly emailError;
@ -48,14 +46,12 @@ export default class SignupPage {
);
this.passwordError = page.locator('text=Must be 5-72 characters long.');
const newsletterBlock = page.locator('.check-input');
this.newsLetterCheckBox = newsletterBlock.getByRole('checkbox', {name: 'newsletter checkbox'});
this.newsLetterPrivacyPolicyLink = newsletterBlock.locator('text=Privacy Policy');
this.newsLetterUnsubscribeLink = newsletterBlock.locator('text=unsubscribe');
const agreementBlock = page.locator('.signup-body-card-agreement');
this.agreementTermsOfUseLink = agreementBlock.locator('text=Terms of Use');
this.agreementPrivacyPolicyLink = agreementBlock.locator('text=Privacy Policy');
const termsAndPrivacyBlock = page.locator('.check-input');
this.termsAndPrivacyCheckBox = termsAndPrivacyBlock.getByRole('checkbox', {
name: 'Terms and privacy policy checkbox',
});
this.termsAndPrivacyAcceptableUsePolicyLink = termsAndPrivacyBlock.locator('text=Acceptable Use Policy');
this.termsAndPrivacyPrivacyPolicyLink = termsAndPrivacyBlock.locator('text=Privacy Policy');
this.header = new components.MainHeader(page.locator('.hfroute-header'));
this.footer = new components.Footer(page.locator('.hfroute-footer'));
@ -79,6 +75,7 @@ export default class SignupPage {
await this.emailInput.fill(user.email);
await this.usernameInput.fill(user.username);
await this.passwordInput.fill(user.password);
await this.termsAndPrivacyCheckBox.check();
await this.createAccountButton.click();
if (waitForRedirect) {

View file

@ -43,25 +43,21 @@ test('/signup_user_complete accessibility tab support', async ({pw}, testInfo) =
await pw.signupPage.passwordInput.press('Tab');
expect(await pw.signupPage.passwordToggleButton).toBeFocused();
// * Should move focus to newsletter checkbox after tab
// * Should move focus to terms and privacy checkbox after tab
await pw.signupPage.passwordToggleButton.press('Tab');
expect(await pw.signupPage.newsLetterCheckBox).toBeFocused();
expect(await pw.signupPage.termsAndPrivacyCheckBox).toBeFocused();
// * Should move focus to newsletter privacy policy link after tab
await pw.signupPage.newsLetterCheckBox.press('Tab');
expect(await pw.signupPage.newsLetterPrivacyPolicyLink).toBeFocused();
// * Should move focus to acceptable use policy link after tab
await pw.signupPage.termsAndPrivacyCheckBox.press('Tab');
expect(await pw.signupPage.termsAndPrivacyAcceptableUsePolicyLink).toBeFocused();
// * Should move focus to newsletter unsubscribe link after tab
await pw.signupPage.newsLetterPrivacyPolicyLink.press('Tab');
expect(await pw.signupPage.newsLetterUnsubscribeLink).toBeFocused();
// * Should move focus to privacy policy link after tab
await pw.signupPage.termsAndPrivacyAcceptableUsePolicyLink.press('Tab');
expect(await pw.signupPage.termsAndPrivacyPrivacyPolicyLink).toBeFocused();
// * Should move focus to agreement terms of use link after tab
await pw.signupPage.newsLetterUnsubscribeLink.press('Tab');
expect(await pw.signupPage.agreementTermsOfUseLink).toBeFocused();
// * Should move focus to agreement privacy policy link after tab
await pw.signupPage.agreementTermsOfUseLink.press('Tab');
expect(await pw.signupPage.agreementPrivacyPolicyLink).toBeFocused();
// * Should move focus to about link after tab (skips disabled create account button)
await pw.signupPage.termsAndPrivacyPrivacyPolicyLink.press('Tab');
expect(await pw.signupPage.footer.aboutLink).toBeFocused();
// * Should move focus to privacy policy link after tab
await pw.signupPage.footer.aboutLink.press('Tab');

View file

@ -4,8 +4,6 @@
package api4
import (
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost/server/public/model"
@ -16,40 +14,9 @@ import (
func (api *API) InitHostedCustomer() {
// POST /api/v4/hosted_customer/available
api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods(http.MethodGet)
api.BaseRoutes.HostedCustomer.Handle("/subscribe-newsletter", api.APIHandler(handleSubscribeToNewsletter)).Methods(http.MethodPost)
}
func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.handleSignupAvailable"
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented)
}
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().ServerId()
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)
}

View file

@ -34,8 +34,6 @@ type CloudInterface interface {
CheckCWSConnection(userId string) error
SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error
ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
GetInstallation(userID string) (*model.Installation, error)

View file

@ -503,24 +503,6 @@ func (_m *CloudInterface) RemoveAuditLoggingCert(userID string) error {
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)
if len(ret) == 0 {
panic("no return value specified for SubscribeToNewsletter")
}
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
func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) {
ret := _m.Called(userID, customerInfo)

View file

@ -3158,10 +3158,6 @@
"id": "api.server.cws.needs_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",
"translation": "Portal unavailable for self-hosted signup."

View file

@ -1,10 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type SubscribeNewsletterRequest struct {
Email string `json:"email"`
ServerID string `json:"server_id"`
SubscribedContent string `json:"subscribed_content"`
}

View file

@ -40,7 +40,6 @@ OUTPUT_EXCLUDING_IGNORED=$(echo "$OUTPUT" | grep -Fv \
-e 'Cannot find /api/v4/hosted_customer/confirm-expand method: POST in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/hosted_customer/invoices method: GET in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf method: GET in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/hosted_customer/subscribe-newsletter method: POST in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/license/review method: POST in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/license/review/status method: GET in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/posts/{post_id}/edit_history method: GET in OpenAPI 3 spec.' \

View file

@ -118,7 +118,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
autoFocus={true}
className="signup-body-card-form-email-input"
customMessage={null}
data-testid="signup-body-card-form-email-input"
disabled={false}
inputSize="large"
name="email"
@ -136,7 +135,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
"value": "You can use lowercase letters, numbers, periods, dashes, and underscores.",
}
}
data-testid="signup-body-card-form-name-input"
disabled={false}
inputSize="large"
name="name"
@ -148,7 +146,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
<ForwardRef
className="signup-body-card-form-password-input"
createMode={true}
data-testid="signup-body-card-form-password-input"
disabled={false}
error=""
info="Your password must be 5-72 characters long."
@ -156,28 +153,29 @@ 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, product, promotions, and company updates updates via newsletter?
</span>
<span
className="link"
>
Sign up at
<ForwardRef
href="https://mattermost.com/security-updates/"
key="1/.1"
location="signup"
>
https://mattermost.com/security-updates/
</ForwardRef>
.
</span>
</div>
<CheckInput
ariaLabel="Terms and privacy policy checkbox"
checked={false}
id="signup-body-card-form-check-terms-and-privacy"
name="terms"
onChange={[Function]}
text={
Array [
"I agree to the ",
<ForwardRef
location="signup-terms-of-use"
>
Acceptable Use Policy
</ForwardRef>,
" and the ",
<ForwardRef
location="signup-privacy-policy"
>
Privacy Policy
</ForwardRef>,
]
}
/>
<SaveButton
defaultMessage="Create account"
disabled={true}
@ -214,21 +212,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
url="/oauth/gitlab/signup"
/>
</div>
<p
className="signup-body-card-agreement"
>
<MemoizedFormattedMessage
defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <termsOfUseLink>Terms of Use</termsOfUseLink> and <privacyPolicyLink>Privacy Policy</privacyPolicyLink>. If you do not agree, you cannot use {siteName}."
id="signup.agreement"
values={
Object {
"privacyPolicyLink": [Function],
"siteName": "Mattermost",
"termsOfUseLink": [Function],
}
}
/>
</p>
</div>
</div>
</div>
@ -284,7 +267,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
autoFocus={true}
className="signup-body-card-form-email-input"
customMessage={null}
data-testid="signup-body-card-form-email-input"
disabled={false}
inputSize="large"
name="email"
@ -302,7 +284,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
"value": "You can use lowercase letters, numbers, periods, dashes, and underscores.",
}
}
data-testid="signup-body-card-form-name-input"
disabled={false}
inputSize="large"
name="name"
@ -314,7 +295,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
<ForwardRef
className="signup-body-card-form-password-input"
createMode={true}
data-testid="signup-body-card-form-password-input"
disabled={false}
error=""
info="Your password must be 5-72 characters long."
@ -322,28 +302,29 @@ 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, product, promotions, and company updates updates via newsletter?
</span>
<span
className="link"
>
Sign up at
<ForwardRef
href="https://mattermost.com/security-updates/"
key="1/.1"
location="signup"
>
https://mattermost.com/security-updates/
</ForwardRef>
.
</span>
</div>
<CheckInput
ariaLabel="Terms and privacy policy checkbox"
checked={false}
id="signup-body-card-form-check-terms-and-privacy"
name="terms"
onChange={[Function]}
text={
Array [
"I agree to the ",
<ForwardRef
location="signup-terms-of-use"
>
Acceptable Use Policy
</ForwardRef>,
" and the ",
<ForwardRef
location="signup-privacy-policy"
>
Privacy Policy
</ForwardRef>,
]
}
/>
<SaveButton
defaultMessage="Create account"
disabled={true}
@ -426,21 +407,6 @@ exports[`components/signup/Signup should match snapshot for all signup options e
url="/login/sso/saml?action=signup"
/>
</div>
<p
className="signup-body-card-agreement"
>
<MemoizedFormattedMessage
defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <termsOfUseLink>Terms of Use</termsOfUseLink> and <privacyPolicyLink>Privacy Policy</privacyPolicyLink>. If you do not agree, you cannot use {siteName}."
id="signup.agreement"
values={
Object {
"privacyPolicyLink": [Function],
"siteName": "Mattermost",
"termsOfUseLink": [Function],
}
}
/>
</p>
</div>
</div>
</div>

View file

@ -167,22 +167,6 @@
margin-top: 22px;
}
.newsletter {
margin-top: 24px;
margin-bottom: 32px;
color: rgba(var(--center-channel-color-rgb), 0.75);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
.interested {
display: block;
color: var(--center-channel-color);
}
}
.signup-body-card-form-button-submit {
@include mixins.primary-button;
@include mixins.button-large;
@ -222,19 +206,6 @@
row-gap: 24px;
}
}
.signup-body-card-agreement {
margin-top: 32px;
color: rgba(var(--center-channel-color-rgb), 0.75);
font-size: 11px;
line-height: 16px;
a {
@include mixins.link;
font-size: 11px;
}
}
}
}

View file

@ -1,24 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ReactWrapper} from 'enzyme';
import {shallow} from 'enzyme';
import React from 'react';
import {IntlProvider} from 'react-intl';
import {BrowserRouter} from 'react-router-dom';
import type {ClientConfig} from '@mattermost/types/config';
import {RequestStatus} from 'mattermost-redux/constants';
import * as useCWSAvailabilityCheckAll from 'components/common/hooks/useCWSAvailabilityCheck';
import SaveButton from 'components/save_button';
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 {redirectUserToDefaultTeam} from 'actions/global_actions';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act, renderWithContext, screen, fireEvent, waitFor} from 'tests/react_testing_utils';
import Signup from 'components/signup/signup';
import {renderWithContext, screen, waitFor, userEvent} from 'tests/react_testing_utils';
import {WindowSizes} from 'utils/constants';
import type {GlobalState} from 'types/store';
@ -30,12 +24,6 @@ let mockLicense = {IsLicensed: 'true', Cloud: 'false'};
let mockConfig: Partial<ClientConfig>;
let mockDispatch = jest.fn();
const intlProviderProps = {
defaultLocale: 'en',
locale: 'en',
messages: {},
};
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
@ -56,13 +44,21 @@ jest.mock('mattermost-redux/selectors/entities/general', () => ({
getConfig: () => mockConfig,
}));
let mockCurrentUserId = '';
jest.mock('mattermost-redux/selectors/entities/users', () => ({
...jest.requireActual('mattermost-redux/selectors/entities/users') as typeof import('mattermost-redux/selectors/entities/users'),
getCurrentUserId: () => '',
getCurrentUserId: () => mockCurrentUserId,
}));
jest.mock('actions/global_actions', () => ({
...jest.requireActual('actions/global_actions'),
redirectUserToDefaultTeam: jest.fn(),
}));
jest.mock('actions/team_actions', () => ({
...jest.requireActual('actions/team_actions') as typeof import('actions/team_actions'),
addUserToTeamFromInvite: jest.fn().mockResolvedValue({data: {}}),
addUsersToTeamFromInvite: jest.fn().mockResolvedValue({name: 'teamName'}),
}));
@ -76,27 +72,14 @@ jest.mock('actions/views/login', () => ({
loginById: jest.fn().mockResolvedValue({data: {}}),
}));
jest.mock('actions/team_actions', () => ({
...jest.requireActual('actions/team_actions') as typeof import('actions/team_actions'),
addUserToTeamFromInvite: jest.fn().mockResolvedValue({data: {}}),
}));
jest.mock('actions/storage');
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/signup/Signup', () => {
beforeEach(() => {
mockLocation = {pathname: '', search: '', hash: ''};
mockHistoryPush.mockClear();
mockDispatch.mockClear();
mockCurrentUserId = '';
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
@ -208,33 +191,27 @@ describe('components/signup/Signup', () => {
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
mockResolvedValueOnce({error: {server_error_id: 'api.user.login.not_verified.app_error'}}); // loginById
const wrapper = mountWithIntl(
<IntlProvider {...intlProviderProps}>
<BrowserRouter>
<Signup/>
</BrowserRouter>
</IntlProvider>,
renderWithContext(
<Signup/>,
);
const emailInput = wrapper.find(Input).first().find('input').first();
emailInput.simulate('change', {target: {value: 'jdoe@mm.com'}});
const emailInput = screen.getByLabelText('Email address');
const usernameInput = screen.getByLabelText('Choose a Username');
const passwordInput = screen.getByLabelText('Choose a Password');
const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i});
const submitButton = screen.getByRole('button', {name: 'Create account'});
const nameInput = wrapper.find('#input_name').first();
nameInput.simulate('change', {target: {value: 'jdoe'}});
await userEvent.type(emailInput, 'jdoe@mm.com');
await userEvent.type(usernameInput, 'jdoe');
await userEvent.type(passwordInput, 'password');
await userEvent.click(termsCheckbox);
const passwordInput = wrapper.find(PasswordInput).first().find('input').first();
passwordInput.simulate('change', {target: {value: 'password'}});
expect(submitButton).not.toBeDisabled();
await userEvent.click(submitButton);
const saveButton = wrapper.find(SaveButton).first();
expect(saveButton.props().disabled).toEqual(false);
saveButton.find('button').first().simulate('click');
await actImmediate(wrapper);
expect(wrapper.find(Input).first().props().disabled).toEqual(true);
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
expect(emailInput).toBeDisabled();
expect(usernameInput).toBeDisabled();
expect(passwordInput).toBeDisabled();
expect(mockHistoryPush).toHaveBeenCalledWith('/should_verify_email?email=jdoe%40mm.com&teamname=teamName');
});
@ -245,67 +222,77 @@ describe('components/signup/Signup', () => {
mockResolvedValueOnce({data: {id: 'userId', password: 'password', email: 'jdoe@mm.com}'}}). // createUser
mockResolvedValueOnce({}); // loginById
const wrapper = mountWithIntl(
<IntlProvider {...intlProviderProps}>
<BrowserRouter>
<Signup/>
</BrowserRouter>
</IntlProvider>,
renderWithContext(
<Signup/>,
);
const emailInput = wrapper.find(Input).first().find('input').first();
emailInput.simulate('change', {target: {value: 'jdoe@mm.com'}});
const emailInput = screen.getByLabelText('Email address');
const usernameInput = screen.getByLabelText('Choose a Username');
const passwordInput = screen.getByLabelText('Choose a Password');
const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i});
const submitButton = screen.getByRole('button', {name: 'Create account'});
const nameInput = wrapper.find('#input_name').first();
nameInput.simulate('change', {target: {value: 'jdoe'}});
await userEvent.type(emailInput, 'jdoe@mm.com');
await userEvent.type(usernameInput, 'jdoe');
await userEvent.type(passwordInput, 'password');
await userEvent.click(termsCheckbox);
const passwordInput = wrapper.find(PasswordInput).first().find('input').first();
passwordInput.simulate('change', {target: {value: 'password'}});
expect(submitButton).not.toBeDisabled();
await userEvent.click(submitButton);
const saveButton = wrapper.find(SaveButton).first();
expect(saveButton.props().disabled).toEqual(false);
expect(emailInput).toBeDisabled();
expect(usernameInput).toBeDisabled();
expect(passwordInput).toBeDisabled();
saveButton.find('button').first().simulate('click');
await actImmediate(wrapper);
expect(wrapper.find(Input).first().props().disabled).toEqual(true);
expect(wrapper.find('#input_name').first().props().disabled).toEqual(true);
expect(wrapper.find(PasswordInput).first().props().disabled).toEqual(true);
expect(redirectUserToDefaultTeam).toHaveBeenCalled();
});
it('should focus email input when email validation fails', async () => {
renderWithContext(<Signup/>, mockState);
const emailInput = screen.getByTestId('signup-body-card-form-email-input');
const submitButton = screen.getByText('Create account');
const emailInput = screen.getByLabelText('Email address');
const usernameInput = screen.getByLabelText('Choose a Username');
const passwordInput = screen.getByLabelText('Choose a Password');
const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i});
const submitButton = screen.getByRole('button', {name: 'Create account'});
// Submit with invalid email
fireEvent.change(emailInput, {target: {value: 'invalid-email'}});
fireEvent.click(submitButton);
await userEvent.type(emailInput, 'invalid-email');
await userEvent.type(usernameInput, 'testuser');
await userEvent.type(passwordInput, '123');
await userEvent.click(termsCheckbox);
await waitFor(() => {
expect(emailInput).toHaveFocus();
});
// The focus should no longer be on the email input before clicking submit
expect(emailInput).not.toHaveFocus();
await userEvent.click(submitButton);
// And now the focus should move back to the email input
expect(emailInput).toHaveFocus();
});
it('should focus password input when password validation fails', async () => {
renderWithContext(<Signup/>, mockState);
const emailInput = screen.getByTestId('signup-body-card-form-email-input');
const usernameInput = screen.getByTestId('signup-body-card-form-name-input');
const passwordInput = screen.getByTestId('signup-body-card-form-password-input');
const emailInput = screen.getByLabelText('Email address');
const usernameInput = screen.getByLabelText('Choose a Username');
const passwordInput = screen.getByLabelText('Choose a Password');
const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i});
const submitButton = screen.getByText('Create account');
// Submit with valid email and username but invalid password
fireEvent.change(emailInput, {target: {value: 'test@example.com'}});
fireEvent.change(usernameInput, {target: {value: 'testuser'}});
fireEvent.change(passwordInput, {target: {value: '123'}});
fireEvent.click(submitButton);
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(usernameInput, 'testuser');
await userEvent.type(passwordInput, '123');
await userEvent.click(termsCheckbox);
await waitFor(() => {
expect(passwordInput).toHaveFocus();
});
// The focus should no longer be on the password input before clicking submit
expect(emailInput).not.toHaveFocus();
await userEvent.click(submitButton);
// And now the focus should move back to the password input
expect(passwordInput).toHaveFocus();
});
it('should focus username input when server returns username exists error', async () => {
@ -319,87 +306,102 @@ describe('components/signup/Signup', () => {
renderWithContext(<Signup/>, mockState);
const emailInput = screen.getByTestId('signup-body-card-form-email-input');
const usernameInput = screen.getByTestId('signup-body-card-form-name-input');
const passwordInput = screen.getByTestId('signup-body-card-form-password-input');
const emailInput = screen.getByLabelText('Email address');
const usernameInput = screen.getByLabelText('Choose a Username');
const passwordInput = screen.getByLabelText('Choose a Password');
const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i});
const submitButton = screen.getByText('Create account');
// Submit with valid data that will trigger server error
fireEvent.change(emailInput, {target: {value: 'test@example.com'}});
fireEvent.change(usernameInput, {target: {value: 'existinguser'}});
fireEvent.change(passwordInput, {target: {value: 'password123'}});
fireEvent.click(submitButton);
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(usernameInput, 'existinguser');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(termsCheckbox);
await waitFor(() => {
expect(usernameInput).toHaveFocus();
});
// The focus should no longer be on the email input before clicking submit
expect(usernameInput).not.toHaveFocus();
await userEvent.click(submitButton);
// And now the focus should move back to the username input
expect(usernameInput).toHaveFocus();
});
it('should add user to team and redirect when team invite valid and logged in', async () => {
mockLocation.search = '?id=ppni7a9t87fn3j4d56rwocdctc';
mockCurrentUserId = 'user1'; // Simulate logged-in user
const wrapper = shallow(
mockDispatch = jest.fn().
mockResolvedValueOnce({}). // removeGlobalItem in useEffect
mockResolvedValueOnce({data: {name: 'teamName'}}); // addUserToTeamFromInvite
renderWithContext(
<Signup/>,
);
setTimeout(() => {
await waitFor(() => {
expect(mockHistoryPush).toHaveBeenCalledWith('/teamName/channels/town-square');
expect(wrapper).toMatchSnapshot();
}, 0);
});
it('should handle failure adding user to team when team invite and logged in', () => {
mockLocation.search = '?id=ppni7a9t87fn3j4d56rwocdctc';
const wrapper = shallow(
<Signup/>,
);
setTimeout(() => {
expect(mockHistoryPush).not.toHaveBeenCalled();
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(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available);
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
it('should handle failure adding user to team when team invite and logged in', async () => {
mockLocation.search = '?id=ppni7a9t87fn3j4d56rwocdctc';
mockCurrentUserId = 'user1'; // Simulate logged-in user
const {container: signupContainer} = renderWithContext(
<Signup/>,
);
mockDispatch = jest.fn().
mockResolvedValueOnce({}). // removeGlobalItem in useEffect
mockResolvedValueOnce({
error: {
server_error_id: 'api.team.add_user_to_team_from_invite.invalid.app_error',
message: 'Invalid invite',
},
}); // addUserToTeamFromInvite with error
screen.getByTestId('signup-body-card-form-check-newsletter');
const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter');
expect(checkInput).toHaveAttribute('type', 'checkbox');
renderWithContext(<Signup/>, mockState);
expect(signupContainer).toHaveTextContent('I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time');
await waitFor(() => {
expect(mockHistoryPush).not.toHaveBeenCalled();
expect(screen.getByText('This invite link is invalid')).toBeInTheDocument();
});
});
it('should NOT show newsletter check box opt-in for self-hosted AND airgapped workspaces', async () => {
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Unavailable);
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
it('should show terms and privacy checkbox', async () => {
mockConfig.TermsOfServiceLink = 'https://mattermost.com/terms';
mockConfig.PrivacyPolicyLink = 'https://mattermost.com/privacy';
const {container: signupContainer} = renderWithContext(
<Signup/>,
);
expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow();
expect(signupContainer).toHaveTextContent('Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?Sign up at https://mattermost.com/security-updates/.');
const checkInput = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i});
expect(checkInput).toHaveAttribute('type', 'checkbox');
expect(checkInput).not.toBeChecked();
expect(signupContainer).toHaveTextContent('I agree to the Acceptable Use Policy and the Privacy Policy');
});
it('should show newsletter related opt-in or text for cloud', async () => {
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available);
mockLicense = {IsLicensed: 'true', Cloud: 'true'};
it('should require terms acceptance before enabling submit button', async () => {
renderWithContext(<Signup/>, mockState);
const {container: signupContainer} = renderWithContext(
<Signup/>,
);
const emailInput = screen.getByLabelText('Email address');
const usernameInput = screen.getByLabelText('Choose a Username');
const passwordInput = screen.getByLabelText('Choose a Password');
const termsCheckbox = screen.getByRole('checkbox', {name: /terms and privacy policy checkbox/i});
screen.getByTestId('signup-body-card-form-check-newsletter');
const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter');
expect(checkInput).toHaveAttribute('type', 'checkbox');
// Fill in all fields but don't check terms
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(usernameInput, 'testuser');
await userEvent.type(passwordInput, 'ValidPassword123!');
expect(signupContainer).toHaveTextContent('I would like to receive Mattermost security updates via newsletter. By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the Privacy Policy and understand that I can unsubscribe at any time');
// Submit button should be disabled (SaveButton uses disabled prop on inner button)
const submitButton = screen.getByRole('button', {name: /Create account/i});
expect(submitButton).toBeDisabled();
// Check terms
await userEvent.click(termsCheckbox);
// Now submit button should be enabled
const enabledButton = screen.getByRole('button', {name: /Create account/i});
expect(enabledButton).not.toBeDisabled();
});
});

View file

@ -4,7 +4,7 @@
import classNames from 'classnames';
import throttle from 'lodash/throttle';
import React, {useState, useEffect, useRef, useCallback} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useIntl} from 'react-intl';
import {useSelector, useDispatch} from 'react-redux';
import {useLocation, useHistory, Route} from 'react-router-dom';
@ -27,7 +27,6 @@ import {getGlobalItem} from 'selectors/storage';
import AlertBanner from 'components/alert_banner';
import type {ModeType, AlertBannerProps} from 'components/alert_banner';
import useCWSAvailabilityCheck, {CSWAvailabilityCheckTypes} from 'components/common/hooks/useCWSAvailabilityCheck';
import DesktopAuthToken from 'components/desktop_auth_token';
import ExternalLink from 'components/external_link';
import ExternalLoginButton from 'components/external_login_button/external_login_button';
@ -48,7 +47,7 @@ import Input, {SIZE} from 'components/widgets/inputs/input/input';
import type {CustomMessageInputType} from 'components/widgets/inputs/input/input';
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants';
import {Constants, ItemStatus, ValidationErrors} from 'utils/constants';
import {isValidPassword} from 'utils/password';
import {isDesktopApp} from 'utils/user_agent';
import {isValidUsername} from 'utils/utils';
@ -139,28 +138,17 @@ 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 [acceptedTerms, setAcceptedTerms] = useState(false);
const [submitClicked, setSubmitClicked] = useState(false);
const cwsAvailability = 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 canSubmit = Boolean(email && name && password && acceptedTerms) && !hasError && !loading;
const passwordConfig = useSelector(getPasswordConfig);
const {error: passwordInfo} = isValidPassword('', passwordConfig, intl);
const [desktopLoginLink, setDesktopLoginLink] = useState('');
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[] = [];
@ -595,9 +583,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
}
await handleSignupSuccess(user, data!);
if (subscribeToSecurityNewsletter) {
subscribeToSecurityNewsletterFunc();
}
} else {
setIsWaiting(false);
}
@ -605,68 +590,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
const handleReturnButtonOnClick = () => history.replace('/');
const getNewsletterCheck = () => {
if (cwsAvailability === CSWAvailabilityCheckTypes.Available) {
return (
<CheckInput
id='signup-body-card-form-check-newsletter'
ariaLabel={formatMessage({id: 'newsletter_optin.checkmark.box', defaultMessage: 'newsletter checkbox'})}
name='newsletter'
onChange={() => setSubscribeToSecurityNewsletter(!subscribeToSecurityNewsletter)}
text={
formatMessage(
{id: 'newsletter_optin.checkmark.text', defaultMessage: '<span>I would like to receive Mattermost security updates via newsletter.</span> By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the <a>Privacy Policy</a> and understand that I can <aa>unsubscribe</aa> at any time'},
{
a: (chunks: React.ReactNode | React.ReactNodeArray) => (
<ExternalLink
location='signup-newsletter-checkmark'
href={HostedCustomerLinks.PRIVACY}
>
{chunks}
</ExternalLink>
),
aa: (chunks: React.ReactNode | React.ReactNodeArray) => (
<ExternalLink
location='signup-newsletter-checkmark'
href={HostedCustomerLinks.NEWSLETTER_UNSUBSCRIBE_LINK}
>
{chunks}
</ExternalLink>
),
span: (chunks: React.ReactNode | React.ReactNodeArray) => (
<span className='header'>{chunks}</span>
),
},
)}
checked={subscribeToSecurityNewsletter}
/>
);
}
return (
<div className='newsletter'>
<span className='interested'>
{formatMessage({id: 'newsletter_optin.title', defaultMessage: 'Interested in receiving Mattermost security, product, promotions, and company updates 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 getContent = () => {
if (!enableSignUpWithEmail && !enableExternalSignup) {
return (
@ -780,7 +703,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
{enableSignUpWithEmail && (
<form className='signup-body-card-form'>
<Input
data-testid='signup-body-card-form-email-input'
ref={emailInput}
name='email'
className='signup-body-card-form-email-input'
@ -797,7 +719,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
customMessage={emailCustomLabelForInput}
/>
<Input
data-testid='signup-body-card-form-name-input'
ref={nameInput}
name='name'
className='signup-body-card-form-name-input'
@ -819,7 +740,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
}
/>
<PasswordInput
data-testid='signup-body-card-form-password-input'
ref={passwordInput}
className='signup-body-card-form-password-input'
value={password}
@ -830,7 +750,35 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
info={passwordInfo as string}
error={passwordError}
/>
{getNewsletterCheck()}
<CheckInput
id='signup-body-card-form-check-terms-and-privacy'
ariaLabel={formatMessage({id: 'signup.terms_and_privacy.checkmark.box', defaultMessage: 'Terms and privacy policy checkbox'})}
name='terms'
onChange={() => setAcceptedTerms(!acceptedTerms)}
text={
formatMessage(
{id: 'signup.terms_and_privacy.checkmark.text', defaultMessage: 'I agree to the <termsOfUseLink>Acceptable Use Policy</termsOfUseLink> and the <privacyPolicyLink>Privacy Policy</privacyPolicyLink>'},
{
privacyPolicyLink: (chunks: React.ReactNode | React.ReactNodeArray) => (
<ExternalLink
href={PrivacyPolicyLink as string}
location='signup-privacy-policy'
>
{chunks}
</ExternalLink>
),
termsOfUseLink: (chunks: React.ReactNode | React.ReactNodeArray) => (
<ExternalLink
href={TermsOfServiceLink as string}
location='signup-terms-of-use'
>
{chunks}
</ExternalLink>
),
},
)}
checked={acceptedTerms}
/>
<SaveButton
extraClasses='signup-body-card-form-button-submit large'
saving={isWaiting}
@ -859,33 +807,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
))}
</div>
)}
{enableSignUpWithEmail && !serverError && (
<p className='signup-body-card-agreement'>
<FormattedMessage
id='signup.agreement'
defaultMessage='By proceeding to create your account and use {siteName}, you agree to our <termsOfUseLink>Terms of Use</termsOfUseLink> and <privacyPolicyLink>Privacy Policy</privacyPolicyLink>. If you do not agree, you cannot use {siteName}.'
values={{
siteName: SiteName,
termsOfUseLink: (chunks) => (
<ExternalLink
href={TermsOfServiceLink as string}
location='signup-terms-of-use'
>
{chunks}
</ExternalLink>
),
privacyPolicyLink: (chunks) => (
<ExternalLink
href={PrivacyPolicyLink as string}
location='signup-privacy-policy'
>
{chunks}
</ExternalLink>
),
}}
/>
</p>
)}
</div>
</div>
</div>

View file

@ -5144,10 +5144,6 @@
"new_window_button.tooltip": "Open in new window",
"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.box": "newsletter checkbox",
"newsletter_optin.checkmark.text": "<span>I would like to receive Mattermost security updates via newsletter.</span> By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news. I have read the <a>Privacy Policy</a> and understand that I can <aa>unsubscribe</aa> at any time",
"newsletter_optin.desc": "Sign up at <a>{link}</a>.",
"newsletter_optin.title": "Interested in receiving Mattermost security, product, promotions, and company updates 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",
@ -5953,8 +5949,9 @@
"signup_user_completed.userHelp": "You can use lowercase letters, numbers, periods, dashes, and underscores.",
"signup_user_completed.usernameLength": "Usernames have to begin with a lowercase letter and be {min}-{max} characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.",
"signup_user_completed.validEmail": "Please enter a valid email address",
"signup.agreement": "By proceeding to create your account and use {siteName}, you agree to our <termsOfUseLink>Terms of Use</termsOfUseLink> and <privacyPolicyLink>Privacy Policy</privacyPolicyLink>. If you do not agree, you cannot use {siteName}.",
"signup.ldap": "AD/LDAP Credentials",
"signup.terms_and_privacy.checkmark.box": "Terms and privacy policy checkbox",
"signup.terms_and_privacy.checkmark.text": "I agree to the <termsOfUseLink>Acceptable Use Policy</termsOfUseLink> and the <privacyPolicyLink>Privacy Policy</privacyPolicyLink>",
"signup.title": "Create Account | {siteName}",
"single_image_view.copied_link_tooltip": "Copied",
"single_image_view.copy_link_tooltip": "Copy link",

View file

@ -38,7 +38,6 @@ import type {
NotifyAdminRequest,
Subscription,
ValidBusinessEmail,
NewsletterRequestBody,
Installation,
PreviewModalContentData,
} from '@mattermost/types/cloud';
@ -4228,13 +4227,6 @@ export default class Client4 {
);
};
subscribeToNewsletter = (newletterRequestBody: NewsletterRequestBody) => {
return this.doFetch<StatusOK>(
`${this.getHostedCustomerRoute()}/subscribe-newsletter`,
{method: 'post', body: JSON.stringify(newletterRequestBody)},
);
};
cwsAvailabilityCheck = () => {
return this.doFetchWithResponse(
`${this.getCloudRoute()}/check-cws-connection`,

View file

@ -191,11 +191,6 @@ export type ValidBusinessEmail = {
is_valid: boolean;
}
export interface NewsletterRequestBody {
email: string;
subscribed_content: string;
}
export const areShippingDetailsValid = (address: Address | null | undefined): boolean => {
if (!address) {
return false;