Per-account product-name branding

Account admins can now replace "WaboSign" in the UI, emails, audit-trail
PDFs, and authenticator-app issuer with their own product name. The
brand override is stored as an AccountConfig row (brand_name key),
managed from /settings/personalization above the logo upload.

Resolution flows through Wabosign.branded_product_name(account = nil):
  1. account&.brand_name if a record is passed
  2. else the deployment's oldest non-archived account's brand_name
     (so anonymous surfaces like the landing page, PWA manifest, and
     og:title get the operator's brand on single-tenant installs)
  3. else Wabosign::PRODUCT_NAME ("WaboSign")

AGPL §7(b) DocuSeal attribution stays untouched:
  - _powered_by.html.erb second line keeps Wabosign::UPSTREAM_NAME
  - _email_attribution.html.erb second paragraph keeps it
  - completed.vue keeps its hardcoded DocuSeal link
The Wabosign::UPSTREAM_NAME and UPSTREAM_URL constants stay constants —
they are never overridable.

Swapped 41 direct Wabosign.product_name callers to pass the most-local
account in scope (current_account, @template.account,
@submitter.submission.account, submission.account, or nil for chrome
without account context). Mailers' default `from:` is now a lambda that
reads @current_account per message. SIGN_REASON constant in
generate_result_attachments became sign_reason_template(account) so
PDF signature reasons reflect the brand.

The two i18n keys actually rendered with literal "WaboSign"
(welcome_to_wabosign in templates_dashboard, connect_to_wabosign_mcp
in mcp_settings) are parameterized to %{product_name} across the 7
locales that defined them. The other ~9 WaboSign-branded i18n keys
are unreferenced dead code from the Pro paywall and stay as-is.

Specs:
  spec/models/account_spec.rb (new) — Account#brand_name
  spec/lib/wabosign_spec.rb (new) — branded_product_name precedence
  spec/requests/personalization_settings_spec.rb (new) — end-to-end

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pull/687/head
Wabo 1 month ago
parent 6033295426
commit 53e677417f

@ -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

@ -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,

@ -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

@ -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

@ -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

@ -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

@ -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,

@ -5,6 +5,6 @@
<p><%= t('if_you_didnt_request_this_you_can_ignore_this_email') %></p>
<p>
<%= t('thanks') %>,<br>
<%= Wabosign.product_name %>
<%= Wabosign.branded_product_name(@resource&.account) %>
</p>
<% content_for(:remove_attribution, true) %>

@ -1,7 +1,7 @@
<div class="max-w-xl mx-auto px-2">
<h1 class="flex text-4xl font-bold items-center justify-center my-8 space-x-2">
<%= svg_icon('waving_hand', class: 'h-10 w-10') %>
<span><%= t('welcome_to_product_name', product_name: Wabosign.product_name) %></span>
<span><%= t('welcome_to_product_name', product_name: Wabosign.branded_product_name(current_account)) %></span>
</h1>
<%= form_for(resource, as: resource_name, url: invitation_path, html: { method: :put, class: 'space-y-6' }) do |f| %>
<div class="space-y-2">

@ -1,4 +1,5 @@
<% brand = Wabosign.branded_product_name(signed_in? ? current_account : nil) %>
<title>
<%= 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") %>
</title>
<%= render 'shared/meta' %>

@ -33,7 +33,7 @@
</p>
<div class="card bg-base-200">
<div class="card-body p-6">
<p class="text-2xl font-semibold"><%= t('connect_to_wabosign_mcp') %></p>
<p class="text-2xl font-semibold"><%= t('connect_to_product_name_mcp', product_name: Wabosign.branded_product_name(current_account)) %></p>
<p class="text-lg"><%= t('add_the_following_to_your_mcp_client_configuration') %>:</p>
<div class="mockup-code overflow-hidden">
<% text = JSON.pretty_generate({ mcpServers: { wabosign: { type: 'http', url: "#{root_url(Wabosign.default_url_options)}mcp", headers: { Authorization: "Bearer #{@mcp_token.token}" } } } }).strip %>

@ -5,7 +5,7 @@
<div class="relative flex flex-col items-center">
<%= render 'shared/logo', width: '100', height: '100' %>
<h1 class="text-6xl font-bold mt-4 mb-4">
<%= Wabosign.product_name %>
<%= Wabosign.branded_product_name %>
</h1>
<% if Wabosign.version.present? %>
<a href="<%= Wabosign::GITHUB_URL %>/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto">

@ -0,0 +1,18 @@
<%= form_with url: settings_personalization_path, method: :post,
html: { class: 'space-y-3', autocomplete: 'off' } do %>
<input type="hidden" name="account_config[key]" value="<%= AccountConfig::BRAND_NAME_KEY %>">
<div class="form-control">
<label class="label" for="brand_name">
<span class="label-text">Replace "<%= Wabosign::PRODUCT_NAME %>" in the UI, emails, audit-trail PDFs, and authenticator-app issuer with your own product name. Leave blank to fall back to the default.</span>
</label>
<input type="text" name="account_config[value]" id="brand_name"
value="<%= current_account.brand_name %>"
maxlength="60" placeholder="e.g. Acme Sign"
autocomplete="off"
class="base-input">
</div>
<button type="submit" class="base-button">Save</button>
<% end %>
<p class="text-sm opacity-70 mt-2">
The <a href="<%= Wabosign::UPSTREAM_URL %>" class="link" target="_blank" rel="noopener"><%= Wabosign::UPSTREAM_NAME %></a> upstream attribution required by AGPL §7(b) stays visible in the footer, post-signing screen, and email footers regardless of this setting.
</p>

@ -10,6 +10,10 @@
<%= render 'submitter_completed_email_form' %>
<%= render 'signature_request_sms_form' %>
</div>
<p class="text-4xl font-bold mb-4 mt-8">
Product name
</p>
<%= render 'brand_name_form' %>
<p class="text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %>
</p>

@ -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"

@ -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] %>

@ -1,13 +1,15 @@
<p>
---
</p>
<% brand = Wabosign.branded_product_name(@current_account) %>
<p>
<% 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 %>
</p>
<%# AGPL §7(b) DocuSeal attribution. Do not remove or rebrand. %>
<p style="font-size: 11px; opacity: 0.7;">
<%= t('based_on') %>
<a href="<%= Wabosign::UPSTREAM_URL %>"><%= Wabosign::UPSTREAM_NAME %></a>

@ -1,14 +1,15 @@
<% if Wabosign.demo? || (request.path != '/' && !devise_controller?) %>
<meta name="robots" content="noindex">
<% 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.' %>
<meta name="description" content="<%= description %>">
<meta property="og:title" content="<%= title %>">
<meta property="og:description" content="<%= description %>">
<meta property="og:type" content="website">
<meta property="og:url" content="<%= root_url %>">
<meta property="og:site_name" content="<%= Wabosign.product_name %>">
<meta property="og:site_name" content="<%= brand %>">
<% if content_for(:disable_image_preview) %>
<meta property="og:image" content="">
<meta name="twitter:image" content="">

@ -9,8 +9,9 @@
<% else %>
<%= t('powered_by') %>
<% end %>
<a href="<%= Wabosign::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Wabosign.product_name %></a> - <%= t('open_source_documents_software') %>
<a href="<%= Wabosign::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Wabosign.branded_product_name(local_assigns[:account]) %></a> - <%= t('open_source_documents_software') %>
</div>
<%# AGPL §7(b) DocuSeal attribution. Do not remove or rebrand. %>
<div class="text-center px-2 text-xs opacity-70 mt-1">
<%= t('based_on') %>
<a href="<%= Wabosign::UPSTREAM_URL %>" class="underline" target="_blank" rel="noopener"><%= Wabosign::UPSTREAM_NAME %></a>

@ -1,2 +1,2 @@
<%= render 'shared/account_logo', account: current_account %>
<span><%= Wabosign.product_name %></span>
<span><%= Wabosign.branded_product_name(current_account) %></span>

@ -2,5 +2,5 @@
<span class="mr-3">
<%= render 'shared/account_logo', account: @template&.account, width: '50px', height: '50px' %>
</span>
<h1 class="text-5xl font-bold text-center"><%= Wabosign.product_name %></h1>
<h1 class="text-5xl font-bold text-center"><%= Wabosign.branded_product_name(@template&.account) %></h1>
</a>

@ -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 %>

@ -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 %>

@ -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 %>

@ -1,4 +1,4 @@
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/account_logo', account: @submitter&.submission&.account, class: 'w-9 h-9 md:w-12 md:h-12' %>
<span><%= Wabosign.product_name %></span>
<span><%= Wabosign.branded_product_name(@submitter&.submission&.account) %></span>
</a>

@ -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 %>

@ -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 %>

@ -65,7 +65,7 @@
<% if user_config.new_record? && !params.key?(:tour) %>
<div id="app_tour_manager" class="h-36 rounded-2xl pt-3 px-7 w-full border border-dashed border-base-300">
<div class="text-xl text-center font-semibold text-base-content">
<%= t('welcome_to_wabosign') %>
<%= t('welcome_to_product_name', product_name: Wabosign.branded_product_name(current_account)) %>
</div>
<div class="my-2 text-center text-xs text-base-content/70">
<%= t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') %>

@ -1,2 +1,2 @@
<%= t('powered_by') %>
<a href="<%= Wabosign::PRODUCT_URL %>" target="_blank" rel="noopener"><%= Wabosign.product_name %></a>
<a href="<%= Wabosign::PRODUCT_URL %>" target="_blank" rel="noopener"><%= Wabosign.branded_product_name(@template&.account) %></a>

@ -1,2 +1,2 @@
<%= render 'shared/account_logo', account: @template&.account %>
<span><%= Wabosign.product_name %></span>
<span><%= Wabosign.branded_product_name(@template&.account) %></span>

@ -1,5 +1,5 @@
<p><%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,</p>
<p><%= 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) %></p>
<p><%= 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)) %></p>
<p><%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>

@ -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

@ -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: {

@ -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 }],

@ -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')

@ -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 %<brand>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)

@ -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

@ -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

@ -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

@ -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
Loading…
Cancel
Save