diff --git a/api4/cloud.go b/api4/cloud.go index 18861c086b..3827597c82 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -111,6 +111,12 @@ func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) { return } + // Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them + // At this point, the upgrade is complete. + if nErr := c.App.SendUpgradeConfirmationEmail(); nErr != nil { + c.Logger.Error("Error sending purchase confirmation email") + } + w.Write(json) } @@ -436,6 +442,11 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = nErr return } + case model.EventTypeSendUpgradeConfirmationEmail: + if nErr := c.App.SendUpgradeConfirmationEmail(); nErr != nil { + c.Err = nErr + return + } case model.EventTypeSendAdminWelcomeEmail: user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName) if appErr != nil { diff --git a/app/app_iface.go b/app/app_iface.go index 1f8dbb1780..a10dff06f8 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -980,6 +980,7 @@ type AppIface interface { SendPasswordReset(email string, siteURL string) (bool, *model.AppError) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError SendTestPushNotification(deviceID string) string + SendUpgradeConfirmationEmail() *model.AppError ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool diff --git a/app/cloud.go b/app/cloud.go index 95b47d22e8..39f2a6c35c 100644 --- a/app/cloud.go +++ b/app/cloud.go @@ -4,7 +4,9 @@ package app import ( + "fmt" "net/http" + "time" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -35,6 +37,50 @@ func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model. return nil } +func (a *App) SendUpgradeConfirmationEmail() *model.AppError { + sysAdmins, e := a.getSysAdminsEmailRecipients() + if e != nil { + return e + } + + if len(sysAdmins) == 0 { + return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError) + } + + subscription, err := a.Cloud().GetSubscription("") + if err != nil { + return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError) + } + + // Build readable trial end date + endTimeStamp := subscription.TrialEndAt + t := time.Unix(endTimeStamp, 0) + trialEndDate := fmt.Sprintf("%s %d, %d", t.Month(), t.Day(), t.Year()) + + // we want to at least have one email sent out to an admin + countNotOks := 0 + + for _, admin := range sysAdmins { + name := admin.FirstName + if name == "" { + name = admin.Username + } + + err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, trialEndDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS()) + if err != nil { + a.Log().Error("Error sending trial ended email to", mlog.String("email", admin.Email), mlog.Err(err)) + countNotOks++ + } + } + + // if not even one admin got an email, we consider that this operation errored + if countNotOks == len(sysAdmins) { + return model.NewAppError("app.SendCloudUpgradeConfirmationEmail", "app.user.send_emails.app_error", nil, "", http.StatusInternalServerError) + } + + return nil +} + // SendNoCardPaymentFailedEmail func (a *App) SendNoCardPaymentFailedEmail() *model.AppError { sysAdmins, err := a.getSysAdminsEmailRecipients() diff --git a/app/email/email.go b/app/email/email.go index 2237590a83..f8c3b65ebb 100644 --- a/app/email/email.go +++ b/app/email/email.go @@ -226,6 +226,32 @@ func (es *Service) SendWelcomeEmail(userID string, email string, verified bool, return nil } +func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string) error { + T := i18n.GetUserTranslations(locale) + subject := T("api.templates.cloud_upgrade_confirmation.subject") + + data := es.NewEmailTemplateData(locale) + data.Props["Title"] = T("api.templates.cloud_upgrade_confirmation.title") + data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation.subtitle", map[string]interface{}{"WorkspaceName": workspaceName, "TrialEnd": trialEndDate}) + data.Props["SiteURL"] = siteURL + data.Props["ButtonURL"] = siteURL + data.Props["Button"] = T("api.templates.cloud_welcome_email.button") + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") + data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail + + body, err := es.templatesContainer.RenderToString("cloud_upgrade_confirmation", data) + if err != nil { + return err + } + + if err := es.sendEmailWithCustomReplyTo(userEmail, subject, body, *es.config().SupportSettings.SupportEmail); err != nil { + return err + } + + return nil +} + func (es *Service) SendCloudTrialEndWarningEmail(userEmail, name, trialEndDate, locale, siteURL string) error { T := i18n.GetUserTranslations(locale) subject := T("api.templates.cloud_trial_ending_email.subject") diff --git a/app/email/email_test.go b/app/email/email_test.go index a9b90a9a10..fd983a8006 100644 --- a/app/email/email_test.go +++ b/app/email/email_test.go @@ -189,6 +189,9 @@ func TestSendCloudTrialEndWarningEmail(t *testing.T) { emailTo := "testclouduser@example.com" emailToUsername := strings.Split(emailTo, "@")[0] + th.UpdateConfig(func(cfg *model.Config) { + *cfg.SupportSettings.SupportEmail = "support@mattermost.com" + }) t.Run("SendCloudTrialEndWarningEmail", func(t *testing.T) { verifyMailbox := func(t *testing.T) { @@ -212,7 +215,7 @@ func TestSendCloudTrialEndWarningEmail(t *testing.T) { require.Contains(t, resultsEmail.Body.HTML, emailToUsername, "Wrong received message %s", resultsEmail.Body.Text) require.Contains(t, resultsEmail.Body.Text, "http://testserver", "Wrong received message %s", resultsEmail.Body.Text) require.Contains(t, resultsEmail.Body.Text, emailToUsername, "Wrong received message %s", resultsEmail.Body.Text) - require.Contains(t, resultsEmail.Body.Text, "feedback-cloud@mattermost.com") + require.Contains(t, resultsEmail.Body.Text, "support@mattermost.com") } mail.DeleteMailBox(emailTo) @@ -249,8 +252,7 @@ func TestSendCloudTrialEndedEmail(t *testing.T) { require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient") resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID) require.NoError(t, err, "Could not get message from mailbox") - require.Contains(t, resultsEmail.Body.Text, "your 14-day free trial of Mattermost Cloud Enterprise has ended today", "Wrong received message %s", resultsEmail.Body.Text) - require.Contains(t, resultsEmail.Body.Text, "we will delete your Cloud workspace permanently", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "Your free 14-day trial of Mattermost has ended", "Wrong received message %s", resultsEmail.Body.Text) } mail.DeleteMailBox(emailTo) @@ -261,6 +263,44 @@ func TestSendCloudTrialEndedEmail(t *testing.T) { }) } +func TestSendCloudUpgradedEmail(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + th.ConfigureInbucketMail() + + emailTo := "testclouduser@example.com" + emailToUsername := strings.Split(emailTo, "@")[0] + + t.Run("SendCloudUpgradedEmail", func(t *testing.T) { + verifyMailbox := func(t *testing.T) { + t.Helper() + + var resultsMailbox mail.JSONMessageHeaderInbucket + err2 := mail.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = mail.GetMailBox(emailTo) + return err + }) + if err2 != nil { + t.Skipf("No email was received, maybe due load on the server: %v", err2) + } + + require.Len(t, resultsMailbox, 1) + require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient") + resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID) + require.NoError(t, err, "Could not get message from mailbox") + require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text) + } + mail.DeleteMailBox(emailTo) + + err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName") + require.NoError(t, err) + + verifyMailbox(t) + }) +} + func TestMailServiceConfig(t *testing.T) { configuredReplyTo := "feedbackexample@test.com" customReplyTo := "customreplyto@test.com" diff --git a/app/email/mocks/ServiceInterface.go b/app/email/mocks/ServiceInterface.go index af7761a2e8..cee0edf0f4 100644 --- a/app/email/mocks/ServiceInterface.go +++ b/app/email/mocks/ServiceInterface.go @@ -153,6 +153,20 @@ func (_m *ServiceInterface) SendCloudTrialEndedEmail(userEmail string, name stri return r0 } +// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName +func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string) error { + ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) error); ok { + r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SendCloudWelcomeEmail provides a mock function with given fields: userEmail, locale, teamInviteID, workSpaceName, dns, siteURL func (_m *ServiceInterface) SendCloudWelcomeEmail(userEmail string, locale string, teamInviteID string, workSpaceName string, dns string, siteURL string) error { ret := _m.Called(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL) diff --git a/app/email/service.go b/app/email/service.go index f78821a706..19d076c12b 100644 --- a/app/email/service.go +++ b/app/email/service.go @@ -131,6 +131,7 @@ type ServiceInterface interface { SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error SendCloudTrialEndWarningEmail(userEmail, name, trialEndDate, locale, siteURL string) error SendCloudTrialEndedEmail(userEmail, name, locale, siteURL string) error + SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string) error SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error SendPasswordChangeEmail(email, method, locale, siteURL string) error SendUserAccessTokenAddedEmail(email, locale, siteURL string) error diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 647e6521bf..37a72fb667 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -14737,6 +14737,28 @@ func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string { return resultVar0 } +func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail() *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendUpgradeConfirmationEmail") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.SendUpgradeConfirmationEmail() + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId string, destinationPluginId string) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ServeInterPluginRequest") diff --git a/i18n/en.json b/i18n/en.json index ffcfffdc0c..6eb506c34b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3105,7 +3105,7 @@ }, { "id": "api.templates.cloud_trial_ended_email.start_subscription", - "translation": "Start subscription" + "translation": "Start Subscription" }, { "id": "api.templates.cloud_trial_ended_email.subject", @@ -3113,11 +3113,11 @@ }, { "id": "api.templates.cloud_trial_ended_email.subtitle", - "translation": "{{.Name}}, your 14-day free trial of Mattermost Cloud Enterprise has ended today, {{.TodayDate}}. Please add your payment information to ensure your team can continue enjoying the benefits of Cloud Enterprise. If you do not add your payment information within 30 days, we will delete your Cloud workspace permanently and you will lose any associated data." + "translation": "{{.Name}}, your 14-day free trial of Mattermost Cloud Enterprise ended on {{.TodayDate}}. We hope you’ve enjoyed our flexible and secure collaboration platform. Please add your payment information to ensure your team can continue collaborating with Mattermost." }, { "id": "api.templates.cloud_trial_ended_email.title", - "translation": "Your free 14-day trial of Mattermost has ended today" + "translation": "Your free 14-day trial of Mattermost has ended" }, { "id": "api.templates.cloud_trial_ending_email.add_payment_method", @@ -3135,6 +3135,18 @@ "id": "api.templates.cloud_trial_ending_email.title", "translation": "Your free 14-day trial of Mattermost is ending soon" }, + { + "id": "api.templates.cloud_upgrade_confirmation.subject", + "translation": "Mattermost Upgrade Confirmation" + }, + { + "id": "api.templates.cloud_upgrade_confirmation.subtitle", + "translation": "Your {{.WorkspaceName}} workspace has now been upgraded. You will be billed starting {{.TrialEnd}}" + }, + { + "id": "api.templates.cloud_upgrade_confirmation.title", + "translation": "You are now upgraded!" + }, { "id": "api.templates.cloud_welcome_email.add_apps_info", "translation": "Add apps to your workspace" @@ -3441,7 +3453,7 @@ }, { "id": "api.templates.questions_footer.info", - "translation": "Email us any time at " + "translation": "Need help or have questions? Email us at " }, { "id": "api.templates.questions_footer.title", diff --git a/model/cloud.go b/model/cloud.go index a05e67d355..0fcfc6aecd 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -6,11 +6,12 @@ package model import "strings" const ( - EventTypeFailedPayment = "failed-payment" - EventTypeFailedPaymentNoCard = "failed-payment-no-card" - EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email" - EventTypeTrialWillEnd = "trial-will-end" - EventTypeTrialEnded = "trial-ended" + EventTypeFailedPayment = "failed-payment" + EventTypeFailedPaymentNoCard = "failed-payment-no-card" + EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email" + EventTypeSendUpgradeConfirmationEmail = "send-upgrade-confirmation-email" + EventTypeTrialWillEnd = "trial-will-end" + EventTypeTrialEnded = "trial-ended" ) var MockCWS string diff --git a/templates/cloud_trial_end_warning.html b/templates/cloud_trial_end_warning.html index 16d618ab83..1f19dc3121 100644 --- a/templates/cloud_trial_end_warning.html +++ b/templates/cloud_trial_end_warning.html @@ -107,6 +107,7 @@ line-height: 36px !important; letter-spacing: -0.01em !important; color: #3F4350 !important; + font-family: Open Sans, sans-serif !important; } .subTitle div { @@ -128,6 +129,16 @@ padding: 15px 24px !important; } + .button-cloud a { + background-color: #1C58D9 !important; + font-weight: 400 !important; + font-family: Open Sans, sans-serif !important; + font-size: 16px !important; + line-height: 18px !important; + color: #FFFFFF !important; + padding: 15px 24px !important; + } + .messageButton a { background-color: #FFFFFF !important; border: 1px solid #FFFFFF !important; @@ -324,29 +335,29 @@
|
-
-
+
+
|