From 4959a1ff1ecdc67c4bd70229dd9d5928908b91af Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Fri, 24 Apr 2026 19:56:01 +0530 Subject: [PATCH] style: [CW-6876] Updated designs for invite email (#14090) Improved the design for the invite emails --- .../devise/mailer/_confirmation_body.html.erb | 89 ++++++++++ .../mailer/confirmation_instructions.html.erb | 88 +++++++--- app/views/layouts/mailer/base.liquid | 153 +++++++++++------- .../mailer/confirmation_instructions.html.erb | 134 ++++++++++----- spec/enterprise/mailers/devise_mailer_spec.rb | 59 ++++--- .../mailers/confirmation_instructions_spec.rb | 40 ++++- 6 files changed, 414 insertions(+), 149 deletions(-) create mode 100644 app/views/devise/mailer/_confirmation_body.html.erb diff --git a/app/views/devise/mailer/_confirmation_body.html.erb b/app/views/devise/mailer/_confirmation_body.html.erb new file mode 100644 index 000000000..b256539f3 --- /dev/null +++ b/app/views/devise/mailer/_confirmation_body.html.erb @@ -0,0 +1,89 @@ + + + + <%= eyebrow %> + + + + + +

<%= heading %>

+ + + + +

Hi <%= recipient_name %>,

+ + + + +

<%= intro_text %>

+ + + + +

<%= supporting_text %>

+ + +<% if detail_rows.any? %> + + + + <% detail_rows.each_with_index do |(label, value), index| %> + + + + <% end %> +
+

+ <%= label %> +

+

<%= value %>

+
+ + +<% end %> +<% if action_url.present? %> + + + + + + +
+ <%= link_to( + action_text, + action_url, + style: 'display:block; width:100%; box-sizing:border-box; padding:12px 24px; font-size:14px; line-height:20px; font-weight:700; color:#FFFFFF; text-align:center; text-decoration:none;' + ) %> +
+ + + + +

+ If the button does not work, open + <%= link_to( + 'this secure link', + action_url, + style: 'color:#2781F6; text-decoration:none; font-weight:600;' + ) %>. +

+ + +<% elsif info_title.present? %> + + + + + + +
+

+ <%= info_title %> +

+

<%= info_text %>

+
+ + +<% end %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb index 4a3b450a3..619c9f5ec 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -1,29 +1,65 @@ -

Hi <%= @resource.name %>,

+<% + brand_name = global_config['BRAND_NAME'] || 'Chatwoot' + recipient_name = @resource.name.presence || @resource.email + account_user = @resource&.account_users&.first + inviter = account_user&.inviter + account_name = account_user&.account&.name + invited_user = inviter.present? && @resource.unconfirmed_email.blank? -<% account_user = @resource&.account_users&.first %> + eyebrow = 'Welcome' + heading = 'Confirm your email to get started' + intro_text = + "Welcome to #{brand_name}. We just need to verify your email address before you can start using your account." + supporting_text = 'This only takes a moment.' + action_text = 'Confirm my account' + action_url = frontend_url('auth/confirmation', confirmation_token: @token) + info_title = nil + info_text = nil + detail_rows = [] + detail_rows << ['New email', @resource.unconfirmed_email] if @resource.unconfirmed_email.present? -<% if account_user&.inviter.present? && @resource.unconfirmed_email.blank? %> -

<%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out <%= global_config['BRAND_NAME'] || 'Chatwoot' %>.

-<% end %> + if @resource.unconfirmed_email.present? + eyebrow = 'Email update' + heading = 'Confirm your new email address' + intro_text = "We received a request to update the email address on your #{brand_name} account." + supporting_text = 'Confirm the new address below to finish the change.' + action_text = 'Confirm email address' + elsif @resource.confirmed? + eyebrow = 'Account ready' + heading = 'Your account is ready' + intro_text = "Your #{brand_name} account is already active." + supporting_text = 'Use the button below to sign in and continue where you left off.' + action_text = 'Open my account' + action_url = frontend_url('auth/sign_in') + detail_rows = [] + elsif invited_user + eyebrow = 'Workspace invitation' + heading = account_name.present? ? "You're invited to join #{account_name}" : "You're invited to try #{brand_name}" + intro_text = if account_name.present? + "#{inviter.name} invited you to join the #{account_name} workspace on #{brand_name}." + else + "#{inviter.name} invited you to try #{brand_name}." + end + supporting_text = 'Create your account to start collaborating with your team.' + action_text = 'Accept invitation' + action_url = frontend_url( + 'auth/password/edit', + reset_password_token: @resource.send(:set_reset_password_token) + ) + detail_rows = [['Invited by', inviter.name]] + detail_rows << ['Workspace', account_name] if account_name.present? + end +%> -<% if @resource.confirmed? %> -

You can login to your <%= global_config['BRAND_NAME'] || 'Chatwoot' %> account through the link below:

-<% else %> - <% if account_user&.inviter.blank? %> -

- Welcome to <%= global_config['BRAND_NAME'] || 'Chatwoot' %>! We have a suite of powerful tools ready for you to explore. Before that we quickly need to verify your email address to know it's really you. -

- <% end %> -

Please take a moment and click the link below and activate your account.

-<% end %> - - -<% if @resource.unconfirmed_email.present? %> -

<%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %>

-<% elsif @resource.confirmed? %> -

<%= link_to 'Login to my account', frontend_url('auth/sign_in') %>

-<% elsif account_user&.inviter.present? %> -

<%= link_to 'Confirm my account', frontend_url('auth/password/edit', reset_password_token: @resource.send(:set_reset_password_token)) %>

-<% else %> -

<%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %>

-<% end %> \ No newline at end of file +<%= render partial: 'devise/mailer/confirmation_body', locals: { + action_text: action_text, + action_url: action_url, + detail_rows: detail_rows, + eyebrow: eyebrow, + heading: heading, + info_text: info_text, + info_title: info_title, + intro_text: intro_text, + recipient_name: recipient_name, + supporting_text: supporting_text +} %> diff --git a/app/views/layouts/mailer/base.liquid b/app/views/layouts/mailer/base.liquid index 5fa07e139..e6c70b3e5 100644 --- a/app/views/layouts/mailer/base.liquid +++ b/app/views/layouts/mailer/base.liquid @@ -7,86 +7,129 @@ - - + {% assign brand_name = global_config['BRAND_NAME'] %} + {% if brand_name == nil %} + {% assign brand_name = 'Chatwoot' %} + {% endif %} + {% assign brand_url = global_config['BRAND_URL'] %} + + +
- diff --git a/enterprise/app/views/devise/mailer/confirmation_instructions.html.erb b/enterprise/app/views/devise/mailer/confirmation_instructions.html.erb index 91837f980..5032db646 100644 --- a/enterprise/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/enterprise/app/views/devise/mailer/confirmation_instructions.html.erb @@ -1,45 +1,99 @@ -

Hi <%= @resource.name %>,

+<% + brand_name = global_config['BRAND_NAME'] || 'Chatwoot' + recipient_name = @resource.name.presence || @resource.email + account_user = @resource&.account_users&.first + inviter = account_user&.inviter + account_name = account_user&.account&.name + is_saml_account = account_user&.account&.saml_enabled? + invited_user = inviter.present? && @resource.unconfirmed_email.blank? -<% account_user = @resource&.account_users&.first %> -<% is_saml_account = account_user&.account&.saml_enabled? %> + eyebrow = 'Welcome' + heading = 'Confirm your email to get started' + intro_text = + "Welcome to #{brand_name}. We just need to verify your email address before you can start using your account." + supporting_text = 'This only takes a moment.' + action_text = 'Confirm my account' + action_url = frontend_url('auth/confirmation', confirmation_token: @token) + info_title = nil + info_text = nil + detail_rows = [] + detail_rows << ['New email', @resource.unconfirmed_email] if @resource.unconfirmed_email.present? -<% if account_user&.inviter.present? && @resource.unconfirmed_email.blank? %> - <% if is_saml_account %> -

<%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to access <%= global_config['BRAND_NAME'] || 'Chatwoot' %> via Single Sign-On (SSO).

-

Your organization uses SSO for secure authentication. You will not need a password to access your account.

- <% else %> -

<%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out <%= global_config['BRAND_NAME'] || 'Chatwoot' %>.

- <% end %> -<% end %> + if @resource.unconfirmed_email.present? + eyebrow = 'Email update' + heading = 'Confirm your new email address' + intro_text = "We received a request to update the email address on your #{brand_name} account." + supporting_text = 'Confirm the new address below to finish the change.' + action_text = 'Confirm email address' + elsif @resource.confirmed? + eyebrow = 'Account ready' -<% if @resource.confirmed? %> -

You can login to your <%= global_config['BRAND_NAME'] || 'Chatwoot' %> account through the link below:

-<% else %> - <% if account_user&.inviter.blank? %> -

- Welcome to <%= global_config['BRAND_NAME'] || 'Chatwoot' %>! We have a suite of powerful tools ready for you to explore. Before that we quickly need to verify your email address to know it's really you. -

- <% end %> - <% unless is_saml_account %> -

Please take a moment and click the link below and activate your account.

- <% end %> -<% end %> + if is_saml_account + heading = 'Your access is ready' + intro_text = "Your #{brand_name} access is already set up." + supporting_text = "Use your organization's Single Sign-On (SSO) portal to access #{brand_name}." + action_text = nil + action_url = nil + info_title = "Sign in with your organization's SSO" + info_text = + "You won't need a separate password for #{brand_name}. Start from your company identity provider portal." + detail_rows = [] + detail_rows << ['Workspace', account_name] if account_name.present? + detail_rows << ['Sign-in method', 'Single Sign-On (SSO)'] + else + heading = 'Your account is ready' + intro_text = "Your #{brand_name} account is already active." + supporting_text = 'Use the button below to sign in and continue where you left off.' + action_text = 'Open my account' + action_url = frontend_url('auth/sign_in') + detail_rows = [] + end + elsif invited_user + eyebrow = 'Workspace invitation' + heading = account_name.present? ? "You're invited to join #{account_name}" : "You're invited to try #{brand_name}" + if is_saml_account + intro_text = if account_name.present? + "#{inviter.name} invited you to access the #{account_name} workspace on #{brand_name}." + else + "#{inviter.name} invited you to access #{brand_name}." + end + supporting_text = + "Your organization uses Single Sign-On (SSO), so you won't need to create a separate password." + action_text = nil + action_url = nil + info_title = "Use your organization's SSO portal" + info_text = "Continue from your company identity provider portal to access #{brand_name}." + detail_rows = [['Invited by', inviter.name]] + detail_rows << ['Workspace', account_name] if account_name.present? + detail_rows << ['Sign-in method', 'Single Sign-On (SSO)'] + else + intro_text = if account_name.present? + "#{inviter.name} invited you to join the #{account_name} workspace on #{brand_name}." + else + "#{inviter.name} invited you to try #{brand_name}." + end + supporting_text = 'Create your account to start collaborating with your team.' + action_text = 'Accept invitation' + action_url = frontend_url( + 'auth/password/edit', + reset_password_token: @resource.send(:set_reset_password_token) + ) + detail_rows = [['Invited by', inviter.name]] + detail_rows << ['Workspace', account_name] if account_name.present? + end + end +%> -<% if @resource.unconfirmed_email.present? %> -

<%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %>

-<% elsif @resource.confirmed? %> - <% if is_saml_account %> -

You can now access your account by logging in through your organization's SSO portal.

- <% else %> -

<%= link_to 'Login to my account', frontend_url('auth/sign_in') %>

- <% end %> -<% elsif account_user&.inviter.present? %> - <% if is_saml_account %> -

You can access your account by logging in through your organization's SSO portal.

- <% else %> -

<%= link_to 'Confirm my account', frontend_url('auth/password/edit', reset_password_token: @resource.send(:set_reset_password_token)) %>

- <% end %> -<% else %> -

<%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %>

-<% end %> +<%= render partial: 'devise/mailer/confirmation_body', locals: { + action_text: action_text, + action_url: action_url, + detail_rows: detail_rows, + eyebrow: eyebrow, + heading: heading, + info_text: info_text, + info_title: info_title, + intro_text: intro_text, + recipient_name: recipient_name, + supporting_text: supporting_text +} %> diff --git a/spec/enterprise/mailers/devise_mailer_spec.rb b/spec/enterprise/mailers/devise_mailer_spec.rb index 286e863f7..61ce92047 100644 --- a/spec/enterprise/mailers/devise_mailer_spec.rb +++ b/spec/enterprise/mailers/devise_mailer_spec.rb @@ -8,12 +8,23 @@ RSpec.describe 'Devise::Mailer' do let!(:confirmable_user) { create(:user, inviter: inviter_val, account: account) } let(:inviter_val) { nil } let(:mail) { Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {}) } + let(:mail_body) { CGI.unescapeHTML(mail.body.to_s) } before do confirmable_user.update!(confirmed_at: nil) confirmable_user.send(:generate_confirmation_token) end + context 'when brand name is intentionally blank' do + before do + create(:installation_config, name: 'BRAND_NAME', value: '') + end + + it 'preserves the blank brand override' do + expect(mail_body).not_to include('Chatwoot') + end + end + context 'with SAML enabled account' do let(:saml_settings) { create(:account_saml_settings, account: account) } @@ -21,12 +32,13 @@ RSpec.describe 'Devise::Mailer' do context 'when user has no inviter' do it 'shows standard welcome message without SSO references' do - expect(mail.body).to match('We have a suite of powerful tools ready for you to explore.') - expect(mail.body).not_to match('via Single Sign-On') + expect(mail_body).to include('Confirm your email to get started') + expect(mail_body).to include('We just need to verify your email address before you can start using your account.') + expect(mail_body).not_to include('Single Sign-On (SSO)') end - it 'does not show activation instructions for SAML accounts' do - expect(mail.body).not_to match('Please take a moment and click the link below and activate your account') + it 'shows the standard confirmation CTA' do + expect(mail_body).to include('Confirm my account') end it 'shows confirmation link' do @@ -38,22 +50,21 @@ RSpec.describe 'Devise::Mailer' do let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) } it 'mentions SSO invitation' do - expect(mail.body).to match( - "#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(account.name)}, has invited you to access.*via Single Sign-On \\(SSO\\)" - ) + expect(mail_body).to include("You're invited to join #{account.name}") + expect(mail_body).to include("#{inviter_val.name} invited you to access the #{account.name} workspace on Chatwoot.") end it 'explains SSO authentication' do - expect(mail.body).to match('Your organization uses SSO for secure authentication') - expect(mail.body).to match('You will not need a password to access your account') + expect(mail_body).to include("Your organization uses Single Sign-On (SSO), so you won't need to create a separate password.") end it 'does not show standard invitation message' do - expect(mail.body).not_to match('has invited you to try out') + expect(mail_body).not_to include('invited you to join') + expect(mail_body).not_to include('Accept invitation') end it 'directs to SSO portal instead of password reset' do - expect(mail.body).to match('You can access your account by logging in through your organization\'s SSO portal') + expect(mail_body).to include("Use your organization's SSO portal") expect(mail.body).not_to include('app/auth/password/edit') end end @@ -66,7 +77,9 @@ RSpec.describe 'Devise::Mailer' do end it 'shows SSO login instructions' do - expect(mail.body).to match('You can now access your account by logging in through your organization\'s SSO portal') + expect(mail_body).to include('Your access is ready') + expect(mail_body).to include("Sign in with your organization's SSO") + expect(mail_body).to include("Use your organization's Single Sign-On (SSO) portal to access") expect(mail.body).not_to include('/auth/sign_in') end end @@ -79,6 +92,7 @@ RSpec.describe 'Devise::Mailer' do end it 'still shows confirmation link for email verification' do + expect(mail_body).to include('Confirm your new email address') expect(mail.body).to include('app/auth/confirmation?confirmation_token') expect(confirmable_user.unconfirmed_email.blank?).to be false end @@ -90,7 +104,8 @@ RSpec.describe 'Devise::Mailer' do end it 'shows SSO login instructions instead of regular login' do - expect(mail.body).to match('You can now access your account by logging in through your organization\'s SSO portal') + expect(mail_body).to include('Your access is ready') + expect(mail_body).to include("Sign in with your organization's SSO") expect(mail.body).not_to include('/auth/sign_in') end end @@ -101,9 +116,10 @@ RSpec.describe 'Devise::Mailer' do let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) } it 'shows standard invitation without SSO references' do - expect(mail.body).to match('has invited you to try out Chatwoot') - expect(mail.body).not_to match('via Single Sign-On') - expect(mail.body).not_to match('SSO portal') + expect(mail_body).to include("You're invited to join #{account.name}") + expect(mail_body).to include("#{inviter_val.name} invited you to join the #{account.name} workspace on") + expect(mail_body).not_to include('Single Sign-On (SSO)') + expect(mail_body).not_to include("Use your organization's SSO portal") end it 'shows password reset link' do @@ -112,9 +128,10 @@ RSpec.describe 'Devise::Mailer' do end context 'when user has no inviter' do - it 'shows standard welcome message and activation instructions' do - expect(mail.body).to match('We have a suite of powerful tools ready for you to explore') - expect(mail.body).to match('Please take a moment and click the link below and activate your account') + it 'shows the standard confirmation state' do + expect(mail_body).to include('Confirm your email to get started') + expect(mail_body).to include('We just need to verify your email address before you can start using your account.') + expect(mail_body).to include('Confirm my account') end it 'shows confirmation link' do @@ -130,8 +147,9 @@ RSpec.describe 'Devise::Mailer' do end it 'shows regular login link' do + expect(mail_body).to include('Your account is ready') expect(mail.body).to include('/auth/sign_in') - expect(mail.body).not_to match('SSO portal') + expect(mail_body).not_to include('SSO portal') end end @@ -141,6 +159,7 @@ RSpec.describe 'Devise::Mailer' do end it 'shows confirmation link for email verification' do + expect(mail_body).to include('Confirm your new email address') expect(mail.body).to include('app/auth/confirmation?confirmation_token') expect(confirmable_user.unconfirmed_email.blank?).to be false end diff --git a/spec/mailers/confirmation_instructions_spec.rb b/spec/mailers/confirmation_instructions_spec.rb index 484001957..b82202c51 100644 --- a/spec/mailers/confirmation_instructions_spec.rb +++ b/spec/mailers/confirmation_instructions_spec.rb @@ -8,6 +8,7 @@ RSpec.describe 'Devise::Mailer' do let!(:confirmable_user) { create(:user, inviter: inviter_val, account: account) } let(:inviter_val) { nil } let(:mail) { Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {}) } + let(:mail_body) { CGI.unescapeHTML(mail.body.to_s) } before do # to verify the token in email @@ -22,12 +23,26 @@ RSpec.describe 'Devise::Mailer' do end it 'uses the user\'s name' do - expect(mail.body).to match("Hi #{CGI.escapeHTML(confirmable_user.name)},") + expect(mail.body.to_s).to include("Hi #{CGI.escapeHTML(confirmable_user.name)},") + expect(mail_body).to include("Hi #{confirmable_user.name},") end - it 'does not refer to the inviter and their account' do - expect(mail.body).not_to match('has invited you to try out Chatwoot!') - expect(mail.body).to match('We have a suite of powerful tools ready for you to explore.') + context 'when the user name contains HTML' do + before do + confirmable_user.update!(name: 'Sony ') + end + + it 'escapes the name in the rendered email body' do + expect(mail.body.to_s).to include("Hi #{CGI.escapeHTML(confirmable_user.name)},") + expect(mail.body.to_s).not_to include("Hi #{confirmable_user.name},") + end + end + + it 'shows the default confirmation state' do + expect(mail_body).to include('Confirm your email to get started') + expect(mail_body).to include('Welcome to Chatwoot. We just need to verify your email address before you can start using your account.') + expect(mail_body).to include('Confirm my account') + expect(mail_body).not_to include('Workspace invitation') end it 'sends a confirmation link' do @@ -39,10 +54,10 @@ RSpec.describe 'Devise::Mailer' do let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) } it 'refers to the inviter and their account' do - expect(mail.body).to match( - "#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(account.name)}, has invited you to try out Chatwoot." - ) - expect(mail.body).not_to match('We have a suite of powerful tools ready for you to explore.') + expect(mail_body).to include("You're invited to join #{account.name}") + expect(mail_body).to include("#{inviter_val.name} invited you to join the #{account.name} workspace on Chatwoot.") + expect(mail_body).to include('Accept invitation') + expect(mail_body).not_to include('Confirm your email to get started') end it 'sends a password reset link' do @@ -58,7 +73,10 @@ RSpec.describe 'Devise::Mailer' do it 'sends a confirmation link' do confirmation_mail = Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {}) + confirmation_body = CGI.unescapeHTML(confirmation_mail.body.to_s) + expect(confirmation_body).to include('Confirm your new email address') + expect(confirmation_body).to include('New email') expect(confirmation_mail.body).to include('app/auth/confirmation?confirmation_token') expect(confirmation_mail.body).not_to include('app/auth/password/edit') expect(confirmable_user.unconfirmed_email.blank?).to be false @@ -73,7 +91,9 @@ RSpec.describe 'Devise::Mailer' do it 'sends a confirmation link' do confirmation_mail = Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {}) + confirmation_body = CGI.unescapeHTML(confirmation_mail.body.to_s) + expect(confirmation_body).to include('Confirm your new email address') expect(confirmation_mail.body).to include('app/auth/confirmation?confirmation_token') expect(confirmation_mail.body).not_to include('app/auth/password/edit') expect(confirmable_user.unconfirmed_email.blank?).to be false @@ -88,6 +108,10 @@ RSpec.describe 'Devise::Mailer' do it 'send instructions with the link to login' do confirmation_mail = Devise::Mailer.confirmation_instructions(confirmable_user.reload, nil, {}) + confirmation_body = CGI.unescapeHTML(confirmation_mail.body.to_s) + + expect(confirmation_body).to include('Your account is ready') + expect(confirmation_body).to include('Open my account') expect(confirmation_mail.body).to include('/auth/sign_in') end end