From 09c4a61fed8580ceae5bd05e64aa4ac9615c4444 Mon Sep 17 00:00:00 2001 From: Matthew Birtch Date: Fri, 23 Jan 2026 13:24:27 -0500 Subject: [PATCH] [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 Co-authored-by: Mattermost Build Co-authored-by: Harrison Healey Co-authored-by: yasserfaraazkhan --- .../auth_sso/authentication_1_spec.ts | 6 + .../auth_sso/authentication_2_spec.ts | 6 + .../auth_sso/authentication_4_spec.ts | 6 + .../auth_sso/authentication_spec.ts | 2 + .../onboarding/existing_email_adress_spec.js | 3 + .../login_page_link_account_creation_spec.js | 1 + .../use_team_invite_link_to_sign_up_spec.js | 3 + .../signin_authentication/signup_spec.js | 42 ++- .../closed_team_invite_by_email_spec.js | 3 + ...closed_team_with_not_allowed_email_spec.js | 5 +- .../playwright/lib/src/ui/pages/signup.ts | 23 +- .../common/signup_user_complete.spec.ts | 26 +- server/channels/api4/hosted_customer.go | 33 -- server/einterfaces/cloud.go | 2 - server/einterfaces/mocks/CloudInterface.go | 18 -- server/i18n/en.json | 4 - server/public/model/hosted_customer.go | 10 - server/scripts/vet-api-check.sh | 1 - .../signup/__snapshots__/signup.test.tsx.snap | 126 +++----- .../src/components/signup/signup.scss | 29 -- .../src/components/signup/signup.test.tsx | 290 +++++++++--------- .../channels/src/components/signup/signup.tsx | 145 ++------- webapp/channels/src/i18n/en.json | 7 +- webapp/platform/client/src/client4.ts | 8 - webapp/platform/types/src/cloud.ts | 5 - 25 files changed, 316 insertions(+), 488 deletions(-) delete mode 100644 server/public/model/hosted_customer.go diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts index 6f4061bb835..1f42c1561a6 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts @@ -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 diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts index 7198829b4f8..2c366bdf523 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_2_spec.ts @@ -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(); diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts index 004d1895015..64234aecb66 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_4_spec.ts @@ -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 diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts index 22b2701697b..0c4713b7aaa 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/auth_sso/authentication_spec.ts @@ -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 diff --git a/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js b/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js index c0c74a79c75..fff60fcc00d 100644 --- a/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/onboarding/existing_email_adress_spec.js @@ -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(); } diff --git a/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js b/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js index 14f55a8f762..8430167e667 100644 --- a/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/onboarding/login_page_link_account_creation_spec.js @@ -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('You’re almost done!').should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js b/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js index 92eb39043d5..35574b8c482 100644 --- a/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/onboarding/use_team_invite_link_to_sign_up_spec.js @@ -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(); diff --git a/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js b/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js index 7054d953939..2f8821ea12c 100644 --- a/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/signin_authentication/signup_spec.js @@ -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'); + }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js b/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js index 9512f2bf3c1..6884526358a 100644 --- a/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js @@ -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(); diff --git a/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js b/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js index 976ad356bc3..105f741ecd9 100644 --- a/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/team_settings/join_closed_team_with_not_allowed_email_spec.js @@ -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 diff --git a/e2e-tests/playwright/lib/src/ui/pages/signup.ts b/e2e-tests/playwright/lib/src/ui/pages/signup.ts index 240bbcbfe20..f695b7dab44 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/signup.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/signup.ts @@ -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) { diff --git a/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts b/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts index e0456404b69..714d2a2d272 100644 --- a/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts +++ b/e2e-tests/playwright/specs/accessibility/common/signup_user_complete.spec.ts @@ -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'); diff --git a/server/channels/api4/hosted_customer.go b/server/channels/api4/hosted_customer.go index 90adf3f9cc6..d502cea41ce 100644 --- a/server/channels/api4/hosted_customer.go +++ b/server/channels/api4/hosted_customer.go @@ -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) -} diff --git a/server/einterfaces/cloud.go b/server/einterfaces/cloud.go index b3eab4d425d..e7b9eb96da0 100644 --- a/server/einterfaces/cloud.go +++ b/server/einterfaces/cloud.go @@ -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) diff --git a/server/einterfaces/mocks/CloudInterface.go b/server/einterfaces/mocks/CloudInterface.go index 01260baec9d..d5ef4fc88df 100644 --- a/server/einterfaces/mocks/CloudInterface.go +++ b/server/einterfaces/mocks/CloudInterface.go @@ -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) diff --git a/server/i18n/en.json b/server/i18n/en.json index 1607abda20f..496817cb4bc 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -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." diff --git a/server/public/model/hosted_customer.go b/server/public/model/hosted_customer.go deleted file mode 100644 index ffaec15cba0..00000000000 --- a/server/public/model/hosted_customer.go +++ /dev/null @@ -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"` -} diff --git a/server/scripts/vet-api-check.sh b/server/scripts/vet-api-check.sh index d5e5d6cc310..7dc1ebc30e5 100755 --- a/server/scripts/vet-api-check.sh +++ b/server/scripts/vet-api-check.sh @@ -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.' \ diff --git a/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap b/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap index 4ff9940f9e4..7e854ebf1ba 100644 --- a/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap +++ b/webapp/channels/src/components/signup/__snapshots__/signup.test.tsx.snap @@ -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 -
- - Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter? - - - Sign up at - - https://mattermost.com/security-updates/ - - . - -
+ + Acceptable Use Policy +
, + " and the ", + + Privacy Policy + , + ] + } + /> -

- -

@@ -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 -
- - Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter? - - - Sign up at - - https://mattermost.com/security-updates/ - - . - -
+ + Acceptable Use Policy +
, + " and the ", + + Privacy Policy + , + ] + } + /> -

- -

diff --git a/webapp/channels/src/components/signup/signup.scss b/webapp/channels/src/components/signup/signup.scss index 915e2ca60f8..24589406710 100644 --- a/webapp/channels/src/components/signup/signup.scss +++ b/webapp/channels/src/components/signup/signup.scss @@ -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; - } - } } } diff --git a/webapp/channels/src/components/signup/signup.test.tsx b/webapp/channels/src/components/signup/signup.test.tsx index 0e4c3ac653c..e33aeb10674 100644 --- a/webapp/channels/src/components/signup/signup.test.tsx +++ b/webapp/channels/src/components/signup/signup.test.tsx @@ -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; 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((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( - - - - - , + renderWithContext( + , ); - 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( - - - - - , + renderWithContext( + , ); - 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(, 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(, 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(, 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( , ); - 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( - , - ); - - 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( - , - ); + 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(, 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( , ); - 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(, mockState); - const {container: signupContainer} = renderWithContext( - , - ); + 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(); }); }); diff --git a/webapp/channels/src/components/signup/signup.tsx b/webapp/channels/src/components/signup/signup.tsx index f37be9bce03..545316cafec 100644 --- a/webapp/channels/src/components/signup/signup.tsx +++ b/webapp/channels/src/components/signup/signup.tsx @@ -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(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 ( - setSubscribeToSecurityNewsletter(!subscribeToSecurityNewsletter)} - text={ - formatMessage( - {id: 'newsletter_optin.checkmark.text', defaultMessage: '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'}, - { - a: (chunks: React.ReactNode | React.ReactNodeArray) => ( - - {chunks} - - ), - aa: (chunks: React.ReactNode | React.ReactNodeArray) => ( - - {chunks} - - ), - span: (chunks: React.ReactNode | React.ReactNodeArray) => ( - {chunks} - ), - }, - )} - checked={subscribeToSecurityNewsletter} - /> - ); - } - return ( -
- - {formatMessage({id: 'newsletter_optin.title', defaultMessage: 'Interested in receiving Mattermost security, product, promotions, and company updates updates via newsletter?'})} - - - {formatMessage( - {id: 'newsletter_optin.desc', defaultMessage: 'Sign up at {link}.'}, - { - link: HostedCustomerLinks.SECURITY_UPDATES, - a: (chunks: React.ReactNode | React.ReactNodeArray) => ( - - {chunks} - - ), - }, - )} - -
- ); - }; - const getContent = () => { if (!enableSignUpWithEmail && !enableExternalSignup) { return ( @@ -780,7 +703,6 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { {enableSignUpWithEmail && (
{ customMessage={emailCustomLabelForInput} /> { } /> { info={passwordInfo as string} error={passwordError} /> - {getNewsletterCheck()} + setAcceptedTerms(!acceptedTerms)} + text={ + formatMessage( + {id: 'signup.terms_and_privacy.checkmark.text', defaultMessage: 'I agree to the Acceptable Use Policy and the Privacy Policy'}, + { + privacyPolicyLink: (chunks: React.ReactNode | React.ReactNodeArray) => ( + + {chunks} + + ), + termsOfUseLink: (chunks: React.ReactNode | React.ReactNodeArray) => ( + + {chunks} + + ), + }, + )} + checked={acceptedTerms} + /> { ))} )} - {enableSignUpWithEmail && !serverError && ( -

- ( - - {chunks} - - ), - privacyPolicyLink: (chunks) => ( - - {chunks} - - ), - }} - /> -

- )} diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index c55621e1925..e5adc09ff09 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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": "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", - "newsletter_optin.desc": "Sign up at {link}.", - "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 Terms of Use and Privacy Policy. 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 Acceptable Use Policy and the Privacy Policy", "signup.title": "Create Account | {siteName}", "single_image_view.copied_link_tooltip": "Copied", "single_image_view.copy_link_tooltip": "Copy link", diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index a88255396b9..caeb26768a6 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -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( - `${this.getHostedCustomerRoute()}/subscribe-newsletter`, - {method: 'post', body: JSON.stringify(newletterRequestBody)}, - ); - }; - cwsAvailabilityCheck = () => { return this.doFetchWithResponse( `${this.getCloudRoute()}/check-cws-connection`, diff --git a/webapp/platform/types/src/cloud.ts b/webapp/platform/types/src/cloud.ts index 71fe1dca577..ab09ec46981 100644 --- a/webapp/platform/types/src/cloud.ts +++ b/webapp/platform/types/src/cloud.ts @@ -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;