diff --git a/server/channels/api4/cloud.go b/server/channels/api4/cloud.go index ca9710b108..7615d6b1b8 100644 --- a/server/channels/api4/cloud.go +++ b/server/channels/api4/cloud.go @@ -35,7 +35,6 @@ func (api *API) InitCloud() { api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET") - api.BaseRoutes.Cloud.Handle("/subscription/self-serve-status", api.APISessionRequired(getLicenseSelfServeStatus)).Methods("GET") // GET /api/v4/cloud/validate-business-email api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods("POST") @@ -378,40 +377,6 @@ func getInstallation(c *Context, w http.ResponseWriter, r *http.Request) { } } -// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc. -func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) { - ensured := ensureCloudInterface(c, "Api4.getLicenseSelfServeStatus") - if !ensured { - return - } - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) { - c.SetPermissionError(model.PermissionManageLicenseInformation) - return - } - - _, token, err := c.App.Srv().GenerateLicenseRenewalLink() - - if err != nil { - c.Err = err - return - } - - status, cloudErr := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token) - if cloudErr != nil { - c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr) - return - } - - json, jsonErr := json.Marshal(status) - if jsonErr != nil { - c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) - return - } - - w.Write(json) -} - func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { ensured := ensureCloudInterface(c, "Api4.updateCloudCustomer") if !ensured { diff --git a/server/channels/api4/license.go b/server/channels/api4/license.go index 29a0fca2cc..58b7a4091c 100644 --- a/server/channels/api4/license.go +++ b/server/channels/api4/license.go @@ -5,9 +5,7 @@ package api4 import ( "bytes" - b64 "encoding/base64" "encoding/json" - "fmt" "io" "net/http" @@ -23,10 +21,7 @@ func (api *API) InitLicense() { api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET") api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense, handlerParamFileAPI)).Methods("POST") api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE") - api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET") api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET") - api.BaseRoutes.APIRoot.Handle("/license/review", api.APISessionRequired(requestTrueUpReview)).Methods("POST") - api.BaseRoutes.APIRoot.Handle("/license/review/status", api.APISessionRequired(trueUpReviewStatus)).Methods("GET") } func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { @@ -238,54 +233,6 @@ func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } -func requestRenewalLink(c *Context, w http.ResponseWriter, r *http.Request) { - auditRec := c.MakeAuditRecord("requestRenewalLink", audit.Fail) - defer c.LogAuditRec(auditRec) - c.LogAudit("attempt") - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) { - c.SetPermissionError(model.PermissionManageLicenseInformation) - return - } - - if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin { - c.Err = model.NewAppError("requestRenewalLink", "api.restricted_system_admin", nil, "", http.StatusForbidden) - return - } - - renewalLink, token, err := c.App.Srv().GenerateLicenseRenewalLink() - if err != nil { - c.Err = err - return - } - - if c.App.Cloud() == nil { - c.Err = model.NewAppError("requestRenewalLink", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden) - return - } - - // check if it is possible to renew license on the portal with generated token - status, e := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token) - if e != nil { - c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, "", http.StatusInternalServerError).Wrap(e) - return - } - - if !status.IsRenewable { - c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, "License is not self-serve renewable", http.StatusBadRequest) - return - } - - auditRec.Success() - c.LogAudit("success") - - _, werr := w.Write([]byte(fmt.Sprintf(`{"renewal_link": "%s"}`, renewalLink))) - if werr != nil { - c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.app_error", nil, "", http.StatusForbidden).Wrap(werr) - return - } -} - func getPrevTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) { if c.App.Srv().Platform().LicenseManager() == nil { c.Err = model.NewAppError("getPrevTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden) @@ -308,102 +255,3 @@ func getPrevTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJSON(clientLicense))) } - -func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) { - // Only admins can request a true up review. - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { - c.SetPermissionError(model.PermissionManageLicenseInformation) - return - } - - license := c.App.Channels().License() - if license == nil { - c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "", http.StatusNotImplemented) - return - } - - if license.IsCloud() { - c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.not_allowed_for_cloud", nil, "", http.StatusNotImplemented) - return - } - - status, appErr := c.App.GetOrCreateTrueUpReviewStatus(c.AppContext) - if appErr != nil { - c.Err = appErr - return - } - - // If a true up review has already been submitted for the current due date, complete the request - // with no errors. - if status.Completed { - ReturnStatusOK(w) - } - - profileMap, err := c.App.GetTrueUpProfile() - if err != nil { - c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "", http.StatusInternalServerError) - return - } - - profileMapJson, err := json.Marshal(profileMap) - if err != nil { - c.SetJSONEncodingError(err) - return - } - - // Only report the true up review to CWS if the connection is available. - if err := c.App.Cloud().CheckCWSConnection(c.AppContext.Session().UserId); err == nil { - err = c.App.Cloud().SubmitTrueUpReview(c.AppContext.Session().UserId, profileMap) - if err != nil { - c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.failed_to_submit", nil, "", http.StatusInternalServerError).Wrap(err) - return - } - } - - // Update the review status to reflect the completion. - status.Completed = true - c.App.Srv().Store().TrueUpReview().Update(status) - - // Encode to string rather than byte[] otherwise json.Marshal will encode it further. - encodedData := b64.StdEncoding.EncodeToString(profileMapJson) - responseContent := struct { - Content string `json:"content"` - }{Content: encodedData} - response, _ := json.Marshal(responseContent) - - w.WriteHeader(http.StatusOK) - w.Write(response) -} - -func trueUpReviewStatus(c *Context, w http.ResponseWriter, r *http.Request) { - // Only admins can request a true up review. - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { - c.SetPermissionError(model.PermissionManageLicenseInformation) - return - } - - // Check for license - license := c.App.Channels().License() - if license == nil { - c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.license_required", nil, "True up review requires a license", http.StatusNotImplemented) - return - } - - if license.IsCloud() { - c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.not_allowed_for_cloud", nil, "True up review is not allowed for cloud instances", http.StatusNotImplemented) - return - } - - status, appErr := c.App.GetOrCreateTrueUpReviewStatus(c.AppContext) - if appErr != nil { - c.Err = appErr - } - - json, err := json.Marshal(status) - if err != nil { - c.Err = model.NewAppError("trueUpReviewStatus", "api.marshal_error", nil, "", http.StatusInternalServerError) - return - } - - w.Write(json) -} diff --git a/server/channels/api4/license_test.go b/server/channels/api4/license_test.go index 884b3fbe7c..d3270974b8 100644 --- a/server/channels/api4/license_test.go +++ b/server/channels/api4/license_test.go @@ -459,116 +459,3 @@ func TestRequestTrialLicense(t *testing.T) { CheckForbiddenStatus(t, resp) }) } - -func TestRequestRenewalLink(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - require.NotPanics(t, func() { - cloudImpl := th.App.Srv().Cloud - defer func() { - th.App.Srv().Cloud = cloudImpl - }() - th.App.Srv().Cloud = nil - resp, err := th.SystemAdminClient.DoAPIGet(context.Background(), "/license/renewal", "") - CheckErrorID(t, err, "app.license.generate_renewal_token.no_license") - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - }) -} - -func TestRequestTrueUpReview(t *testing.T) { - t.Run("returns status 200 when telemetry data sent", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - th.App.Srv().SetLicense(model.NewTestLicense()) - - th.Client.Login(context.Background(), th.SystemAdminUser.Email, th.SystemAdminUser.Password) - - cloud := mocks.CloudInterface{} - cloud.Mock.On("SubmitTrueUpReview", mock.Anything, mock.Anything).Return(nil) - cloud.Mock.On("CheckCWSConnection", mock.Anything).Return(nil) - - cloudImpl := th.App.Srv().Cloud - defer func() { - th.App.Srv().Cloud = cloudImpl - }() - th.App.Srv().Cloud = &cloud - - var reviewProfile map[string]any - resp, err := th.Client.SubmitTrueUpReview(context.Background(), reviewProfile) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("returns 501 when ran by cloud user", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - th.App.Srv().SetLicense(model.NewTestLicense()) - - th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - - resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/license/review", "") - require.Error(t, err) - require.Equal(t, http.StatusNotImplemented, resp.StatusCode) - - th.App.Srv().SetLicense(model.NewTestLicense()) - }) - - t.Run("returns 403 when user does not have permissions", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - th.App.Srv().SetLicense(model.NewTestLicense()) - - resp, err := th.Client.DoAPIPost(context.Background(), "/license/review", "") - require.Error(t, err) - require.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - t.Run("returns 400 when license is nil", func(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - th.App.Srv().SetLicense(nil) - - resp, err := th.SystemAdminClient.DoAPIPost(context.Background(), "/license/review", "") - require.Error(t, err) - require.Equal(t, http.StatusNotImplemented, resp.StatusCode) - }) -} - -func TestTrueUpReviewStatus(t *testing.T) { - th := Setup(t) - - defer th.TearDown() - th.App.Srv().SetLicense(model.NewTestLicense()) - - t.Run("returns 200 when status retrieved", func(t *testing.T) { - resp, err := th.SystemAdminClient.DoAPIGet(context.Background(), "/license/review/status", "") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("returns 501 when ran by cloud user", func(t *testing.T) { - th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - - resp, err := th.SystemAdminClient.DoAPIGet(context.Background(), "/license/review/status", "") - require.Error(t, err) - require.Equal(t, http.StatusNotImplemented, resp.StatusCode) - - th.App.Srv().SetLicense(model.NewTestLicense()) - }) - - t.Run("returns 403 when user does not have permissions", func(t *testing.T) { - resp, err := th.Client.DoAPIGet(context.Background(), "/license/review/status", "") - require.Error(t, err) - require.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - t.Run("returns 400 when license is nil", func(t *testing.T) { - th.App.Srv().SetLicense(nil) - - resp, err := th.SystemAdminClient.DoAPIGet(context.Background(), "/license/review/status", "") - require.Error(t, err) - require.Equal(t, http.StatusNotImplemented, resp.StatusCode) - }) -} diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 8dcb64b4a2..ecb2849387 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -747,7 +747,6 @@ type AppIface interface { GetOnboarding() (*model.System, *model.AppError) GetOpenGraphMetadata(requestURL string) ([]byte, error) GetOrCreateDirectChannel(c request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) - GetOrCreateTrueUpReviewStatus(rctx request.CTX) (*model.TrueUpReviewStatus, *model.AppError) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) GetOutgoingWebhooksForChannelPageByUser(channelID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) GetOutgoingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError) @@ -852,7 +851,6 @@ type AppIface interface { GetThreadMembershipsForUser(userID, teamID string) ([]*model.ThreadMembership, error) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) GetTokenById(token string) (*model.Token, *model.AppError) - GetTrueUpProfile() (map[string]any, error) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError) GetUser(userID string) (*model.User, *model.AppError) diff --git a/server/channels/app/license.go b/server/channels/app/license.go index 8c5a71d265..fdc1234756 100644 --- a/server/channels/app/license.go +++ b/server/channels/app/license.go @@ -156,13 +156,3 @@ func (s *Server) RemoveLicenseListener(id string) { func (s *Server) GetSanitizedClientLicense() map[string]string { return s.platform.GetSanitizedClientLicense() } - -// GenerateRenewalToken returns a renewal token that expires after duration expiration -func (s *Server) GenerateRenewalToken(expiration time.Duration) (string, *model.AppError) { - return s.platform.GenerateRenewalToken(expiration) -} - -// GenerateLicenseRenewalLink returns a link that points to the CWS where clients can renew license -func (s *Server) GenerateLicenseRenewalLink() (string, string, *model.AppError) { - return s.platform.GenerateLicenseRenewalLink() -} diff --git a/server/channels/app/license_test.go b/server/channels/app/license_test.go index 26b4b58b70..344a34f10a 100644 --- a/server/channels/app/license_test.go +++ b/server/channels/app/license_test.go @@ -73,24 +73,6 @@ func TestGetSanitizedClientLicense(t *testing.T) { assert.False(t, ok) } -func TestGenerateRenewalToken(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - t.Run("renewal token generated correctly", func(t *testing.T) { - setLicense(th, nil) - token, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration) - require.Nil(t, appErr) - require.NotEmpty(t, token) - }) - - t.Run("return error if there is no active license", func(t *testing.T) { - th.App.Srv().SetLicense(nil) - _, appErr := th.App.Srv().GenerateRenewalToken(JWTDefaultTokenExpiration) - require.NotNil(t, appErr) - }) -} - func setLicense(th *TestHelper, customer *model.Customer) { l1 := &model.License{} l1.Features = &model.Features{} diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index 6499b65ae3..5e73aee526 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -8005,28 +8005,6 @@ func (a *OpenTracingAppLayer) GetOrCreateDirectChannel(c request.CTX, userID str return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetOrCreateTrueUpReviewStatus(rctx request.CTX) (*model.TrueUpReviewStatus, *model.AppError) { - origCtx := a.ctx - span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOrCreateTrueUpReviewStatus") - - a.ctx = newCtx - a.app.Srv().Store().SetContext(newCtx) - defer func() { - a.app.Srv().Store().SetContext(origCtx) - a.ctx = origCtx - }() - - defer span.Finish() - resultVar0, resultVar1 := a.app.GetOrCreateTrueUpReviewStatus(rctx) - - if resultVar1 != nil { - span.LogFields(spanlog.Error(resultVar1)) - ext.Error.Set(span, true) - } - - return resultVar0, resultVar1 -} - func (a *OpenTracingAppLayer) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhook") @@ -10617,28 +10595,6 @@ func (a *OpenTracingAppLayer) GetTotalUsersStats(viewRestrictions *model.ViewUse return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetTrueUpProfile() (map[string]any, error) { - origCtx := a.ctx - span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTrueUpProfile") - - a.ctx = newCtx - a.app.Srv().Store().SetContext(newCtx) - defer func() { - a.app.Srv().Store().SetContext(origCtx) - a.ctx = origCtx - }() - - defer span.Finish() - resultVar0, resultVar1 := a.app.GetTrueUpProfile() - - if resultVar1 != nil { - span.LogFields(spanlog.Error(resultVar1)) - ext.Error.Set(span, true) - } - - return resultVar0, resultVar1 -} - func (a *OpenTracingAppLayer) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUploadSession") diff --git a/server/channels/app/platform/license.go b/server/channels/app/platform/license.go index 2f068266b1..d9b5fa0cde 100644 --- a/server/channels/app/platform/license.go +++ b/server/channels/app/platform/license.go @@ -343,54 +343,6 @@ func (ps *PlatformService) RequestTrialLicense(trialRequest *model.TrialLicenseR return nil } -// GenerateRenewalToken returns a renewal token that expires after duration expiration -func (ps *PlatformService) GenerateRenewalToken(expiration time.Duration) (string, *model.AppError) { - license := ps.License() - if license == nil { - return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.no_license", nil, "", http.StatusBadRequest) - } - - if license.IsCloud() { - return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.bad_license", nil, "", http.StatusBadRequest) - } - - activeUsers, err := ps.Store.User().Count(model.UserCountOptions{}) - if err != nil { - return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error", - nil, "", http.StatusInternalServerError).Wrap(err) - } - - expirationTime := time.Now().UTC().Add(expiration) - claims := &JWTClaims{ - LicenseID: license.Id, - ActiveUsers: activeUsers, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(expirationTime), - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(license.Customer.Email)) - if err != nil { - return "", model.NewAppError("GenerateRenewalToken", "app.license.generate_renewal_token.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - - return tokenString, nil -} - -// GenerateLicenseRenewalLink returns a link that points to the CWS where clients can renew license -func (ps *PlatformService) GenerateLicenseRenewalLink() (string, string, *model.AppError) { - renewalToken, err := ps.GenerateRenewalToken(JWTDefaultTokenExpiration) - if err != nil { - return "", "", err - } - return fmt.Sprintf("%s?token=%s", ps.getLicenseRenewalURL(), renewalToken), renewalToken, nil -} - -func (ps *PlatformService) getLicenseRenewalURL() string { - return fmt.Sprintf("%s/subscribe/renew", *ps.Config().CloudSettings.CWSURL) -} - func (ps *PlatformService) getRequestTrialURL() string { return fmt.Sprintf("%s/api/v1/trials", *ps.Config().CloudSettings.CWSURL) } diff --git a/server/channels/app/platform/license_test.go b/server/channels/app/platform/license_test.go index 3f1af98ad1..6df2a84bfe 100644 --- a/server/channels/app/platform/license_test.go +++ b/server/channels/app/platform/license_test.go @@ -73,24 +73,6 @@ func TestGetSanitizedClientLicense(t *testing.T) { assert.False(t, ok) } -func TestGenerateRenewalToken(t *testing.T) { - th := Setup(t) - defer th.TearDown() - - t.Run("renewal token generated correctly", func(t *testing.T) { - setLicense(th, nil) - token, appErr := th.Service.GenerateRenewalToken(JWTDefaultTokenExpiration) - require.Nil(t, appErr) - require.NotEmpty(t, token) - }) - - t.Run("return error if there is no active license", func(t *testing.T) { - th.Service.SetLicense(nil) - _, appErr := th.Service.GenerateRenewalToken(JWTDefaultTokenExpiration) - require.NotNil(t, appErr) - }) -} - func setLicense(th *TestHelper, customer *model.Customer) { l1 := &model.License{} l1.Features = &model.Features{} diff --git a/server/channels/app/server.go b/server/channels/app/server.go index a7fbeade32..ae937125f9 100644 --- a/server/channels/app/server.go +++ b/server/channels/app/server.go @@ -1296,16 +1296,6 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice daysToExpiration := license.DaysToExpiration() - ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink() - if appErr != nil { - return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr) - } - - status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew) - if err != nil { - return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - // we want to at least have one email sent out to an admin countNotOks := 0 @@ -1315,13 +1305,10 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice name = user.Username } T := i18n.GetUserTranslations(user.Locale) - ctaTitle := T("api.templates.license_up_for_renewal_subtitle_two") - ctaText := T("api.templates.license_up_for_renewal_renew_now") - if !status.IsRenewable { - ctaTitle = "" - ctaText = T("api.templates.license_up_for_renewal_contact_sales") - ctaLink = "https://mattermost.com/contact-sales/" - } + + ctaTitle := "" + ctaText := T("api.templates.license_up_for_renewal_contact_sales") + ctaLink := "https://mattermost.com/contact-sales/" if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.platform.Config().ServiceSettings.SiteURL, ctaTitle, ctaLink, ctaText, daysToExpiration); err != nil { mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err)) @@ -1383,18 +1370,6 @@ func (s *Server) doLicenseExpirationCheck() { return } - ctaLink, tokenToBeUsedForRenew, appErr := s.GenerateLicenseRenewalLink() - if appErr != nil { - mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, "", http.StatusInternalServerError).Wrap(appErr).Error()) - return - } - - status, err := s.Cloud.GetLicenseSelfServeStatus("", tokenToBeUsedForRenew) - if err != nil { - mlog.Debug(model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err).Error()) - return - } - //send email to admin(s) for _, user := range users { user := user @@ -1404,11 +1379,8 @@ func (s *Server) doLicenseExpirationCheck() { } T := i18n.GetUserTranslations(user.Locale) - ctaText := T("api.templates.remove_expired_license.body.renew_button") - if !status.IsRenewable { - ctaText = T("api.templates.license_up_for_renewal_contact_sales") - ctaLink = "https://mattermost.com/contact-sales/" - } + ctaText := T("api.templates.license_up_for_renewal_contact_sales") + ctaLink := "https://mattermost.com/contact-sales/" mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email)) s.Go(func() { diff --git a/server/channels/app/true_up.go b/server/channels/app/true_up.go deleted file mode 100644 index 171bb2122b..0000000000 --- a/server/channels/app/true_up.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "encoding/json" - "errors" - "net/http" - "os" - "strings" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/mattermost/mattermost/server/v8/channels/utils" - "github.com/mattermost/mattermost/server/v8/platform/services/telemetry" -) - -func pluginActivated(pluginStates map[string]*model.PluginState, pluginId string) bool { - state, ok := pluginStates[pluginId] - if !ok { - return false - } - return state.Enable -} - -func (a *App) getMarketplacePlugins() ([]string, error) { - ts := a.Srv().telemetryService - config := a.Srv().Config() - - marketplacePlugins, err := ts.GetAllMarketplacePlugins(model.PluginSettingsDefaultMarketplaceURL) - if err != nil { - return nil, err - } - - activePlugins := []string{} - for _, p := range marketplacePlugins { - id := p.Manifest.Id - if pluginActivated(config.PluginSettings.PluginStates, id) { - activePlugins = append(activePlugins, id) - } - } - - return activePlugins, nil -} - -func (a *App) getTrueUpProfile() (*model.TrueUpReviewProfile, error) { - license := a.Channels().License() - if license == nil { - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "Could not get the total active users count", http.StatusInternalServerError) - } - - // Customer Info & Usage Analytics - - // active registered users - activatedUsers, err := a.Srv().Store().User().Count(model.UserCountOptions{}) - if err != nil { - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.user_count_fail", nil, "Could not get the total activated users count", http.StatusInternalServerError).Wrap(err) - } - - // daily active users - dau, err := a.Srv().Store().User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}) - if err != nil { - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.user_count_fail", nil, "Could not get the total daily active users count", http.StatusInternalServerError) - } - - // monthly active users - mau, err := a.Srv().Store().User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}) - if err != nil { - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.user_count_fail", nil, "Could not get the total monthly active users count", http.StatusInternalServerError).Wrap(err) - } - - // Webhook, calls, boards, and playbook counts - incomingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsIncomingCount("") - if err != nil { - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_in_count_fail", nil, "Could not get the total incoming webhook count", http.StatusInternalServerError) - } - outgoingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsOutgoingCount("") - if err != nil { - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_out_count_fail", nil, "Could not get the total outgoing webhook count", http.StatusInternalServerError) - } - - // Plugin Data - trueUpReviewPlugins := model.TrueUpReviewPlugins{ - PluginNames: []string{}, - } - - if plugins, err := a.getMarketplacePlugins(); err == nil { - trueUpReviewPlugins.PluginNames = plugins - trueUpReviewPlugins.TotalPlugins = len(plugins) - } - - // Authentication Features - config := a.Config() - mfaUsed := config.ServiceSettings.EnforceMultifactorAuthentication - ldapUsed := config.LdapSettings.Enable - samlUsed := config.SamlSettings.Enable - openIdUsed := config.OpenIdSettings.Enable - guestAccessAllowed := config.GuestAccountsSettings.Enable - - authFeatures := map[string]*bool{ - model.TrueUpReviewAuthFeaturesMfa: mfaUsed, - model.TrueUpReviewAuthFeaturesADLdap: ldapUsed, - model.TrueUpReviewAuthFeaturesSaml: samlUsed, - model.TrueUpReviewAuthFeatureOpenId: openIdUsed, - model.TrueUpReviewAuthFeatureGuestAccess: guestAccessAllowed, - } - - authFeatureList := []string{} - for feature, used := range authFeatures { - if used != nil && *used { - authFeatureList = append(authFeatureList, feature) - } - } - - reviewProfile := model.TrueUpReviewProfile{ - ServerId: a.TelemetryId(), - ServerVersion: model.CurrentVersion, - ServerInstallationType: os.Getenv(telemetry.EnvVarInstallType), - LicenseId: license.Id, - LicensedSeats: *license.Features.Users, - LicensePlan: license.SkuName, - CustomerName: license.Customer.Name, - ActivatedUsers: activatedUsers, - DailyActiveUsers: dau, - MonthlyActiveUsers: mau, - TotalIncomingWebhooks: incomingWebhookCount, - TotalOutgoingWebhooks: outgoingWebhookCount, - Plugins: trueUpReviewPlugins, - AuthenticationFeatures: authFeatureList, - } - - return &reviewProfile, nil -} - -func (a *App) GetTrueUpProfile() (map[string]any, error) { - profile, err := a.getTrueUpProfile() - - if err != nil { - return nil, err - } - - profileJson, err := json.Marshal(profile) - if err != nil { - return nil, err - } - telemetryProperties := map[string]any{} - - json.Unmarshal(profileJson, &telemetryProperties) - delete(telemetryProperties, "plugins") - plugins := profile.Plugins.ToMap() - for key, pluginValue := range plugins { - telemetryProperties[key] = pluginValue - } - - delete(telemetryProperties, "authentication_features") - telemetryProperties["authentication_features"] = strings.Join(profile.AuthenticationFeatures, ",") - - return telemetryProperties, nil -} - -func (a *App) GetOrCreateTrueUpReviewStatus(rctx request.CTX) (*model.TrueUpReviewStatus, *model.AppError) { - nextDueDate := utils.GetNextTrueUpReviewDueDate(time.Now()) - status, err := a.Srv().Store().TrueUpReview().GetTrueUpReviewStatus(nextDueDate.UnixMilli()) - if err != nil { - var nfErr *store.ErrNotFound - switch { - case errors.As(err, &nfErr): - rctx.Logger().Warn("Could not find true up review status") - default: - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "Could not get true up status records", http.StatusInternalServerError).Wrap(err) - } - - status, err = a.Srv().Store().TrueUpReview().CreateTrueUpReviewStatusRecord(&model.TrueUpReviewStatus{DueDate: nextDueDate.UnixMilli(), Completed: false}) - if err != nil { - return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.create_error", nil, "Could not create true up status record", http.StatusInternalServerError) - } - } - - return status, nil -} diff --git a/server/channels/app/true_up_test.go b/server/channels/app/true_up_test.go deleted file mode 100644 index 650ebe742f..0000000000 --- a/server/channels/app/true_up_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "strings" - "testing" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestGetTrueUpProfile(t *testing.T) { - th := SetupWithStoreMock(t) - defer th.TearDown() - - mockStore := th.App.Srv().Store().(*mocks.Store) - mockUserStore := mocks.UserStore{} - //Activated userss set to 10 - mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) - //Mau set to 5 - mockUserStore.On("AnalyticsActiveCount", int64(MonthMilliseconds), model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}).Return(int64(5), nil) - //dau set to 2 - mockUserStore.On("AnalyticsActiveCount", int64(DayMilliseconds), model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false}).Return(int64(2), nil) - mockStore.On("User").Return(&mockUserStore) - - mockWebhookStore := mocks.WebhookStore{} - mockWebhookStore.On("AnalyticsIncomingCount", mock.Anything).Return(int64(1), nil) - mockWebhookStore.On("AnalyticsOutgoingCount", mock.Anything).Return(int64(1), nil) - mockStore.On("Webhook").Return(&mockWebhookStore) - - t.Run("missing license", func(t *testing.T) { - _, err := th.App.GetTrueUpProfile() - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "True up review requires a license")) - }) - - t.Run("happy path - returns correct mau and activated users", func(t *testing.T) { - th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) - - profile, err := th.App.GetTrueUpProfile() - assert.NoError(t, err, "Unexpected error") - - require.NotNil(t, profile) - assert.Equal(t, float64(5), profile["monthly_active_users"]) - assert.Equal(t, float64(2), profile["daily_active_users"]) - assert.Equal(t, float64(10), profile["total_activated_users"]) - assert.Equal(t, float64(1), profile["incoming_webhooks_count"]) - assert.Equal(t, float64(1), profile["outgoing_webhooks_count"]) - }) -} diff --git a/server/channels/db/migrations/migrations.list b/server/channels/db/migrations/migrations.list index 9e3075b56b..17408699e4 100644 --- a/server/channels/db/migrations/migrations.list +++ b/server/channels/db/migrations/migrations.list @@ -238,6 +238,8 @@ channels/db/migrations/mysql/000119_msteams_shared_channels_opts.down.sql channels/db/migrations/mysql/000119_msteams_shared_channels_opts.up.sql channels/db/migrations/mysql/000120_create_channelbookmarks_table.down.sql channels/db/migrations/mysql/000120_create_channelbookmarks_table.up.sql +channels/db/migrations/mysql/000121_remove_true_up_review_history.down.sql +channels/db/migrations/mysql/000121_remove_true_up_review_history.up.sql channels/db/migrations/postgres/000001_create_teams.down.sql channels/db/migrations/postgres/000001_create_teams.up.sql channels/db/migrations/postgres/000002_create_team_members.down.sql @@ -476,3 +478,5 @@ channels/db/migrations/postgres/000119_msteams_shared_channels_opts.down.sql channels/db/migrations/postgres/000119_msteams_shared_channels_opts.up.sql channels/db/migrations/postgres/000120_create_channelbookmarks_table.down.sql channels/db/migrations/postgres/000120_create_channelbookmarks_table.up.sql +channels/db/migrations/postgres/000121_remove_true_up_review_history.down.sql +channels/db/migrations/postgres/000121_remove_true_up_review_history.up.sql diff --git a/server/channels/db/migrations/mysql/000121_remove_true_up_review_history.down.sql b/server/channels/db/migrations/mysql/000121_remove_true_up_review_history.down.sql new file mode 100644 index 0000000000..5b25ffdb80 --- /dev/null +++ b/server/channels/db/migrations/mysql/000121_remove_true_up_review_history.down.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS TrueUpReviewHistory ( + DueDate bigint(20), + Completed boolean, + PRIMARY KEY (DueDate) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/server/channels/db/migrations/mysql/000121_remove_true_up_review_history.up.sql b/server/channels/db/migrations/mysql/000121_remove_true_up_review_history.up.sql new file mode 100644 index 0000000000..e3fc2d3011 --- /dev/null +++ b/server/channels/db/migrations/mysql/000121_remove_true_up_review_history.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS TrueUpReviewHistory; \ No newline at end of file diff --git a/server/channels/db/migrations/postgres/000121_remove_true_up_review_history.down.sql b/server/channels/db/migrations/postgres/000121_remove_true_up_review_history.down.sql new file mode 100644 index 0000000000..640d3cd87a --- /dev/null +++ b/server/channels/db/migrations/postgres/000121_remove_true_up_review_history.down.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS trueupreviewhistory ( + duedate bigint, + completed boolean, + PRIMARY KEY (duedate) +); diff --git a/server/channels/db/migrations/postgres/000121_remove_true_up_review_history.up.sql b/server/channels/db/migrations/postgres/000121_remove_true_up_review_history.up.sql new file mode 100644 index 0000000000..1e43ddfb0c --- /dev/null +++ b/server/channels/db/migrations/postgres/000121_remove_true_up_review_history.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS trueupreviewhistory; \ No newline at end of file diff --git a/server/channels/store/opentracinglayer/opentracinglayer.go b/server/channels/store/opentracinglayer/opentracinglayer.go index 6144789d17..ccb6ad2873 100644 --- a/server/channels/store/opentracinglayer/opentracinglayer.go +++ b/server/channels/store/opentracinglayer/opentracinglayer.go @@ -59,7 +59,6 @@ type OpenTracingLayer struct { TermsOfServiceStore store.TermsOfServiceStore ThreadStore store.ThreadStore TokenStore store.TokenStore - TrueUpReviewStore store.TrueUpReviewStore UploadSessionStore store.UploadSessionStore UserStore store.UserStore UserAccessTokenStore store.UserAccessTokenStore @@ -227,10 +226,6 @@ func (s *OpenTracingLayer) Token() store.TokenStore { return s.TokenStore } -func (s *OpenTracingLayer) TrueUpReview() store.TrueUpReviewStore { - return s.TrueUpReviewStore -} - func (s *OpenTracingLayer) UploadSession() store.UploadSessionStore { return s.UploadSessionStore } @@ -451,11 +446,6 @@ type OpenTracingLayerTokenStore struct { Root *OpenTracingLayer } -type OpenTracingLayerTrueUpReviewStore struct { - store.TrueUpReviewStore - Root *OpenTracingLayer -} - type OpenTracingLayerUploadSessionStore struct { store.UploadSessionStore Root *OpenTracingLayer @@ -11117,60 +11107,6 @@ func (s *OpenTracingLayerTokenStore) Save(recovery *model.Token) error { return err } -func (s *OpenTracingLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - origCtx := s.Root.Store.Context() - span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.CreateTrueUpReviewStatusRecord") - s.Root.Store.SetContext(newCtx) - defer func() { - s.Root.Store.SetContext(origCtx) - }() - - defer span.Finish() - result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus) - if err != nil { - span.LogFields(spanlog.Error(err)) - ext.Error.Set(span, true) - } - - return result, err -} - -func (s *OpenTracingLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) { - origCtx := s.Root.Store.Context() - span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.GetTrueUpReviewStatus") - s.Root.Store.SetContext(newCtx) - defer func() { - s.Root.Store.SetContext(origCtx) - }() - - defer span.Finish() - result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate) - if err != nil { - span.LogFields(spanlog.Error(err)) - ext.Error.Set(span, true) - } - - return result, err -} - -func (s *OpenTracingLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - origCtx := s.Root.Store.Context() - span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.Update") - s.Root.Store.SetContext(newCtx) - defer func() { - s.Root.Store.SetContext(origCtx) - }() - - defer span.Finish() - result, err := s.TrueUpReviewStore.Update(reviewStatus) - if err != nil { - span.LogFields(spanlog.Error(err)) - ext.Error.Set(span, true) - } - - return result, err -} - func (s *OpenTracingLayerUploadSessionStore) Delete(id string) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Delete") @@ -13475,7 +13411,6 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer { newStore.TermsOfServiceStore = &OpenTracingLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore} newStore.ThreadStore = &OpenTracingLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore} newStore.TokenStore = &OpenTracingLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore} - newStore.TrueUpReviewStore = &OpenTracingLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore} newStore.UploadSessionStore = &OpenTracingLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore} newStore.UserStore = &OpenTracingLayerUserStore{UserStore: childStore.User(), Root: &newStore} newStore.UserAccessTokenStore = &OpenTracingLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore} diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index 4620097a52..114d9cac24 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -63,7 +63,6 @@ type RetryLayer struct { TermsOfServiceStore store.TermsOfServiceStore ThreadStore store.ThreadStore TokenStore store.TokenStore - TrueUpReviewStore store.TrueUpReviewStore UploadSessionStore store.UploadSessionStore UserStore store.UserStore UserAccessTokenStore store.UserAccessTokenStore @@ -231,10 +230,6 @@ func (s *RetryLayer) Token() store.TokenStore { return s.TokenStore } -func (s *RetryLayer) TrueUpReview() store.TrueUpReviewStore { - return s.TrueUpReviewStore -} - func (s *RetryLayer) UploadSession() store.UploadSessionStore { return s.UploadSessionStore } @@ -455,11 +450,6 @@ type RetryLayerTokenStore struct { Root *RetryLayer } -type RetryLayerTrueUpReviewStore struct { - store.TrueUpReviewStore - Root *RetryLayer -} - type RetryLayerUploadSessionStore struct { store.UploadSessionStore Root *RetryLayer @@ -12717,69 +12707,6 @@ func (s *RetryLayerTokenStore) Save(recovery *model.Token) error { } -func (s *RetryLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - - tries := 0 - for { - result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus) - if err == nil { - return result, nil - } - if !isRepeatableError(err) { - return result, err - } - tries++ - if tries >= 3 { - err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err - } - timepkg.Sleep(100 * timepkg.Millisecond) - } - -} - -func (s *RetryLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) { - - tries := 0 - for { - result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate) - if err == nil { - return result, nil - } - if !isRepeatableError(err) { - return result, err - } - tries++ - if tries >= 3 { - err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err - } - timepkg.Sleep(100 * timepkg.Millisecond) - } - -} - -func (s *RetryLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - - tries := 0 - for { - result, err := s.TrueUpReviewStore.Update(reviewStatus) - if err == nil { - return result, nil - } - if !isRepeatableError(err) { - return result, err - } - tries++ - if tries >= 3 { - err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err - } - timepkg.Sleep(100 * timepkg.Millisecond) - } - -} - func (s *RetryLayerUploadSessionStore) Delete(id string) error { tries := 0 @@ -15372,7 +15299,6 @@ func New(childStore store.Store) *RetryLayer { newStore.TermsOfServiceStore = &RetryLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore} newStore.ThreadStore = &RetryLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore} newStore.TokenStore = &RetryLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore} - newStore.TrueUpReviewStore = &RetryLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore} newStore.UploadSessionStore = &RetryLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore} newStore.UserStore = &RetryLayerUserStore{UserStore: childStore.User(), Root: &newStore} newStore.UserAccessTokenStore = &RetryLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore} diff --git a/server/channels/store/retrylayer/retrylayer_test.go b/server/channels/store/retrylayer/retrylayer_test.go index 5729799cb4..49102bd73a 100644 --- a/server/channels/store/retrylayer/retrylayer_test.go +++ b/server/channels/store/retrylayer/retrylayer_test.go @@ -60,7 +60,6 @@ func genStore() *mocks.Store { mock.On("PostPriority").Return(&mocks.PostPriorityStore{}) mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{}) mock.On("PostPersistentNotification").Return(&mocks.PostPersistentNotificationStore{}) - mock.On("TrueUpReview").Return(&mocks.TrueUpReviewStore{}) mock.On("DesktopTokens").Return(&mocks.DesktopTokensStore{}) mock.On("ChannelBookmark").Return(&mocks.ChannelBookmarkStore{}) return mock diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index 817c1b6541..dabeaa3cd0 100644 --- a/server/channels/store/sqlstore/store.go +++ b/server/channels/store/sqlstore/store.go @@ -109,7 +109,6 @@ type SqlStoreStores struct { postPriority store.PostPriorityStore postAcknowledgement store.PostAcknowledgementStore postPersistentNotification store.PostPersistentNotificationStore - trueUpReview store.TrueUpReviewStore desktopTokens store.DesktopTokensStore channelBookmarks store.ChannelBookmarkStore } @@ -235,7 +234,6 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface store.stores.postPriority = newSqlPostPriorityStore(store) store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store) store.stores.postPersistentNotification = newSqlPostPersistentNotificationStore(store) - store.stores.trueUpReview = newSqlTrueUpReviewStore(store) store.stores.desktopTokens = newSqlDesktopTokensStore(store, metrics) store.stores.channelBookmarks = newSqlChannelBookmarkStore(store) @@ -1033,10 +1031,6 @@ func (ss *SqlStore) PostPersistentNotification() store.PostPersistentNotificatio return ss.stores.postPersistentNotification } -func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore { - return ss.stores.trueUpReview -} - func (ss *SqlStore) DesktopTokens() store.DesktopTokensStore { return ss.stores.desktopTokens } diff --git a/server/channels/store/sqlstore/true_up_review_store.go b/server/channels/store/sqlstore/true_up_review_store.go deleted file mode 100644 index 4c5a0a3c57..0000000000 --- a/server/channels/store/sqlstore/true_up_review_store.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "strconv" - - sq "github.com/mattermost/squirrel" - "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/store" -) - -// SqlLicenseStore encapsulates the database writes and reads for -// model.LicenseRecord objects. -type SqlTrueUpReviewStore struct { - *SqlStore -} - -func newSqlTrueUpReviewStore(sqlStore *SqlStore) store.TrueUpReviewStore { - return &SqlTrueUpReviewStore{sqlStore} -} - -func trueUpReviewStatusColumns() []string { - return []string{ - "DueDate", - "Completed", - } -} - -func (s *SqlTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) { - query := s.getQueryBuilder(). - Select("*"). - From("TrueUpReviewHistory"). - Where(sq.Eq{"DueDate": dueDate}) - - queryString, args, err := query.ToSql() - if err != nil { - return nil, errors.Wrap(err, "get_trueUpReviewStatusRecord_tosql") - } - var trueUpReviewStatus model.TrueUpReviewStatus - if err := s.GetReplicaX().Get(&trueUpReviewStatus, queryString, args...); err != nil { - if err == sql.ErrNoRows { - return nil, store.NewErrNotFound("TrueUpReviewStatus", strconv.FormatInt(dueDate, 10)) - } - - return nil, err - } - - return &trueUpReviewStatus, nil -} - -func (s *SqlTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - builder := s.getQueryBuilder().Insert("TrueUpReviewHistory").Columns(trueUpReviewStatusColumns()...).Values(reviewStatus.ToSlice()...) - query, args, err := builder.ToSql() - if err != nil { - return nil, errors.Wrap(err, "create_trueUpReviewStatusRecord_tosql") - } - - if _, err = s.GetMasterX().Exec(query, args...); err != nil { - return nil, errors.Wrap(err, "fail to create true up review status record") - } - - return reviewStatus, nil -} - -func (s *SqlTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - query := s.getQueryBuilder(). - Update("TrueUpReviewHistory"). - Set("Completed", reviewStatus.Completed). - Where(sq.Eq{"DueDate": reviewStatus.DueDate}) - - if _, err := s.GetMasterX().ExecBuilder(query); err != nil { - return nil, errors.Wrapf(err, "failed to update true up review status with DueDate=%d", reviewStatus.DueDate) - } - - return reviewStatus, nil -} diff --git a/server/channels/store/sqlstore/true_up_review_store_test.go b/server/channels/store/sqlstore/true_up_review_store_test.go deleted file mode 100644 index 115ae91498..0000000000 --- a/server/channels/store/sqlstore/true_up_review_store_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "testing" - - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" -) - -func TestTrueUpReviewStore(t *testing.T) { - StoreTestWithSqlStore(t, storetest.TestTrueUpReviewStatusStore) -} diff --git a/server/channels/store/store.go b/server/channels/store/store.go index 35e8637d58..a726117e9d 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -89,7 +89,6 @@ type Store interface { PostPriority() PostPriorityStore PostAcknowledgement() PostAcknowledgementStore PostPersistentNotification() PostPersistentNotificationStore - TrueUpReview() TrueUpReviewStore DesktopTokens() DesktopTokensStore ChannelBookmark() ChannelBookmarkStore } @@ -1028,13 +1027,6 @@ type PostPersistentNotificationStore interface { DeleteByChannel(channelIds []string) error DeleteByTeam(teamIds []string) error } - -type TrueUpReviewStore interface { - GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) - CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) - Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) -} - type ChannelBookmarkStore interface { ErrorIfBookmarkFileInfoAlreadyAttached(fileId string) error Get(Id string, includeDeleted bool) (b *model.ChannelBookmarkWithFileInfo, err error) diff --git a/server/channels/store/storetest/mocks/Store.go b/server/channels/store/storetest/mocks/Store.go index cbf8a6ba4f..e047cca940 100644 --- a/server/channels/store/storetest/mocks/Store.go +++ b/server/channels/store/storetest/mocks/Store.go @@ -1158,26 +1158,6 @@ func (_m *Store) TotalSearchDbConnections() int { return r0 } -// TrueUpReview provides a mock function with given fields: -func (_m *Store) TrueUpReview() store.TrueUpReviewStore { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for TrueUpReview") - } - - var r0 store.TrueUpReviewStore - if rf, ok := ret.Get(0).(func() store.TrueUpReviewStore); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(store.TrueUpReviewStore) - } - } - - return r0 -} - // UnlockFromMaster provides a mock function with given fields: func (_m *Store) UnlockFromMaster() { _m.Called() diff --git a/server/channels/store/storetest/mocks/TrueUpReviewStore.go b/server/channels/store/storetest/mocks/TrueUpReviewStore.go deleted file mode 100644 index 4fe8d16cd9..0000000000 --- a/server/channels/store/storetest/mocks/TrueUpReviewStore.go +++ /dev/null @@ -1,119 +0,0 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. - -// Regenerate this file using `make store-mocks`. - -package mocks - -import ( - model "github.com/mattermost/mattermost/server/public/model" - mock "github.com/stretchr/testify/mock" -) - -// TrueUpReviewStore is an autogenerated mock type for the TrueUpReviewStore type -type TrueUpReviewStore struct { - mock.Mock -} - -// CreateTrueUpReviewStatusRecord provides a mock function with given fields: reviewStatus -func (_m *TrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - ret := _m.Called(reviewStatus) - - if len(ret) == 0 { - panic("no return value specified for CreateTrueUpReviewStatusRecord") - } - - var r0 *model.TrueUpReviewStatus - var r1 error - if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)); ok { - return rf(reviewStatus) - } - if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok { - r0 = rf(reviewStatus) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.TrueUpReviewStatus) - } - } - - if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok { - r1 = rf(reviewStatus) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetTrueUpReviewStatus provides a mock function with given fields: dueDate -func (_m *TrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) { - ret := _m.Called(dueDate) - - if len(ret) == 0 { - panic("no return value specified for GetTrueUpReviewStatus") - } - - var r0 *model.TrueUpReviewStatus - var r1 error - if rf, ok := ret.Get(0).(func(int64) (*model.TrueUpReviewStatus, error)); ok { - return rf(dueDate) - } - if rf, ok := ret.Get(0).(func(int64) *model.TrueUpReviewStatus); ok { - r0 = rf(dueDate) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.TrueUpReviewStatus) - } - } - - if rf, ok := ret.Get(1).(func(int64) error); ok { - r1 = rf(dueDate) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Update provides a mock function with given fields: reviewStatus -func (_m *TrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - ret := _m.Called(reviewStatus) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 *model.TrueUpReviewStatus - var r1 error - if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)); ok { - return rf(reviewStatus) - } - if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok { - r0 = rf(reviewStatus) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.TrueUpReviewStatus) - } - } - - if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok { - r1 = rf(reviewStatus) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewTrueUpReviewStore creates a new instance of TrueUpReviewStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewTrueUpReviewStore(t interface { - mock.TestingT - Cleanup(func()) -}) *TrueUpReviewStore { - mock := &TrueUpReviewStore{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/server/channels/store/storetest/store.go b/server/channels/store/storetest/store.go index 90175b5203..dfe1d4c416 100644 --- a/server/channels/store/storetest/store.go +++ b/server/channels/store/storetest/store.go @@ -63,7 +63,6 @@ type Store struct { PostPriorityStore mocks.PostPriorityStore PostAcknowledgementStore mocks.PostAcknowledgementStore PostPersistentNotificationStore mocks.PostPersistentNotificationStore - TrueUpReviewStore mocks.TrueUpReviewStore DesktopTokensStore mocks.DesktopTokensStore ChannelBookmarkStore mocks.ChannelBookmarkStore } @@ -112,7 +111,6 @@ func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore { return &s.ChannelMemberHistoryStore } func (s *Store) ChannelBookmark() store.ChannelBookmarkStore { return &s.ChannelBookmarkStore } -func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore } func (s *Store) DesktopTokens() store.DesktopTokensStore { return &s.DesktopTokensStore } func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore } func (s *Store) Group() store.GroupStore { return &s.GroupStore } diff --git a/server/channels/store/storetest/true_up_review_store.go b/server/channels/store/storetest/true_up_review_store.go deleted file mode 100644 index 2d4756128b..0000000000 --- a/server/channels/store/storetest/true_up_review_store.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package storetest - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/request" - "github.com/mattermost/mattermost/server/v8/channels/store" - "github.com/mattermost/mattermost/server/v8/channels/utils" -) - -func TestTrueUpReviewStatusStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) { - t.Run("CreateTrueUpReviewStatusRecord", func(t *testing.T) { testCreateTrueUpReviewStatus(t, rctx, ss) }) - t.Run("GetTrueUpReviewStatus", func(t *testing.T) { testGetTrueUpReviewStatus(t, rctx, ss) }) - t.Run("Update", func(t *testing.T) { testUpdateTrueUpReviewStatus(t, rctx, ss) }) -} - -func testCreateTrueUpReviewStatus(t *testing.T, rctx request.CTX, ss store.Store) { - now := time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.Local) - - reviewStatus := model.TrueUpReviewStatus{ - Completed: true, - DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(), - } - - t.Run("create true up review status", func(t *testing.T) { - resp, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus) - assert.NoError(t, err) - - assert.Equal(t, reviewStatus.Completed, resp.Completed) - assert.Equal(t, reviewStatus.DueDate, resp.DueDate) - }) -} - -func testGetTrueUpReviewStatus(t *testing.T, rctx request.CTX, ss store.Store) { - now := time.Date(time.Now().Year(), time.August, 1, 0, 0, 0, 0, time.Local) - dueDate := utils.GetNextTrueUpReviewDueDate(now).UnixMilli() - - reviewStatus := model.TrueUpReviewStatus{ - Completed: true, - DueDate: dueDate, - } - - _, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus) - assert.NoError(t, err) - - t.Run("get true up review status", func(t *testing.T) { - resp, err := ss.TrueUpReview().GetTrueUpReviewStatus(dueDate) - assert.NoError(t, err) - - assert.Equal(t, resp.Completed, resp.Completed) - assert.Equal(t, resp.DueDate, resp.DueDate) - }) -} - -func testUpdateTrueUpReviewStatus(t *testing.T, rctx request.CTX, ss store.Store) { - now := time.Date(time.Now().Year(), time.April, 1, 0, 0, 0, 0, time.Local) - - reviewStatus := model.TrueUpReviewStatus{ - Completed: false, - DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(), - } - - _, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus) - assert.NoError(t, err) - - t.Run("save ", func(t *testing.T) { - reviewStatus.Completed = true - resp, err := ss.TrueUpReview().Update(&reviewStatus) - assert.NoError(t, err) - - assert.Equal(t, resp.Completed, resp.Completed) - assert.Equal(t, resp.DueDate, resp.DueDate) - }) -} diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 0942e2a099..355095c2c9 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -59,7 +59,6 @@ type TimerLayer struct { TermsOfServiceStore store.TermsOfServiceStore ThreadStore store.ThreadStore TokenStore store.TokenStore - TrueUpReviewStore store.TrueUpReviewStore UploadSessionStore store.UploadSessionStore UserStore store.UserStore UserAccessTokenStore store.UserAccessTokenStore @@ -227,10 +226,6 @@ func (s *TimerLayer) Token() store.TokenStore { return s.TokenStore } -func (s *TimerLayer) TrueUpReview() store.TrueUpReviewStore { - return s.TrueUpReviewStore -} - func (s *TimerLayer) UploadSession() store.UploadSessionStore { return s.UploadSessionStore } @@ -451,11 +446,6 @@ type TimerLayerTokenStore struct { Root *TimerLayer } -type TimerLayerTrueUpReviewStore struct { - store.TrueUpReviewStore - Root *TimerLayer -} - type TimerLayerUploadSessionStore struct { store.UploadSessionStore Root *TimerLayer @@ -10000,54 +9990,6 @@ func (s *TimerLayerTokenStore) Save(recovery *model.Token) error { return err } -func (s *TimerLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - start := time.Now() - - result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus) - - elapsed := float64(time.Since(start)) / float64(time.Second) - if s.Root.Metrics != nil { - success := "false" - if err == nil { - success = "true" - } - s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.CreateTrueUpReviewStatusRecord", success, elapsed) - } - return result, err -} - -func (s *TimerLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) { - start := time.Now() - - result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate) - - elapsed := float64(time.Since(start)) / float64(time.Second) - if s.Root.Metrics != nil { - success := "false" - if err == nil { - success = "true" - } - s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.GetTrueUpReviewStatus", success, elapsed) - } - return result, err -} - -func (s *TimerLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) { - start := time.Now() - - result, err := s.TrueUpReviewStore.Update(reviewStatus) - - elapsed := float64(time.Since(start)) / float64(time.Second) - if s.Root.Metrics != nil { - success := "false" - if err == nil { - success = "true" - } - s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.Update", success, elapsed) - } - return result, err -} - func (s *TimerLayerUploadSessionStore) Delete(id string) error { start := time.Now() @@ -12140,7 +12082,6 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay newStore.TermsOfServiceStore = &TimerLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore} newStore.ThreadStore = &TimerLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore} newStore.TokenStore = &TimerLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore} - newStore.TrueUpReviewStore = &TimerLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore} newStore.UploadSessionStore = &TimerLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore} newStore.UserStore = &TimerLayerUserStore{UserStore: childStore.User(), Root: &newStore} newStore.UserAccessTokenStore = &TimerLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore} diff --git a/server/channels/utils/true_up.go b/server/channels/utils/true_up.go deleted file mode 100644 index 1be96d86f8..0000000000 --- a/server/channels/utils/true_up.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "time" -) - -const trueUpReviewDueDay = 15 -const day = time.Hour * 24 - -type DueDateWindow struct { - Start time.Time - End time.Time -} - -func GetNextTrueUpReviewDueDate(now time.Time) time.Time { - nowYear := now.Year() - nowMonth := now.Month() - nowDay := now.Day() - finalQuarterYear := nowYear - if nowMonth >= time.October && nowMonth <= time.December { - finalQuarterYear = nowYear + 1 - } - trueUpSubmissionWindows := []DueDateWindow{ - { - Start: time.Date(now.Year(), time.January, 16, 0, 0, 0, 0, now.Location()), - End: time.Date(now.Year(), time.April, 15, 0, 0, 0, 0, now.Location()), - }, - { - Start: time.Date(now.Year(), time.April, 16, 0, 0, 0, 0, now.Location()), - End: time.Date(now.Year(), time.July, 15, 0, 0, 0, 0, now.Location()), - }, - { - Start: time.Date(now.Year(), time.July, 16, 0, 0, 0, 0, now.Location()), - End: time.Date(now.Year(), time.October, 15, 0, 0, 0, 0, now.Location()), - }, - { - Start: time.Date(now.Year(), time.October, 16, 0, 0, 0, 0, now.Location()), - End: time.Date(finalQuarterYear, time.January, 15, 0, 0, 0, 0, now.Location()), - }, - } - - for _, window := range trueUpSubmissionWindows { - withinWindow := false - // Our due dates "wrap" around (i.e. can go into the next year), so we'll need to check the months different. Since January = 1 and December = 12, the checks - // for the current month being greater or equal to the start month and less than or equal to the end month will not work. - if window.End.Month() == time.January { - withinWindow = (nowMonth != time.January && nowMonth >= window.Start.Month()) || nowMonth == window.End.Month() - } else { - withinWindow = nowMonth >= window.Start.Month() && nowMonth <= window.End.Month() - } - - // Only check the days if the current month is equal to the start or end months. - // The dates of the middle month(s) don't matter so much. - isFirstMonth := nowMonth == window.Start.Month() - if isFirstMonth { - withinWindow = withinWindow && nowDay >= window.Start.Day() - } - isFinalMonth := nowMonth == window.End.Month() - if isFinalMonth { - withinWindow = withinWindow && nowDay <= window.End.Day() - } - - if withinWindow { - return window.End - } - } - - return trueUpSubmissionWindows[0].End -} - -func IsTrueUpReviewDueDateWithinTheNext30Days(now time.Time, dueDate time.Time) bool { - dueDateWindow := dueDate.Add(-day * 30) - - if now.Before(dueDateWindow) || now.After(dueDate) { - return false - } - - return true -} diff --git a/server/channels/utils/true_up_test.go b/server/channels/utils/true_up_test.go deleted file mode 100644 index 3ba0085649..0000000000 --- a/server/channels/utils/true_up_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestGetNextTrueUpReviewDueDate(t *testing.T) { - t.Run("Due date always falls on the 15th", func(t *testing.T) { - // Before the 15th - now := time.Date(2022, time.March, 14, 0, 0, 0, 0, time.Local) - due := GetNextTrueUpReviewDueDate(now) - assert.Equal(t, trueUpReviewDueDay, due.Day()) - - // On the 15th - now = time.Date(2022, time.December, 15, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, trueUpReviewDueDay, due.Day()) - - // After the 15th - now = time.Date(2022, time.September, 16, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, trueUpReviewDueDay, due.Day()) - }) - - t.Run("Due date will always be in next quarter if the current date is past the 15th", func(t *testing.T) { - now := time.Date(2022, time.March, 16, 0, 0, 0, 0, time.Local) - due := GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.April, due.Month()) - - now = time.Date(2022, time.June, 16, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.July, due.Month()) - - now = time.Date(2022, time.September, 16, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.October, due.Month()) - - now = time.Date(2022, time.December, 16, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.January, due.Month()) - }) - - t.Run("Due date will always be in the current quarter if the current date is before or on the 15th", func(t *testing.T) { - now := time.Date(2022, time.April, 15, 0, 0, 0, 0, time.Local) - due := GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.April, due.Month()) - - now = time.Date(2022, time.July, 15, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.July, due.Month()) - - now = time.Date(2022, time.October, 14, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.October, due.Month()) - - now = time.Date(2022, time.January, 14, 0, 0, 0, 0, time.Local) - due = GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.January, due.Month()) - }) - - t.Run("Due date will be in the next year if the next quarter is not within the current year", func(t *testing.T) { - now := time.Date(2022, time.October, 21, 0, 0, 0, 0, time.Local) - due := GetNextTrueUpReviewDueDate(now) - assert.Equal(t, time.January, due.Month()) - assert.Equal(t, 2023, due.Year()) - }) -} - -func TestIsTrueUpReviewDueDateWithinTheNext15Days(t *testing.T) { - t.Run("Ensure a date within 30 days before the due date returns true", func(t *testing.T) { - // 1 Day before the due date - now := time.Date(2022, time.March, 16, 0, 0, 0, 0, time.Local) - // Due date is December 15th, 2022 - due := GetNextTrueUpReviewDueDate(now) - - res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due) - assert.True(t, res) - }) - - t.Run("Ensure a date that is more than two weeks before the due date returns false", func(t *testing.T) { - // 15 Days before the due date - now := time.Date(2022, time.October, 16, 0, 0, 0, 0, time.Local) - // Due date is December 15th, 2022 - due := GetNextTrueUpReviewDueDate(now) - - res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due) - assert.False(t, res) - }) - - t.Run("Ensure a date that is past the due date returns false", func(t *testing.T) { - now := time.Date(2022, time.April, 15, 0, 0, 0, 0, time.Local) - - // Due date is April 16th, 2022 - dueNow := time.Date(2022, time.April, 16, 0, 0, 0, 0, time.Local) - due := GetNextTrueUpReviewDueDate(dueNow) - - res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due) - assert.False(t, res) - }) - - t.Run("Ensure a date that is on the due date returns true", func(t *testing.T) { - now := time.Date(2022, time.January, 15, 0, 0, 0, 0, time.Local) - due := GetNextTrueUpReviewDueDate(now) - fmt.Printf("\n\ndue date: %s\n\n", due.Format("2006-Jan-02")) - fmt.Printf("\n\nnow: %s\n\n", now.Format("2006-Jan-02")) - - res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due) - assert.True(t, res) - }) - - t.Run("Ensure a date that is on the first day of the due date window returns true", func(t *testing.T) { - now := time.Date(2022, time.December, 16, 0, 0, 0, 0, time.Local) - due := GetNextTrueUpReviewDueDate(now) - - res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due) - assert.True(t, res) - }) -} diff --git a/server/channels/web/handlers.go b/server/channels/web/handlers.go index 97d5ad5165..4d5d1e85c9 100644 --- a/server/channels/web/handlers.go +++ b/server/channels/web/handlers.go @@ -258,11 +258,6 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "no-referrer") - cloudCSP := "" - if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedPurchase { - cloudCSP = " js.stripe.com/v3" - } - if h.IsStatic { // Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking w.Header().Set("X-Frame-Options", "SAMEORIGIN") @@ -271,9 +266,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Set content security policy. This is also specified in the root.html of the webapp in a meta tag. w.Header().Set("Content-Security-Policy", fmt.Sprintf( - "frame-ancestors %s; script-src 'self' cdn.rudderlabs.com%s%s%s", + "frame-ancestors %s; script-src 'self' cdn.rudderlabs.com%s%s", frameAncestors, - cloudCSP, h.cspShaDirective, devCSP, )) diff --git a/server/channels/web/handlers_test.go b/server/channels/web/handlers_test.go index 12caec6b30..48ce17c2c1 100644 --- a/server/channels/web/handlers_test.go +++ b/server/channels/web/handlers_test.go @@ -336,29 +336,6 @@ func TestHandlerServeCSPHeader(t *testing.T) { IsStatic: true, } - request := httptest.NewRequest("POST", "/", nil) - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) - }) - - t.Run("static, without subpath or SelfHostedPurchase, does not allow Stripe in CSP", func(t *testing.T) { - th := Setup(t).InitBasic() - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SelfHostedPurchase = false }) - defer th.TearDown() - - web := New(th.Server) - - handler := Handler{ - Srv: web.srv, - HandleFunc: handlerForCSPHeader, - RequireSession: false, - TrustRequester: false, - RequireMfa: false, - IsStatic: true, - } - request := httptest.NewRequest("POST", "/", nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) @@ -404,7 +381,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) // TODO: It's hard to unit test this now that the CSP directive is effectively // decided in Setup(). Circle back to this in master once the memory store is @@ -419,7 +396,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response = httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"]) // TODO: See above. // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed") }) @@ -449,7 +426,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors " + frameAncestors + "; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"]) }) } diff --git a/server/einterfaces/cloud.go b/server/einterfaces/cloud.go index be77852f0e..47a5b36ab3 100644 --- a/server/einterfaces/cloud.go +++ b/server/einterfaces/cloud.go @@ -5,7 +5,6 @@ package einterfaces import ( "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/request" ) type CloudInterface interface { @@ -14,11 +13,7 @@ type CloudInterface interface { GetSelfHostedProducts(userID string) ([]*model.Product, error) GetCloudLimits(userID string) (*model.ProductLimits, error) - CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error) - ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error - GetCloudCustomer(userID string) (*model.CloudCustomer, error) - GetLicenseSelfServeStatus(userID string, token string) (*model.SubscriptionLicenseSelfServeStatusResponse, error) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) UpdateCloudCustomerAddress(userID string, address *model.Address) (*model.CloudCustomer, error) @@ -28,32 +23,17 @@ type CloudInterface interface { ChangeSubscription(userID, subscriptionID string, subscriptionChange *model.SubscriptionChange) (*model.Subscription, error) - RequestCloudTrial(userID, subscriptionID, newValidBusinessEmail string) (*model.Subscription, error) ValidateBusinessEmail(userID, email string) error InvalidateCaches() error - // hosted customer methods - SelfHostedSignupAvailable() error - BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error) - CreateCustomerSelfHostedSignup(req model.SelfHostedCustomerForm, requesterEmail string) (*model.SelfHostedSignupCustomerResponse, error) - ConfirmSelfHostedSignup(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) - ConfirmSelfHostedExpansion(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) - ConfirmSelfHostedSignupLicenseApplication() error - GetSelfHostedInvoices(rctx request.CTX) ([]*model.Invoice, error) - GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error) - CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) HandleLicenseChange() error CheckCWSConnection(userId string) error - SelfServeDeleteWorkspace(userID string, deletionRequest *model.WorkspaceDeletionRequest) error SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error - // Used only for when a customer has telemetry disabled. In this scenario, true up review telemetry will be submitted via CWS. - SubmitTrueUpReview(userID string, trueUpReviewProfile map[string]any) 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 adafc72dd3..e3faf5ee27 100644 --- a/server/einterfaces/mocks/CloudInterface.go +++ b/server/einterfaces/mocks/CloudInterface.go @@ -6,7 +6,6 @@ package mocks import ( model "github.com/mattermost/mattermost/server/public/model" - request "github.com/mattermost/mattermost/server/public/shared/request" mock "github.com/stretchr/testify/mock" ) @@ -45,36 +44,6 @@ func (_m *CloudInterface) ApplyIPFilters(userID string, ranges *model.AllowedIPR return r0, r1 } -// BootstrapSelfHostedSignup provides a mock function with given fields: req -func (_m *CloudInterface) BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error) { - ret := _m.Called(req) - - if len(ret) == 0 { - panic("no return value specified for BootstrapSelfHostedSignup") - } - - var r0 *model.BootstrapSelfHostedSignupResponse - var r1 error - if rf, ok := ret.Get(0).(func(model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error)); ok { - return rf(req) - } - if rf, ok := ret.Get(0).(func(model.BootstrapSelfHostedSignupRequest) *model.BootstrapSelfHostedSignupResponse); ok { - r0 = rf(req) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.BootstrapSelfHostedSignupResponse) - } - } - - if rf, ok := ret.Get(1).(func(model.BootstrapSelfHostedSignupRequest) error); ok { - r1 = rf(req) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // ChangeSubscription provides a mock function with given fields: userID, subscriptionID, subscriptionChange func (_m *CloudInterface) ChangeSubscription(userID string, subscriptionID string, subscriptionChange *model.SubscriptionChange) (*model.Subscription, error) { ret := _m.Called(userID, subscriptionID, subscriptionChange) @@ -123,162 +92,6 @@ func (_m *CloudInterface) CheckCWSConnection(userId string) error { return r0 } -// ConfirmCustomerPayment provides a mock function with given fields: userID, confirmRequest -func (_m *CloudInterface) ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error { - ret := _m.Called(userID, confirmRequest) - - if len(ret) == 0 { - panic("no return value specified for ConfirmCustomerPayment") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, *model.ConfirmPaymentMethodRequest) error); ok { - r0 = rf(userID, confirmRequest) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ConfirmSelfHostedExpansion provides a mock function with given fields: req, requesterEmail -func (_m *CloudInterface) ConfirmSelfHostedExpansion(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) { - ret := _m.Called(req, requesterEmail) - - if len(ret) == 0 { - panic("no return value specified for ConfirmSelfHostedExpansion") - } - - var r0 *model.SelfHostedSignupConfirmResponse - var r1 error - if rf, ok := ret.Get(0).(func(model.SelfHostedConfirmPaymentMethodRequest, string) (*model.SelfHostedSignupConfirmResponse, error)); ok { - return rf(req, requesterEmail) - } - if rf, ok := ret.Get(0).(func(model.SelfHostedConfirmPaymentMethodRequest, string) *model.SelfHostedSignupConfirmResponse); ok { - r0 = rf(req, requesterEmail) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.SelfHostedSignupConfirmResponse) - } - } - - if rf, ok := ret.Get(1).(func(model.SelfHostedConfirmPaymentMethodRequest, string) error); ok { - r1 = rf(req, requesterEmail) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ConfirmSelfHostedSignup provides a mock function with given fields: req, requesterEmail -func (_m *CloudInterface) ConfirmSelfHostedSignup(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) { - ret := _m.Called(req, requesterEmail) - - if len(ret) == 0 { - panic("no return value specified for ConfirmSelfHostedSignup") - } - - var r0 *model.SelfHostedSignupConfirmResponse - var r1 error - if rf, ok := ret.Get(0).(func(model.SelfHostedConfirmPaymentMethodRequest, string) (*model.SelfHostedSignupConfirmResponse, error)); ok { - return rf(req, requesterEmail) - } - if rf, ok := ret.Get(0).(func(model.SelfHostedConfirmPaymentMethodRequest, string) *model.SelfHostedSignupConfirmResponse); ok { - r0 = rf(req, requesterEmail) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.SelfHostedSignupConfirmResponse) - } - } - - if rf, ok := ret.Get(1).(func(model.SelfHostedConfirmPaymentMethodRequest, string) error); ok { - r1 = rf(req, requesterEmail) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ConfirmSelfHostedSignupLicenseApplication provides a mock function with given fields: -func (_m *CloudInterface) ConfirmSelfHostedSignupLicenseApplication() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ConfirmSelfHostedSignupLicenseApplication") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// CreateCustomerPayment provides a mock function with given fields: userID -func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error) { - ret := _m.Called(userID) - - if len(ret) == 0 { - panic("no return value specified for CreateCustomerPayment") - } - - var r0 *model.StripeSetupIntent - var r1 error - if rf, ok := ret.Get(0).(func(string) (*model.StripeSetupIntent, error)); ok { - return rf(userID) - } - if rf, ok := ret.Get(0).(func(string) *model.StripeSetupIntent); ok { - r0 = rf(userID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.StripeSetupIntent) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(userID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CreateCustomerSelfHostedSignup provides a mock function with given fields: req, requesterEmail -func (_m *CloudInterface) CreateCustomerSelfHostedSignup(req model.SelfHostedCustomerForm, requesterEmail string) (*model.SelfHostedSignupCustomerResponse, error) { - ret := _m.Called(req, requesterEmail) - - if len(ret) == 0 { - panic("no return value specified for CreateCustomerSelfHostedSignup") - } - - var r0 *model.SelfHostedSignupCustomerResponse - var r1 error - if rf, ok := ret.Get(0).(func(model.SelfHostedCustomerForm, string) (*model.SelfHostedSignupCustomerResponse, error)); ok { - return rf(req, requesterEmail) - } - if rf, ok := ret.Get(0).(func(model.SelfHostedCustomerForm, string) *model.SelfHostedSignupCustomerResponse); ok { - r0 = rf(req, requesterEmail) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.SelfHostedSignupCustomerResponse) - } - } - - if rf, ok := ret.Get(1).(func(model.SelfHostedCustomerForm, string) error); ok { - r1 = rf(req, requesterEmail) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) { ret := _m.Called(userID, userCount) @@ -556,103 +369,6 @@ func (_m *CloudInterface) GetInvoicesForSubscription(userID string) ([]*model.In return r0, r1 } -// GetLicenseSelfServeStatus provides a mock function with given fields: userID, token -func (_m *CloudInterface) GetLicenseSelfServeStatus(userID string, token string) (*model.SubscriptionLicenseSelfServeStatusResponse, error) { - ret := _m.Called(userID, token) - - if len(ret) == 0 { - panic("no return value specified for GetLicenseSelfServeStatus") - } - - var r0 *model.SubscriptionLicenseSelfServeStatusResponse - var r1 error - if rf, ok := ret.Get(0).(func(string, string) (*model.SubscriptionLicenseSelfServeStatusResponse, error)); ok { - return rf(userID, token) - } - if rf, ok := ret.Get(0).(func(string, string) *model.SubscriptionLicenseSelfServeStatusResponse); ok { - r0 = rf(userID, token) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.SubscriptionLicenseSelfServeStatusResponse) - } - } - - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(userID, token) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetSelfHostedInvoicePDF provides a mock function with given fields: invoiceID -func (_m *CloudInterface) GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error) { - ret := _m.Called(invoiceID) - - if len(ret) == 0 { - panic("no return value specified for GetSelfHostedInvoicePDF") - } - - var r0 []byte - var r1 string - var r2 error - if rf, ok := ret.Get(0).(func(string) ([]byte, string, error)); ok { - return rf(invoiceID) - } - if rf, ok := ret.Get(0).(func(string) []byte); ok { - r0 = rf(invoiceID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(string) string); ok { - r1 = rf(invoiceID) - } else { - r1 = ret.Get(1).(string) - } - - if rf, ok := ret.Get(2).(func(string) error); ok { - r2 = rf(invoiceID) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetSelfHostedInvoices provides a mock function with given fields: rctx -func (_m *CloudInterface) GetSelfHostedInvoices(rctx request.CTX) ([]*model.Invoice, error) { - ret := _m.Called(rctx) - - if len(ret) == 0 { - panic("no return value specified for GetSelfHostedInvoices") - } - - var r0 []*model.Invoice - var r1 error - if rf, ok := ret.Get(0).(func(request.CTX) ([]*model.Invoice, error)); ok { - return rf(rctx) - } - if rf, ok := ret.Get(0).(func(request.CTX) []*model.Invoice); ok { - r0 = rf(rctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Invoice) - } - } - - if rf, ok := ret.Get(1).(func(request.CTX) error); ok { - r1 = rf(rctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetSelfHostedProducts provides a mock function with given fields: userID func (_m *CloudInterface) GetSelfHostedProducts(userID string) ([]*model.Product, error) { ret := _m.Called(userID) @@ -749,90 +465,6 @@ func (_m *CloudInterface) InvalidateCaches() error { return r0 } -// RequestCloudTrial provides a mock function with given fields: userID, subscriptionID, newValidBusinessEmail -func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string, newValidBusinessEmail string) (*model.Subscription, error) { - ret := _m.Called(userID, subscriptionID, newValidBusinessEmail) - - if len(ret) == 0 { - panic("no return value specified for RequestCloudTrial") - } - - var r0 *model.Subscription - var r1 error - if rf, ok := ret.Get(0).(func(string, string, string) (*model.Subscription, error)); ok { - return rf(userID, subscriptionID, newValidBusinessEmail) - } - if rf, ok := ret.Get(0).(func(string, string, string) *model.Subscription); ok { - r0 = rf(userID, subscriptionID, newValidBusinessEmail) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Subscription) - } - } - - if rf, ok := ret.Get(1).(func(string, string, string) error); ok { - r1 = rf(userID, subscriptionID, newValidBusinessEmail) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SelfHostedSignupAvailable provides a mock function with given fields: -func (_m *CloudInterface) SelfHostedSignupAvailable() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for SelfHostedSignupAvailable") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SelfServeDeleteWorkspace provides a mock function with given fields: userID, deletionRequest -func (_m *CloudInterface) SelfServeDeleteWorkspace(userID string, deletionRequest *model.WorkspaceDeletionRequest) error { - ret := _m.Called(userID, deletionRequest) - - if len(ret) == 0 { - panic("no return value specified for SelfServeDeleteWorkspace") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, *model.WorkspaceDeletionRequest) error); ok { - r0 = rf(userID, deletionRequest) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SubmitTrueUpReview provides a mock function with given fields: userID, trueUpReviewProfile -func (_m *CloudInterface) SubmitTrueUpReview(userID string, trueUpReviewProfile map[string]interface{}) error { - ret := _m.Called(userID, trueUpReviewProfile) - - if len(ret) == 0 { - panic("no return value specified for SubmitTrueUpReview") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, map[string]interface{}) error); ok { - r0 = rf(userID, trueUpReviewProfile) - } else { - r0 = ret.Error(0) - } - - 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) diff --git a/server/i18n/en.json b/server/i18n/en.json index 948da04a2d..2b6afa0a76 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2272,14 +2272,6 @@ "id": "api.license.request-trial.can-start-trial.not-allowed", "translation": "Failed to apply new trial license. You have previously applied a trial license to this Mattermost instance.. If you would like to extend your trial period please [contact our sales team](https://mattermost.com/contact-us/)." }, - { - "id": "api.license.request_renewal_link.app_error", - "translation": "Error getting the license renewal link" - }, - { - "id": "api.license.request_renewal_link.cannot_renew_on_cws", - "translation": "Renewing this license on the portal is not possible" - }, { "id": "api.license.request_trial_license.app_error", "translation": "Unable to get a trial license, please try again or contact with support@mattermost.com." @@ -2288,38 +2280,6 @@ "id": "api.license.request_trial_license.embargoed", "translation": "We were unable to process the request due to limitations for embargoed countries. [Learn more in our documentation](https://mattermost.com/pl/limitations-for-embargoed-countries), or reach out to legal@mattermost.com for questions around export limitations." }, - { - "id": "api.license.true_up_review.create_error", - "translation": "Could not create true up status record" - }, - { - "id": "api.license.true_up_review.failed_to_submit", - "translation": "Failed to submit true up review profile to CWS." - }, - { - "id": "api.license.true_up_review.get_status_error", - "translation": "Could not get true up status records" - }, - { - "id": "api.license.true_up_review.license_required", - "translation": "True up review requires a license" - }, - { - "id": "api.license.true_up_review.not_allowed_for_cloud", - "translation": "True up review is not allowed for cloud instances" - }, - { - "id": "api.license.true_up_review.user_count_fail", - "translation": "Could not get the total active users count" - }, - { - "id": "api.license.true_up_review.webhook_in_count_fail", - "translation": "Could not get the total incoming webhook count" - }, - { - "id": "api.license.true_up_review.webhook_out_count_fail", - "translation": "Could not get the total outgoing webhook count" - }, { "id": "api.license.upgrade_needed.app_error", "translation": "Feature requires an upgrade to Enterprise Edition." @@ -2854,10 +2814,6 @@ "id": "api.server.hosted_signup_unavailable.error", "translation": "Portal unavailable for self-hosted signup." }, - { - "id": "api.server.license_up_for_renewal.error_generating_link", - "translation": "Failed to generate the license renewal link" - }, { "id": "api.server.license_up_for_renewal.error_sending_email", "translation": "Failed to send license up for renewal emails" @@ -3490,10 +3446,6 @@ "id": "api.templates.license_up_for_renewal_contact_sales", "translation": "Contact sales" }, - { - "id": "api.templates.license_up_for_renewal_renew_now", - "translation": "Renew now" - }, { "id": "api.templates.license_up_for_renewal_subject", "translation": "Your license is up for renewal" @@ -3502,10 +3454,6 @@ "id": "api.templates.license_up_for_renewal_subtitle", "translation": "{{.UserName}}, your subscription is set to expire in {{.Days}} days. We hope you’re experiencing the flexible, secure team collaboration that Mattermost enables. Renew soon to ensure your team can keep enjoying these benefits." }, - { - "id": "api.templates.license_up_for_renewal_subtitle_two", - "translation": "Log in to your Customer Account to renew" - }, { "id": "api.templates.license_up_for_renewal_title", "translation": "Your Mattermost subscription is up for renewal" @@ -3554,10 +3502,6 @@ "id": "api.templates.questions_footer.title", "translation": "Questions?" }, - { - "id": "api.templates.remove_expired_license.body.renew_button", - "translation": "Renew License Now" - }, { "id": "api.templates.remove_expired_license.body.title", "translation": "Your Enterprise Edition license has expired and some features may be disabled. Please renew your license now." @@ -5734,18 +5678,6 @@ "id": "app.last_accessible_post.app_error", "translation": "Error fetching last accessible post" }, - { - "id": "app.license.generate_renewal_token.app_error", - "translation": "Failed to generate a new renewal token." - }, - { - "id": "app.license.generate_renewal_token.bad_license", - "translation": "This type of license doesn't support renewal token generation" - }, - { - "id": "app.license.generate_renewal_token.no_license", - "translation": "No license present" - }, { "id": "app.limits.get_app_limits.user_count.store_error", "translation": "Failed to get user count" diff --git a/server/platform/services/telemetry/telemetry.go b/server/platform/services/telemetry/telemetry.go index e6c44dc821..6ff2756c16 100644 --- a/server/platform/services/telemetry/telemetry.go +++ b/server/platform/services/telemetry/telemetry.go @@ -490,7 +490,6 @@ func (ts *TelemetryService) trackConfig() { "persistent_notification_interval_minutes": *cfg.ServiceSettings.PersistentNotificationIntervalMinutes, "persistent_notification_max_count": *cfg.ServiceSettings.PersistentNotificationMaxCount, "persistent_notification_max_recipients": *cfg.ServiceSettings.PersistentNotificationMaxRecipients, - "self_hosted_purchase": *cfg.ServiceSettings.SelfHostedPurchase, "allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts, "refresh_post_stats_run_time": *cfg.ServiceSettings.RefreshPostStatsRunTime, "maximum_payload_size": *cfg.ServiceSettings.MaximumPayloadSizeBytes, diff --git a/server/public/model/client4.go b/server/public/model/client4.go index 33f90417d9..85008487be 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -340,10 +340,6 @@ func (c *Client4) cloudRoute() string { return "/cloud" } -func (c *Client4) hostedCustomerRoute() string { - return "/hosted_customer" -} - func (c *Client4) testEmailRoute() string { return "/email/test" } @@ -584,10 +580,6 @@ func (c *Client4) permissionsRoute() string { return "/permissions" } -func (c *Client4) limitsRoute() string { - return "/limits" -} - func (c *Client4) bookmarksRoute(channelId string) string { return c.channelRoute(channelId) + "/bookmarks" } @@ -8305,50 +8297,6 @@ func (c *Client4) GetMyIP(ctx context.Context) (*GetIPAddressResponse, *Response return response, BuildResponse(r), nil } -func (c *Client4) CreateCustomerPayment(ctx context.Context) (*StripeSetupIntent, *Response, error) { - r, err := c.DoAPIPost(ctx, c.cloudRoute()+"/payment", "") - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - - var setupIntent *StripeSetupIntent - json.NewDecoder(r.Body).Decode(&setupIntent) - - return setupIntent, BuildResponse(r), nil -} - -func (c *Client4) ConfirmCustomerPayment(ctx context.Context, confirmRequest *ConfirmPaymentMethodRequest) (*Response, error) { - json, err := json.Marshal(confirmRequest) - if err != nil { - return nil, NewAppError("ConfirmCustomerPayment", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - r, err := c.DoAPIPostBytes(ctx, c.cloudRoute()+"/payment/confirm", json) - if err != nil { - return BuildResponse(r), err - } - defer closeBody(r) - - return BuildResponse(r), nil -} - -func (c *Client4) RequestCloudTrial(ctx context.Context, cloudTrialRequest *StartCloudTrialRequest) (*Subscription, *Response, error) { - payload, err := json.Marshal(cloudTrialRequest) - if err != nil { - return nil, nil, NewAppError("RequestCloudTrial", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - r, err := c.DoAPIPutBytes(ctx, c.cloudRoute()+"/request-trial", payload) - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - - var subscription *Subscription - json.NewDecoder(r.Body).Decode(&subscription) - - return subscription, BuildResponse(r), nil -} - func (c *Client4) ValidateWorkspaceBusinessEmail(ctx context.Context) (*Response, error) { r, err := c.DoAPIPost(ctx, c.cloudRoute()+"/validate-workspace-business-email", "") if err != nil { @@ -8415,19 +8363,6 @@ func (c *Client4) GetCloudCustomer(ctx context.Context) (*CloudCustomer, *Respon return cloudCustomer, BuildResponse(r), nil } -func (c *Client4) GetSubscriptionStatus(ctx context.Context, licenseId string) (*SubscriptionLicenseSelfServeStatusResponse, *Response, error) { - r, err := c.DoAPIGet(ctx, fmt.Sprintf("%s%s?licenseID=%s", c.cloudRoute(), "/subscription/self-serve-status", licenseId), "") - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - - var status *SubscriptionLicenseSelfServeStatusResponse - json.NewDecoder(r.Body).Decode(&status) - - return status, BuildResponse(r), nil -} - func (c *Client4) GetSubscription(ctx context.Context) (*Subscription, *Response, error) { r, err := c.DoAPIGet(ctx, c.cloudRoute()+"/subscription", "") if err != nil { @@ -8488,23 +8423,6 @@ func (c *Client4) UpdateCloudCustomerAddress(ctx context.Context, address *Addre return customer, BuildResponse(r), nil } -func (c *Client4) BootstrapSelfHostedSignup(ctx context.Context, req BootstrapSelfHostedSignupRequest) (*BootstrapSelfHostedSignupResponse, *Response, error) { - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, nil, NewAppError("BootstrapSelfHostedSignup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - r, err := c.DoAPIPostBytes(ctx, c.hostedCustomerRoute()+"/bootstrap", reqBytes) - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - - var res *BootstrapSelfHostedSignupResponse - json.NewDecoder(r.Body).Decode(&res) - - return res, BuildResponse(r), nil -} - func (c *Client4) ListImports(ctx context.Context) ([]string, *Response, error) { r, err := c.DoAPIGet(ctx, c.importsRoute(), "") if err != nil { @@ -8793,96 +8711,6 @@ func (c *Client4) GetTeamsUsage(ctx context.Context) (*TeamsUsage, *Response, er return usage, BuildResponse(r), err } -func (c *Client4) SelfHostedSignupAvailable(ctx context.Context) (*Response, error) { - r, err := c.DoAPIGet(ctx, c.hostedCustomerRoute()+"/signup_available", "") - - if err != nil { - return BuildResponse(r), err - } - defer closeBody(r) - - return BuildResponse(r), nil -} - -func (c *Client4) SelfHostedSignupCustomer(ctx context.Context, form *SelfHostedCustomerForm) (*Response, *SelfHostedSignupCustomerResponse, error) { - payloadBytes, err := json.Marshal(form) - if err != nil { - return nil, nil, NewAppError("SelfHostedSignupCustomer", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - - r, err := c.DoAPIPost(ctx, c.hostedCustomerRoute()+"/customer", string(payloadBytes)) - - if err != nil { - return BuildResponse(r), nil, err - } - data, err := io.ReadAll(r.Body) - if err != nil { - return BuildResponse(r), nil, err - } - defer closeBody(r) - - response := SelfHostedSignupCustomerResponse{} - err = json.Unmarshal(data, &response) - if err != nil { - return BuildResponse(r), nil, err - } - - return BuildResponse(r), &response, nil -} - -func (c *Client4) SelfHostedSignupConfirm(ctx context.Context, form *SelfHostedConfirmPaymentMethodRequest) (*Response, *SelfHostedSignupConfirmClientResponse, error) { - payloadBytes, err := json.Marshal(form) - if err != nil { - return nil, nil, NewAppError("SelfHostedSignupConfirm", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - - r, err := c.DoAPIPost(ctx, c.hostedCustomerRoute()+"/confirm", string(payloadBytes)) - - if err != nil { - return BuildResponse(r), nil, err - } - - data, err := io.ReadAll(r.Body) - if err != nil { - return BuildResponse(r), nil, err - } - defer closeBody(r) - - response := SelfHostedSignupConfirmClientResponse{} - err = json.Unmarshal(data, &response) - if err != nil { - return BuildResponse(r), nil, err - } - - defer closeBody(r) - - return BuildResponse(r), &response, nil -} - -func (c *Client4) GetSelfHostedInvoices(ctx context.Context) (*Response, []*Invoice, error) { - r, err := c.DoAPIGet(ctx, c.hostedCustomerRoute()+"/invoices", "") - - if err != nil { - return BuildResponse(r), nil, err - } - - data, err := io.ReadAll(r.Body) - if err != nil { - return BuildResponse(r), nil, err - } - defer closeBody(r) - - invoices := []*Invoice{} - err = json.Unmarshal(data, &invoices) - if err != nil { - return BuildResponse(r), nil, err - } - - defer closeBody(r) - - return BuildResponse(r), invoices, nil -} - func (c *Client4) GetPostInfo(ctx context.Context, postId string) (*PostInfo, *Response, error) { r, err := c.DoAPIGet(ctx, c.postRoute(postId)+"/info", "") if err != nil { @@ -8939,36 +8767,6 @@ func (c *Client4) CheckCWSConnection(ctx context.Context, userId string) (*Respo return BuildResponse(r), nil } -func (c *Client4) SubmitTrueUpReview(ctx context.Context, req map[string]any) (*Response, error) { - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, NewAppError("SubmitTrueUpReview", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - r, err := c.DoAPIPostBytes(ctx, c.licenseRoute()+"/review", reqBytes) - if err != nil { - return BuildResponse(r), nil - } - defer closeBody(r) - - return BuildResponse(r), nil -} - -func (c *Client4) GetServerLimits(ctx context.Context) (*ServerLimits, *Response, error) { - r, err := c.DoAPIGet(ctx, c.limitsRoute()+"/users", "") - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - var serverLimits ServerLimits - if r.StatusCode == http.StatusNotModified { - return &serverLimits, BuildResponse(r), nil - } - if err := json.NewDecoder(r.Body).Decode(&serverLimits); err != nil { - return nil, nil, NewAppError("GetServerLimits", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - return &serverLimits, BuildResponse(r), nil -} - // CreateChannelBookmark creates a channel bookmark based on the provided struct. func (c *Client4) CreateChannelBookmark(ctx context.Context, channelBookmark *ChannelBookmark) (*ChannelBookmark, *Response, error) { channelBookmarkJSON, err := json.Marshal(channelBookmark) diff --git a/server/public/model/config.go b/server/public/model/config.go index 298a6d7d81..bbd464fc17 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -403,7 +403,6 @@ type ServiceSettings struct { CollapsedThreads *string `access:"experimental_features"` ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` EnableCustomGroups *bool `access:"site_users_and_teams"` - SelfHostedPurchase *bool `access:"write_restrictable,cloud_restrictable"` AllowSyncedDrafts *bool `access:"site_posts"` UniqueEmojiReactionLimitPerPost *int `access:"site_posts"` RefreshPostStatsRunTime *string `access:"site_users_and_teams"` @@ -903,10 +902,6 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { s.AllowSyncedDrafts = NewBool(true) } - if s.SelfHostedPurchase == nil { - s.SelfHostedPurchase = NewBool(true) - } - if s.UniqueEmojiReactionLimitPerPost == nil { s.UniqueEmojiReactionLimitPerPost = NewInt(ServiceSettingsDefaultUniqueReactionsPerPost) } diff --git a/server/public/model/hosted_customer.go b/server/public/model/hosted_customer.go index 0b40f69c32..ffaec15cba 100644 --- a/server/public/model/hosted_customer.go +++ b/server/public/model/hosted_customer.go @@ -3,71 +3,8 @@ package model -type BootstrapSelfHostedSignupRequest struct { - Email string `json:"email"` - Reset bool `json:"reset"` -} - type SubscribeNewsletterRequest struct { Email string `json:"email"` ServerID string `json:"server_id"` SubscribedContent string `json:"subscribed_content"` } - -type BootstrapSelfHostedSignupResponse struct { - Progress string `json:"progress"` - // email listed on the JWT claim - Email string `json:"email"` -} - -type BootstrapSelfHostedSignupResponseInternal struct { - Progress string `json:"progress"` - License string `json:"license"` -} - -// email contained in token, so not in the request body. -type SelfHostedCustomerForm struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - BillingAddress *Address `json:"billing_address"` - ShippingAddress *Address `json:"shipping_address"` - Organization string `json:"organization"` -} - -type SelfHostedConfirmPaymentMethodRequest struct { - StripeSetupIntentID string `json:"stripe_setup_intent_id"` - Subscription *CreateSubscriptionRequest `json:"subscription"` - ExpandRequest *SelfHostedExpansionRequest `json:"expand_request"` -} - -// SelfHostedSignupPaymentResponse contains feels needed for self hosted signup to confirm payment and receive license. -type SelfHostedSignupCustomerResponse struct { - CustomerId string `json:"customer_id"` - SetupIntentId string `json:"setup_intent_id"` - SetupIntentSecret string `json:"setup_intent_secret"` - Progress string `json:"progress"` -} - -// SelfHostedSignupConfirmResponse contains data received on successful self hosted signup -type SelfHostedSignupConfirmResponse struct { - License string `json:"license"` - Progress string `json:"progress"` -} - -type SelfHostedSignupConfirmClientResponse struct { - License map[string]string `json:"license"` - Progress string `json:"progress"` -} - -type SelfHostedBillingAccessRequest struct { - LicenseId string `json:"license_id"` -} - -type SelfHostedBillingAccessResponse struct { - Token string `json:"token"` -} - -type SelfHostedExpansionRequest struct { - Seats int `json:"seats"` - LicenseId string `json:"license_id"` -} diff --git a/server/public/model/license.go b/server/public/model/license.go index f31b26b21b..b1625fd01a 100644 --- a/server/public/model/license.go +++ b/server/public/model/license.go @@ -39,15 +39,6 @@ var ( sanctionedTrialDurationUpperBound = 29*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 696 hours (29 days) + 23 hours, 59 mins and 59 seconds ) -const ( - TrueUpReviewTelemetryName = "true_up_review_sent" - TrueUpReviewAuthFeaturesMfa = "multi_factor_authentication" - TrueUpReviewAuthFeaturesADLdap = "ad_ldap_sign_in" - TrueUpReviewAuthFeaturesSaml = "saml_sign_in" - TrueUpReviewAuthFeatureOpenId = "openid_connect" - TrueUpReviewAuthFeatureGuestAccess = "guest_access" -) - type LicenseRecord struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` diff --git a/server/public/model/true_up_review_profile.go b/server/public/model/true_up_review_profile.go deleted file mode 100644 index 39741c3bb2..0000000000 --- a/server/public/model/true_up_review_profile.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package model - -import "strings" - -type TrueUpReviewProfile struct { - ServerId string `json:"server_id"` - ServerVersion string `json:"server_version"` - ServerInstallationType string `json:"server_installation_type"` - LicenseId string `json:"license_id"` - LicensedSeats int `json:"licensed_seats"` - LicensePlan string `json:"license_plan"` - CustomerName string `json:"customer_name"` - ActivatedUsers int64 `json:"total_activated_users"` - DailyActiveUsers int64 `json:"daily_active_users"` - MonthlyActiveUsers int64 `json:"monthly_active_users"` - AuthenticationFeatures []string `json:"authentication_features"` - Plugins TrueUpReviewPlugins `json:"plugins"` - TotalIncomingWebhooks int64 `json:"incoming_webhooks_count"` - TotalOutgoingWebhooks int64 `json:"outgoing_webhooks_count"` -} - -type TrueUpReviewPlugins struct { - TotalPlugins int `json:"total_plugins"` - PluginNames []string `json:"plugin_names"` -} - -func (t *TrueUpReviewPlugins) ToMap() map[string]interface{} { - return map[string]interface{}{ - "total_plugins": t.TotalPlugins, - "plugin_names": strings.Join(t.PluginNames, ","), - } -} - -type TrueUpReviewStatus struct { - Completed bool `json:"complete"` - DueDate int64 `json:"due_date"` -} - -func (t *TrueUpReviewStatus) ToSlice() []interface{} { - return []interface{}{ - t.DueDate, - t.Completed, - } -} diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 81c51a31c0..b792f4452f 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -18,8 +18,6 @@ "@mui/base": "5.0.0-alpha.127", "@mui/material": "5.11.16", "@mui/styled-engine-sc": "5.11.11", - "@stripe/react-stripe-js": "1.13.0", - "@stripe/stripe-js": "1.41.0", "@tanstack/react-table": "8.10.7", "@tippyjs/react": "4.2.6", "@types/color-hash": "1.0.2", diff --git a/webapp/channels/src/actions/cloud.tsx b/webapp/channels/src/actions/cloud.tsx index c6acab385e..7c5ebe4ab0 100644 --- a/webapp/channels/src/actions/cloud.tsx +++ b/webapp/channels/src/actions/cloud.tsx @@ -1,9 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {Stripe} from '@stripe/stripe-js'; - -import type {Address, CloudCustomerPatch, Feedback, WorkspaceDeletionRequest} from '@mattermost/types/cloud'; import type {ServerError} from '@mattermost/types/errors'; import {CloudTypes} from 'mattermost-redux/action_types'; @@ -14,77 +11,8 @@ import type {ActionFunc, ThunkActionFunc} from 'mattermost-redux/types/actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; -import {getConfirmCardSetup} from 'components/payment_form/stripe'; - -import {getBlankAddressWithCountry} from 'utils/utils'; - -import type {StripeSetupIntent, BillingDetails} from 'types/cloud/sku'; import type {GlobalState} from 'types/store'; -// Returns true for success, and false for any error -export function completeStripeAddPaymentMethod( - stripe: Stripe, - billingDetails: BillingDetails, - cwsMockMode: boolean, -) { - return async () => { - let paymentSetupIntent: StripeSetupIntent; - try { - paymentSetupIntent = await Client4.createPaymentMethod() as StripeSetupIntent; - } catch (error) { - return error; - } - const cardSetupFunction = getConfirmCardSetup(cwsMockMode); - const confirmCardSetup = cardSetupFunction(stripe.confirmCardSetup); - - const result = await confirmCardSetup( - paymentSetupIntent.client_secret, - { - payment_method: { - card: billingDetails.card, - billing_details: { - name: billingDetails.name, - address: { - line1: billingDetails.address, - line2: billingDetails.address2, - city: billingDetails.city, - state: billingDetails.state, - country: billingDetails.country, - postal_code: billingDetails.postalCode, - }, - }, - }, - }, - ); - - if (!result) { - return false; - } - - const {setupIntent, error: stripeError} = result; - - if (stripeError) { - return false; - } - - if (setupIntent == null) { - return false; - } - - if (setupIntent.status !== 'succeeded') { - return false; - } - - try { - await Client4.confirmPaymentMethod(setupIntent.id); - } catch (error) { - return false; - } - - return true; - }; -} - export function getInstallation() { return async () => { try { @@ -96,47 +24,6 @@ export function getInstallation() { }; } -export function subscribeCloudSubscription( - productId: string, - shippingAddress: Address = getBlankAddressWithCountry(), - seats = 0, - downgradeFeedback?: Feedback, - customerPatch?: CloudCustomerPatch, -) { - return async () => { - try { - const subscription = await Client4.subscribeCloudProduct( - productId, - shippingAddress, - seats, - downgradeFeedback, - customerPatch, - ); - - return {data: subscription}; - } catch (e: any) { - // In the event that the status code returned is 422, this request has been blocked by export compliance - return {data: false, error: {error: e.message, status: e.status_code}}; - } - }; -} - -export function requestCloudTrial(page: string, subscriptionId: string, email = ''): ThunkActionFunc> { - trackEvent('api', 'api_request_cloud_trial_license', {from_page: page}); - return async (dispatch) => { - try { - const newSubscription = await Client4.requestCloudTrial(subscriptionId, email); - dispatch({ - type: CloudTypes.RECEIVED_CLOUD_SUBSCRIPTION, - data: newSubscription.data, - }); - } catch (error) { - return false; - } - return true; - }; -} - export function validateBusinessEmail(email = '') { trackEvent('api', 'api_validate_business_email'); return async () => { @@ -238,17 +125,6 @@ export function getTeamsUsage(): ThunkActionFunc> }; } -export function deleteWorkspace(deletionRequest: WorkspaceDeletionRequest) { - return async () => { - try { - await Client4.deleteWorkspace(deletionRequest); - } catch (error) { - return error; - } - return true; - }; -} - export function retryFailedCloudFetches(): ActionFunc { return (dispatch, getState) => { const errors = getCloudErrors(getState()); diff --git a/webapp/channels/src/actions/hosted_customer.tsx b/webapp/channels/src/actions/hosted_customer.tsx index 078aa0ef73..7e2e145346 100644 --- a/webapp/channels/src/actions/hosted_customer.tsx +++ b/webapp/channels/src/actions/hosted_customer.tsx @@ -1,124 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {Stripe} from '@stripe/stripe-js'; -import {getCode} from 'country-list'; - -import type {CreateSubscriptionRequest} from '@mattermost/types/cloud'; import type {ServerError} from '@mattermost/types/errors'; -import type {SelfHostedExpansionRequest, SelfHostedSignupSuccessResponse} from '@mattermost/types/hosted_customer'; -import {SelfHostedSignupProgress} from '@mattermost/types/hosted_customer'; -import type {ValueOf} from '@mattermost/types/utilities'; import {HostedCustomerTypes} from 'mattermost-redux/action_types'; -import {bindClientFunc} from 'mattermost-redux/actions/helpers'; import {Client4} from 'mattermost-redux/client'; -import {getSelfHostedErrors} from 'mattermost-redux/selectors/entities/hosted_customer'; -import type {ActionFunc, ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions'; - -import {getConfirmCardSetup} from 'components/payment_form/stripe'; - -import type {StripeSetupIntent, BillingDetails} from 'types/cloud/sku'; -import type {GlobalState} from 'types/store'; - -function selfHostedNeedsConfirmation(progress: ValueOf): boolean { - switch (progress) { - case SelfHostedSignupProgress.START: - case SelfHostedSignupProgress.CREATED_CUSTOMER: - case SelfHostedSignupProgress.CREATED_INTENT: - return true; - default: - return false; - } -} - -const STRIPE_UNEXPECTED_STATE = 'setup_intent_unexpected_state'; -const STRIPE_ALREADY_SUCCEEDED = 'You cannot update this SetupIntent because it has already succeeded.'; - -export function confirmSelfHostedSignup( - stripe: Stripe, - stripeSetupIntent: StripeSetupIntent, - cwsMockMode: boolean, - billingDetails: BillingDetails, - initialProgress: ValueOf, - subscriptionRequest: CreateSubscriptionRequest, -): ActionFuncAsync { - return async (dispatch) => { - const cardSetupFunction = getConfirmCardSetup(cwsMockMode); - const confirmCardSetup = cardSetupFunction(stripe.confirmCardSetup); - - const shouldConfirmCard = selfHostedNeedsConfirmation(initialProgress); - if (shouldConfirmCard) { - const result = await confirmCardSetup( - stripeSetupIntent.client_secret, - { - payment_method: { - card: billingDetails.card, - billing_details: { - name: billingDetails.name, - address: { - line1: billingDetails.address, - line2: billingDetails.address2, - city: billingDetails.city, - state: billingDetails.state, - country: getCode(billingDetails.country), - postal_code: billingDetails.postalCode, - }, - }, - }, - }, - ); - if (!result) { - return {data: false, error: 'failed to confirm card with Stripe'}; - } - - const {setupIntent, error: stripeError} = result; - - if (stripeError) { - if (stripeError.code === STRIPE_UNEXPECTED_STATE && stripeError.message === STRIPE_ALREADY_SUCCEEDED && stripeError.setup_intent?.status === 'succeeded') { - dispatch({ - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS, - data: SelfHostedSignupProgress.CONFIRMED_INTENT, - }); - } else { - return {data: false, error: stripeError.message || 'Stripe failed to confirm payment method'}; - } - } else { - if (setupIntent === null || setupIntent === undefined) { - return {data: false, error: 'Stripe did not return successful setup intent'}; - } - - if (setupIntent.status !== 'succeeded') { - return {data: false, error: `Stripe setup intent status was: ${setupIntent.status}`}; - } - dispatch({ - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS, - data: SelfHostedSignupProgress.CONFIRMED_INTENT, - }); - } - } - - let confirmResult; - try { - confirmResult = await Client4.confirmSelfHostedSignup(stripeSetupIntent.id, subscriptionRequest); - dispatch({ - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS, - data: confirmResult.progress, - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - - // unprocessable entity, e.g. failed export compliance - if (error.status_code === 422) { - return {data: false, error: error.status_code}; - } - return {data: false, error}; - } - - return {data: confirmResult.license}; - }; -} +import type {ThunkActionFunc} from 'mattermost-redux/types/actions'; export function getSelfHostedProducts(): ThunkActionFunc> { return async (dispatch) => { @@ -143,147 +30,3 @@ export function getSelfHostedProducts(): ThunkActionFunc> { - return async (dispatch) => { - try { - dispatch({ - type: HostedCustomerTypes.SELF_HOSTED_INVOICES_REQUEST, - }); - const result = await Client4.getSelfHostedInvoices(); - if (result) { - dispatch({ - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_INVOICES, - data: result, - }); - } - } catch (error) { - dispatch({ - type: HostedCustomerTypes.SELF_HOSTED_INVOICES_FAILED, - }); - return error; - } - return true; - }; -} -export function retryFailedHostedCustomerFetches(): ActionFunc { - return (dispatch, getState) => { - const errors = getSelfHostedErrors(getState()); - if (Object.keys(errors).length === 0) { - return {data: true}; - } - - if (errors.products) { - dispatch(getSelfHostedProducts()); - } - - if (errors.invoices) { - dispatch(getSelfHostedInvoices()); - } - - return {data: true}; - }; -} - -export function submitTrueUpReview() { - return bindClientFunc({ - clientFunc: Client4.submitTrueUpReview, - onSuccess: [HostedCustomerTypes.RECEIVED_TRUE_UP_REVIEW_BUNDLE], - onFailure: HostedCustomerTypes.TRUE_UP_REVIEW_PROFILE_FAILED, - onRequest: HostedCustomerTypes.TRUE_UP_REVIEW_PROFILE_REQUEST, - }); -} - -export function getTrueUpReviewStatus() { - return bindClientFunc({ - clientFunc: Client4.getTrueUpReviewStatus, - onSuccess: [HostedCustomerTypes.RECEIVED_TRUE_UP_REVIEW_STATUS], - onFailure: HostedCustomerTypes.TRUE_UP_REVIEW_STATUS_FAILED, - onRequest: HostedCustomerTypes.TRUE_UP_REVIEW_STATUS_REQUEST, - }); -} - -export function confirmSelfHostedExpansion( - stripe: Stripe, - stripeSetupIntent: StripeSetupIntent, - cwsMockMode: boolean, - billingDetails: BillingDetails, - initialProgress: ValueOf, - expansionRequest: SelfHostedExpansionRequest, -): ActionFuncAsync { - return async (dispatch) => { - const cardSetupFunction = getConfirmCardSetup(cwsMockMode); - const confirmCardSetup = cardSetupFunction(stripe.confirmCardSetup); - - const shouldConfirmCard = selfHostedNeedsConfirmation(initialProgress); - if (shouldConfirmCard) { - const result = await confirmCardSetup( - stripeSetupIntent.client_secret, - { - payment_method: { - card: billingDetails.card, - billing_details: { - name: billingDetails.name, - address: { - line1: billingDetails.address, - line2: billingDetails.address2, - city: billingDetails.city, - state: billingDetails.state, - country: getCode(billingDetails.country), - postal_code: billingDetails.postalCode, - }, - }, - }, - }, - ); - - if (!result) { - return {data: false, error: 'failed to confirm card with Stripe'}; - } - - const {setupIntent, error: stripeError} = result; - - if (stripeError) { - if (stripeError.code === STRIPE_UNEXPECTED_STATE && stripeError.message === STRIPE_ALREADY_SUCCEEDED && stripeError.setup_intent?.status === 'succeeded') { - dispatch({ - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS, - data: SelfHostedSignupProgress.CONFIRMED_INTENT, - }); - } else { - return {data: false, error: stripeError.message || 'Stripe failed to confirm payment method'}; - } - } else { - if (setupIntent === null || setupIntent === undefined) { - return {data: false, error: 'Stripe did not return successful setup intent'}; - } - - if (setupIntent.status !== 'succeeded') { - return {data: false, error: `Stripe setup intent status was: ${setupIntent.status}`}; - } - dispatch({ - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS, - data: SelfHostedSignupProgress.CONFIRMED_INTENT, - }); - } - } - - let confirmResult; - try { - confirmResult = await Client4.confirmSelfHostedExpansion(stripeSetupIntent.id, expansionRequest); - dispatch({ - type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS, - data: confirmResult.progress, - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - - // unprocessable entity, e.g. failed export compliance - if (error.status_code === 422) { - return {data: false, error: error.status_code}; - } - return {data: false, error}; - } - - return {data: confirmResult.license}; - }; -} diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index 779fd850fb..29a1881a7a 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -326,15 +326,7 @@ const AdminDefinition: AdminDefinitionType = { /> ), sectionTitle: defineMessage({id: 'admin.sidebar.billing', defaultMessage: 'Billing & Account'}), - isHidden: it.any( - it.not(it.enterpriseReady), - it.not(it.userHasReadPermissionOnResource('billing')), - it.not(it.licensed), - it.all( - it.not(it.licensedForFeature('Cloud')), - it.configIsFalse('ServiceSettings', 'SelfHostedPurchase'), - ), - ), + isHidden: it.not(it.licensedForFeature('Cloud')), subsections: { subscription: { url: 'billing/subscription', @@ -357,6 +349,7 @@ const AdminDefinition: AdminDefinitionType = { id: 'BillingHistory', component: BillingHistory, }, + isHidden: it.not(it.licensedForFeature('Cloud')), isDisabled: it.not(it.userHasWritePermissionOnResource('billing')), }, company_info: { diff --git a/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx b/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx index d02f4c2d07..009672710f 100644 --- a/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx +++ b/webapp/channels/src/components/admin_console/admin_sidebar/admin_sidebar.test.tsx @@ -3,7 +3,6 @@ import React from 'react'; -import {SelfHostedSignupProgress} from '@mattermost/types/cloud'; import type {ExperimentalSettings, PluginSettings, SSOSettings, Office365Settings} from '@mattermost/types/config'; import {RESOURCE_KEYS} from 'mattermost-redux/constants/permissions_sysconsole'; @@ -93,9 +92,6 @@ describe('components/AdminSidebar', () => { limits: {}, }, errors: {}, - selfHostedSignup: { - progress: SelfHostedSignupProgress.START, - }, }, showTaskList: false, }; diff --git a/webapp/channels/src/components/admin_console/billing/billing_history.test.tsx b/webapp/channels/src/components/admin_console/billing/billing_history.test.tsx index e1cd6bdd6a..8eed69ca35 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_history.test.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_history.test.tsx @@ -163,79 +163,6 @@ describe('components/admin_console/billing/billing_history', () => { }); }); -describe('BillingHistory -- self-hosted', () => { - // required state to mount using the provider - const state = { - entities: { - general: { - license: { - IsLicensed: 'true', - Cloud: 'false', - }, - config: { - DiagnosticsEnabled: 'false', - }, - }, - users: { - currentUserId: 'current_user_id', - profiles: { - current_user_id: {roles: 'system_role'}, - }, - }, - hostedCustomer: { - errors: {}, - invoices: { - invoices: { - in_1KNb3DI67GP2qpb4ueaJYBt8: invoiceA, - in_1KIWNTI67GP2qpb4KjGj1KAy: invoiceB, - }, - invoicesLoaded: true, - }, - }, - }, - views: {}, - }; - - test('Billing history section shows template when no invoices have been emitted yet', () => { - const noBillingHistoryState = { - ...state, - entities: {...state.entities, hostedCustomer: {invoices: {invoices: {}, invoicesLoaded: true}, errors: {}}}, - }; - renderWithContext( - , - noBillingHistoryState, - ); - - expect(screen.queryByText('Date')).not.toBeInTheDocument(); - expect(screen.queryByText('Description')).not.toBeInTheDocument(); - expect(screen.queryByText('Total')).not.toBeInTheDocument(); - expect(screen.queryByText('Status')).not.toBeInTheDocument(); - - expect(screen.queryByTestId(invoiceA.number)).not.toBeInTheDocument(); - expect(screen.queryByTestId(invoiceB.number)).not.toBeInTheDocument(); - - expect(screen.queryByTestId(invoiceA.id)).not.toBeInTheDocument(); - expect(screen.queryByTestId(invoiceB.id)).not.toBeInTheDocument(); - - expect(screen.getByRole('link')).toHaveAttribute('href', HostedCustomerLinks.SELF_HOSTED_BILLING + '?utm_source=mattermost&utm_medium=in-product&utm_content=billing_history&uid=current_user_id&sid='); - expect(screen.getByRole('link')).toHaveTextContent('See how billing works'); - expect(screen.getByTestId('no-invoices')).toHaveTextContent(NO_INVOICES_LEGEND); - }); - - test('Billing history section shows two invoices to download', () => { - renderWithContext( - , - state, - ); - - expect(screen.queryByText('Date')).toBeInTheDocument(); - expect(screen.queryByText('Description')).toBeInTheDocument(); - expect(screen.queryByText('Total')).toBeInTheDocument(); - expect(screen.queryByText('Status')).toBeInTheDocument(); - expect(screen.getAllByTestId('billingHistoryTableRow')).toHaveLength(2); - }); -}); - describe('NoBillingHistorySection', () => { const state = {entities: {users: {}, general: {config: {}, license: {}}}} as any; test('goes to cloud docs on cloud', () => { diff --git a/webapp/channels/src/components/admin_console/billing/billing_history.tsx b/webapp/channels/src/components/admin_console/billing/billing_history.tsx index c351adddfa..c3e10d5f9a 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_history.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_history.tsx @@ -7,9 +7,7 @@ import {useDispatch, useSelector} from 'react-redux'; import {getInvoices} from 'mattermost-redux/actions/cloud'; import {getCloudErrors, getCloudInvoices, isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud'; -import {getSelfHostedErrors, getSelfHostedInvoices} from 'mattermost-redux/selectors/entities/hosted_customer'; -import {getSelfHostedInvoices as getSelfHostedInvoicesAction} from 'actions/hosted_customer'; import {pageVisited, trackEvent} from 'actions/telemetry_actions'; import CloudFetchError from 'components/cloud_fetch_error'; @@ -65,14 +63,14 @@ export const NoBillingHistorySection = (props: NoBillingHistorySectionProps) => const BillingHistory = () => { const dispatch = useDispatch(); const isCloud = useSelector(isCurrentLicenseCloud); - const invoices = useSelector(isCloud ? getCloudInvoices : getSelfHostedInvoices); - const {invoices: invoicesError} = useSelector(isCloud ? getCloudErrors : getSelfHostedErrors); + const invoices = useSelector(getCloudInvoices); + const {invoices: invoicesError} = useSelector(getCloudErrors); useEffect(() => { pageVisited('cloud_admin', 'pageview_billing_history'); }, []); useEffect(() => { - dispatch(isCloud ? getInvoices() : getSelfHostedInvoicesAction()); + dispatch(getInvoices()); }, [isCloud]); const billingHistoryTable = invoices && ; const areInvoicesEmpty = Object.keys(invoices || {}).length === 0; diff --git a/webapp/channels/src/components/admin_console/billing/billing_history_table.tsx b/webapp/channels/src/components/admin_console/billing/billing_history_table.tsx index 319a10dce4..33b0656e2e 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_history_table.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_history_table.tsx @@ -3,12 +3,11 @@ import React, {useState, useEffect} from 'react'; import {FormattedDate, FormattedMessage, FormattedNumber} from 'react-intl'; -import {useSelector, useDispatch} from 'react-redux'; +import {useDispatch} from 'react-redux'; import type {Invoice} from '@mattermost/types/cloud'; import {Client4} from 'mattermost-redux/client'; -import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud'; import {openModal} from 'actions/views/modals'; @@ -61,8 +60,6 @@ const getPaymentStatus = (status: string) => { export default function BillingHistoryTable({invoices}: BillingHistoryTableProps) { const dispatch = useDispatch(); - const isCloud = useSelector(isCurrentLicenseCloud); - const [billingHistory, setBillingHistory] = useState( undefined, ); @@ -161,7 +158,7 @@ export default function BillingHistoryTable({invoices}: BillingHistoryTableProps {''} {billingHistory?.map((invoice: Invoice) => { - const url = isCloud ? Client4.getInvoicePdfUrl(invoice.id) : Client4.getSelfHostedInvoicePdfUrl(invoice.id); + const url = Client4.getInvoicePdfUrl(invoice.id); return ( void; - ctaPrimary?: boolean; - upsellIsTrial?: boolean; -} - -const andMore = { - id: t('upsell_advantages.more'), - defaultMessage: 'And more...', -}; - -export default function UpsellCard(props: Props) { - const intl = useIntl(); - - const ctaClassname = classNames( - 'UpsellCard__cta', - { - btn: props.ctaPrimary, - 'btn-primary': props.ctaPrimary, - }, - ); - - let callToAction = ( - - ); - if (props.upsellIsTrial) { - callToAction = ( - <> - -

- -

- - ); - } - return ( -
-
- -
-
- {intl.formatMessage(props.title)} -
-
- {props.advantages.map((message: Message) => { - return ( -
- {intl.formatMessage(message)} -
- ); - })} - {props.andMore &&
- {intl.formatMessage(andMore)} -
- } -
-
- {callToAction} -
-
- ); -} - -export const tryEnterpriseCard = ( - -); - -export const ExploreEnterpriseCard = () => { - return ( - - ); -}; diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_feedback.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_feedback.tsx deleted file mode 100644 index c6a28fb4d5..0000000000 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_feedback.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {injectIntl} from 'react-intl'; -import type {WrappedComponentProps} from 'react-intl'; - -import type {Feedback} from '@mattermost/types/cloud'; - -import FeedbackModal from 'components/feedback_modal/feedback'; -import type {FeedbackOption} from 'components/feedback_modal/feedback'; - -type Props = { - onSubmit: (deleteFeedback: Feedback) => void; -} &WrappedComponentProps - -const DeleteFeedbackModal = (props: Props) => { - const deleteFeedbackModalTitle = props.intl.formatMessage({ - id: 'feedback.deleteWorkspace.feedbackTitle', - defaultMessage: 'Please share your reason for deleting', - }); - - const placeHolder = props.intl.formatMessage({ - id: 'feedback.deleteWorkspace.feedbackPlaceholder', - defaultMessage: 'Please tell us why you are deleting', - }); - - const deleteButtonText = props.intl.formatMessage({ - id: 'feedback.deleteWorkspace.submitText', - defaultMessage: 'Delete Workspace', - }); - - const deleteFeedbackOptions: FeedbackOption[] = [ - { - translatedMessage: props.intl.formatMessage({ - id: 'feedback.deleteWorkspace.feedbackNoValue', - defaultMessage: 'No longer found value', - }), - submissionValue: 'No longer found value', - }, - { - translatedMessage: props.intl.formatMessage({ - id: 'feedback.deleteWorkspace.feedbackMoving', - defaultMessage: 'Moving to a different solution', - }), - submissionValue: 'Moving to a different solution', - }, - { - translatedMessage: props.intl.formatMessage({ - id: 'feedback.deleteWorkspace.feedbackMistake', - defaultMessage: 'Created a workspace by mistake', - }), - submissionValue: 'Created a workspace by mistake', - }, - { - translatedMessage: props.intl.formatMessage({ - id: 'feedback.deleteWorkspace.feedbackHosting', - defaultMessage: 'Moving to hosting my own Mattermost instance (self-hosted)', - }), - submissionValue: 'Moving to hosting my own Mattermost instance (self-hosted)', - }, - ]; - - return ( - - ); -}; - -export default injectIntl(DeleteFeedbackModal); diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_cta.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_cta.tsx deleted file mode 100644 index 05a9a2fb55..0000000000 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_cta.tsx +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage, defineMessages} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; - -import {getCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud'; -import {getLicense} from 'mattermost-redux/selectors/entities/general'; - -import {trackEvent} from 'actions/telemetry_actions'; -import {openModal} from 'actions/views/modals'; - -import {CloudProducts, ModalIdentifiers} from 'utils/constants'; -import {isCloudLicense} from 'utils/license_utils'; - -import DeleteWorkspaceModal from './delete_workspace_modal'; - -export const messages = defineMessages({ - title: {id: 'admin.billing.subscription.deleteWorkspaceSection.title', defaultMessage: 'Delete your workspace'}, -}); -export default function DeleteWorkspaceCTA() { - const dispatch = useDispatch(); - - const workspaceUrl = window.location.host; - - const license = useSelector(getLicense); - const subscription = useSelector(getCloudSubscription); - const product = useSelector(getSubscriptionProduct); - - const isNotCloud = !isCloudLicense(license); - const isFreeTrial = subscription?.is_free_trial === 'true'; - const isEnterprise = product?.sku === CloudProducts.ENTERPRISE; - - const handleOnClickDelete = () => { - trackEvent('cloud_admin', 'click_delete_workspace'); - - dispatch( - openModal({ - modalId: ModalIdentifiers.DELETE_WORKSPACE, - dialogType: DeleteWorkspaceModal, - dialogProps: { - callerCTA: 'system_console > billing > subscription > delete_workspace_cta', - }, - }), - ); - }; - - // Can only delete or downgrade via workspace deletion modal if: - // - the user has a cloud product - // - the user is on a free trial (enterprise product with trial status) - // - the user is on a starter subscription - // - the user is on a monthly professional subscription - // - // For clarity, workspaces with the following subscriptions may be deleted: - // - Cloud-Starter - // - Cloud-Professional (monthly) - // - Enterprise Free Trial - if (isNotCloud || (isEnterprise && !isFreeTrial)) { - return null; - } - - return ( -
-
-
- -
-
- {workspaceUrl} - ), - }} - /> -
- -
-
- ); -} diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss deleted file mode 100644 index 514dd90e0a..0000000000 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.scss +++ /dev/null @@ -1,71 +0,0 @@ -.DeleteWorkspaceModal { - width: 600px; - - .modal-body { - .GenericModal__body { - padding: 24px 24px 0 24px; - text-align: center; - - * { - margin-bottom: 10px; - } - } - } - - &__Icon { - padding-top: 8px; - } - - &__Title { - color: var(--sys-denim-center-channel-text); - font-family: Metropolis; - font-size: 22px; - font-weight: 700; - line-height: 28px; - } - - &__Usage { - color: var(--center-channel-color); - text-align: left; - - &-Highlighted { - color: black; - font-weight: bold; - } - } - - &__Warning { - color: var(--center-channel-color); - text-align: left; - } - - &__Buttons { - display: flex; - justify-content: space-between; - - button { - border-radius: 4px; - } - - &-Delete { - padding: 0; - border: none; - background: none; - color: var(--dnd-indicator); - font-weight: 600; - } - - &-Downgrade { - border-color: var(--denim-button-bg); - margin-left: auto; - background: none; - color: var(--denim-button-bg); - font-weight: 600; - } - - &-Cancel { - margin-left: 10px; - font-weight: 600; - } - } -} diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx deleted file mode 100644 index 7547a0f3a4..0000000000 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_modal.tsx +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage, defineMessages} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; - -import {GenericModal} from '@mattermost/components'; -import type {Feedback} from '@mattermost/types/cloud'; - -import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud'; -import {getLicense} from 'mattermost-redux/selectors/entities/general'; - -import {subscribeCloudSubscription, deleteWorkspace as deleteWorkspaceRequest} from 'actions/cloud'; -import {closeModal, openModal} from 'actions/views/modals'; - -import DeleteFeedbackModal from 'components/admin_console/billing/delete_workspace/delete_feedback'; -import DeleteWorkspaceProgressModal from 'components/admin_console/billing/delete_workspace/progress_modal'; -import ErrorModal from 'components/cloud_subscribe_result_modal/error'; -import SuccessModal from 'components/cloud_subscribe_result_modal/success'; -import useGetSubscription from 'components/common/hooks/useGetSubscription'; -import useGetUsage from 'components/common/hooks/useGetUsage'; -import useOpenDowngradeModal from 'components/common/hooks/useOpenDowngradeModal'; -import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg'; -import DowngradeFeedbackModal from 'components/feedback_modal/downgrade_feedback'; - -import {CloudProducts, ModalIdentifiers, StatTypes} from 'utils/constants'; -import {isCloudLicense} from 'utils/license_utils'; -import {fileSizeToString} from 'utils/utils'; - -import type {GlobalState} from 'types/store'; - -import DeleteWorkspaceFailureModal from './failure_modal'; -import DeleteWorkspaceSuccessModal from './success_modal'; - -import './delete_workspace_modal.scss'; - -type Props = { - callerCTA: string; -} - -export const messages = defineMessages({ - deleteButton: {id: 'admin.billing.subscription.deleteWorkspaceModal.deleteButton', defaultMessage: 'Delete Workspace'}, -}); - -export default function DeleteWorkspaceModal(props: Props) { - const dispatch = useDispatch(); - const openDowngradeModal = useOpenDowngradeModal(); - - // License/product checks. - const subscription = useGetSubscription(); - const product = useSelector(getSubscriptionProduct); - const isStarter = product?.sku === CloudProducts.STARTER; - const isEnterprise = product?.sku === CloudProducts.ENTERPRISE; - const license = useSelector(getLicense); - const isNotCloud = !isCloudLicense(license); - - // Starter product for downgrade purposes. - const starterProduct = useSelector((state: GlobalState) => { - return Object.values(state.entities.cloud.products || {}).find((product) => { - return product.sku === CloudProducts.STARTER; - }); - }); - - // Get usage information in an attempt to defer customer from deleting. - const usage = useGetUsage(); - const totalFileSize = fileSizeToString(usage.files.totalStorage); - const totalMessages = useSelector((state: GlobalState) => { - if (!state.entities.admin.analytics) { - return 0; - } - return state.entities.admin.analytics[StatTypes.TOTAL_POSTS]; - }); - - // Handles the delete button clicks. - const handleClickDeleteWorkspace = () => { - // Close the delete workspace modal and ope na feedback modal, with a workspace - // deletion upon completion of the feedback. - dispatch(closeModal(ModalIdentifiers.DELETE_WORKSPACE)); - dispatch(openModal({ - modalId: ModalIdentifiers.FEEDBACK, - dialogType: DeleteFeedbackModal, - dialogProps: { - onSubmit: deleteWorkspace, - }, - })); - }; - - // Handles the downgrade button clicks. - const handleClickDowngradeWorkspace = () => { - // Close the delete workspace modal and ope na feedback modal, with a workspace - // downgrade upon completion of the feedback. - dispatch(closeModal(ModalIdentifiers.DELETE_WORKSPACE)); - dispatch(openModal({ - modalId: ModalIdentifiers.FEEDBACK, - dialogType: DowngradeFeedbackModal, - dialogProps: { - onSubmit: downgradeWorkspace, - }, - })); - }; - - // Handles the cancel button clicks. - const handleClickCancel = () => { - dispatch(closeModal(ModalIdentifiers.DELETE_WORKSPACE)); - dispatch(closeModal(ModalIdentifiers.FEEDBACK)); - }; - - // Processes the workspace deletion, opening and closing the appropriate modals (progress, success/failure). - const deleteWorkspace = async (deleteFeedback: Feedback) => { - dispatch(openModal({ - modalId: ModalIdentifiers.DELETE_WORKSPACE_PROGRESS, - dialogType: DeleteWorkspaceProgressModal, - })); - dispatch(closeModal(ModalIdentifiers.DOWNGRADE_MODAL)); - - if (subscription === undefined) { - return; - } - - const result = await dispatch(deleteWorkspaceRequest({subscription_id: subscription?.id, delete_feedback: deleteFeedback})); - - if (typeof result === 'boolean' && result) { - dispatch(closeModal(ModalIdentifiers.DELETE_WORKSPACE_PROGRESS)); - dispatch(openModal({ - modalId: ModalIdentifiers.DELETE_WORKSPACE_RESULT, - dialogType: DeleteWorkspaceSuccessModal, - })); - } else { // Failure - dispatch(openModal({ - modalId: ModalIdentifiers.DELETE_WORKSPACE_RESULT, - dialogType: DeleteWorkspaceFailureModal, - })); - dispatch(closeModal(ModalIdentifiers.DELETE_WORKSPACE_PROGRESS)); - } - }; - - // Processes the workspace downgrade, opening and closing the appropriate modals (progress, success/failure). - const downgradeWorkspace = async (downgradeFeedback: Feedback) => { - if (!starterProduct) { - return; - } - - const telemetryInfo = props.callerCTA + ' > delete_workspace_modal'; - openDowngradeModal({trackingLocation: telemetryInfo}); - - const result = await dispatch(subscribeCloudSubscription(starterProduct.id, undefined, 0, downgradeFeedback)); - - // Success - if (result.data) { - dispatch(closeModal(ModalIdentifiers.DOWNGRADE_MODAL)); - dispatch( - openModal({ - modalId: ModalIdentifiers.SUCCESS_MODAL, - dialogType: SuccessModal, - dialogProps: { - newProductName: starterProduct.name, - }, - }), - ); - } else { // Failure - dispatch(closeModal(ModalIdentifiers.DOWNGRADE_MODAL)); - dispatch( - openModal({ - modalId: ModalIdentifiers.ERROR_MODAL, - dialogType: ErrorModal, - dialogProps: { - backButtonAction: () => { - dispatch(openModal({ - modalId: ModalIdentifiers.DELETE_WORKSPACE, - dialogType: DeleteWorkspaceModal, - dialogProps: { - callerCTA: props.callerCTA, - }, - })); - }, - }, - }), - ); - } - }; - - if (isNotCloud) { - return null; - } - - return ( - -
- -
-
- -
-
- - - - -
-
- -
-
- - {!isStarter && !isEnterprise && - - } - -
-
- ); -} diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/failure_modal.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/failure_modal.tsx deleted file mode 100644 index 3d7a6d884a..0000000000 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/failure_modal.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {useDispatch} from 'react-redux'; - -import {closeModal, openModal} from 'actions/views/modals'; - -import PaymentFailedSvg from 'components/common/svg_images_components/payment_failed_svg'; - -import {ModalIdentifiers} from 'utils/constants'; - -import DeleteWorkspaceModal from './delete_workspace_modal'; -import ResultModal from './result_modal'; - -export default function DeleteWorkspaceFailureModal() { - const dispatch = useDispatch(); - - const handleButtonClick = () => { - dispatch(closeModal(ModalIdentifiers.DELETE_WORKSPACE_RESULT)); - dispatch(openModal({ - modalId: ModalIdentifiers.DELETE_WORKSPACE, - dialogType: DeleteWorkspaceModal, - dialogProps: { - callerCTA: 'delete_workspace_failure_modal', - }, - })); - }; - - const title = ( - - ); - - const subtitle = ( - - ); - - const buttonText = ( - - ); - - return ( - - } - contactSupportButtonVisible={true} - /> - ); -} diff --git a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx index 4df678ce60..1449293f5f 100644 --- a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx +++ b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.test.tsx @@ -113,13 +113,10 @@ describe('components/feature_discovery', () => { expect(screen.queryByText('Foo')).toBeInTheDocument(); //this option is visible only when it is cloud environment - expect(screen.getByRole('button', {name: 'Try free for 30 days'})).toBeInTheDocument(); - expect(screen.getAllByText('Try free for 30 days')).toHaveLength(2); + expect(screen.getByRole('button', {name: 'Contact sales'})).toBeInTheDocument(); expect(screen.getByTestId('featureDiscovery_secondaryCallToAction')).toHaveAttribute('href', 'https://test.mattermost.com/secondary/?utm_source=mattermost&utm_medium=in-product&utm_content=feature_discovery&uid=&sid='); - expect(screen.getByText('Privacy Policy')).toHaveAttribute('href', 'https://mattermost.com/pl/privacy-policy/?utm_source=mattermost&utm_medium=in-product&utm_content=feature_discovery&uid=&sid='); - const featureLink = screen.getByTestId('featureDiscovery_secondaryCallToAction'); expect(featureLink).toBeInTheDocument(); diff --git a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx index 1bdf9b1cea..31822c923a 100644 --- a/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx +++ b/webapp/channels/src/components/admin_console/feature_discovery/feature_discovery.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; import type {AnalyticsState} from '@mattermost/types/admin'; import type {CloudCustomer} from '@mattermost/types/cloud'; @@ -13,13 +13,11 @@ import {trackEvent} from 'actions/telemetry_actions'; import {EmbargoedEntityTrialError} from 'components/admin_console/license_settings/trial_banner/trial_banner'; import AlertBanner from 'components/alert_banner'; import PurchaseLink from 'components/announcement_bar/purchase_link/purchase_link'; -import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn'; import ExternalLink from 'components/external_link'; import FormattedMarkdownMessage from 'components/formatted_markdown_message'; import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn'; import LoadingSpinner from 'components/widgets/loading/loading_spinner'; -import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils'; import {TELEMETRY_CATEGORIES, AboutLinks, LicenseLinks, LicenseSkus} from 'utils/constants'; import {goToMattermostContactSalesForm} from 'utils/contact_support_sales'; import * as Utils from 'utils/utils'; @@ -147,13 +145,8 @@ export default class FeatureDiscovery extends React.PureComponent renderStartTrial = (learnMoreURL: string, gettingTrialError: React.ReactNode) => { const { isCloud, - isCloudTrial, - hadPrevCloudTrial, - isPaidSubscription, } = this.props; - const canRequestCloudFreeTrial = isCloud && !isCloudTrial && !hadPrevCloudTrial && !isPaidSubscription; - // by default we assume is not cloud, so the cta button is Start Trial (which will request a trial license) let ctaPrimaryButton = ( ); if (isCloud) { - // if all conditions are set for being able to request a cloud trial, then show the cta start cloud trial button - if (canRequestCloudFreeTrial) { - ctaPrimaryButton = ( - { + trackEvent(TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'click_enterprise_contact_sales_feature_discovery'); + this.contactSalesFunc(); + }} + > + - ); - if (this.props.cloudFreeDeprecated) { - ctaPrimaryButton = ( - - ); - } - } + + ); } return ( @@ -212,62 +195,35 @@ export default class FeatureDiscovery extends React.PureComponent /> {gettingTrialError} - {((!this.props.isCloud || canRequestCloudFreeTrial) && !this.props.cloudFreeDeprecated) &&

- {canRequestCloudFreeTrial ? ( - ( - {msg} - ), - linkEvaluation: (msg: React.ReactNode) => ( - - {msg} - - ), - linkPrivacy: (msg: React.ReactNode) => ( - - {msg} - - ), - }} - /> - ) : ( - ( - {msg} - ), - linkEvaluation: (msg: React.ReactNode) => ( - - {msg} - - ), - linkPrivacy: (msg: React.ReactNode) => ( - - {msg} - - ), - }} - /> - )} -

} + {(!this.props.isCloud) && (

+ + ( + {msg} + ), + linkEvaluation: (msg: React.ReactNode) => ( + + {msg} + + ), + linkPrivacy: (msg: React.ReactNode) => ( + + {msg} + + ), + }} + /> + +

)} ); }; @@ -375,22 +331,3 @@ export default class FeatureDiscovery extends React.PureComponent ); } } - -function FeatureDiscoveryCloudStartTrialButton(props: Omit, 'message'>) { - const message = useIntl().formatMessage( - { - id: 'admin.ldap_feature_discovery.call_to_action.primary.cloudFree', - defaultMessage: 'Try free for {trialLength} days', - }, - { - trialLength: FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS, - }, - ); - - return ( - - ); -} 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 c8d765a939..efb60cdcf3 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 @@ -20,7 +20,6 @@ exports[`components/admin_console/license_settings/LicenseSettings load screen a className="admin-console__banner_section" > { - const testLicense = { - ...license, - ExpiresAt: moment().add(61, 'days').valueOf().toString(), - }; - - const testState = mergeObjects(initialState, { - entities: { - general: { - license: testLicense, - }, - }, - }); - const props = { - ...baseProps, - license: testLicense, - }; - - jest.spyOn(useCanSelfHostedExpand, 'default').mockImplementation(() => true); - - renderWithContext( - , - testState, - ); - - expect(screen.getByText('+ Add seats')).toBeVisible(); - }); }); 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 68d7419a09..d57a26911f 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 @@ -394,8 +394,6 @@ export default class LicenseSettings extends React.PureComponent { } renewLicenseCard = () => { - const {isDisabled} = this.props; - if (isTrialLicense(this.props.license)) { return ( { license={this.props.license} isLicenseExpired={isLicenseExpired(this.props.license)} totalUsers={this.props.totalUsers} - isDisabled={isDisabled} /> ); } diff --git a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx index 13b4ac67a5..d862f667ae 100644 --- a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.test.tsx @@ -6,8 +6,6 @@ import React from 'react'; import {act} from 'react-dom/test-utils'; import {Provider} from 'react-redux'; -import {Client4} from 'mattermost-redux/client'; - import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import mockStore from 'tests/test_store'; @@ -71,31 +69,7 @@ describe('components/RenewalLicenseCard', () => { isDisabled: false, }; - test('should show Renew and Contact sales buttons when a renewal link is successfully returned', async () => { - const getRenewalLinkSpy = jest.spyOn(Client4, 'getRenewalLink'); - const promise = new Promise<{renewal_link: string}>((resolve) => { - resolve({ - renewal_link: 'https://testrenewallink', - }); - }); - getRenewalLinkSpy.mockImplementation(() => promise); - const store = mockStore(initialState); - const wrapper = mountWithIntl(); - - // wait for the promise to resolve and component to update - await actImmediate(wrapper); - - expect(wrapper.find('button').length).toEqual(2); - expect(wrapper.find('button').at(0).text().includes('Renew')).toBe(true); - expect(wrapper.find('button').at(1).text().includes('Contact sales')).toBe(true); - }); - - test('should show only Contact sales button when a renewal link is not able to renew license', async () => { - const getRenewalLinkSpy = jest.spyOn(Client4, 'getRenewalLink'); - const promise = new Promise<{renewal_link: string}>((resolve, reject) => { - reject(new Error('License cannot be renewed from portal')); - }); - getRenewalLinkSpy.mockImplementation(() => promise); + test('should show Contact sales button', async () => { const store = mockStore(initialState); const wrapper = mountWithIntl(); diff --git a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.tsx b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.tsx index 7d956740a2..e779baa344 100644 --- a/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/renew_license_card/renew_license_card.tsx @@ -2,16 +2,13 @@ // See LICENSE.txt for license information. import moment from 'moment'; -import React, {useEffect, useState} from 'react'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; import type {ClientLicense} from '@mattermost/types/config'; -import {Client4} from 'mattermost-redux/client'; - import AlertBanner from 'components/alert_banner'; import ContactUsButton from 'components/announcement_bar/contact_sales/contact_us'; -import RenewalLink from 'components/announcement_bar/renewal_link/'; import FormattedMarkdownMessage from 'components/formatted_markdown_message'; import {getSkuDisplayName} from 'utils/subscription'; @@ -23,23 +20,12 @@ export interface RenewLicenseCardProps { license: ClientLicense; isLicenseExpired: boolean; totalUsers: number; - isDisabled: boolean; } -const RenewLicenseCard: React.FC = ({license, totalUsers, isLicenseExpired, isDisabled}: RenewLicenseCardProps) => { - const [showContactSalesBtn, setShowContactSalesBtn] = useState(true); - useEffect(() => { - Client4.getRenewalLink().catch(() => { - // if we have an error with getting the renewal link, do not show contact sales button because - // it is already shown by the RenewalLink component - setShowContactSalesBtn(false); - }); - }, []); - +const RenewLicenseCard: React.FC = ({license, totalUsers, isLicenseExpired}: RenewLicenseCardProps) => { let bannerType: 'info' | 'warning' | 'danger' = 'info'; const endOfLicense = moment.utc(new Date(parseInt(license?.ExpiresAt, 10))); const daysToEndLicense = getRemainingDaysFromFutureTimestamp(parseInt(license?.ExpiresAt, 10)); - const renewLinkTelemetry = {success: 'renew_license_admin_console_success', error: 'renew_license_admin_console_fail'}; const contactSalesBtn = (
= ({license, totalUsers, /> ); } - const customBtnText = ( - - ); const message = (
= ({license, totalUsers, />
- - {showContactSalesBtn && contactSalesBtn} + {contactSalesBtn}
); diff --git a/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.scss b/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.scss index 31156cfa5e..17d3770c74 100644 --- a/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.scss +++ b/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.scss @@ -11,8 +11,11 @@ } .RenewLicenseCard__buttons { - button { - padding: 6px 12px !important; + .contact_us_primary_cta { + padding: 6px 12px; + margin-left: 0px; + background-color: var(--sys-button-bg); + color: var(--sys-center-channel-bg); } } @@ -28,6 +31,5 @@ button.contact-us { padding: 11px 19px; - margin-left: 10px; } } diff --git a/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.tsx b/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.tsx index 109387131b..beab7983f7 100644 --- a/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/trial_license_card/trial_license_card.tsx @@ -9,7 +9,6 @@ import type {ClientLicense} from '@mattermost/types/config'; import AlertBanner from 'components/alert_banner'; import ContactUsButton from 'components/announcement_bar/contact_sales/contact_us'; -import PurchaseLink from 'components/announcement_bar/purchase_link/purchase_link'; import FormattedMarkdownMessage from 'components/formatted_markdown_message'; import {daysToLicenseExpire} from 'utils/license_utils'; @@ -57,16 +56,8 @@ const TrialLicenseCard: React.FC = ({license}: Props) => { {messageBody()}
- - } - />
diff --git a/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx b/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx index f8547735a0..3f3e19ef5b 100644 --- a/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx +++ b/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx @@ -10,7 +10,6 @@ import type {ClientLicense} from '@mattermost/types/config'; import * as AdminActions from 'actions/admin_actions.jsx'; import ActivatedUserCard from 'components/analytics/activated_users_card'; -import TrueUpReview from 'components/analytics/true_up_review'; import ExternalLink from 'components/external_link'; import AdminHeader from 'components/widgets/admin_console/admin_header'; @@ -444,7 +443,6 @@ export default class SystemAnalytics extends React.PureComponent {
{banner} -
{systemCards} {dailyActiveUsers} diff --git a/webapp/channels/src/components/analytics/team_analytics/team_analytics.tsx b/webapp/channels/src/components/analytics/team_analytics/team_analytics.tsx index c5cddaa8cc..0eb1fa1291 100644 --- a/webapp/channels/src/components/analytics/team_analytics/team_analytics.tsx +++ b/webapp/channels/src/components/analytics/team_analytics/team_analytics.tsx @@ -21,7 +21,6 @@ import {messages as activatedUsersCardsMessages} from 'components/analytics/acti import LineChart from 'components/analytics/line_chart'; import StatisticCount from 'components/analytics/statistic_count'; import TableChart from 'components/analytics/table_chart'; -import TrueUpReview from 'components/analytics/true_up_review'; import ExternalLink from 'components/external_link'; import LoadingScreen from 'components/loading_screen'; import AdminHeader from 'components/widgets/admin_console/admin_header'; @@ -316,7 +315,6 @@ export default class TeamAnalytics extends React.PureComponent {
- {banner}
* { - margin-bottom: 10px; - } - - &__cardBody > svg { - margin-left: 15px; - } - - &__dueDate { - :first-child { - color: rgba(var(--sys-center-channel-color-rgb), 0.75); - } - } - - &__warning { - color: var(--warning-text); - font-size: 24px; - } - - &__submit { - font-weight: 600; - - &--error { - background: rgba(var(--button-bg-rgb), 0.16) !important; - color: var(--button-bg) !important; - font-weight: 600; - } - } -} diff --git a/webapp/channels/src/components/analytics/true_up_review.test.tsx b/webapp/channels/src/components/analytics/true_up_review.test.tsx deleted file mode 100644 index d233b30d58..0000000000 --- a/webapp/channels/src/components/analytics/true_up_review.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import type {GlobalState} from '@mattermost/types/store'; -import type {DeepPartial} from '@mattermost/types/utilities'; - -import * as useCWSAvailabilityCheckAll from 'components/common/hooks/useCWSAvailabilityCheck'; - -import {renderWithContext, screen} from 'tests/react_testing_utils'; -import {LicenseSkus} from 'utils/constants'; -import {TestHelper as TH} from 'utils/test_helper'; - -import TrueUpReview from './true_up_review'; - -describe('TrueUpReview', () => { - const showsTrueUpReviewState: DeepPartial = { - entities: { - general: { - license: TH.getLicenseMock({ - IsGovSku: 'false', - Cloud: 'false', - SkuShortName: LicenseSkus.Enterprise, - IsLicensed: 'true', - }), - config: { - EnableDiagnostics: 'true', - }, - }, - users: { - currentUserId: 'userId', - profiles: { - userId: TH.getUserMock({ - id: 'userId', - roles: 'system_admin', - }), - }, - }, - hostedCustomer: { - trueUpReviewStatus: { - - // one day in future so we're sure it will display, - // regardless of future changes to "do we show it if it already passed" - due_date: Date.now() + (1000 * 60 * 60 * 24), - complete: false, - getRequestState: 'IDLE', - }, - trueUpReviewProfile: { - getRequestState: 'IDLE', - content: '', - }, - errors: {}, - }, - }, - - }; - - it('regular self hosted license (NOT air-gapped) in the true up window sees content', () => { - jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available); - - renderWithContext(, showsTrueUpReviewState); - screen.getByText('Share to Mattermost'); - }); - - it('regular self hosted license thats air gapped sees download button only', () => { - jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Unavailable); - - renderWithContext(, showsTrueUpReviewState); - screen.getByText('Download Data'); - expect(screen.queryByText('Share to Mattermost')).not.toBeInTheDocument(); - }); - - it('displays the panel regardless of the config value for EnableDiagnostic', () => { - const store = JSON.parse(JSON.stringify(showsTrueUpReviewState)); - store.entities.general.config.EnableDiagnostics = 'false'; - jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available); - - renderWithContext(, store); - screen.getByText('Share to Mattermost'); - }); - - it('gov sku self-hosted license does not see true up content', () => { - const store = JSON.parse(JSON.stringify(showsTrueUpReviewState)); - store.entities.general.license.IsGovSku = 'true'; - jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available); - - renderWithContext(, store); - expect(screen.queryByText('Share to Mattermost')).not.toBeInTheDocument(); - }); -}); diff --git a/webapp/channels/src/components/analytics/true_up_review.tsx b/webapp/channels/src/components/analytics/true_up_review.tsx deleted file mode 100644 index 7a5a973cb8..0000000000 --- a/webapp/channels/src/components/analytics/true_up_review.tsx +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import classNames from 'classnames'; -import moment from 'moment'; -import React, {useEffect} from 'react'; -import {FormattedMessage} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; - -import type {GlobalState} from '@mattermost/types/store'; - -import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud'; -import {getLicense} from 'mattermost-redux/selectors/entities/general'; -import { - getSelfHostedErrors, - getTrueUpReviewProfile as trueUpReviewProfileSelector, - getTrueUpReviewStatus as trueUpReviewStatusSelector, -} from 'mattermost-redux/selectors/entities/hosted_customer'; -import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; - -import {submitTrueUpReview, getTrueUpReviewStatus} from 'actions/hosted_customer'; -import {pageVisited} from 'actions/telemetry_actions'; - -import useCWSAvailabilityCheck, {CSWAvailabilityCheckTypes} from 'components/common/hooks/useCWSAvailabilityCheck'; -import ExternalLink from 'components/external_link'; -import CheckMarkSvg from 'components/widgets/icons/check_mark_icon'; -import WarningIcon from 'components/widgets/icons/fa_warning_icon'; - -import {DocLinks, TELEMETRY_CATEGORIES} from 'utils/constants'; -import {getIsStarterLicense, getIsGovSku} from 'utils/license_utils'; - -import './true_up_review.scss'; - -const TrueUpReview: React.FC = () => { - const dispatch = useDispatch(); - const isCloud = useSelector(isCurrentLicenseCloud); - const cwsAvailability = useCWSAvailabilityCheck(); - const isAirGapped = cwsAvailability !== CSWAvailabilityCheckTypes.Available; - const reviewProfile = useSelector(trueUpReviewProfileSelector); - const reviewStatus = useSelector(trueUpReviewStatusSelector); - const isSystemAdmin = useSelector(isCurrentUserSystemAdmin); - const license = useSelector(getLicense); - const isLicensed = license.IsLicensed === 'true'; - const isStarter = getIsStarterLicense(license); - const isGovSku = getIsGovSku(license); - - // A license is eligible for true up if: - // * a license exists for the customer - // * are self-hosted (not cloud) - // * are not on starter/free - // * are not a government sku - const licenseIsTrueUpEligible = isLicensed && !isCloud && !isStarter && !isGovSku; - const trueUpReviewError = useSelector((state: GlobalState) => { - const errors = getSelfHostedErrors(state); - return Boolean(errors.trueUpReview); - }); - - useEffect(() => { - if (reviewStatus.getRequestState !== 'IDLE' || !licenseIsTrueUpEligible) { - return; - } - - dispatch(getTrueUpReviewStatus()); - }, [dispatch, reviewStatus.getRequestState, licenseIsTrueUpEligible]); - - // Download the review profile as a base64 encoded json file when the review request is submitted. - useEffect(() => { - if (reviewProfile.getRequestState === 'LOADING') { - return; - } - - if (reviewProfile.getRequestState === 'OK' && !reviewStatus.complete && isAirGapped && !trueUpReviewError && reviewProfile.content.length > 0) { - // Create the bundle as a blob containing base64 encoded json data and assign it to a link element. - const blob = new Blob([reviewProfile.content], {type: 'application/text'}); - const href = URL.createObjectURL(blob); - - const link = document.createElement('a'); - const date = moment().format('MM-DD-YYYY'); - link.href = href; - link.download = `True Up-${license.Id}-${date}.txt`; - document.body.appendChild(link); - link.click(); - - // Remove link and revoke object url to avoid memory leaks. - document.body.removeChild(link); - URL.revokeObjectURL(href); - dispatch(getTrueUpReviewStatus()); - } - }, [isAirGapped, reviewProfile, reviewProfile.getRequestState, trueUpReviewError]); - - const formattedDueDate = (): string => { - if (!reviewStatus.due_date) { - return ''; - } - - // Convert from milliseconds - const date = new Date(reviewStatus.due_date); - return moment(date).format('MMMM DD, YYYY'); - }; - - const handleSubmitReview = () => { - dispatch(submitTrueUpReview()); - }; - - const dueDate = ( -
- - - - - {formattedDueDate()} - -
- ); - - const submitButton = ( - - ); - - const errorStatus = ( - <> - - - {submitButton} - - ); - - const successStatus = ( - <> - - - - - ); - - const trueUpDocsLink = ( - - - - ); - - const reviewDetails = ( - <> - {dueDate} - - {submitButton} - - ); - - const cardContent = () => { - if (reviewProfile.getRequestState !== 'OK' && trueUpReviewError) { - return errorStatus; - } - - // If we just submitted and the review status is set as complete, show the success - // status details. - if (reviewProfile.getRequestState === 'OK') { - return successStatus; - } - - // If the due date is empty we still have the default state. - if (!reviewStatus.due_date) { - return null; - } - - return reviewDetails; - }; - - // Only show the true up review section if the user is an admin and we're not using a cloud instance. - if (!licenseIsTrueUpEligible || !isSystemAdmin) { - return null; - } - - // Only display the review details if we are within 2 weeks of the review due date. - const visibilityStart = moment(reviewStatus.due_date).startOf('day').subtract(30, 'days'); - if (moment().isSameOrBefore(visibilityStart)) { - return null; - } - - // If the review has already been submitted, don't show anything. - if (reviewStatus.complete) { - return null; - } - - pageVisited(TELEMETRY_CATEGORIES.TRUE_UP_REVIEW, 'pageview_true_up_review'); - - return ( -
-
-
-
- -
-
-
-
- {cardContent()} -
-
- ); -}; - -export default TrueUpReview; - diff --git a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx index f837e001a6..992bde0532 100644 --- a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx +++ b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx @@ -8,20 +8,17 @@ import {useDispatch, useSelector} from 'react-redux'; import type {PreferenceType} from '@mattermost/types/preferences'; import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {getConfig} from 'mattermost-redux/selectors/entities/admin'; import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; -import useCanSelfHostedExpand from 'components/common/hooks/useCanSelfHostedExpand'; import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; -import {StatTypes, Preferences, AnnouncementBarTypes, ConsolePages} from 'utils/constants'; +import {StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants'; import {calculateOverageUserActivated} from 'utils/overage_team'; -import {getSiteURL} from 'utils/url'; import type {GlobalState} from 'types/store'; @@ -60,9 +57,6 @@ const OverageUsersBanner = () => { activeUsers, seatsPurchased, }); - const isSelfHostedExpansionEnabled = useSelector(getConfig)?.ServiceSettings?.SelfHostedPurchase; - const canSelfHostedExpand = useCanSelfHostedExpand() && isSelfHostedExpansionEnabled; - const siteURL = getSiteURL(); const prefixPreferences = isOver10PercerntPurchasedSeats ? 'error' : 'warn'; const prefixLicenseId = (license.Id || '').substring(0, 8); const preferenceName = `${prefixPreferences}_overage_seats_${prefixLicenseId}`; @@ -73,16 +67,10 @@ const OverageUsersBanner = () => { const hasPermission = isAdmin && isOverageState && !isCloud; const { cta, - expandableLink, trackEventFn, - getRequestState, - isExpandable, } = useExpandOverageUsersCheck({ - shouldRequest: hasPermission && !adminHasDismissed({isWarningBanner: isBetween5PercerntAnd10PercentPurchasedSeats, overagePreferences, preferenceName}), - licenseId: license.Id, isWarningState: isBetween5PercerntAnd10PercentPurchasedSeats, banner: 'global banner', - canSelfHostedExpand: canSelfHostedExpand || false, }); const handleClose = () => { @@ -94,31 +82,19 @@ const OverageUsersBanner = () => { }])); }; - const handleUpdateSeatsSelfServeClick = (e: React.MouseEvent) => { - e.preventDefault(); - trackEventFn('Self Serve'); - - if (canSelfHostedExpand) { - window.open(`${siteURL}/${ConsolePages.LICENSE}?action=show_expansion_modal`); - return; - } - - window.open(expandableLink(license.Id), '_blank'); - }; - const handleContactSalesClick = (e: React.MouseEvent) => { e.preventDefault(); trackEventFn('Contact Sales'); openContactSales(); }; - const handleClick = isExpandable ? handleUpdateSeatsSelfServeClick : handleContactSalesClick; + const handleClick = handleContactSalesClick; if (!hasPermission || adminHasDismissed({isWarningBanner: isBetween5PercerntAnd10PercentPurchasedSeats, overagePreferences, preferenceName})) { return null; } - let message = ( + const message = ( { }} />); - if (canSelfHostedExpand) { - message = ( - ); - } - return ( { isTallBanner={true} icon={} handleClose={handleClose} - showCTA={getRequestState !== 'IDLE' && getRequestState !== 'LOADING'} /> ); }; diff --git a/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx b/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx index 4c6a8389d7..6278a06e46 100644 --- a/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx +++ b/webapp/channels/src/components/announcement_bar/overage_users_banner/overage_users_banner.test.tsx @@ -5,7 +5,6 @@ import React from 'react'; import type {DeepPartial} from '@mattermost/types/utilities'; -import {getLicenseSelfServeStatus} from 'mattermost-redux/actions/cloud'; import {savePreferences} from 'mattermost-redux/actions/preferences'; import {General} from 'mattermost-redux/constants'; @@ -48,7 +47,6 @@ const text5PercentageState = `(Only visible to admins) Your workspace user count const text10PercentageState = `(Only visible to admins) Your workspace user count has exceeded your paid license seat count by ${seatsMinimumFor10PercentageState - seatsPurchased} seats. Purchase additional seats to remain compliant.`; const contactSalesTextLink = 'Contact Sales'; -const expandSeatsTextLink = 'Purchase additional seats'; const licenseId = generateId(); @@ -98,10 +96,6 @@ describe('components/overage_users_banner', () => { myPreferences: {}, }, cloud: { - subscriptionStats: { - is_expandable: false, - getRequestState: 'IDLE', - }, }, hostedCustomer: { products: { @@ -134,7 +128,6 @@ describe('components/overage_users_banner', () => { renderWithContext(); expect(screen.queryByText('(Only visible to admins) Your workspace user count has exceeded your paid license seat count by', {exact: false})).not.toBeInTheDocument(); - expect(getLicenseSelfServeStatus).not.toBeCalled(); }); it('should not render the banner because we are not admins', () => { @@ -154,7 +147,6 @@ describe('components/overage_users_banner', () => { renderWithContext(, store); expect(screen.queryByText('Your workspace user count has exceeded your paid license seat count by', {exact: false})).not.toBeInTheDocument(); - expect(getLicenseSelfServeStatus).not.toBeCalled(); }); it('should not render the banner because it\'s cloud licenese', () => { @@ -168,7 +160,6 @@ describe('components/overage_users_banner', () => { renderWithContext(, store); expect(screen.queryByText('Your workspace user count has exceeded your paid license seat count by', {exact: false})).not.toBeInTheDocument(); - expect(getLicenseSelfServeStatus).not.toBeCalled(); }); it('should not render the 5% banner because we have dissmised it', () => { @@ -194,7 +185,6 @@ describe('components/overage_users_banner', () => { renderWithContext(, store); expect(screen.queryByText(text5PercentageState)).not.toBeInTheDocument(); - expect(getLicenseSelfServeStatus).not.toBeCalled(); }); it('should render the banner because we are over 5% and we don\'t have any preferences', () => { @@ -202,10 +192,6 @@ describe('components/overage_users_banner', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; store.entities.admin = { @@ -226,10 +212,6 @@ describe('components/overage_users_banner', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; store.entities.admin = { @@ -259,10 +241,6 @@ describe('components/overage_users_banner', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; store.entities.preferences.myPreferences = TestHelper.getPreferencesMock( @@ -316,10 +294,6 @@ describe('components/overage_users_banner', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; store.entities.admin = { @@ -340,10 +314,6 @@ describe('components/overage_users_banner', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; store.entities.admin = { @@ -367,114 +337,4 @@ describe('components/overage_users_banner', () => { banner: 'global banner', }); }); - - it('should render the warning banner with expansion seats CTA if the license is expandable', () => { - const store = JSON.parse(JSON.stringify(initialState)); - - store.entities.cloud = { - ...store.entities.cloud, - subscriptionStats: { - ...store.entities.cloud.subscriptionStats, - is_expandable: true, - getRequestState: 'OK', - }, - }; - - store.entities.admin = { - ...store.entities.admin, - analytics: { - [StatTypes.TOTAL_USERS]: seatsMinimumFor5PercentageState, - }, - }; - - renderWithContext(, store); - - expect(screen.getByText(expandSeatsTextLink)).toBeInTheDocument(); - }); - - it('should track if the admin click expansion seats CTA in a 5% overage state', () => { - const store = JSON.parse(JSON.stringify(initialState)); - - store.entities.cloud = { - ...store.entities.cloud, - subscriptionStats: { - ...store.entities.cloud.subscriptionStats, - is_expandable: true, - getRequestState: 'OK', - }, - }; - - store.entities.admin = { - ...store.entities.admin, - analytics: { - [StatTypes.TOTAL_USERS]: seatsMinimumFor5PercentageState, - }, - }; - - renderWithContext(, store); - - fireEvent.click(screen.getByText(expandSeatsTextLink)); - expect(windowSpy).toBeCalledTimes(1); - expect(windowSpy).toBeCalledWith(`http://testing/subscribe/expand?licenseId=${licenseId}`, '_blank'); - expect(trackEvent).toBeCalledTimes(1); - expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', { - cta: 'Self Serve', - banner: 'global banner', - }); - }); - - it('should render the error banner with expansion seats CTA if the license is be expandable', () => { - const store = JSON.parse(JSON.stringify(initialState)); - - store.entities.cloud = { - ...store.entities.cloud, - subscriptionStats: { - ...store.entities.cloud.subscriptionStats, - is_expandable: true, - getRequestState: 'OK', - }, - }; - - store.entities.admin = { - ...store.entities.admin, - analytics: { - [StatTypes.TOTAL_USERS]: seatsMinimumFor10PercentageState, - }, - }; - - renderWithContext(, store); - - expect(screen.getByText(expandSeatsTextLink)).toBeInTheDocument(); - }); - - it('should track if the admin click expansion seats CTA in a 10% overage state', () => { - const store = JSON.parse(JSON.stringify(initialState)); - - store.entities.cloud = { - ...store.entities.cloud, - subscriptionStats: { - ...store.entities.cloud.subscriptionStats, - is_expandable: true, - getRequestState: 'OK', - }, - }; - - store.entities.admin = { - ...store.entities.admin, - analytics: { - [StatTypes.TOTAL_USERS]: seatsMinimumFor10PercentageState, - }, - }; - - renderWithContext(, store); - - fireEvent.click(screen.getByText(expandSeatsTextLink)); - expect(windowSpy).toBeCalledTimes(1); - expect(windowSpy).toBeCalledWith(`http://testing/subscribe/expand?licenseId=${licenseId}`, '_blank'); - expect(trackEvent).toBeCalledTimes(1); - expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', { - cta: 'Self Serve', - banner: 'global banner', - }); - }); }); diff --git a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx index b314531be1..dbb0bb0601 100644 --- a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx +++ b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx @@ -6,8 +6,6 @@ import React from 'react'; import {act} from 'react-dom/test-utils'; import {Provider} from 'react-redux'; -import {Client4} from 'mattermost-redux/client'; - import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import mockStore from 'tests/test_store'; @@ -66,29 +64,7 @@ describe('components/RenewalLink', () => { }, }; - test('should show Renew now when a renewal link is successfully returned', async () => { - const getRenewalLinkSpy = jest.spyOn(Client4, 'getRenewalLink'); - const promise = new Promise<{renewal_link: string}>((resolve) => { - resolve({ - renewal_link: 'https://testrenewallink', - }); - }); - getRenewalLinkSpy.mockImplementation(() => promise); - const store = mockStore(initialState); - const wrapper = mountWithIntl(); - - // wait for the promise to resolve and component to update - await actImmediate(wrapper); - - expect(wrapper.find('.btn').text().includes('Renew license now')).toBe(true); - }); - - test('should show Contact sales when a renewal link is not returned', async () => { - const getRenewalLinkSpy = jest.spyOn(Client4, 'getRenewalLink'); - const promise = new Promise<{renewal_link: string}>((resolve, reject) => { - reject(new Error('License cannot be renewed from portal')); - }); - getRenewalLinkSpy.mockImplementation(() => promise); + test('should show Contact sales button', async () => { const store = mockStore(initialState); const wrapper = mountWithIntl(); diff --git a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx index 5dbfeee03a..9256097a46 100644 --- a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx +++ b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.tsx @@ -1,27 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useState} from 'react'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; -import {Client4} from 'mattermost-redux/client'; - -import {trackEvent} from 'actions/telemetry_actions'; - import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; -import { - ModalIdentifiers, -} from 'utils/constants'; - import type {ModalData} from 'types/actions'; -import NoInternetConnection from '../no_internet_connection/no_internet_connection'; - import './renew_link.scss'; export interface RenewalLinkProps { - telemetryInfo?: {success: string; error: string}; + telemetryInfo?: { success: string; error: string }; actions: { openModal:

(modalData: ModalData

) => void; }; @@ -30,70 +20,20 @@ export interface RenewalLinkProps { } const RenewalLink = (props: RenewalLinkProps) => { - const [renewalLink, setRenewalLink] = useState(''); - const [manualInterventionRequired, setManualInterventionRequired] = useState(false); - const [openContactSales] = useOpenSalesLink(); - useEffect(() => { - Client4.getRenewalLink().then(({renewal_link: renewalLinkParam}) => { - try { - if (renewalLinkParam && (/^http[s]?:\/\//).test(renewalLinkParam)) { - setRenewalLink(renewalLinkParam); - } - } catch (error) { - console.error('No link returned', error); // eslint-disable-line no-console - } - }).catch(() => { - setManualInterventionRequired(true); - }); - }, []); - const handleLinkClick = async (e: React.MouseEvent) => { e.preventDefault(); - try { - const {status} = await Client4.ping(false); - if (status === 'OK' && renewalLink !== '') { - if (props.telemetryInfo?.success) { - trackEvent('renew_license', props.telemetryInfo.success); - } - window.open(renewalLink, '_blank'); - } else if (manualInterventionRequired) { - openContactSales(); - } else { - showConnectionErrorModal(); - } - } catch (error) { - showConnectionErrorModal(); - } + openContactSales(); }; - const showConnectionErrorModal = () => { - if (props.telemetryInfo?.error) { - trackEvent('renew_license', props.telemetryInfo.error); - } - props.actions.openModal({ - modalId: ModalIdentifiers.NO_INTERNET_CONNECTION, - dialogType: NoInternetConnection, - }); - }; - - let btnText = props.customBtnText ? props.customBtnText : ( + const btnText = ( ); - if (manualInterventionRequired) { - btnText = ( - - ); - } - return ( - ); -}; - -export default CloudStartTrialButton; diff --git a/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.scss b/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.scss deleted file mode 100644 index 0149bb9459..0000000000 --- a/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.scss +++ /dev/null @@ -1,115 +0,0 @@ -@import 'utils/variables'; -@import 'utils/mixins'; - -.RequestBusinessEmailModal { - height: 320px; - - &.modal-dialog { - margin-top: calc(50vh - 350px) !important; - } - - .modal-content { - padding: 0 !important; - border-color: rgba(var(--center-channel-color-rgb), 0.16); - border-radius: 8px; - background: var(--center-channel-bg); - color: var(--center-channel-color); - } - - .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.8); - opacity: 1; - } - - top: 6px; - right: 4px; - width: 4rem; - height: 4rem; - border-radius: 4px; - color: rgba(var(--center-channel-color-rgb), 0.75) !important; - font-family: - 'Open Sans', - sans-serif; - font-size: 32px; - font-weight: 400; - } - - height: 38px; - padding: 0; - border: 0; - border-radius: 8px; - background: var(--center-channel-bg) !important; - color: var(--center-channel-color); - } - - .modal-body { - display: flex; - overflow: hidden; - width: 100%; - height: calc(100% - 38px); - flex-direction: column; - padding: 0; - - .GenericModal__body { - height: 100%; - padding: 0 24px 24px 24px; - - .container-footer { - bottom: 0; - height: 36px; - } - } - - .request-business-email-input { - height: 34px !important; - border: 0 !important; - border-radius: 0 !important; - } - - .start-trial-email-title { - margin-bottom: 22px; - color: var(--center-channel-color); - font-size: 22px; - font-weight: 600; - line-height: 28px; - } - - .start-trial-email-description { - margin-bottom: 16px; - color: var(--center-channel-color); - font-size: 14px; - font-weight: 400; - line-height: 20px; - } - - .start-trial-email-disclaimer { - margin-top: 56px; - } - - .start-trial-button { - display: flex; - - button { - @include primary-button; - - margin-left: auto; - } - } - } - - .modal-centered { - text-align: center; - } - - .modal-footer { - padding: 12px 24px 24px; - border: none; - border-radius: 4px; - } -} diff --git a/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.test.tsx b/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.test.tsx deleted file mode 100644 index d70fa75566..0000000000 --- a/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.test.tsx +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {shallow} from 'enzyme'; -import React from 'react'; -import {act} from 'react-dom/test-utils'; -import {Provider} from 'react-redux'; - -import {GenericModal} from '@mattermost/components'; - -import * as cloudActions from 'actions/cloud'; - -import {mountWithIntl} from 'tests/helpers/intl-test-helper'; -import mockStore from 'tests/test_store'; - -import RequestBusinessEmailModal from './request_business_email_modal'; - -jest.useFakeTimers(); -jest.mock('lodash/debounce', () => jest.fn((fn) => fn)); - -describe('components/request_business_email_modal/request_business_email_modal', () => { - const state = { - entities: { - users: { - currentUserId: 'current_user_id', - }, - admin: {}, - general: { - license: { - IsLicensed: 'true', - Cloud: 'true', - }, - config: {}, - }, - cloud: { - subscription: {id: 'subscriptionID'}, - }, - }, - views: { - modals: { - modalState: { - request_business_email_modal: { - open: true, - }, - }, - }, - }, - }; - - const props = { - onExited: jest.fn(), - }; - - const store = mockStore(state); - - test('should match snapshot', () => { - const wrapper = shallow( - - - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should show the Start Cloud Trial Button', async () => { - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - const startTrialBtn = wrapper.find('CloudStartTrialButton'); - expect(startTrialBtn).toHaveLength(1); - }); - }); - - test('should call on close', async () => { - const mockOnClose = jest.fn(); - - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - wrapper.find(GenericModal).props().onExited(); - expect(mockOnClose).toHaveBeenCalled(); - }); - }); - - test('should call on exited', async () => { - const mockOnExited = jest.fn(); - - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - wrapper.find(GenericModal).props().onExited(); - expect(mockOnExited).toHaveBeenCalled(); - }); - }); - - test('should show the Input to enter the valid Business Email', async () => { - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - expect(wrapper.find('InputBusinessEmail')).toHaveLength(1); - }); - }); - - test('should start with Start Cloud Trial Button disabled', async () => { - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - const startTrialBtn = wrapper.find('CloudStartTrialButton'); - expect(startTrialBtn.props().disabled).toEqual(true); - }); - }); - - test('should ENABLE the trial button if email is VALID', async () => { - // mock validation response to TRUE meaning the email is a valid email - const validateBusinessEmail = () => () => Promise.resolve(true); - jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail); - - const event = { - target: {value: 'valid-email@domain.com'}, - }; - - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - const inputBusinessEmail = wrapper.find('InputBusinessEmail'); - const input = inputBusinessEmail.find('input'); - input.find('input').at(0).simulate('change', event); - }); - - act(() => { - wrapper.update(); - const startTrialBtn = wrapper.find('CloudStartTrialButton'); - expect(startTrialBtn.props().disabled).toEqual(false); - }); - }); - - test('should show the success custom message if the email is valid', async () => { - // mock validation response to TRUE meaning the email is a valid email - const validateBusinessEmail = () => () => Promise.resolve(true); - - jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail); - - const event = { - target: {value: 'valid-email@domain.com'}, - }; - - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - const inputBusinessEmail = wrapper.find('InputBusinessEmail'); - const input = inputBusinessEmail.find('input'); - input.find('input').at(0).simulate('change', event); - }); - - act(() => { - wrapper.update(); - const customMessageElement = wrapper.find('.Input___customMessage.Input___success'); - expect(customMessageElement.length).toBe(1); - }); - }); - - test('should DISABLE the trial button if email is INVALID', async () => { - // mock validation response to FALSE meaning the email is an invalid email - const validateBusinessEmail = () => () => Promise.resolve(false); - jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail); - - const event = { - target: {value: 'INvalid-email@domain.com'}, - }; - - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - const inputBusinessEmail = wrapper.find('InputBusinessEmail'); - const input = inputBusinessEmail.find('input'); - input.find('input').at(0).simulate('change', event); - }); - - act(() => { - wrapper.update(); - const startTrialBtn = wrapper.find('CloudStartTrialButton'); - expect(startTrialBtn.props().disabled).toEqual(true); - }); - }); - - test('should show the error custom message if the email is invalid', async () => { - // mock validation response to FALSE meaning the email is an invalid email - const validateBusinessEmail = () => () => Promise.resolve(false); - jest.spyOn(cloudActions, 'validateBusinessEmail').mockImplementation(validateBusinessEmail); - - const event = { - target: {value: 'INvalid-email@domain.com'}, - }; - - const wrapper = mountWithIntl( - - - , - ); - - await act(async () => { - const inputBusinessEmail = wrapper.find('InputBusinessEmail'); - const input = inputBusinessEmail.find('input'); - input.find('input').at(0).simulate('change', event); - }); - - act(() => { - wrapper.update(); - const customMessageElement = wrapper.find('.Input___customMessage.Input___error'); - expect(customMessageElement.length).toBe(1); - }); - }); -}); diff --git a/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.tsx b/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.tsx deleted file mode 100644 index 8dd09ac6d6..0000000000 --- a/webapp/channels/src/components/cloud_start_trial/request_business_email_modal.tsx +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import debounce from 'lodash/debounce'; -import React, {useCallback, useEffect, useState} from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; -import {useDispatch} from 'react-redux'; - -import {GenericModal} from '@mattermost/components'; - -import {isEmail} from 'mattermost-redux/utils/helpers'; - -import {validateBusinessEmail} from 'actions/cloud'; -import {trackEvent} from 'actions/telemetry_actions'; -import {closeModal} from 'actions/views/modals'; - -import ExternalLink from 'components/external_link'; -import type {CustomMessageInputType} from 'components/widgets/inputs/input/input'; - -import {ItemStatus, TELEMETRY_CATEGORIES, ModalIdentifiers, LicenseLinks, AboutLinks} from 'utils/constants'; - -import StartCloudTrialBtn from './cloud_start_trial_btn'; -import InputBusinessEmail from './input_business_email'; - -import './request_business_email_modal.scss'; - -type Props = { - onClose?: () => void; - onExited: () => void; -} - -const RequestBusinessEmailModal = ( - { - onClose, - onExited, - }: Props): JSX.Element | null => { - const {formatMessage} = useIntl(); - const dispatch = useDispatch(); - const [email, setEmail] = useState(''); - const [customInputLabel, setCustomInputLabel] = useState(null); - const [trialBtnDisabled, setTrialBtnDisabled] = useState(true); - - useEffect(() => { - trackEvent( - TELEMETRY_CATEGORIES.REQUEST_BUSINESS_EMAIL, - 'request_business_email', - ); - }, []); - - const handleOnClose = useCallback(() => { - if (onClose) { - onClose(); - } - - onExited(); - }, [onClose, onExited]); - - const handleEmailValues = useCallback((e: React.ChangeEvent) => { - const email = e.target.value; - setEmail(email.trim().toLowerCase()); - - validateEmail(email); - }, []); - - const validateEmail = useCallback(debounce(async (email: string) => { - // no value set, no validation and clean the custom input label - if (!email) { - setTrialBtnDisabled(true); - setCustomInputLabel(null); - return; - } - - // function isEmail aready handle empty / null value - if (!isEmail(email)) { - const errMsg = formatMessage({id: 'request_business_email_modal.invalidEmail', defaultMessage: 'This doesn\'t look like a valid email'}); - setCustomInputLabel({type: ItemStatus.WARNING, value: errMsg}); - setTrialBtnDisabled(true); - return; - } - - // go and validate the email against the validateBusinessEmail endpoint - const isValidBusinessEmail = await validateBusinessEmail(email)(); - if (!isValidBusinessEmail) { - const errMsg = formatMessage({id: 'request_business_email_modal.not_business_email', defaultMessage: 'This doesn\'t look like a business email'}); - setCustomInputLabel({type: ItemStatus.ERROR, value: errMsg}); - setTrialBtnDisabled(true); - return; - } - - // if it is a valid business email, proceed, enable the start trial button and notify the user about the email is valid - const okMsg = formatMessage({id: 'request_business_email_modal.valid_business_email', defaultMessage: 'This is a valid email'}); - setCustomInputLabel({type: ItemStatus.SUCCESS, value: okMsg}); - setTrialBtnDisabled(false); - }, 250), []); - - // this function will be executed after successfull trial request, closing this request business email modal - const closeMeAfterSuccessTrialReq = async () => { - await dispatch(closeModal(ModalIdentifiers.REQUEST_BUSINESS_EMAIL_MODAL)); - }; - - return ( - -

- -
-
- -
-
- -
-
- ( - - {msg} - - ), - linkEvaluation: (msg: React.ReactNode) => ( - - {msg} - - ), - linkPrivacy: (msg: React.ReactNode) => ( - - {msg} - - ), - }} - /> -
-
- -
- - ); -}; - -export default RequestBusinessEmailModal; diff --git a/webapp/channels/src/components/common/hooks/useCanSelfHostedExpand.ts b/webapp/channels/src/components/common/hooks/useCanSelfHostedExpand.ts deleted file mode 100644 index d57f36da46..0000000000 --- a/webapp/channels/src/components/common/hooks/useCanSelfHostedExpand.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {useEffect, useState} from 'react'; -import {useSelector} from 'react-redux'; - -import {Client4} from 'mattermost-redux/client'; -import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; -import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; - -import {BillingSchemes, SelfHostedProducts} from 'utils/constants'; -import {findSelfHostedProductBySku} from 'utils/hosted_customer'; -import {isCloudLicense} from 'utils/license_utils'; - -import useGetSelfHostedProducts from './useGetSelfHostedProducts'; - -export default function useCanSelfHostedExpand() { - const [expansionAvailable, setExpansionAvailable] = useState(false); - const config = useSelector(getConfig); - const isEnterpriseReady = config.BuildEnterpriseReady === 'true'; - const isSalesServeOnly = useSelector(getSubscriptionProduct)?.billing_scheme === BillingSchemes.SALES_SERVE; - const license = useSelector(getLicense); - const isCloud = isCloudLicense(license); - const [products] = useGetSelfHostedProducts(); - const currentProduct = findSelfHostedProductBySku(products, license.SkuShortName); - const isAdmin = useSelector(isCurrentUserSystemAdmin); - - // Self Hosted Products never contains a product for starter, additional check is done out of caution. - const isSelfHostedStarter = currentProduct === null || currentProduct?.sku === SelfHostedProducts.STARTER; - - useEffect(() => { - if (!isEnterpriseReady || !isAdmin) { - return; - } - Client4.getLicenseSelfServeStatus(). - then((res) => { - setExpansionAvailable(res.is_expandable ?? false); - }). - catch(() => { - setExpansionAvailable(false); - }); - }, [isEnterpriseReady, isAdmin]); - - return !isCloud && !isSelfHostedStarter && !isSalesServeOnly && expansionAvailable; -} diff --git a/webapp/channels/src/components/common/hooks/useCanSelfHostedSignup.ts b/webapp/channels/src/components/common/hooks/useCanSelfHostedSignup.ts deleted file mode 100644 index bb00a76b5e..0000000000 --- a/webapp/channels/src/components/common/hooks/useCanSelfHostedSignup.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {useEffect, useMemo, useState} from 'react'; -import {useSelector} from 'react-redux'; - -import {Client4} from 'mattermost-redux/client'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; - -import useLoadStripe from './useLoadStripe'; - -interface CWSSignupAvailability { - cwsContacted: boolean; - cwsServiceOn: boolean; - screeningInProgress: boolean; -} - -const cwsAvailable: CWSSignupAvailability = { - cwsContacted: true, - cwsServiceOn: true, - screeningInProgress: false, -}; -const cwsAvailableEmptyState: CWSSignupAvailability = { - cwsContacted: false, - cwsServiceOn: false, - screeningInProgress: false, -}; - -type SignupAvailability = CWSSignupAvailability & { - stripeAvailable: boolean; - ok: boolean; -} - -export default function useCanSelfHostedSignup(): SignupAvailability { - const [cwsAvailability, setCwsAvailability] = useState(cwsAvailableEmptyState); - const config = useSelector(getConfig); - const isEnterpriseReady = config.BuildEnterpriseReady === 'true'; - const stripeAvailable = Boolean(useLoadStripe().current); - useEffect(() => { - if (!isEnterpriseReady) { - return; - } - Client4.getAvailabilitySelfHostedSignup(). - then(() => { - setCwsAvailability(cwsAvailable); - }). - catch((err) => { - let errorValue = {...cwsAvailableEmptyState}; - switch (err.status_code) { - case 503: { - errorValue = { - cwsServiceOn: false, - cwsContacted: true, - screeningInProgress: false, - }; - break; - } - case 425: { - errorValue = { - cwsServiceOn: true, - cwsContacted: true, - screeningInProgress: true, - }; - break; - } - default: { - errorValue = {...cwsAvailableEmptyState}; - break; - } - } - setCwsAvailability(errorValue); - }); - }, []); - - return useMemo(() => { - return { - ...cwsAvailability, - stripeAvailable, - ok: stripeAvailable && cwsAvailability.cwsContacted && cwsAvailability.cwsServiceOn && !cwsAvailability.screeningInProgress, - }; - }, [stripeAvailable, cwsAvailability]); -} diff --git a/webapp/channels/src/components/common/hooks/useDelinquencySubscription.ts b/webapp/channels/src/components/common/hooks/useDelinquencySubscription.ts deleted file mode 100644 index b650f748ce..0000000000 --- a/webapp/channels/src/components/common/hooks/useDelinquencySubscription.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import useGetSubscription from './useGetSubscription'; - -export const useDelinquencySubscription = () => { - const subscription = useGetSubscription(); - - const isDelinquencySubscription = (): boolean => { - if (!subscription) { - return false; - } - - if (!subscription.delinquent_since) { - return false; - } - - return true; - }; - - const isDelinquencySubscriptionHigherThan90Days = (): boolean => { - if (!isDelinquencySubscription()) { - return false; - } - - if (!subscription) { - return false; - } - - const delinquencyDate = new Date((subscription.delinquent_since || 0) * 1000); - - const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds - const today = new Date(); - const diffDays = Math.round( - Math.abs((today.valueOf() - delinquencyDate.valueOf()) / oneDay), - ); - - return diffDays > 90; - }; - - return {isDelinquencySubscription, isDelinquencySubscriptionHigherThan90Days, subscription}; -}; diff --git a/webapp/channels/src/components/common/hooks/useExpandOverageUsersCheck.ts b/webapp/channels/src/components/common/hooks/useExpandOverageUsersCheck.ts index 297c7a558c..c52e94311f 100644 --- a/webapp/channels/src/components/common/hooks/useExpandOverageUsersCheck.ts +++ b/webapp/channels/src/components/common/hooks/useExpandOverageUsersCheck.ts @@ -1,56 +1,28 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {useEffect, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; - -import type {LicenseSelfServeStatusReducer} from '@mattermost/types/cloud'; - -import {getLicenseSelfServeStatus} from 'mattermost-redux/actions/cloud'; +import {useSelector} from 'react-redux'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import {getExpandSeatsLink} from 'selectors/cloud'; -import type {GlobalState} from 'types/store'; - type UseExpandOverageUsersCheckArgs = { isWarningState: boolean; - shouldRequest: boolean; - licenseId?: string; banner: 'global banner' | 'invite modal'; - canSelfHostedExpand: boolean; } export const useExpandOverageUsersCheck = ({ - shouldRequest, isWarningState, - licenseId, banner, - canSelfHostedExpand, }: UseExpandOverageUsersCheckArgs) => { const {formatMessage} = useIntl(); - const dispatch = useDispatch(); - const {getRequestState, is_expandable: isExpandable}: LicenseSelfServeStatusReducer = useSelector((state: GlobalState) => state.entities.cloud.subscriptionStats || {is_expandable: false, getRequestState: 'IDLE'}); const expandableLink = useSelector(getExpandSeatsLink); - const cta = useMemo(() => { - if (isExpandable && !canSelfHostedExpand) { - return formatMessage({ - id: 'licensingPage.overageUsersBanner.ctaExpandSeats', - defaultMessage: 'Purchase additional seats', - }); - } else if (isExpandable && canSelfHostedExpand) { - return formatMessage({ - id: 'licensingPage.overageUsersBanner.ctaUpdateSeats', - defaultMessage: 'Update seat count', - }); - } - return formatMessage({ - id: 'licensingPage.overageUsersBanner.cta', - defaultMessage: 'Contact Sales', - }); - }, [isExpandable]); + const cta = formatMessage({ + id: 'licensingPage.overageUsersBanner.cta', + defaultMessage: 'Contact Sales', + }); const trackEventFn = (cta: 'Contact Sales' | 'Self Serve') => { trackEvent('insights', isWarningState ? 'click_true_up_warning' : 'click_true_up_error', { @@ -59,17 +31,9 @@ export const useExpandOverageUsersCheck = ({ }); }; - useEffect(() => { - if (shouldRequest && licenseId && getRequestState === 'IDLE') { - dispatch(getLicenseSelfServeStatus()); - } - }, [dispatch, getRequestState, licenseId, shouldRequest]); - return { cta, expandableLink, trackEventFn, - getRequestState, - isExpandable, }; }; diff --git a/webapp/channels/src/components/common/hooks/useLoadStripe.ts b/webapp/channels/src/components/common/hooks/useLoadStripe.ts deleted file mode 100644 index 0a00068792..0000000000 --- a/webapp/channels/src/components/common/hooks/useLoadStripe.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import type {Stripe} from '@stripe/stripe-js'; -import {loadStripe} from '@stripe/stripe-js/pure'; // https://github.com/stripe/stripe-js#importing-loadstripe-without-side-effects -import {useEffect, useRef, useState} from 'react'; -import {useSelector} from 'react-redux'; - -import {getStripePublicKey} from 'components/payment_form/stripe'; - -import type {GlobalState} from 'types/store'; - -// reloadHint -export default function useLoadStripe(reloadHint?: number) { - const stripeRef = useRef(null); - const [, setDone] = useState(false); - const stripePublicKey = useSelector((state: GlobalState) => getStripePublicKey(state)); - - useEffect(() => { - if (stripeRef.current) { - return; - } - loadStripe(stripePublicKey).then((stripe: Stripe | null) => { - stripeRef.current = stripe; - - // deliberately cause a rerender so that the input can render. - // otherwise, the input does not show up. - setDone(true); - }); - }, [reloadHint]); - return stripeRef; -} - diff --git a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.test.tsx b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.test.tsx index 8a37afaeee..cd8b105ffc 100644 --- a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.test.tsx +++ b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.test.tsx @@ -110,32 +110,4 @@ describe('components/global/product_switcher_menu', () => { expect(wrapper.find('.button-plans').length).toEqual(1); expect(wrapper.find('StartTrialBtn').length).toEqual(1); }); - - test('should show with system admin pre trial for cloud', () => { - mockState.entities.users.profiles.user1.roles = 'system_admin'; - mockState.entities.general.license = { - Cloud: 'true', - }; - - const wrapper = shallow(); - - expect(wrapper.find('.FeatureRestrictedModal__description').text()).toEqual(defaultProps.messageAdminPreTrial); - expect(wrapper.find('.FeatureRestrictedModal__terms').length).toEqual(1); - expect(wrapper.find('.FeatureRestrictedModal__buttons').hasClass('single')).toEqual(false); - expect(wrapper.find('.button-plans').length).toEqual(1); - expect(wrapper.find('CloudStartTrialButton').length).toEqual(1); - }); - - test('should match snapshot with system admin post trial', () => { - mockState.entities.users.profiles.user1.roles = 'system_admin'; - mockState.entities.cloud.subscription.is_free_trial = 'false'; - mockState.entities.cloud.subscription.trial_end_at = 1; - - const wrapper = shallow(); - - expect(wrapper.find('.FeatureRestrictedModal__description').text()).toEqual(defaultProps.messageAdminPostTrial); - expect(wrapper.find('.FeatureRestrictedModal__terms').length).toEqual(0); - expect(wrapper.find('.button-plans').length).toEqual(1); - expect(wrapper.find('CloudStartTrialButton').length).toEqual(0); - }); }); diff --git a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx index c35b8343f3..6b68c61a7b 100644 --- a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx +++ b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx @@ -9,15 +9,12 @@ import {useSelector, useDispatch} from 'react-redux'; import {GenericModal} from '@mattermost/components'; import {getPrevTrialLicense} from 'mattermost-redux/actions/admin'; -import {checkHadPriorTrial} from 'mattermost-redux/selectors/entities/cloud'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; -import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences'; import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {closeModal} from 'actions/views/modals'; import {isModalOpen} from 'selectors/views/modals'; -import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn'; import {NotifyStatus} from 'components/common/hooks/useGetNotifyAdmin'; import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal'; import ExternalLink from 'components/external_link'; @@ -38,7 +35,7 @@ type FeatureRestrictedModalProps = { messageAdminPostTrial?: string; titleEndUser?: string; messageEndUser?: string; - customSecondaryButton?: {msg: string; action: () => void}; + customSecondaryButton?: { msg: string; action: () => void }; feature?: string; minimumPlanRequiredForFeature?: string; } @@ -61,12 +58,10 @@ const FeatureRestrictedModal = ({ dispatch(getPrevTrialLicense()); }, []); - const cloudFreeDeprecated = useSelector(deprecateCloudFree); - const hasCloudPriorTrial = useSelector(checkHadPriorTrial); const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense); const hasSelfHostedPriorTrial = prevTrialLicense.IsLicensed === 'true'; - const hasPriorTrial = hasCloudPriorTrial || hasSelfHostedPriorTrial; + const hasPriorTrial = hasSelfHostedPriorTrial; const isSystemAdmin = useSelector(isCurrentUserSystemAdmin); const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.FEATURE_RESTRICTED_MODAL)); const license = useSelector(getLicense); @@ -103,7 +98,7 @@ const FeatureRestrictedModal = ({ const getTitle = () => { if (isSystemAdmin) { - return (hasPriorTrial || cloudFreeDeprecated) ? titleAdminPostTrial : titleAdminPreTrial; + return (hasPriorTrial) ? titleAdminPostTrial : titleAdminPreTrial; } return titleEndUser; @@ -111,13 +106,13 @@ const FeatureRestrictedModal = ({ const getMessage = () => { if (isSystemAdmin) { - return (hasPriorTrial || cloudFreeDeprecated) ? messageAdminPostTrial : messageAdminPreTrial; + return (hasPriorTrial) ? messageAdminPostTrial : messageAdminPreTrial; } return messageEndUser; }; - const showStartTrial = isSystemAdmin && !hasPriorTrial && !cloudFreeDeprecated; + const showStartTrial = isSystemAdmin && !hasPriorTrial && !isCloud; // define what is the secondary button text and action, by default will be the View Plan button let secondaryBtnMsg = formatMessage({id: 'feature_restricted_modal.button.plans', defaultMessage: 'View plans'}); @@ -130,26 +125,15 @@ const FeatureRestrictedModal = ({ secondaryBtnAction = customSecondaryButton.action; } - let trialBtn; - if (isCloud) { - trialBtn = ( - - ); - } else { - trialBtn = ( - ); - } + const trialBtn = ( + + ); return ( { const currentUser = useSelector((state: GlobalState) => getCurrentUser(state)); const overagePreferences = useSelector((state: GlobalState) => getPreferencesCategory(state, Preferences.OVERAGE_USERS_BANNER)); const activeUsers = ((stats || {})[StatTypes.TOTAL_USERS]) as number || 0; - const isSelfHostedPurchaseEnabled = useSelector(getConfig)?.ServiceSettings?.SelfHostedPurchase; - const canSelfHostedExpand = useCanSelfHostedExpand() && isSelfHostedPurchaseEnabled; - const siteURL = getSiteURL(); const { isBetween5PercerntAnd10PercentPurchasedSeats, @@ -69,16 +63,10 @@ const OverageUsersBannerNotice = () => { const hasPermission = isAdmin && isOverageState && !isCloud; const { cta, - expandableLink, trackEventFn, - getRequestState, - isExpandable, } = useExpandOverageUsersCheck({ - shouldRequest: hasPermission && !adminHasDismissed({overagePreferences, preferenceName}), - licenseId: license.Id, isWarningState: isBetween5PercerntAnd10PercentPurchasedSeats, banner: 'invite modal', - canSelfHostedExpand: canSelfHostedExpand || false, }); if (!hasPermission || adminHasDismissed({overagePreferences, preferenceName})) { @@ -96,44 +84,21 @@ const OverageUsersBannerNotice = () => { let message; - if (canSelfHostedExpand) { - message = ( - Purchase additional seats to remain compliant.'} - values={{ - a: (chunks: React.ReactNode) => { - return ( - - {chunks} - - ); - }, - }} - /> - ); - } else if (!isGovSku) { + if (!isGovSku) { message = ( { - if (getRequestState === 'IDLE' || getRequestState === 'LOADING') { - return null; - } - const handleClick = () => { - trackEventFn(isExpandable ? 'Self Serve' : 'Contact Sales'); + trackEventFn('Contact Sales'); }; return ( {cta} diff --git a/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/overage_users_banner_notice.test.tsx b/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/overage_users_banner_notice.test.tsx index cbc46b3951..49d8039988 100644 --- a/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/overage_users_banner_notice.test.tsx +++ b/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/overage_users_banner_notice.test.tsx @@ -50,7 +50,6 @@ const text10PercentageState = `Your workspace user count has exceeded your paid const notifyText = 'Notify your Customer Success Manager on your next true-up check'; const contactSalesTextLink = 'Contact Sales'; -const expandSeatsTextLink = 'Purchase additional seats'; const licenseId = generateId(); @@ -90,12 +89,7 @@ describe('components/invitation_modal/overage_users_banner_notice', () => { preferences: { myPreferences: {}, }, - cloud: { - subscriptionStats: { - is_expandable: false, - getRequestState: 'IDLE', - }, - }, + cloud: {}, hostedCustomer: { products: { productsLoaded: true, @@ -226,10 +220,6 @@ describe('components/invitation_modal/overage_users_banner_notice', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; renderWithContext( @@ -336,10 +326,6 @@ describe('components/invitation_modal/overage_users_banner_notice', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; renderWithContext( @@ -444,70 +430,6 @@ describe('components/invitation_modal/overage_users_banner_notice', () => { }]); }); - it('should track if the admin click expansion seats CTA in a 5% overage state', () => { - const store: GlobalState = JSON.parse(JSON.stringify(initialState)); - - store.entities.admin = { - ...store.entities.admin, - analytics: { - [StatTypes.TOTAL_USERS]: seatsMinimumFor5PercentageState, - }, - }; - - store.entities.cloud = { - ...store.entities.cloud, - subscriptionStats: { - is_expandable: true, - getRequestState: 'OK', - }, - }; - - renderWithContext( - , - store, - ); - - fireEvent.click(screen.getByText(expandSeatsTextLink)); - expect(screen.getByRole('link')).toHaveAttribute('href', `http://testing/subscribe/expand?licenseId=${licenseId}`); - expect(trackEvent).toBeCalledTimes(2); - expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', { - cta: 'Self Serve', - banner: 'invite modal', - }); - }); - - it('should track if the admin click expansion seats CTA in a 10% overage state', () => { - const store: GlobalState = JSON.parse(JSON.stringify(initialState)); - - store.entities.admin = { - ...store.entities.admin, - analytics: { - [StatTypes.TOTAL_USERS]: seatsMinimumFor10PercentageState, - }, - }; - - store.entities.cloud = { - ...store.entities.cloud, - subscriptionStats: { - is_expandable: true, - getRequestState: 'OK', - }, - }; - - renderWithContext( - , - store, - ); - - fireEvent.click(screen.getByText(expandSeatsTextLink)); - expect(screen.getByRole('link')).toHaveAttribute('href', `http://testing/subscribe/expand?licenseId=${licenseId}`); - expect(trackEvent).toBeCalledTimes(2); - expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', { - cta: 'Self Serve', - banner: 'invite modal', - }); - }); - it('gov sku sees overage notice but not a call to do true up', async () => { const store: GlobalState = JSON.parse(JSON.stringify(initialState)); @@ -520,10 +442,6 @@ describe('components/invitation_modal/overage_users_banner_notice', () => { store.entities.cloud = { ...store.entities.cloud, - subscriptionStats: { - is_expandable: false, - getRequestState: 'OK', - }, }; store.entities.general.license.IsGovSku = 'true'; diff --git a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx index 3d45331174..e0e92cf1de 100644 --- a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx +++ b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.test.tsx @@ -21,11 +21,6 @@ jest.mock('actions/telemetry_actions.jsx', () => { }; }); -const CloudStartTrialButton = () => { - return (); -}; - -jest.mock('components/cloud_start_trial/cloud_start_trial_btn', () => CloudStartTrialButton); describe('components/learn_more_trial_modal/learn_more_trial_modal', () => { // required state to mount using the provider const state = { @@ -50,15 +45,12 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => { general: { license: { IsLicensed: 'false', - Cloud: 'true', + Cloud: 'false', }, config: { DiagnosticsEnabled: 'false', }, }, - cloud: { - subscription: {id: 'subscription'}, - }, }, views: { modals: { @@ -172,20 +164,6 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => { expect(activeSlideId).toBe('ldap'); }); - test('should have the start cloud trial button when is cloud workspace and cloud free is enabled', () => { - const wrapper = mountWithIntl( - - - , - ); - - const trialButton = wrapper.find('CloudStartTrialButton'); - - expect(trialButton).toHaveLength(1); - }); - test('should have the self hosted request trial button cloud free is disabled', () => { const nonCloudState = { ...state, @@ -210,10 +188,6 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => { , ); - // validate the cloud start trial button is not present - const trialButton = wrapper.find('CloudStartTrialButton'); - expect(trialButton).toHaveLength(0); - // validate the cloud start trial button is not present const selfHostedRequestTrialButton = wrapper.find('StartTrialBtn'); expect(selfHostedRequestTrialButton).toHaveLength(1); diff --git a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx index 6fddbd2277..e508070c80 100644 --- a/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx +++ b/webapp/channels/src/components/learn_more_trial_modal/learn_more_trial_modal.tsx @@ -2,25 +2,21 @@ // See LICENSE.txt for license information. import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {useIntl} from 'react-intl'; import {useSelector, useDispatch} from 'react-redux'; import {GenericModal} from '@mattermost/components'; import {getLicense} from 'mattermost-redux/selectors/entities/general'; -import {deprecateCloudFree} from 'mattermost-redux/selectors/entities/preferences'; import {trackEvent} from 'actions/telemetry_actions'; import {closeModal} from 'actions/views/modals'; import SystemRolesSVG from 'components/admin_console/feature_discovery/features/images/system_roles_svg'; -import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn'; import Carousel from 'components/common/carousel/carousel'; import {BtnStyle} from 'components/common/carousel/carousel_button'; -import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; import GuestAccessSvg from 'components/common/svg_images_components/guest_access_svg'; import MonitorImacLikeSVG from 'components/common/svg_images_components/monitor_imaclike_svg'; -import ExternalLink from 'components/external_link'; import {ConsolePages, DocLinks, ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants'; @@ -44,25 +40,22 @@ const LearnMoreTrialModal = ( const [embargoed, setEmbargoed] = useState(false); const dispatch = useDispatch(); - const [, salesLink] = useOpenSalesLink(); - // Cloud conditions const license = useSelector(getLicense); - const cloudFreeDeprecated = useSelector(deprecateCloudFree); const isCloud = license?.Cloud === 'true'; const handleEmbargoError = useCallback(() => { setEmbargoed(true); }, []); - let startTrialBtnMsg = formatMessage({id: 'start_trial.modal_btn.start_free_trial', defaultMessage: 'Start free 30-day trial'}); + const startTrialBtnMsg = formatMessage({id: 'start_trial.modal_btn.start_free_trial', defaultMessage: 'Start free 30-day trial'}); // close this modal once start trial btn is clicked and trial has started successfully const dismissAction = useCallback(() => { dispatch(closeModal(ModalIdentifiers.LEARN_MORE_TRIAL_MODAL)); }, []); - let startTrialBtn = ( + const startTrialBtn = ( ); - // no need to check if is cloud trial or if it have had prev cloud trial because the button that show this modal takes care of that - if (isCloud) { - startTrialBtnMsg = formatMessage({id: 'trial_btn.free.tryFreeFor30Days', defaultMessage: 'Start trial'}); - startTrialBtn = ( - - ); - if (cloudFreeDeprecated) { - startTrialBtn = ( - - - - ); - } - } - const handleOnClose = useCallback(() => { if (onClose) { onClose(); @@ -185,6 +151,11 @@ const LearnMoreTrialModal = ( const headerText = formatMessage({id: 'learn_more_trial_modal.pretitle', defaultMessage: 'With Enterprise, you can...'}); + if (isCloud) { + // Cloud users shouldn't be able to reach this modal, but in case they do, return nothing. + return null; + } + return ( { const isCurrentLicensed = license?.IsLicensed; // Cloud conditions - const subscription = useSelector((state: GlobalState) => state.entities.cloud.subscription); const isCloud = license?.Cloud === 'true'; - const isFreeTrial = subscription?.is_free_trial === 'true'; - const hadPrevCloudTrial = subscription?.is_free_trial === 'false' && subscription?.trial_end_at > 0; - const isPaidSubscription = isCloud && license?.SkuShortName !== LicenseSkus.Starter && !isFreeTrial; // Show this CTA if the instance is currently not licensed and has never had a trial license loaded before // also check that the user is a system admin (this after the onboarding task list is shown to all users) const selfHostedTrialCondition = (isCurrentLicensed === 'false' && isPrevLicensed === 'false') && - (props.isCurrentUserSystemAdmin || props.isFirstAdmin); + (props.isCurrentUserSystemAdmin || props.isFirstAdmin); - // if Cloud, show if not in trial and had never been on trial - const cloudTrialCondition = isCloud && !isFreeTrial && !hadPrevCloudTrial && !isPaidSubscription; - - const showStartTrialBtn = selfHostedTrialCondition || cloudTrialCondition; + // if Cloud, don't show + const showStartTrialBtn = selfHostedTrialCondition && !isCloud; const {formatMessage} = useIntl(); @@ -196,20 +189,11 @@ const Completed = (props: Props): JSX.Element => { defaultMessage='Start your free Enterprise trial now!' /> - {isCloud ? ( - - ) : ( - - )} + - - ); -}; - -describe('components/gather_intent/gather_intent.tsx', () => { - const gatherIntentText = 'gatherIntentText'; - const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch'); - - const initialState = { - entities: { - cloud: { - customer: TestHelper.getCloudCustomerMock(), - }, - }, - }; - - const baseProps: GatherIntentProps = { - modalComponent: DummyModal as any, - gatherIntentText, - typeGatherIntent: 'monthlySubscription', - }; - - it('should display modal if the user click on the modal opener', () => { - renderWithContext( - , - initialState, - ); - - fireEvent.click(screen.getByText(gatherIntentText)); - - expect(screen.getByText('Body')).toBeInTheDocument(); - }); - - it('should display the modal opener after close the modal', () => { - renderWithContext( - , - initialState, - ); - - fireEvent.click(screen.getByText(gatherIntentText)); - fireEvent.click(screen.getByLabelText('Close')); - - expect(screen.queryByText('Body')).not.toBeInTheDocument(); - }); - - it('should render the submitted modal after save the configuration', async () => { - useDispatchMock.mockReturnValue(jest.fn().mockImplementation(() => new Promise((resolve) => { - resolve({}); - }))); - renderWithContext( - , - initialState, - ); - - fireEvent.click(screen.getByText(gatherIntentText)); - - await act(async () => { - fireEvent.click(screen.getByText('Test')); - }); - - expect(screen.queryByText('Thanks for sharing feedback!')).toBeInTheDocument(); - }); - - it('should render the submitted modal after save the configuration and reopening the modal', async () => { - useDispatchMock.mockReturnValue(jest.fn().mockImplementation(() => new Promise((resolve) => { - resolve({}); - }))); - renderWithContext( - , - initialState, - ); - - fireEvent.click(screen.getByText(gatherIntentText)); - - await act(async () => { - fireEvent.click(screen.getByText('Test')); - }); - - fireEvent.click(screen.getByText('Done')); - fireEvent.click(screen.getByText(gatherIntentText)); - - expect(screen.queryByText('Thanks for sharing feedback!')).toBeInTheDocument(); - }); - - it('should render the submitted modal when the user has a feedback recorded', async () => { - useDispatchMock.mockReturnValue(jest.fn().mockImplementation(() => new Promise((resolve) => { - resolve({}); - }))); - const newState = JSON.parse(JSON.stringify(initialState)); - newState.entities.cloud.customer = { - ...newState.entities.cloud.customer, - monthly_subscription_alt_payment_method: 'Dummy feedback', - }; - - renderWithContext( - , - newState, - ); - - fireEvent.click(screen.getByText(gatherIntentText)); - - expect(screen.queryByText('Thanks for sharing feedback!')).toBeInTheDocument(); - }); -}); diff --git a/webapp/channels/src/components/payment_form/gather_intent/gather_intent.tsx b/webapp/channels/src/components/payment_form/gather_intent/gather_intent.tsx deleted file mode 100644 index 3cb3a81e8e..0000000000 --- a/webapp/channels/src/components/payment_form/gather_intent/gather_intent.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import type {JSXElementConstructor} from 'react'; -import {Modal} from 'react-bootstrap'; -import {FormattedMessage} from 'react-intl'; - -import type {TypePurchases} from '@mattermost/types/cloud'; - -import type {GatherIntentModalProps} from './gather_intent_modal'; -import {GatherIntentSubmittedModal} from './gather_intent_submitted_modal'; -import {useGatherIntent} from './useGatherIntent'; - -import './gather_intent.scss'; - -export interface GatherIntentProps { - typeGatherIntent: keyof typeof TypePurchases; - gatherIntentText: React.ReactNode; - modalComponent: JSXElementConstructor; -} - -export const GatherIntent = ({gatherIntentText, typeGatherIntent, modalComponent: ModalComponent}: GatherIntentProps) => { - const { - feedbackSaved, - handleSaveFeedback, - showModal, - handleOpenModal, - handleCloseModal, - submittingFeedback, - showError, - } = useGatherIntent({typeGatherIntent}); - - return ( -
- - {(text) => ( -

- {text} -

) - } -
- - {showModal && - - {!feedbackSaved && - } - {feedbackSaved && - } - } -
); -}; diff --git a/webapp/channels/src/components/payment_form/gather_intent/gather_intent_modal.test.tsx b/webapp/channels/src/components/payment_form/gather_intent/gather_intent_modal.test.tsx deleted file mode 100644 index 08902e84b9..0000000000 --- a/webapp/channels/src/components/payment_form/gather_intent/gather_intent_modal.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {fireEvent, renderWithContext, screen} from 'tests/react_testing_utils'; - -import {GatherIntentModal} from './gather_intent_modal'; -import type {GatherIntentModalProps} from './gather_intent_modal'; - -describe('components/gather_intent/gather_intent_modal.tsx', () => { - const baseProps: GatherIntentModalProps = { - onClose: jest.fn(), - onSave: jest.fn(), - isSubmitting: false, - showError: false, - }; - - it('shouldn\'t be able to save the feedback if the user don\'t click on any option', () => { - renderWithContext(); - - expect(screen.queryByText('Save')).toBeDisabled(); - }); - - it('shouldn\'t be able to save the feedback if the user only click in other and leave the input empty', () => { - renderWithContext(); - - fireEvent.click(screen.getByText('Other')); - - expect(screen.queryByText('Save')).toBeDisabled(); - }); - - it('shouldn\'t be able to save the feedback if the user only click in other and write only white spaces in the input', () => { - renderWithContext(); - - fireEvent.click(screen.getByText('Other')); - fireEvent.change(screen.getByPlaceholderText('Enter payment option here'), {target: {value: ' \n\t'}}); - - expect(screen.queryByText('Save')).toBeDisabled(); - }); - - it('should be able to save the feedback if the user only click in other, leave the input empty and press other option', () => { - renderWithContext(); - - fireEvent.click(screen.getByText('Other')); - fireEvent.click(screen.getByText('Wire')); - - expect(screen.queryByText('Save')).not.toHaveAttribute('disabled'); - }); - - it('should be able save the feedback if the user click in Wire option', () => { - renderWithContext(); - - fireEvent.click(screen.getByText('Wire')); - - expect(screen.queryByText('Save')).not.toHaveAttribute('disabled'); - }); - - it('should be able save the feedback if the user click in ACH option', () => { - renderWithContext(); - - fireEvent.click(screen.getByText('ACH')); - - expect(screen.queryByText('Save')).not.toHaveAttribute('disabled'); - }); - - it('should be able save the feedback if the user click in other option and fill the option', () => { - renderWithContext(); - - fireEvent.click(screen.getByText('Other')); - fireEvent.change(screen.getByPlaceholderText('Enter payment option here'), {target: {value: 'Test'}}); - - expect(screen.queryByText('Save')).not.toBeDisabled(); - }); -}); diff --git a/webapp/channels/src/components/payment_form/gather_intent/gather_intent_modal.tsx b/webapp/channels/src/components/payment_form/gather_intent/gather_intent_modal.tsx deleted file mode 100644 index 880a95df13..0000000000 --- a/webapp/channels/src/components/payment_form/gather_intent/gather_intent_modal.tsx +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useState} from 'react'; -import {Modal} from 'react-bootstrap'; -import {FormattedMessage, useIntl} from 'react-intl'; - -import warningIcon from 'images/icons/warning-icon.svg'; - -import './gather_intent.scss'; -import type {FormDataState} from './useGatherIntent'; - -export interface GatherIntentModalProps { - onClose: () => void; - onSave: (formData: FormDataState) => void; - isSubmitting: boolean; - showError: boolean; -} - -const isOtherUnchecked = (name: string, value: boolean): boolean => { - return name === 'other' && value === false; -}; - -const isOtherChecked = (name: string, value: boolean): boolean => { - return name === 'other' && value === true; -}; - -const isEmptyInput = (value: undefined | string) => { - return value == null || value.trim() === ''; -}; - -const isFormEmpty = (formDataState: FormDataState) => { - if (formDataState.other) { - return isEmptyInput(formDataState.otherPaymentOption) && !formDataState.wire && !formDataState.ach; - } - - return Object.values(formDataState).every((value) => value === false || value == null); -}; - -export const GatherIntentModal = ({onClose, onSave, isSubmitting, showError}: GatherIntentModalProps) => { - const [formState, setFormState] = useState({ - ach: false, - wire: false, - other: false, - otherPaymentOption: undefined, - }); - const intl = useIntl(); - - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - event.stopPropagation(); - - onSave(formState); - }; - - const handleTextAreaChange = (event: React.ChangeEvent) => { - const {name, value} = event.target; - - setFormState((formDataState) => ({ - ...formDataState, - [name]: value, - })); - }; - const handleCheckboxChange = (event: React.ChangeEvent) => { - const {name, checked} = event.target; - - if (isOtherUnchecked(name, checked)) { - setFormState((formDataState) => ({ - ...formDataState, - other: false, - otherPaymentOption: undefined, - })); - } - - if (isOtherChecked(name, checked)) { - setFormState((formDataState) => ({ - ...formDataState, - other: true, - otherPaymentOption: '', - })); - } - - setFormState((formDataState) => ({ - ...formDataState, - [name]: checked, - })); - }; - - return ( - <> - - - {(text) => ( -

- {text} -

) - } -
-