diff --git a/app/controllers/mfa_setup_controller.rb b/app/controllers/mfa_setup_controller.rb index 9e12a445..1065fcfb 100644 --- a/app/controllers/mfa_setup_controller.rb +++ b/app/controllers/mfa_setup_controller.rb @@ -20,7 +20,8 @@ class MfaSetupController < ApplicationController redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured') else - @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Wabosign.product_name) + @provision_url = current_user.otp_provisioning_uri(current_user.email, + issuer: Wabosign.branded_product_name(current_account)) @error_message = I18n.t('code_is_invalid') @@ -49,6 +50,7 @@ class MfaSetupController < ApplicationController current_user.save! - @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Wabosign.product_name) + @provision_url = current_user.otp_provisioning_uri(current_user.email, + issuer: Wabosign.branded_product_name(current_account)) end end diff --git a/app/controllers/personalization_settings_controller.rb b/app/controllers/personalization_settings_controller.rb index 600e3bbb..86c97cad 100644 --- a/app/controllers/personalization_settings_controller.rb +++ b/app/controllers/personalization_settings_controller.rb @@ -2,6 +2,7 @@ class PersonalizationSettingsController < ApplicationController ALLOWED_KEYS = [ + AccountConfig::BRAND_NAME_KEY, AccountConfig::FORM_COMPLETED_BUTTON_KEY, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY, diff --git a/app/controllers/sms_settings_controller.rb b/app/controllers/sms_settings_controller.rb index 5c6498b8..88acdf00 100644 --- a/app/controllers/sms_settings_controller.rb +++ b/app/controllers/sms_settings_controller.rb @@ -29,7 +29,7 @@ class SmsSettingsController < ApplicationController Sms.send_message(account: current_account, to: to, - text: "Test SMS from #{Wabosign.product_name}.") + text: "Test SMS from #{Wabosign.branded_product_name(current_account)}.") redirect_to settings_sms_path, notice: "Test SMS dispatched to #{to}." rescue Sms::Error => e diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index abc32e6d..658a891c 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: "#{Wabosign.product_name} <#{Wabosign::SUPPORT_EMAIL}>" + # Lambda is evaluated per-message in mailer context, so @current_account + # (set by each mailer action) is available here. + default from: lambda { + account = instance_variable_defined?(:@current_account) ? @current_account : nil + "#{Wabosign.branded_product_name(account)} <#{Wabosign::SUPPORT_EMAIL}>" + } layout 'mailer' register_interceptor ActionMailerConfigsInterceptor diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index a2402aa2..6df7c651 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -10,7 +10,8 @@ class UserMailer < ApplicationMailer I18n.with_locale(@current_account.locale) do mail(to: @user.friendly_name, - subject: I18n.t('you_are_invited_to_product_name', product_name: Wabosign.product_name)) + subject: I18n.t('you_are_invited_to_product_name', + product_name: Wabosign.branded_product_name(@current_account))) end end end diff --git a/app/models/account.rb b/app/models/account.rb index aab90683..eb3959d7 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -66,6 +66,10 @@ class Account < ApplicationRecord linked_account_account&.testing? end + def brand_name + account_configs.find_by(key: AccountConfig::BRAND_NAME_KEY)&.value.to_s.strip.presence + end + def tz_info @tz_info ||= TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone) end diff --git a/app/models/account_config.rb b/app/models/account_config.rb index eb4aabd7..c42bfb1e 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -61,6 +61,7 @@ class AccountConfig < ApplicationRecord TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields' POLICY_LINKS_KEY = 'policy_links' ENABLE_MCP_KEY = 'enable_mcp' + BRAND_NAME_KEY = 'brand_name' EMAIL_VARIABLES = { SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze, diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index e6e9060c..5163b1aa 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -5,6 +5,6 @@

<%= t('if_you_didnt_request_this_you_can_ignore_this_email') %>

<%= t('thanks') %>,
- <%= Wabosign.product_name %> + <%= Wabosign.branded_product_name(@resource&.account) %>

<% content_for(:remove_attribution, true) %> diff --git a/app/views/invitations/edit.html.erb b/app/views/invitations/edit.html.erb index 2f348964..fb3a26f3 100644 --- a/app/views/invitations/edit.html.erb +++ b/app/views/invitations/edit.html.erb @@ -1,7 +1,7 @@

<%= svg_icon('waving_hand', class: 'h-10 w-10') %> - <%= t('welcome_to_product_name', product_name: Wabosign.product_name) %> + <%= t('welcome_to_product_name', product_name: Wabosign.branded_product_name(current_account)) %>

<%= form_for(resource, as: resource_name, url: invitation_path, html: { method: :put, class: 'space-y-6' }) do |f| %>
diff --git a/app/views/layouts/_head_tags.html.erb b/app/views/layouts/_head_tags.html.erb index 913af2e9..1f50e55e 100644 --- a/app/views/layouts/_head_tags.html.erb +++ b/app/views/layouts/_head_tags.html.erb @@ -1,4 +1,5 @@ +<% brand = Wabosign.branded_product_name(signed_in? ? current_account : nil) %> - <%= content_for(:html_title) || (signed_in? ? Wabosign.product_name : "#{Wabosign.product_name} | Open Source Document Signing") %> + <%= content_for(:html_title) || (signed_in? ? brand : "#{brand} | Open Source Document Signing") %> <%= render 'shared/meta' %> diff --git a/app/views/mcp_settings/index.html.erb b/app/views/mcp_settings/index.html.erb index ca0d50ff..c6000dd0 100644 --- a/app/views/mcp_settings/index.html.erb +++ b/app/views/mcp_settings/index.html.erb @@ -33,7 +33,7 @@

-

<%= t('connect_to_wabosign_mcp') %>

+

<%= t('connect_to_product_name_mcp', product_name: Wabosign.branded_product_name(current_account)) %>

<%= t('add_the_following_to_your_mcp_client_configuration') %>:

<% text = JSON.pretty_generate({ mcpServers: { wabosign: { type: 'http', url: "#{root_url(Wabosign.default_url_options)}mcp", headers: { Authorization: "Bearer #{@mcp_token.token}" } } } }).strip %> diff --git a/app/views/pages/landing.html.erb b/app/views/pages/landing.html.erb index 27f0074e..d6dfc532 100644 --- a/app/views/pages/landing.html.erb +++ b/app/views/pages/landing.html.erb @@ -5,7 +5,7 @@
<%= render 'shared/logo', width: '100', height: '100' %>

- <%= Wabosign.product_name %> + <%= Wabosign.branded_product_name %>

<% if Wabosign.version.present? %> diff --git a/app/views/personalization_settings/_brand_name_form.html.erb b/app/views/personalization_settings/_brand_name_form.html.erb new file mode 100644 index 00000000..f65a8e7d --- /dev/null +++ b/app/views/personalization_settings/_brand_name_form.html.erb @@ -0,0 +1,18 @@ +<%= form_with url: settings_personalization_path, method: :post, + html: { class: 'space-y-3', autocomplete: 'off' } do %> + +
+ + +
+ +<% end %> +

+ The <%= Wabosign::UPSTREAM_NAME %> upstream attribution required by AGPL §7(b) stays visible in the footer, post-signing screen, and email footers regardless of this setting. +

diff --git a/app/views/personalization_settings/show.html.erb b/app/views/personalization_settings/show.html.erb index f4ba1639..2618526f 100644 --- a/app/views/personalization_settings/show.html.erb +++ b/app/views/personalization_settings/show.html.erb @@ -10,6 +10,10 @@ <%= render 'submitter_completed_email_form' %> <%= render 'signature_request_sms_form' %>
+

+ Product name +

+ <%= render 'brand_name_form' %>

<%= t('company_logo') %>

diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb index 18aafb96..3211fd12 100644 --- a/app/views/pwa/manifest.json.erb +++ b/app/views/pwa/manifest.json.erb @@ -1,6 +1,7 @@ +<% brand = Wabosign.branded_product_name %> { - "name": "<%= Wabosign.product_name %>", - "short_name": "<%= Wabosign.product_name %>", + "name": "<%= brand %>", + "short_name": "<%= brand %>", "id": "/", "icons": [ { @@ -18,7 +19,7 @@ "display": "standalone", "scope": "/", "orientation": "any", - "description": "<%= Wabosign.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.", + "description": "<%= brand %> is an open source platform that provides secure and efficient digital document signing and processing.", "categories": ["productivity", "utilities"], "theme_color": "#FAF7F4", "background_color": "#FAF7F4" diff --git a/app/views/shared/_attribution.html.erb b/app/views/shared/_attribution.html.erb index bc464bba..9a006d55 100644 --- a/app/views/shared/_attribution.html.erb +++ b/app/views/shared/_attribution.html.erb @@ -1 +1 @@ -<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path] %> +<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path], account: local_assigns[:account] %> diff --git a/app/views/shared/_email_attribution.html.erb b/app/views/shared/_email_attribution.html.erb index 23788a1c..401246a0 100644 --- a/app/views/shared/_email_attribution.html.erb +++ b/app/views/shared/_email_attribution.html.erb @@ -1,13 +1,15 @@

---

+<% brand = Wabosign.branded_product_name(@current_account) %>

<% if @current_account&.testing? %> - <%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Wabosign::PRODUCT_EMAIL_URL}/start", product_name: Wabosign.product_name) %> + <%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Wabosign::PRODUCT_EMAIL_URL}/start", product_name: brand) %> <% else %> - <%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Wabosign::PRODUCT_EMAIL_URL}/start", product_name: Wabosign.product_name) %> + <%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Wabosign::PRODUCT_EMAIL_URL}/start", product_name: brand) %> <% end %>

+<%# AGPL §7(b) DocuSeal attribution. Do not remove or rebrand. %>

<%= t('based_on') %> <%= Wabosign::UPSTREAM_NAME %> diff --git a/app/views/shared/_meta.html.erb b/app/views/shared/_meta.html.erb index 00f49b18..edab477b 100644 --- a/app/views/shared/_meta.html.erb +++ b/app/views/shared/_meta.html.erb @@ -1,14 +1,15 @@ <% if Wabosign.demo? || (request.path != '/' && !devise_controller?) %> <% end %> -<% title = content_for(:html_title) || (signed_in? ? Wabosign.product_name : "#{Wabosign.product_name} | Open Source Document Signing") %> +<% brand = Wabosign.branded_product_name(signed_in? ? current_account : nil) %> +<% title = content_for(:html_title) || (signed_in? ? brand : "#{brand} | Open Source Document Signing") %> <% description = content_for(:html_description) || 'Open source, self-hosted tool to streamline document filling and signing. Create custom PDF forms to complete and sign with an easy to use online tool.' %> - + <% if content_for(:disable_image_preview) %> diff --git a/app/views/shared/_powered_by.html.erb b/app/views/shared/_powered_by.html.erb index 90b151b2..113c4030 100644 --- a/app/views/shared/_powered_by.html.erb +++ b/app/views/shared/_powered_by.html.erb @@ -9,8 +9,9 @@ <% else %> <%= t('powered_by') %> <% end %> - <%= Wabosign.product_name %> - <%= t('open_source_documents_software') %> + <%= Wabosign.branded_product_name(local_assigns[:account]) %> - <%= t('open_source_documents_software') %>

+<%# AGPL §7(b) DocuSeal attribution. Do not remove or rebrand. %>
<%= t('based_on') %> <%= Wabosign::UPSTREAM_NAME %> diff --git a/app/views/shared/_title.html.erb b/app/views/shared/_title.html.erb index c5e7b611..8b3969af 100644 --- a/app/views/shared/_title.html.erb +++ b/app/views/shared/_title.html.erb @@ -1,2 +1,2 @@ <%= render 'shared/account_logo', account: current_account %> -<%= Wabosign.product_name %> +<%= Wabosign.branded_product_name(current_account) %> diff --git a/app/views/start_form/_brand_logo.html.erb b/app/views/start_form/_brand_logo.html.erb index 04dbb467..fcc0672d 100644 --- a/app/views/start_form/_brand_logo.html.erb +++ b/app/views/start_form/_brand_logo.html.erb @@ -2,5 +2,5 @@ <%= render 'shared/account_logo', account: @template&.account, width: '50px', height: '50px' %> -

<%= Wabosign.product_name %>

+

<%= Wabosign.branded_product_name(@template&.account) %>

diff --git a/app/views/start_form/email_verification.html.erb b/app/views/start_form/email_verification.html.erb index 91b25ca1..6e4b29f6 100644 --- a/app/views/start_form/email_verification.html.erb +++ b/app/views/start_form/email_verification.html.erb @@ -1,4 +1,4 @@ -<% content_for(:html_title, "#{@template.name} | #{Wabosign.product_name}") %> +<% content_for(:html_title, "#{@template.name} | #{Wabosign.branded_product_name(@template.account)}") %> <% I18n.with_locale(@template.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% end %> diff --git a/app/views/start_form/private.html.erb b/app/views/start_form/private.html.erb index f8610bf2..cdc1e738 100644 --- a/app/views/start_form/private.html.erb +++ b/app/views/start_form/private.html.erb @@ -1,4 +1,4 @@ -<% content_for(:html_title, "#{@template.name} | #{Wabosign.product_name}") %> +<% content_for(:html_title, "#{@template.name} | #{Wabosign.branded_product_name(@template.account)}") %> <% I18n.with_locale(@template.account.locale) do %> <% content_for(:html_description, t('share_link_is_currently_disabled')) %> <% end %> diff --git a/app/views/start_form/show.html.erb b/app/views/start_form/show.html.erb index 34911190..d81d0c37 100644 --- a/app/views/start_form/show.html.erb +++ b/app/views/start_form/show.html.erb @@ -1,4 +1,4 @@ -<% content_for(:html_title, "#{@template.name} | #{Wabosign.product_name}") %> +<% content_for(:html_title, "#{@template.name} | #{Wabosign.branded_product_name(@template.account)}") %> <% I18n.with_locale(@template.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% end %> diff --git a/app/views/submit_form/_brand_logo.html.erb b/app/views/submit_form/_brand_logo.html.erb index ce58f268..ff23e5c9 100644 --- a/app/views/submit_form/_brand_logo.html.erb +++ b/app/views/submit_form/_brand_logo.html.erb @@ -1,4 +1,4 @@ <%= render 'shared/account_logo', account: @submitter&.submission&.account, class: 'w-9 h-9 md:w-12 md:h-12' %> - <%= Wabosign.product_name %> + <%= Wabosign.branded_product_name(@submitter&.submission&.account) %> diff --git a/app/views/submit_form/email_2fa.html.erb b/app/views/submit_form/email_2fa.html.erb index 62584cc7..4033d354 100644 --- a/app/views/submit_form/email_2fa.html.erb +++ b/app/views/submit_form/email_2fa.html.erb @@ -1,4 +1,4 @@ -<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Wabosign.product_name}") %> +<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Wabosign.branded_product_name(@submitter.account)}") %> <% I18n.with_locale(@submitter.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %> <% end %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 898e2393..a4ae78d4 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -1,4 +1,4 @@ -<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Wabosign.product_name}") %> +<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Wabosign.branded_product_name(@submitter.account)}") %> <% I18n.with_locale(@submitter.account.locale) do %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %> <% end %> diff --git a/app/views/templates_dashboard/index.html.erb b/app/views/templates_dashboard/index.html.erb index 0a6432db..931937e7 100644 --- a/app/views/templates_dashboard/index.html.erb +++ b/app/views/templates_dashboard/index.html.erb @@ -65,7 +65,7 @@ <% if user_config.new_record? && !params.key?(:tour) %>
- <%= t('welcome_to_wabosign') %> + <%= t('welcome_to_product_name', product_name: Wabosign.branded_product_name(current_account)) %>
<%= t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') %> diff --git a/app/views/templates_share_link_qr/_branding.html.erb b/app/views/templates_share_link_qr/_branding.html.erb index 4377ca34..389b7eff 100644 --- a/app/views/templates_share_link_qr/_branding.html.erb +++ b/app/views/templates_share_link_qr/_branding.html.erb @@ -1,2 +1,2 @@ <%= t('powered_by') %> -<%= Wabosign.product_name %> +<%= Wabosign.branded_product_name(@template&.account) %> diff --git a/app/views/templates_share_link_qr/_logo.html.erb b/app/views/templates_share_link_qr/_logo.html.erb index 6ad9ee61..277a6afd 100644 --- a/app/views/templates_share_link_qr/_logo.html.erb +++ b/app/views/templates_share_link_qr/_logo.html.erb @@ -1,2 +1,2 @@ <%= render 'shared/account_logo', account: @template&.account %> -<%= Wabosign.product_name %> +<%= Wabosign.branded_product_name(@template&.account) %> diff --git a/app/views/user_mailer/invitation_email.html.erb b/app/views/user_mailer/invitation_email.html.erb index bb3392ff..d8243b93 100644 --- a/app/views/user_mailer/invitation_email.html.erb +++ b/app/views/user_mailer/invitation_email.html.erb @@ -1,5 +1,5 @@

<%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,

-

<%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Wabosign.product_name) %>

+

<%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Wabosign.branded_product_name(@current_account)) %>

<%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %>

<%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %>

diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 3e3a3175..a36d497a 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -929,7 +929,7 @@ en: &en mcp_token_has_been_removed: MCP token has been removed. enable_mcp_server: Enable MCP server all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled: All existing MCP connections will be stopped immediately when this setting is disabled. - connect_to_wabosign_mcp: Connect to WaboSign MCP + connect_to_product_name_mcp: Connect to %{product_name} MCP add_the_following_to_your_mcp_client_configuration: Add the following to your MCP client configuration works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Works with Claude Desktop, Cursor, Windsurf, VS Code, and any MCP-compatible client. your_email_address_has_been_changed: Your email address has been changed @@ -1977,7 +1977,7 @@ es: &es mcp_token_has_been_removed: El token MCP ha sido eliminado. enable_mcp_server: Habilitar servidor MCP all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled: Todas las conexiones MCP existentes se detendrán inmediatamente cuando se desactive esta configuración. - connect_to_wabosign_mcp: Conectar a WaboSign MCP + connect_to_product_name_mcp: Conectar a %{product_name} MCP add_the_following_to_your_mcp_client_configuration: Agregue lo siguiente a la configuración de su cliente MCP works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funciona con Claude Desktop, Cursor, Windsurf, VS Code y cualquier cliente compatible con MCP. your_email_address_has_been_changed: Tu dirección de correo electrónico ha sido cambiada @@ -3025,7 +3025,7 @@ it: &it mcp_token_has_been_removed: Il token MCP è stato rimosso. enable_mcp_server: Abilita server MCP all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled: Tutte le connessioni MCP esistenti verranno interrotte immediatamente quando questa impostazione viene disattivata. - connect_to_wabosign_mcp: Connetti a WaboSign MCP + connect_to_product_name_mcp: Connetti a %{product_name} MCP add_the_following_to_your_mcp_client_configuration: Aggiungi quanto segue alla configurazione del tuo client MCP works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funziona con Claude Desktop, Cursor, Windsurf, VS Code e qualsiasi client compatibile con MCP. your_email_address_has_been_changed: Il tuo indirizzo email è stato modificato @@ -4070,7 +4070,7 @@ fr: &fr mcp_token_has_been_removed: Le jeton MCP a été supprimé. enable_mcp_server: Activer le serveur MCP all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled: Toutes les connexions MCP existantes seront arrêtées immédiatement lorsque ce paramètre est désactivé. - connect_to_wabosign_mcp: Se connecter à WaboSign MCP + connect_to_product_name_mcp: Se connecter à %{product_name} MCP add_the_following_to_your_mcp_client_configuration: Ajoutez ce qui suit à la configuration de votre client MCP works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Fonctionne avec Claude Desktop, Cursor, Windsurf, VS Code et tout client compatible MCP. your_email_address_has_been_changed: Votre adresse e-mail a été modifiée @@ -5118,7 +5118,7 @@ pt: &pt mcp_token_has_been_removed: O token MCP foi removido. enable_mcp_server: Ativar servidor MCP all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled: Todas as conexões MCP existentes serão interrompidas imediatamente quando esta configuração for desativada. - connect_to_wabosign_mcp: Conectar ao WaboSign MCP + connect_to_product_name_mcp: Conectar ao %{product_name} MCP add_the_following_to_your_mcp_client_configuration: Adicione o seguinte à configuração do seu cliente MCP works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funciona com Claude Desktop, Cursor, Windsurf, VS Code e qualquer cliente compatível com MCP. your_email_address_has_been_changed: Seu endereço de e-mail foi alterado @@ -6166,7 +6166,7 @@ de: &de mcp_token_has_been_removed: Das MCP-Token wurde entfernt. enable_mcp_server: MCP-Server aktivieren all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled: Alle bestehenden MCP-Verbindungen werden sofort gestoppt, wenn diese Einstellung deaktiviert wird. - connect_to_wabosign_mcp: Mit WaboSign MCP verbinden + connect_to_product_name_mcp: Mit %{product_name} MCP verbinden add_the_following_to_your_mcp_client_configuration: Fügen Sie Folgendes zu Ihrer MCP-Client-Konfiguration hinzu works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Funktioniert mit Claude Desktop, Cursor, Windsurf, VS Code und jedem MCP-kompatiblen Client. your_email_address_has_been_changed: Ihre E-Mail-Adresse wurde geändert @@ -7623,7 +7623,7 @@ nl: &nl mcp_token_has_been_removed: Het MCP-token is verwijderd. enable_mcp_server: MCP-server inschakelen all_existing_mcp_connections_will_be_stopped_immediately_when_this_setting_is_disabled: Alle bestaande MCP-verbindingen worden onmiddellijk gestopt wanneer deze instelling wordt uitgeschakeld. - connect_to_wabosign_mcp: Verbinden met WaboSign MCP + connect_to_product_name_mcp: Verbinden met %{product_name} MCP add_the_following_to_your_mcp_client_configuration: Voeg het volgende toe aan uw MCP-clientconfiguratie works_with_claude_desktop_cursor_windsurf_vs_code_and_any_mcp_compatible_client: Werkt met Claude Desktop, Cursor, Windsurf, VS Code en elke MCP-compatibele client. your_email_address_has_been_changed: Uw e-mailadres is gewijzigd diff --git a/lib/mcp/handle_request.rb b/lib/mcp/handle_request.rb index 70fd628c..3a7b66c9 100644 --- a/lib/mcp/handle_request.rb +++ b/lib/mcp/handle_request.rb @@ -26,7 +26,7 @@ module Mcp result: { protocolVersion: '2025-11-25', serverInfo: { - name: Wabosign.product_name, + name: Wabosign.branded_product_name(current_user&.account), version: Wabosign.version.to_s }, capabilities: { diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 1671ccec..a87eafa7 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -43,11 +43,11 @@ module Submissions io = StringIO.new - document.trailer.info[:Creator] = "#{Wabosign.product_name} (#{Wabosign::PRODUCT_URL})" + document.trailer.info[:Creator] = "#{Wabosign.branded_product_name(account)} (#{Wabosign::PRODUCT_URL})" if pkcs sign_params = { - reason: sign_reason, + reason: sign_reason(account), **Submissions::GenerateResultAttachments.build_signing_params(last_submitter, pkcs, tsa_url) } @@ -510,8 +510,8 @@ module Submissions composer.document end - def sign_reason - "Signed with #{Wabosign.product_name}" + def sign_reason(account = nil) + "Signed with #{Wabosign.branded_product_name(account)}" end def select_attachments(submitter) @@ -534,7 +534,7 @@ module Submissions column.image(PdfIcons.account_logo_io(submission&.account), width: 40, height: 40, position: :float) - column.formatted_text([{ text: Wabosign.product_name, + column.formatted_text([{ text: Wabosign.branded_product_name(submission&.account), link: Wabosign::PRODUCT_EMAIL_URL }], font_size: 20, font: [FONT_NAME, { variant: :bold }], diff --git a/lib/submissions/generate_combined_attachment.rb b/lib/submissions/generate_combined_attachment.rb index 3320e2b6..9b728211 100644 --- a/lib/submissions/generate_combined_attachment.rb +++ b/lib/submissions/generate_combined_attachment.rb @@ -15,7 +15,7 @@ module Submissions io = StringIO.new - pdf.trailer.info[:Creator] = "#{Wabosign.product_name} (#{Wabosign::PRODUCT_URL})" + pdf.trailer.info[:Creator] = "#{Wabosign.branded_product_name(account)} (#{Wabosign::PRODUCT_URL})" if Wabosign.pdf_format == 'pdf/a-3b' pdf.task(:pdfa, level: '3b') diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index c4d24a1d..9e9d3c68 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -37,7 +37,11 @@ module Submissions bold_italic: FONT_BOLD_NAME }.freeze - SIGN_REASON = "Signed with #{Wabosign.product_name}".freeze + # PDF signature "reason" template. Per-account branded — the actual + # template is computed by `sign_reason_template(account)` below; this + # constant is the format placeholder. Historically the format kwarg + # `name:` was unused. + SIGN_REASON_FORMAT = 'Signed with %s' RTL_REGEXP = TextUtils::RTL_REGEXP @@ -728,7 +732,7 @@ module Submissions def build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:) io = StringIO.new - pdf.trailer.info[:Creator] = info_creator + pdf.trailer.info[:Creator] = info_creator(submitter&.account) if Wabosign.pdf_format == 'pdf/a-3b' pdf.task(:pdfa, level: '3b') @@ -970,14 +974,14 @@ module Submissions HexaPDF::Document.new(io:) end - def sign_reason(name) - format(SIGN_REASON, name:) + def sign_reason(_name, account: nil) + format(SIGN_REASON_FORMAT, brand: Wabosign.branded_product_name(account)) end def single_sign_reason(submitter) - signers = submitter.submission.submitters.sort_by(&:completed_at).map { |s| s.email || s.name || s.phone } + submitter.submission.submitters.sort_by(&:completed_at).map { |s| s.email || s.name || s.phone } - format(SIGN_REASON, name: signers.reverse.join(', ')) + format(SIGN_REASON_FORMAT, brand: Wabosign.branded_product_name(submitter&.account)) end def fetch_sign_reason(submitter) @@ -992,7 +996,7 @@ module Submissions .first_or_initialize(value: 'single') end - return sign_reason(reason_name) if config.value == 'multiple' + return sign_reason(reason_name, account: submitter.account) if config.value == 'multiple' if !submitter.submission.submitters.exists?(completed_at: nil) && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at) @@ -1002,8 +1006,8 @@ module Submissions nil end - def info_creator - "#{Wabosign.product_name} (#{Wabosign::PRODUCT_URL})" + def info_creator(account = nil) + "#{Wabosign.branded_product_name(account)} (#{Wabosign::PRODUCT_URL})" end def detached_signature?(_submitter) diff --git a/lib/wabosign.rb b/lib/wabosign.rb index f85aa47f..4730f803 100644 --- a/lib/wabosign.rb +++ b/lib/wabosign.rb @@ -98,6 +98,29 @@ module Wabosign PRODUCT_NAME end + # Returns the account's custom brand name (if set), the deployment's + # default-account brand (for anonymous surfaces like the landing page, + # PWA manifest, or og:title), or PRODUCT_NAME as the ultimate fallback. + # Never overrides Wabosign::UPSTREAM_NAME — the AGPL §7(b) DocuSeal + # credit in _powered_by, _email_attribution, and completed.vue stays + # untouched. + def branded_product_name(account = nil) + account&.brand_name.presence || + default_brand_account&.brand_name.presence || + PRODUCT_NAME + end + + # The deployment-wide fallback account whose brand name is used for + # anonymous surfaces (no current_account in scope). Single-tenant + # installs have exactly one account; multi-tenant picks the oldest. + # Not memoized: the underlying query is fast and memoization would + # need cache invalidation on every personalization save. + def default_brand_account + Account.where(archived_at: nil).order(:created_at).first + rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished + nil + end + def refresh_default_url_options! @default_url_options = nil end diff --git a/spec/lib/wabosign_spec.rb b/spec/lib/wabosign_spec.rb new file mode 100644 index 00000000..ceef2c71 --- /dev/null +++ b/spec/lib/wabosign_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Wabosign do + describe '.branded_product_name' do + context 'with no accounts in the database' do + before { Account.delete_all } + + it 'falls back to the PRODUCT_NAME constant' do + expect(described_class.branded_product_name).to eq(Wabosign::PRODUCT_NAME) + expect(described_class.branded_product_name(nil)).to eq(Wabosign::PRODUCT_NAME) + end + end + + context 'when the passed-in account has a brand_name configured' do + let(:account) do + create(:account).tap do |a| + a.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign') + end + end + + it 'returns the account brand' do + expect(described_class.branded_product_name(account)).to eq('Acme Sign') + end + + it 'returns the account brand even when newer accounts also have brands' do + newer = create(:account) + newer.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Other Brand') + expect(described_class.branded_product_name(account)).to eq('Acme Sign') + end + end + + context 'when no account is passed but the oldest account has a brand' do + it 'uses the default-account fallback' do + create(:account).account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Default Brand') + create(:account) # newer, no brand + expect(described_class.branded_product_name).to eq('Default Brand') + expect(described_class.branded_product_name(nil)).to eq('Default Brand') + end + end + + context 'when the passed-in account has no brand but the default account does' do + it 'still uses the default-account fallback' do + create(:account).account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Default Brand') + other = create(:account) + expect(described_class.branded_product_name(other)).to eq('Default Brand') + end + end + + context 'when an archived account is the oldest' do + it 'is skipped when looking up the default brand' do + archived = create(:account, archived_at: Time.current) + archived.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Archived Brand') + + live = create(:account) + live.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Live Brand') + + expect(described_class.branded_product_name).to eq('Live Brand') + end + end + end + + describe '.default_brand_account' do + it 'returns the oldest non-archived account' do + first = create(:account) + _second = create(:account) + expect(described_class.default_brand_account).to eq(first) + end + + it 'skips archived accounts' do + _archived = create(:account, archived_at: Time.current) + live = create(:account) + expect(described_class.default_brand_account).to eq(live) + end + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb new file mode 100644 index 00000000..00fe692e --- /dev/null +++ b/spec/models/account_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Account do + describe '#brand_name' do + let(:account) { create(:account) } + + it 'returns nil when no brand_name AccountConfig is set' do + expect(account.brand_name).to be_nil + end + + it 'returns the configured value' do + account.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign') + expect(account.brand_name).to eq('Acme Sign') + end + + it 'strips surrounding whitespace from non-blank values' do + account.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: ' Acme Sign ') + expect(account.brand_name).to eq('Acme Sign') + end + end +end diff --git a/spec/requests/personalization_settings_spec.rb b/spec/requests/personalization_settings_spec.rb new file mode 100644 index 00000000..42873352 --- /dev/null +++ b/spec/requests/personalization_settings_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Personalization settings: brand name', type: :request do + let!(:account) { create(:account) } + let!(:admin) { create(:user, account: account, role: User::ADMIN_ROLE, email: 'admin@wabo.cc') } + + before { sign_in admin } + + describe 'GET /settings/personalization' do + it 'renders the brand-name input with the current value' do + account.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign') + + get settings_personalization_path + + expect(response).to have_http_status(:ok) + expect(response.body).to include('value="Acme Sign"') + end + + it 'renders an empty brand-name input when none is set' do + get settings_personalization_path + + expect(response).to have_http_status(:ok) + expect(response.body).to include('id="brand_name"') + end + end + + describe 'POST /settings/personalization with brand_name' do + it 'saves the brand name and redirects back' do + post settings_personalization_path, params: { + account_config: { key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign' } + } + + expect(response).to redirect_to(settings_personalization_path) + expect(account.reload.brand_name).to eq('Acme Sign') + end + + it 'clears the brand name when posted blank' do + account.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign') + + post settings_personalization_path, params: { + account_config: { key: AccountConfig::BRAND_NAME_KEY, value: '' } + } + + expect(response).to redirect_to(settings_personalization_path) + expect(account.reload.brand_name).to be_nil + end + + it 'rejects an unknown key' do + # Production renders 500 on this; in test env the exception propagates. + expect do + post settings_personalization_path, params: { + account_config: { key: 'definitely_not_allowed', value: 'anything' } + } + end.to raise_error(PersonalizationSettingsController::InvalidKey) + + expect(AccountConfig.where(account: account, key: 'definitely_not_allowed')).not_to exist + end + end + + describe 'branded navbar' do + it 'reflects the saved brand name in the rendered chrome' do + account.account_configs.create!(key: AccountConfig::BRAND_NAME_KEY, value: 'Acme Sign') + + get root_path + + expect(response.body).to include('Acme Sign') + end + + it 'shows the default brand when no override is set' do + get root_path + + expect(response.body).to include(Wabosign::PRODUCT_NAME) + end + end +end