diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/cloud_pricing_modal_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/cloud_pricing_modal_spec.ts index 70b026d72d..0134ee1b68 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/cloud_pricing_modal_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/cloud_pricing_modal_spec.ts @@ -334,7 +334,7 @@ describe('Pricing modal', () => { // * Check that enterprise card action button shows Try free for 30 days cy.get('#pricingModal').should('be.visible'); - cy.get('#start_cloud_trial_btn').contains('Try free for 30 days'); + cy.get('#start_cloud_trial_btn').contains('Start trial'); }); it('should open pricing modal when Upgrade button clicked while in enterprise trial sku', () => { @@ -365,7 +365,7 @@ describe('Pricing modal', () => { // * Check that enterprise card action button is disabled cy.get('#enterprise > .bottom > .bottom_container').should('be.visible'); - cy.get('#start_cloud_trial_btn').contains('Try free for 30 days'); + cy.get('#start_cloud_trial_btn').contains('Start trial'); cy.get('#enterprise > .bottom > .bottom_container').should('be.visible'); cy.get('#start_cloud_trial_btn').should('be.disabled'); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts index 37196627a9..16ab7c3d6e 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts @@ -102,7 +102,7 @@ describe('Self hosted Pricing modal', () => { // * Check that enteprise trial button is available cy.get('#pricingModal').should('be.visible'); cy.get('#enterprise').should('be.visible'); - cy.get('#start_trial_btn').should('not.be.disabled').contains('Try free for 30 days'); + cy.get('#start_trial_btn').should('not.be.disabled').contains('Start trial'); }); it('Upgrade button should open pricing modal admin users when the server has requested a trial before on free plan', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/feature_discovery_cloud_spec.js b/e2e-tests/cypress/tests/integration/channels/system_console/feature_discovery_cloud_spec.js index eba774b224..72aae691a0 100644 --- a/e2e-tests/cypress/tests/integration/channels/system_console/feature_discovery_cloud_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/system_console/feature_discovery_cloud_spec.js @@ -53,7 +53,7 @@ describe('Feature discovery cloud', () => { const testForTrialButton = () => { cy.get('#start_cloud_trial_btn').should('exist'); - cy.get('#start_cloud_trial_btn').contains('Try free for 30 days'); + cy.get('#start_cloud_trial_btn').contains('Start trial'); }; const testForUpgradeToProfessionalOption = () => { diff --git a/model/client4.go b/model/client4.go index 853cfdd080..d6cc62ba0f 100644 --- a/model/client4.go +++ b/model/client4.go @@ -7812,7 +7812,23 @@ func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezon return ch, BuildResponse(r), nil } +func (c *Client4) RequestTrialLicenseWithExtraFields(trialRequest *TrialLicenseRequest) (*Response, error) { + b, err := json.Marshal(trialRequest) + if err != nil { + return nil, NewAppError("RequestTrialLicense", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + r, err := c.DoAPIPost("/trial-license", string(b)) + if err != nil { + return BuildResponse(r), err + } + + defer closeBody(r) + return BuildResponse(r), nil +} + // RequestTrialLicense will request a trial license and install it in the server +// DEPRECATED - USE RequestTrialLicenseWithExtraFields (this method remains for backwards compatibility) func (c *Client4) RequestTrialLicense(users int) (*Response, error) { b, err := json.Marshal(map[string]any{"users": users, "terms_accepted": true}) if err != nil { diff --git a/model/license.go b/model/license.go index cf5421bf02..7fa211e4e8 100644 --- a/model/license.go +++ b/model/license.go @@ -84,6 +84,48 @@ type TrialLicenseRequest struct { Users int `json:"users"` TermsAccepted bool `json:"terms_accepted"` ReceiveEmailsAccepted bool `json:"receive_emails_accepted"` + ContactName string `json:"contact_name"` + ContactEmail string `json:"contact_email"` + CompanyName string `json:"company_name"` + CompanyCountry string `json:"company_country"` + CompanySize string `json:"company_size"` +} + +// If any of the below fields are set, this is not a legacy request, and all fields should be validated +func (tlr *TrialLicenseRequest) IsLegacy() bool { + return tlr.CompanyCountry == "" && tlr.CompanyName == "" && tlr.CompanySize == "" && tlr.ContactName == "" +} + +func (tlr *TrialLicenseRequest) IsValid() bool { + if !tlr.TermsAccepted { + return false + } + + if tlr.Email == "" { + return false + } + + if tlr.Users <= 0 { + return false + } + + if tlr.CompanyCountry == "" { + return false + } + + if tlr.CompanyName == "" { + return false + } + + if tlr.CompanySize == "" { + return false + } + + if tlr.ContactName == "" { + return false + } + + return true } type Features struct { diff --git a/model/license_test.go b/model/license_test.go index 1d1a5f1acf..5b413e00a3 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -200,6 +200,100 @@ func TestLicenseRecordPreSave(t *testing.T) { assert.NotZero(t, lr.CreateAt) } +func TestIsLegacyTrialRequest(t *testing.T) { + legacyTr := &TrialLicenseRequest{ + Email: "test@mattermost.com", + TermsAccepted: true, + SiteURL: "https://mattermost.com", + SiteName: "Mattermost", + Users: 100, + } + t.Run("legacy trial request", func(t *testing.T) { + assert.True(t, legacyTr.IsLegacy()) + }) + + t.Run("legacy trial request with any non-legacy field set is not a legacy request", func(t *testing.T) { + legacyTr.CompanyCountry = "US" + assert.False(t, legacyTr.IsLegacy()) + legacyTr.CompanyCountry = "" + legacyTr.CompanyName = "test company" + assert.False(t, legacyTr.IsLegacy()) + legacyTr.CompanyName = "" + legacyTr.CompanySize = "50-100" + assert.False(t, legacyTr.IsLegacy()) + legacyTr.CompanySize = "" + legacyTr.ContactName = "test user" + assert.False(t, legacyTr.IsLegacy()) + legacyTr.ContactName = "" + assert.True(t, legacyTr.IsLegacy()) + }) + +} + +func TestTrialLicenseRequestIsValid(t *testing.T) { + validTlr := &TrialLicenseRequest{ + Email: "test@test.com", + Users: 100, + CompanyCountry: "US", + CompanyName: "Test Company", + CompanySize: "50-100", + ContactName: "Test User", + TermsAccepted: true, + } + + resetBaseRequest := func() { + validTlr = &TrialLicenseRequest{ + Email: "test@test.com", + Users: 100, + CompanyCountry: "US", + CompanyName: "Test Company", + CompanySize: "50-100", + ContactName: "Test User", + TermsAccepted: true, + } + } + t.Run("valid request", func(t *testing.T) { + resetBaseRequest() + assert.Equal(t, true, validTlr.IsValid()) + }) + + t.Run("no terms", func(t *testing.T) { + resetBaseRequest() + validTlr.TermsAccepted = false + assert.Equal(t, false, validTlr.IsValid()) + }) + + t.Run("no email", func(t *testing.T) { + resetBaseRequest() + validTlr.Email = "" + assert.Equal(t, false, validTlr.IsValid()) + }) + + t.Run("no CompanyCountry", func(t *testing.T) { + resetBaseRequest() + validTlr.CompanyCountry = "" + assert.Equal(t, false, validTlr.IsValid()) + }) + + t.Run("no CompanyName", func(t *testing.T) { + resetBaseRequest() + validTlr.CompanyName = "" + assert.Equal(t, false, validTlr.IsValid()) + }) + + t.Run("no CompanySize", func(t *testing.T) { + resetBaseRequest() + validTlr.CompanySize = "" + assert.Equal(t, false, validTlr.IsValid()) + }) + + t.Run("Bad User Count", func(t *testing.T) { + resetBaseRequest() + validTlr.Users = 0 + assert.Equal(t, false, validTlr.IsValid()) + }) +} + func TestLicense_IsTrialLicense(t *testing.T) { t.Run("detect trial license directly from the flag", func(t *testing.T) { license := &License{ diff --git a/server/channels/api4/cloud.go b/server/channels/api4/cloud.go index 3769403950..48a8ddf83f 100644 --- a/server/channels/api4/cloud.go +++ b/server/channels/api4/cloud.go @@ -246,16 +246,6 @@ func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.Channels().License().IsCloud() { - c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden) - return - } - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) { - c.SetPermissionError(model.PermissionSysconsoleWriteBilling) - return - } - user, appErr := c.App.GetUser(c.AppContext.Session().UserId) if appErr != nil { c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr) diff --git a/server/channels/api4/cloud_test.go b/server/channels/api4/cloud_test.go index 1114ef4ba4..6b2067ec15 100644 --- a/server/channels/api4/cloud_test.go +++ b/server/channels/api4/cloud_test.go @@ -328,30 +328,6 @@ func Test_requestTrial(t *testing.T) { } func Test_validateBusinessEmail(t *testing.T) { - t.Run("Returns forbidden for non admin executors", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) - - invalidEmail := model.ValidateBusinessEmailRequest{Email: "invalid@gmail.com"} - - th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - - cloud := mocks.CloudInterface{} - - cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, invalidEmail.Email).Return(errors.New("invalid email")) - - cloudImpl := th.App.Srv().Cloud - defer func() { - th.App.Srv().Cloud = cloudImpl - }() - th.App.Srv().Cloud = &cloud - - res, err := th.Client.ValidateBusinessEmail(&invalidEmail) - require.Error(t, err) - require.Equal(t, http.StatusForbidden, res.StatusCode, "403") - }) t.Run("Returns forbidden for invalid business email", func(t *testing.T) { th := Setup(t).InitBasic() diff --git a/server/channels/api4/license.go b/server/channels/api4/license.go index 456f5b9fcb..9911c241e1 100644 --- a/server/channels/api4/license.go +++ b/server/channels/api4/license.go @@ -210,11 +210,7 @@ func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) { return } - var trialRequest struct { - Users int `json:"users"` - TermsAccepted bool `json:"terms_accepted"` - ReceiveEmailsAccepted bool `json:"receive_emails_accepted"` - } + var trialRequest *model.TrialLicenseRequest b, readErr := io.ReadAll(r.Body) if readErr != nil { @@ -223,8 +219,16 @@ func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) { } json.Unmarshal(b, &trialRequest) - if err := c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted); err != nil { - c.Err = err + var appErr *model.AppError + // If any of the newly supported trial request fields are set (ie, not a legacy request), process this as a new trial request (requiring the new fields) otherwise fall back on the old method. + if !trialRequest.IsLegacy() { + appErr = c.App.Channels().RequestTrialLicenseWithExtraFields(c.AppContext.Session().UserId, trialRequest) + } else { + appErr = c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted) + } + + if appErr != nil { + c.Err = appErr return } diff --git a/server/channels/api4/license_test.go b/server/channels/api4/license_test.go index 33f54eeecc..7762e3fc0f 100644 --- a/server/channels/api4/license_test.go +++ b/server/channels/api4/license_test.go @@ -223,6 +223,154 @@ func TestRemoveLicenseFile(t *testing.T) { }) } +func TestRequestTrialLicenseWithExtraFields(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + licenseManagerMock := &mocks.LicenseInterface{} + licenseManagerMock.On("CanStartTrial").Return(true, nil) + th.App.Srv().Platform().SetLicenseManager(licenseManagerMock) + cloud := mocks.CloudInterface{} + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065/" }) + nUsers := 1 + validTrialRequest := &model.TrialLicenseRequest{ + Email: "test@mattermost.com", + Users: nUsers, + TermsAccepted: true, + CompanyCountry: "US", + CompanyName: "mattermost", + CompanySize: "1-10", + ContactName: "Matter Most", + } + + t.Run("permission denied", func(t *testing.T) { + resp, err := th.Client.RequestTrialLicenseWithExtraFields(&model.TrialLicenseRequest{}) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) + + t.Run("trial license user count less than current users", func(t *testing.T) { + license := model.NewTestLicense() + license.Features.Users = model.NewInt(nUsers) + licenseJSON, jsonErr := json.Marshal(license) + require.NoError(t, jsonErr) + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + response := map[string]string{ + "license": string(licenseJSON), + } + err := json.NewEncoder(res).Encode(response) + require.NoError(t, err) + })) + defer testServer.Close() + + mockLicenseValidator := mocks2.LicenseValidatorIface{} + defer testutils.ResetLicenseValidator() + + mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON)) + utils.LicenseValidator = &mockLicenseValidator + licenseManagerMock := &mocks.LicenseInterface{} + licenseManagerMock.On("CanStartTrial").Return(true, nil).Once() + th.App.Srv().Platform().SetLicenseManager(licenseManagerMock) + originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL }) + defer func(requestTrialURL string) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL }) + }(originalCwsUrl) + + cloud.On("ValidateBusinessEmail", mock.Anything, mock.Anything).Return(nil) + + resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest) + CheckErrorID(t, err, "api.license.add_license.unique_users.app_error") + CheckBadRequestStatus(t, resp) + }) + + t.Run("returns status 451 when it receives status 451", func(t *testing.T) { + + license := model.NewTestLicense() + license.Features.Users = model.NewInt(nUsers) + licenseJSON, jsonErr := json.Marshal(license) + require.NoError(t, jsonErr) + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusUnavailableForLegalReasons) + })) + defer testServer.Close() + + mockLicenseValidator := mocks2.LicenseValidatorIface{} + defer testutils.ResetLicenseValidator() + + mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON)) + utils.LicenseValidator = &mockLicenseValidator + licenseManagerMock := &mocks.LicenseInterface{} + licenseManagerMock.On("CanStartTrial").Return(true, nil).Once() + th.App.Srv().Platform().SetLicenseManager(licenseManagerMock) + + originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL }) + defer func(requestTrialURL string) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL }) + }(originalCwsUrl) + + resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest) + require.Error(t, err) + require.Equal(t, resp.StatusCode, 451) + }) + + t.Run("returns status 400 if request is a mix of legacy and new fields", func(t *testing.T) { + validTrialRequest.CompanyCountry = "" + validTrialRequest.Users = 100 + defer func() { validTrialRequest.CompanyCountry = "US" }() + license := model.NewTestLicense() + license.Features.Users = model.NewInt(nUsers) + licenseJSON, jsonErr := json.Marshal(license) + require.NoError(t, jsonErr) + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + response := map[string]string{ + "license": string(licenseJSON), + } + err := json.NewEncoder(res).Encode(response) + require.NoError(t, err) + })) + defer testServer.Close() + + mockLicenseValidator := mocks2.LicenseValidatorIface{} + defer testutils.ResetLicenseValidator() + + mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON)) + utils.LicenseValidator = &mockLicenseValidator + licenseManagerMock := &mocks.LicenseInterface{} + licenseManagerMock.On("CanStartTrial").Return(true, nil).Once() + th.App.Srv().Platform().SetLicenseManager(licenseManagerMock) + originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL }) + defer func(requestTrialURL string) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL }) + }(originalCwsUrl) + + cloud.On("ValidateBusinessEmail", mock.Anything, mock.Anything).Return(nil) + + resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest) + CheckErrorID(t, err, "api.license.request-trial.bad-request") + CheckBadRequestStatus(t, resp) + }) + + th.App.Srv().Platform().SetLicenseManager(nil) + t.Run("trial license should fail if LicenseManager is nil", func(t *testing.T) { + resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest) + CheckErrorID(t, err, "api.license.upgrade_needed.app_error") + CheckForbiddenStatus(t, resp) + }) + +} + func TestRequestTrialLicense(t *testing.T) { th := Setup(t) defer th.TearDown() diff --git a/server/channels/app/channels.go b/server/channels/app/channels.go index c0241c9689..f2ae0e6d11 100644 --- a/server/channels/app/channels.go +++ b/server/channels/app/channels.go @@ -30,6 +30,7 @@ const ServerKey product.ServiceKey = "server" type licenseSvc interface { GetLicense() *model.License RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError + RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError } // Channels contains all channels related state. @@ -323,6 +324,10 @@ func (ch *Channels) License() *model.License { return ch.licenseSvc.GetLicense() } +func (ch *Channels) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError { + return ch.licenseSvc.RequestTrialLicenseWithExtraFields(requesterID, trialRequest) +} + func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError { return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted) diff --git a/server/channels/app/license.go b/server/channels/app/license.go index 252c232ad3..4b05cf1cf1 100644 --- a/server/channels/app/license.go +++ b/server/channels/app/license.go @@ -36,6 +36,51 @@ func (w *licenseWrapper) GetLicense() *model.License { return w.srv.License() } +func (w *licenseWrapper) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError { + if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin { + return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden) + } + + requester, err := w.srv.userService.GetUser(requesterID) + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return model.NewAppError("RequestTrialLicense", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err) + default: + return model.NewAppError("RequestTrialLicense", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + } + + if w.srv.Cloud.ValidateBusinessEmail(requesterID, trialRequest.ContactEmail) != nil { + return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request.business-email", nil, "", http.StatusBadRequest) + } + + // Create a new struct only using the fields from the request that are allowed to be set by the client + sanitizedRequest := &model.TrialLicenseRequest{ + ServerID: w.srv.TelemetryId(), + Name: requester.GetDisplayName(model.ShowFullName), + Email: requester.Email, + SiteName: *w.srv.platform.Config().TeamSettings.SiteName, + SiteURL: *w.srv.platform.Config().ServiceSettings.SiteURL, + Users: trialRequest.Users, + TermsAccepted: trialRequest.TermsAccepted, + ReceiveEmailsAccepted: trialRequest.ReceiveEmailsAccepted, + ContactName: trialRequest.ContactName, + ContactEmail: trialRequest.ContactEmail, + CompanyName: trialRequest.CompanyName, + CompanySize: trialRequest.CompanySize, + CompanyCountry: trialRequest.CompanyCountry, + } + + if !sanitizedRequest.IsValid() { + return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest) + } + + return w.srv.platform.RequestTrialLicense(sanitizedRequest) +} + +// DEPRECATED - use RequestTrialLicenseWithExtraFields instead. This function remains to support the Plugin API. func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError { if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin { return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden) diff --git a/server/i18n/en.json b/server/i18n/en.json index 492fee9d58..0994b13552 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2049,6 +2049,10 @@ "id": "api.license.request-trial.bad-request", "translation": "The number of users requested is not correct." }, + { + "id": "api.license.request-trial.bad-request.business-email", + "translation": "Invalid business email for trial" + }, { "id": "api.license.request-trial.bad-request.terms-not-accepted", "translation": "You must accept the Mattermost Software Evaluation Agreement and Privacy Policy to request a license." diff --git a/webapp/channels/src/actions/admin_actions.jsx b/webapp/channels/src/actions/admin_actions.jsx index 2d97882800..fc60696af9 100644 --- a/webapp/channels/src/actions/admin_actions.jsx +++ b/webapp/channels/src/actions/admin_actions.jsx @@ -451,12 +451,12 @@ export function ping() { }; } -export function requestTrialLicense(users, termsAccepted, receiveEmailsAccepted, page) { +export function requestTrialLicense(requestLicenseBody, page) { return async () => { try { trackEvent('api', 'api_request_trial_license', {from_page: page}); - const response = await Client4.requestTrialLicense({users, terms_accepted: termsAccepted, receive_emails_accepted: receiveEmailsAccepted}); + const response = await Client4.requestTrialLicense(requestLicenseBody); return {data: response}; } catch (e) { // In the event that the status code returned is 451, this request has been blocked because it originated from an embargoed country_dropdown diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.scss b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.scss index 2e5b6800f3..413d71dcdd 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.scss +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.scss @@ -3,3 +3,77 @@ transform: translateY(50%); } } + +.ResultModal__small { + .modal-header { + .close { + &:hover, + &:active, + &:focus, + &:active:focus { + background-color: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.72); + opacity: 1; + } + + top: 26px; + right: 26px; + width: 24px; + height: 24px; + border-radius: 4px; + color: rgba(var(--center-channel-color-rgb), 0.56) !important; + font-family: + 'Open Sans', + sans-serif; + font-size: 32px; + font-weight: 400; + } + } + + .modal-dialog { + position: absolute; + top: 50%; + left: 50%; + width: 600px; + height: 360px; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + margin: auto; + border-radius: 8px; + transform: translate(-50%, -50%) !important; + + .modal-header { + border: none; + background: var(--center-channel-bg) !important; + } + + .modal-content { + height: 100%; + border-radius: 8px; + + .IconMessage__svg-wrapper { + margin-left: 0; + } + + .IconMessage-h3 { + margin-top: 0; + } + + .IconMessage-sub { + max-width: 504px; + margin-top: 0; + } + + .IconMessage-buttons { + justify-content: right; + padding-right: 24px; + margin-top: 60px; + + .btn { + font-family: 'Open sans', sans-serif; + font-size: 12px; + font-weight: 600; + } + } + } + } +} diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx index bc452d3c57..6e9948756a 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/result_modal.tsx @@ -12,10 +12,12 @@ import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZen import {closeModal} from 'actions/views/modals'; import {isModalOpen} from 'selectors/views/modals'; import {GlobalState} from 'types/store'; +import {Modal} from 'react-bootstrap'; import './result_modal.scss'; type Props = { + type?: string; onHide?: () => void; icon: JSX.Element; title: JSX.Element; @@ -28,47 +30,77 @@ type Props = { ignoreExit: boolean; }; -export default function ResultModal(props: Props) { +export default function ResultModal({type, icon, title, subtitle, primaryButtonText, primaryButtonHandler, identifier, contactSupportButtonVisible, resultType, ignoreExit, onHide}: Props) { const dispatch = useDispatch(); const [openContactSupport] = useOpenCloudZendeskSupportForm('Delete workspace', ''); const isResultModalOpen = useSelector((state: GlobalState) => - isModalOpen(state, props.identifier), + isModalOpen(state, identifier), ); - const onHide = () => { - dispatch(closeModal(props.identifier)); - if (typeof props.onHide === 'function') { - props.onHide(); - } + const handleHide = () => { + dispatch(closeModal(identifier)); + onHide?.(); }; - const modalType = `delete-workspace-result_modal__${props.resultType}`; + const modalType = `delete-workspace-result_modal__${resultType}`; + if (type === 'small') { + return ( + + +
+ : + undefined + } + tertiaryButtonHandler={contactSupportButtonVisible ? openContactSupport : undefined} + /> +
+
+ ); + } return (
) : undefined } - tertiaryButtonHandler={props.contactSupportButtonVisible ? openContactSupport : undefined} + tertiaryButtonHandler={contactSupportButtonVisible ? openContactSupport : undefined} />
diff --git a/webapp/channels/src/components/admin_console/license_settings/__snapshots__/license_settings.test.tsx.snap b/webapp/channels/src/components/admin_console/license_settings/__snapshots__/license_settings.test.tsx.snap index f5bb34aea6..393b44eff3 100644 --- a/webapp/channels/src/components/admin_console/license_settings/__snapshots__/license_settings.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/license_settings/__snapshots__/license_settings.test.tsx.snap @@ -208,117 +208,6 @@ exports[`components/admin_console/license_settings/LicenseSettings load screen w `; -exports[`components/admin_console/license_settings/LicenseSettings should match snapshot after starting trial and removing license 1`] = ` -
- -
-
-
- -
-
-
-
- - - Current Plan -
- } - fileInputRef={ - Object { - "current": null, - } - } - handleChange={[Function]} - openEELicenseModal={[Function]} - upgradedFromTE={false} - /> -
-
- See also - - Enterprise Edition Terms of Use - - and - - Privacy Policy - -
-
-
-
- -
-
- Curious about upgrading? - - Compare Plans - -
-
-
-
-
- -`; - exports[`components/admin_console/license_settings/LicenseSettings should match snapshot enterprise build with E10 license 1`] = `
- - - { expect(wrapper).toMatchSnapshot(); }); - test('should match snapshot after starting trial and removing license', async () => { - const actions = { - ...defaultProps.actions, - getLicenseConfig: jest.fn(), - upgradeToE0: jest.fn(), - upgradeToE0Status: jest.fn().mockImplementation(() => Promise.resolve({percentage: 0, error: null})), - }; - const props = {...defaultProps, license: {IsLicensed: 'false'}, prevTrialLicense: {IsLicensed: 'false'}, actions}; - - const wrapper = shallow(); - - const instance = wrapper.instance(); - - // First start trial - actions.requestTrialLicense = jest.fn().mockImplementation(() => Promise.resolve({percentage: 1, error: null})); - actions.getLicenseConfig = jest.fn().mockImplementation(() => Promise.resolve({})); - await instance.requestLicense({preventDefault: jest.fn()} as unknown as React.MouseEvent); - expect(wrapper.state('gettingTrial')).toBe(false); - - // Then remove license - actions.removeLicense = jest.fn().mockImplementation(() => Promise.resolve({percentage: 1, error: null})); - actions.getPrevTrialLicense = jest.fn().mockImplementation(() => Promise.resolve({})); - await instance.handleRemove({preventDefault: jest.fn()} as unknown as React.MouseEvent); - expect(wrapper.state('removing')).toBe(false); - - expect(wrapper).toMatchSnapshot(); - }); - test('should match snapshot enterprise build with E20 license', () => { const props = {...defaultProps, license: {...defaultProps.license, SkuShortName: LicenseSkus.E20}}; const wrapper = shallow(); diff --git a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx index 2d55c9fc76..298c80ea5b 100644 --- a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx @@ -202,23 +202,6 @@ export default class LicenseSettings extends React.PureComponent { } } - requestLicense = async (e?: React.MouseEvent) => { - if (e) { - e.preventDefault(); - } - if (this.state.gettingTrial) { - return; - } - this.setState({gettingTrial: true, gettingTrialError: null}); - const requestedUsers = Math.max(this.props.totalUsers, 30) || 30; - const {error, data} = await this.props.actions.requestTrialLicense(requestedUsers, true, true, 'license'); - if (error) { - this.setState({gettingTrialError: error}); - } - this.setState({gettingTrial: false, gettingTrialResponseCode: data?.status}); - await this.props.actions.getLicenseConfig(); - } - checkRestarted = () => { this.props.actions.ping().then(() => { window.location.reload(); @@ -363,7 +346,6 @@ export default class LicenseSettings extends React.PureComponent { isDisabled={isDisabled} gettingTrialResponseCode={this.state.gettingTrialResponseCode} gettingTrialError={this.state.gettingTrialError} - requestLicense={this.requestLicense} gettingTrial={this.state.gettingTrial} enterpriseReady={this.props.enterpriseReady} upgradingPercentage={this.state.upgradingPercentage} diff --git a/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx b/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx index 3db6a6e0e2..ba97c6a2f0 100644 --- a/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx @@ -13,6 +13,8 @@ import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import AlertBanner from 'components/alert_banner'; import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; import FormattedMarkdownMessage from 'components/formatted_markdown_message'; +import withOpenStartTrialFormModal from 'components/common/hocs/cloud/with_open_start_trial_form_modal'; +import {TelemetryProps} from 'components/common/hooks/useOpenPricingModal'; import {format} from 'utils/markdown'; @@ -26,13 +28,13 @@ interface TrialBannerProps { isDisabled: boolean; gettingTrialError: string | null; gettingTrialResponseCode: number | null; - requestLicense: (e?: React.MouseEvent, reload?: boolean) => Promise; gettingTrial: boolean; enterpriseReady: boolean; upgradingPercentage: number; handleUpgrade: () => Promise; upgradeError: string | null; restartError: string | null; + openTrialForm?: (telemetryProps?: TelemetryProps) => void; handleRestart: () => Promise; @@ -72,7 +74,6 @@ const TrialBanner = ({ isDisabled, gettingTrialError, gettingTrialResponseCode, - requestLicense, gettingTrial, enterpriseReady, upgradingPercentage, @@ -82,6 +83,7 @@ const TrialBanner = ({ handleRestart, restarting, openEEModal, + openTrialForm, }: TrialBannerProps) => { let trialButton; let upgradeTermsMessage; @@ -119,6 +121,12 @@ const TrialBanner = ({ } }; + const handleRequestLicense = () => { + if (openTrialForm) { + openTrialForm({trackingLocation: 'license_settings.trial_banner'}); + } + }; + useEffect(() => { async function savePrefsAndRequestTrial() { await savePrefsRestartedAfterUpgrade(); @@ -150,7 +158,7 @@ const TrialBanner = ({ const clickedBtn = Unique.CLICKED_UPGRADE_AND_TRIAL_BTN; dispatch(savePreferences(userId, [{category, name: reqLicense, user_id: userId, value: ''}, {category, name: clickedBtn, user_id: userId, value: ''}])); - requestLicense(); + handleRequestLicense(); } }, [restartedAfterUpgradePrefs, clickedUpgradeAndTrialBtn]); @@ -213,7 +221,7 @@ const TrialBanner = ({ ) : ( - {btnText(status)} + {btnText} ); }; diff --git a/webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist_completed.tsx b/webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist_completed.tsx index 43134d1aac..a829b14bc1 100644 --- a/webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist_completed.tsx +++ b/webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist_completed.tsx @@ -199,7 +199,7 @@ const Completed = (props: Props): JSX.Element => { {isCloud ? ( +