mirror of https://github.com/docusealco/docuseal
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
parent
6033295426
commit
53e677417f
@ -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' %>
|
||||
|
||||
@ -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>
|
||||
@ -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,2 +1,2 @@
|
||||
<%= render 'shared/account_logo', account: current_account %>
|
||||
<span><%= Wabosign.product_name %></span>
|
||||
<span><%= Wabosign.branded_product_name(current_account) %></span>
|
||||
|
||||
@ -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,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>
|
||||
|
||||
@ -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…
Reference in new issue