pull/627/head^2
chapsjust 2 months ago
parent b5cd4e6dad
commit ceecff224a

@ -6,6 +6,8 @@ class ApplicationController < ActionController::Base
include ActiveStorage::SetCurrent
include Pagy::Method
helper WhitelabelHelper
check_authorization unless: :devise_controller?
around_action :with_locale
@ -122,6 +124,8 @@ class ApplicationController < ActionController::Base
end
def maybe_redirect_com
# NOTE: upstream DocuSeal cloud redirect — no-op for self-hosted / white-label
return unless Docuseal.multitenant?
return if request.domain != 'docuseal.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently

@ -1,39 +1,43 @@
# frozen_string_literal: true
class EmbedScriptsController < ActionController::Metal
DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
const DummyBuilder = class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
<h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More
</a>
</div>
</div>
`;
}
};
def show
headers['Content-Type'] = 'application/javascript'
const DummyForm = class extends DummyBuilder {};
self.response_body = dummy_script
if (!window.customElements.get('docuseal-builder')) {
window.customElements.define('docuseal-builder', DummyBuilder);
}
self.status = 200
end
if (!window.customElements.get('docuseal-form')) {
window.customElements.define('docuseal-form', DummyForm);
}
JAVASCRIPT
private
def show
headers['Content-Type'] = 'application/javascript'
def dummy_script
<<~JAVASCRIPT
const DummyBuilder = class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
<h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More
</a>
</div>
</div>
`;
}
};
self.response_body = DUMMY_SCRIPT
const DummyForm = class extends DummyBuilder {};
self.status = 200
if (!window.customElements.get('docuseal-builder')) {
window.customElements.define('docuseal-builder', DummyBuilder);
}
if (!window.customElements.get('docuseal-form')) {
window.customElements.define('docuseal-form', DummyForm);
}
JAVASCRIPT
end
end

@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
"This feature is available in Pro Edition: #{Whitelabel.website_url}/pricing"
ENTERPRISE_PATHS = [
'/submissions/html',

@ -1,7 +1,7 @@
# frozen_string_literal: true
class EsignSettingsController < ApplicationController
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
DEFAULT_CERT_NAME = Whitelabel.cert_name
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
include ActiveModel::Validations

@ -1,16 +1,6 @@
# frozen_string_literal: true
class PersonalizationSettingsController < ApplicationController
ALLOWED_KEYS = [
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze
InvalidKey = Class.new(StandardError)
before_action :load_and_authorize_account_config, only: :create
@ -45,11 +35,23 @@ class PersonalizationSettingsController < ApplicationController
authorize!(:create, @account_config)
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
raise InvalidKey unless allowed_keys.include?(@account_config.key)
@account_config
end
def allowed_keys
[
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
]
end
def account_config_params
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })

@ -0,0 +1,18 @@
# frozen_string_literal: true
# =============================================================================
# WhitelabelHelper — makes Whitelabel config available in all views
# =============================================================================
# Include this in ApplicationController to use `wl` in all ERB templates.
#
# Usage in views:
# <%= wl.brand_name %>
# <%= wl.logo_path %>
# <%= wl.support_email %>
# =============================================================================
module WhitelabelHelper
def wl
Whitelabel
end
end

@ -1,36 +1,18 @@
<template>
<div
id="form_completed"
class="mx-auto max-w-md flex flex-col completed-form"
dir="auto"
>
<div id="form_completed" class="mx-auto max-w-md flex flex-col completed-form" dir="auto">
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
<IconCircleCheck
class="inline text-green-600"
:width="30"
:height="30"
/>
<IconCircleCheck class="inline text-green-600" :width="30" :height="30" />
<span class="completed-form-message-title">
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t("documents_have_been_signed") : t("document_has_been_signed")) : t("form_has_been_completed")) }}
</span>
</div>
<div
v-if="completedMessage.body"
class="mt-2 completed-form-message-body"
>
<MarkdownContent
:string="completedMessage.body"
/>
<div v-if="completedMessage.body" class="mt-2 completed-form-message-body">
<MarkdownContent :string="completedMessage.body" />
</div>
<div class="space-y-3 mt-5">
<a
v-if="completedButton.url"
:href="sanitizeUrl(completedButton.url)"
rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full completed-form-completed-button"
>
<a v-if="completedButton.url" :href="sanitizeUrl(completedButton.url)" rel="noopener noreferrer nofollow" class="white-button flex items-center w-full completed-form-completed-button">
<span>
{{ completedButton.title || 'Back to Website' }}
{{ completedButton.title || "Back to Website" }}
</span>
</a>
<button
@ -39,73 +21,44 @@
:disabled="isSendingCopy"
@click.prevent="sendCopyToEmail"
>
<IconInnerShadowTop
v-if="isSendingCopy"
class="animate-spin"
/>
<IconInnerShadowTop v-if="isSendingCopy" class="animate-spin" />
<IconMail v-else />
<span>
{{ t('send_copy_via_email') }}
{{ t("send_copy_via_email") }}
</span>
</button>
<button
v-if="!isWebView && withDownloadButton"
class="base-button flex items-center space-x-1 w-full completed-form-download-button"
:disabled="isDownloading"
@click.prevent="download"
>
<IconInnerShadowTop
v-if="isDownloading"
class="animate-spin"
/>
<button v-if="!isWebView && withDownloadButton" class="base-button flex items-center space-x-1 w-full completed-form-download-button" :disabled="isDownloading" @click.prevent="download">
<IconInnerShadowTop v-if="isDownloading" class="animate-spin" />
<IconDownload v-else />
<span>
{{ t('download') }}
{{ t("download") }}
</span>
</button>
<a
v-if="isDemo"
target="_blank"
href="https://github.com/docusealco/docuseal"
class="white-button flex items-center space-x-1 w-full"
>
<a v-if="isDemo" target="_blank" :href="brandGithubUrl" class="white-button flex items-center space-x-1 w-full">
<IconBrandGithub />
<span>
Star on Github
</span>
<span> Star on Github </span>
</a>
<a
v-if="isDemo"
href="https://docuseal.com/sign_up"
class="white-button flex items-center space-x-1 w-full"
>
<a v-if="isDemo" :href="brandWebsiteUrl" class="white-button flex items-center space-x-1 w-full">
<IconLogin />
<span>
{{ t('create_a_free_account') }}
{{ t("create_a_free_account") }}
</span>
</a>
</div>
<div
v-if="attribution"
class="text-center mt-4"
>
{{ t('powered_by') }}
<a
href="https://www.docuseal.com/start"
target="_blank"
class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }}
<div v-if="attribution" class="text-center mt-4">
{{ t("powered_by") }}
<a :href="brandWebsiteUrl" target="_blank" class="underline">{{ brandName }}</a> - {{ t("open_source_documents_software") }}
</div>
</div>
</template>
<script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content'
import { sanitizeUrl } from '@braintree/sanitize-url'
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from "@tabler/icons-vue";
import MarkdownContent from "./markdown_content";
import { sanitizeUrl } from "@braintree/sanitize-url";
export default {
name: 'FormCompleted',
name: "FormCompleted",
components: {
MarkdownContent,
IconCircleCheck,
@ -113,181 +66,193 @@ export default {
IconBrandGithub,
IconMail,
IconLogin,
IconDownload
IconDownload,
},
inject: ['baseUrl', 't'],
inject: ["baseUrl", "t"],
props: {
submitterSlug: {
type: String,
required: true
required: true,
},
isDemo: {
type: Boolean,
required: false,
default: false
default: false,
},
attribution: {
type: Boolean,
required: false,
default: true
default: true,
},
hasSignatureFields: {
type: Boolean,
required: false,
default: false
default: false,
},
hasMultipleDocuments: {
type: Boolean,
required: false,
default: false
default: false,
},
withDownloadButton: {
type: Boolean,
required: false,
default: true
default: true,
},
withSendCopyButton: {
type: Boolean,
required: false,
default: true
default: true,
},
withConfetti: {
type: Boolean,
required: false,
default: false
default: false,
},
canSendEmail: {
type: Boolean,
required: false,
default: false
default: false,
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
default: () => ({}),
},
completedButton: {
type: Object,
required: false,
default: () => ({})
default: () => ({}),
},
completedMessage: {
type: Object,
required: false,
default: () => ({})
}
default: () => ({}),
},
},
data () {
data() {
return {
isSendingCopy: false,
isDownloading: false
}
isDownloading: false,
};
},
computed: {
isWebView () {
return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent)
}
brandName() {
return document.querySelector('meta[name="brand-name"]')?.content || "Intébec";
},
brandWebsiteUrl() {
return document.querySelector('meta[name="brand-website-url"]')?.content || "/";
},
brandGithubUrl() {
return document.querySelector('meta[name="brand-github-url"]')?.content || "";
},
isWebView() {
return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent);
},
},
async mounted () {
async mounted() {
if (this.withConfetti) {
const { default: confetti } = await import('canvas-confetti')
const { default: confetti } = await import("canvas-confetti");
confetti({
particleCount: 50,
startVelocity: 30,
spread: 140
})
spread: 140,
});
}
document.querySelectorAll('#decline_button').forEach((button) => {
button.setAttribute('disabled', 'true')
})
document.querySelectorAll("#decline_button").forEach((button) => {
button.setAttribute("disabled", "true");
});
},
methods: {
sanitizeUrl,
sendCopyToEmail () {
this.isSendingCopy = true
sendCopyToEmail() {
this.isSendingCopy = true;
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
method: 'POST'
}).then(() => {
alert(this.t('email_has_been_sent'))
}).finally(() => {
this.isSendingCopy = false
method: "POST",
})
.then(() => {
alert(this.t("email_has_been_sent"));
})
.finally(() => {
this.isSendingCopy = false;
});
},
download () {
this.isDownloading = true
download() {
this.isDownloading = true;
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
method: 'GET',
...this.fetchOptions
method: "GET",
...this.fetchOptions,
}).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
const urls = await response.json();
const isMobileSafariIos = "ontouchstart" in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent);
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent);
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
this.downloadSafariIos(urls);
} else {
this.downloadUrls(urls)
this.downloadUrls(urls);
}
} else {
alert(this.t('failed_to_download_files'))
alert(this.t("failed_to_download_files"));
}
})
});
},
downloadUrls (urls) {
downloadUrls(urls) {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
const blobUrl = URL.createObjectURL(await resp.blob());
const link = document.createElement("a");
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.href = blobUrl;
link.setAttribute("download", decodeURI(url.split("/").pop()));
link.click()
link.click();
URL.revokeObjectURL(blobUrl)
})
}
})
URL.revokeObjectURL(blobUrl);
});
};
});
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
).finally(() => {
this.isDownloading = false
})
fileRequests
.reduce((prevPromise, request) => prevPromise.then(() => request()), Promise.resolve())
.finally(() => {
this.isDownloading = false;
});
},
downloadSafariIos (urls) {
downloadSafariIos(urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
const blob = await resp.blob()
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
const link = document.createElement('a')
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, "application/octet-stream"));
const link = document.createElement("a");
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.href = blobUrl;
link.setAttribute("download", decodeURI(url.split("/").pop()));
return link
})
})
return link;
});
});
Promise.all(fileRequests).then((links) => {
links.forEach((link, index) => {
setTimeout(() => {
link.click()
Promise.all(fileRequests)
.then((links) => {
links.forEach((link, index) => {
setTimeout(() => {
link.click();
URL.revokeObjectURL(link.href)
}, index * 50)
URL.revokeObjectURL(link.href);
}, index * 50);
});
})
}).finally(() => {
this.isDownloading = false
})
}
}
}
.finally(() => {
this.isDownloading = false;
});
},
},
};
</script>

@ -288,7 +288,7 @@
class="text-base-content/60 text-xs text-center w-full mt-1 select-none"
>
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
href="https://www.docuseal.com/esign-disclosure"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/esign-disclosure'"
target="_blank"
>
<span class="inline md:hidden">

@ -23,7 +23,7 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.com/pricing"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/pricing'"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>

File diff suppressed because it is too large Load Diff

@ -23,7 +23,7 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.com/pricing"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/pricing'"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>

@ -197,7 +197,7 @@
<a
v-if="!isConnected"
class="block link text-center mt-1"
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/blog/accept-payments-and-request-signatures-with-ease'"
target="_blank"
data-turbo="false"
>{{ t('learn_more') }}</a>

@ -5,7 +5,7 @@ class SendTestWebhookRequestJob
sidekiq_options retry: 0
USER_AGENT = 'DocuSeal.com Webhook'
USER_AGENT = Whitelabel.webhook_user_agent
HttpsError = Class.new(StandardError)
LocalhostError = Class.new(StandardError)

@ -1,7 +1,7 @@
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
default from: 'DocuSeal <info@docuseal.com>'
default from: -> { Whitelabel.email_from }
layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor

@ -118,7 +118,7 @@
</div>
</div>
<div class="text-center">
<%= link_to t('open_full_api_reference'), "#{Docuseal::PRODUCT_URL}/docs/api", class: 'btn btn-warning text-base mt-4 px-8', target: '_blank', rel: 'noopener' %>
<%= link_to t('open_full_api_reference'), "#{Whitelabel.website_url}/docs/api", class: 'btn btn-warning text-base mt-4 px-8', target: '_blank', rel: 'noopener' %>
</div>
</div>
</div>

@ -1,5 +1,5 @@
<title>
<%= content_for(:html_title) || (signed_in? ? 'Intébec Signature' : 'Intébec | Signature') %>
<%= content_for(:html_title) || (signed_in? ? wl.page_title(signed_in: true) : wl.page_title(signed_in: false)) %>
</title>
<%= render 'shared/meta' %>
<link rel="stylesheet" href="/intebec.css">

@ -2,6 +2,9 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>">
<head>
<%= render 'layouts/head_tags' %>
<meta name="brand-name" content="<%= Whitelabel.brand_name %>">
<meta name="brand-website-url" content="<%= Whitelabel.website_url %>">
<meta name="brand-github-url" content="<%= Whitelabel.github_url %>">
<% if Docuseal.enable_pwa? %>
<link rel="manifest" href="/manifest.json">
<% end %>

@ -2,6 +2,9 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>">
<head>
<%= render 'layouts/head_tags' %>
<meta name="brand-name" content="<%= Whitelabel.brand_name %>">
<meta name="brand-website-url" content="<%= Whitelabel.website_url %>">
<meta name="brand-github-url" content="<%= Whitelabel.github_url %>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tags %>
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %>

@ -1,15 +1,15 @@
{
"name": "<%= Docuseal.product_name %>",
"short_name": "<%= Docuseal.product_name %>",
"name": "<%= Whitelabel.brand_name %>",
"short_name": "<%= Whitelabel.brand_short_name %>",
"id": "/",
"icons": [
{
"src": "/favicon.svg",
"src": "<%= Whitelabel.favicon_svg %>",
"type": "image/svg+xml",
"sizes": "any"
},
{
"src": "/apple-touch-icon.png",
"src": "<%= Whitelabel.apple_touch_icon %>",
"type": "image/png",
"sizes": "192x192"
}
@ -18,8 +18,8 @@
"display": "standalone",
"scope": "/",
"orientation": "any",
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"description": "<%= Whitelabel.pwa_description %>",
"categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4",
"background_color": "#FAF7F4"
"theme_color": "<%= Whitelabel.pwa_theme_color %>",
"background_color": "<%= Whitelabel.pwa_background_color %>"
}

@ -5,5 +5,5 @@
---
</p>
<p>
Envoyé avec <a href="https://intebec.ca">Intébec</a> — signature de documents sécurisée.
<%= Whitelabel.email_attribution_html.html_safe %>
</p>

@ -1,10 +1,12 @@
<a href="https://github.com/docusealco/docuseal" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" target="_blank" alt="Star on GitHub" style="height: 37px">
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</span>
<span class="flex">
<span class="hidden lg:block">Star on&nbsp</span>GitHub
</span>
</a>
<% if Whitelabel.show_github_button? && Whitelabel.github_url.present? %>
<a href="<%= Whitelabel.github_url %>" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" target="_blank" alt="Star on GitHub" style="height: 37px">
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</span>
<span class="flex">
<span class="hidden lg:block">Star on&nbsp</span>GitHub
</span>
</a>
<% end %>

@ -1 +1 @@
<img src="/logo.svg" class="<%= local_assigns[:class] %>" style="max-width: 100%; max-height: 100%;" width="<%= local_assigns[:width] || '37' %>" height="<%= local_assigns[:height] || '37' %>" />
<img src="<%= wl.logo_path %>" class="<%= local_assigns[:class] %>" style="max-width: 100%; max-height: 100%;" width="<%= local_assigns[:width] || wl.logo_width %>" height="<%= local_assigns[:height] || wl.logo_height %>" />

@ -1,14 +1,14 @@
<% if Docuseal.demo? || (request.path != '/' && !devise_controller?) %>
<meta name="robots" content="noindex">
<% end %>
<% title = content_for(:html_title) || (signed_in? ? 'Intébec Signature' : 'Intébec | Signature') %>
<% description = content_for(:html_description) || 'Outil de signature fait par Intébec. Permet de signer des documents facilement.' %>
<% title = content_for(:html_title) || (signed_in? ? wl.page_title(signed_in: true) : wl.page_title(signed_in: false)) %>
<% description = content_for(:html_description) || wl.description %>
<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="DocuSeal">
<meta property="og:site_name" content="<%= wl.brand_name %>">
<% if content_for(:disable_image_preview) %>
<meta property="og:image" content="">
<meta name="twitter:image" content="">
@ -19,13 +19,15 @@
<meta name="twitter:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>">
<% end %>
<meta name="twitter:card" content="summary">
<meta name="twitter:creator" content="@docusealco">
<meta name="twitter:site" content="@docusealco">
<% if wl.twitter_handle.present? %>
<meta name="twitter:creator" content="<%= wl.twitter_handle %>">
<meta name="twitter:site" content="<%= wl.twitter_handle %>">
<% end %>
<meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= description %>">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/svg+xml" href="/favicon.ico">
<meta name="theme-color" content="#faf7f5">
<link rel="apple-touch-icon" sizes="180x180" href="<%= wl.apple_touch_icon %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= wl.favicon_32 %>">
<link rel="icon" type="image/png" sizes="96x96" href="<%= wl.favicon_96 %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= wl.favicon_16 %>">
<link rel="icon" type="image/svg+xml" href="<%= wl.favicon_svg %>">
<meta name="theme-color" content="<%= wl.pwa_theme_color %>">

@ -11,7 +11,7 @@
<% if signed_in? %>
<div class="space-x-4 flex items-center">
<% if Docuseal.demo? %>
<a href="https://docuseal.com/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
<a href="<%= Whitelabel.website_url %>" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
<%= t('sign_up') %>
</a>
<span class="hidden sm:inline">
@ -50,7 +50,7 @@
<% end %>
</li>
<% end %>
<% if Docuseal.multitenant? || current_user.role == 'superadmin' %>
<% if Whitelabel.show_ai_link? && (Docuseal.multitenant? || current_user.role == 'superadmin') %>
<li>
<%= link_to Docuseal::CHATGPT_URL, target: 'blank', class: 'flex items-center' do %>
<%= svg_icon('sparkles', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>

@ -1,13 +1,15 @@
<div class="text-center px-2">
<% if local_assigns[:with_counter] %>
<% count = CompletedSubmitter.distinct.count(:submission_id) %>
<% if count > 1 %>
<%= t('count_documents_signed_with_html', count:) %>
<% if Whitelabel.show_powered_by? %>
<div class="text-center px-2">
<% if local_assigns[:with_counter] %>
<% count = CompletedSubmitter.distinct.count(:submission_id) %>
<% if count > 1 %>
<%= t('count_documents_signed_with_html', count:) %>
<% else %>
<%= t('powered_by') %>
<% end %>
<% else %>
<%= t('powered_by') %>
<% end %>
<% else %>
<%= t('powered_by') %>
<% end %>
<a href="<%= Docuseal::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Docuseal.product_name %></a> - <%= t('open_source_documents_software') %>
</div>
<a href="<%= Whitelabel.website_url %><%= local_assigns[:link_path] %>" class="underline"><%= Whitelabel.powered_by_text %></a> - <%= t('open_source_documents_software') %>
</div>
<% end %>

@ -1,2 +1,2 @@
<%= render 'shared/logo' %>
<span>Intébec</span>
<span><%= wl.brand_name %></span>

@ -2,5 +2,5 @@
<span class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</span>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
<h1 class="text-5xl font-bold text-center"><%= Whitelabel.brand_name %></h1>
</a>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_title, "#{@template.name} | #{Whitelabel.brand_name}") %>
<% 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} | DocuSeal") %>
<% content_for(:html_title, "#{@template.name} | #{Whitelabel.brand_name}") %>
<% 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} | DocuSeal") %>
<% content_for(:html_title, "#{@template.name} | #{Whitelabel.brand_name}") %>
<% 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/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span><%= Docuseal.product_name %></span>
<span><%= Whitelabel.brand_name %></span>
</a>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Whitelabel.brand_name}") %>
<% 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} | DocuSeal") %>
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Whitelabel.brand_name}") %>
<% 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 %>

@ -0,0 +1,59 @@
# frozen_string_literal: true
# =============================================================================
# Whitelabel initializer
# =============================================================================
# Loads config/whitelabel.yml and patches the Docuseal module constants so that
# every existing call to Docuseal.product_name, Docuseal::PRODUCT_URL, etc.
# automatically returns the white-labelled value.
#
# This approach means we do NOT have to change every single call-site — the
# existing code keeps working, but returns branded values.
# =============================================================================
require_relative '../../lib/whitelabel'
# Ensure lib/docuseal.rb is fully loaded before we reopen and patch the module.
# Without this, Zeitwerk sees `module Docuseal` below and marks the constant as
# already defined, so it never loads lib/docuseal.rb — leaving multitenant? and
# other module_function methods undefined during eager loading.
require Rails.root.join('lib/docuseal')
# Patch Docuseal module to delegate brand-related values to Whitelabel
module Docuseal
# Override the product_name method to use Whitelabel config
def self.product_name
Whitelabel.brand_name
end
# Override constants that are used in views/mailers — we make them
# methods instead so they pick up the Whitelabel config dynamically.
# The constants still exist for backward compat but the methods take
# precedence when called as Docuseal.xxx.
def self.product_url
Whitelabel.website_url
end
def self.support_email_address
Whitelabel.support_email
end
def self.github_url_value
Whitelabel.github_url || ''
end
def self.twitter_url_value
Whitelabel.twitter_url || ''
end
def self.twitter_handle_value
Whitelabel.twitter_handle || ''
end
def self.discord_url_value
Whitelabel.discord_url || ''
end
end
Rails.logger.info "[Whitelabel] Loaded brand: #{Whitelabel.brand_name}"

@ -0,0 +1,77 @@
# =============================================================================
# White-Label Locale Overrides
# =============================================================================
# This file overrides i18n keys that contain brand-specific text (DocuSeal).
# Rails automatically merges locale files, so keys here take precedence.
#
# When upstream DocuSeal updates add new branded keys to i18n.yml,
# add the override here — keeping the main file untouched for easy merging.
#
# Languages: English (en) + French (fr)
# =============================================================================
en:
docuseal_trusted_signature: "Intébec Trusted Signature"
sent_with_docuseal_pro_html: 'Sent with <a href="%{product_url}">Intébec Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: 'Show "Sent with <span class="link">Intébec Pro</span>" attribution in emails'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Sign documents with trusted certificate provided by Intébec. Your documents and data are never shared with Intébec. PDF checksum is provided to generate a trusted signature."
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://intebec.ca/privacy">Privacy Policy</a> and <a target="_blank" href="https://intebec.ca/terms">Terms of Service</a>.'
connect_salesforce_account_to_integrate_with_docuseal: "Connect Salesforce account to integrate with Intébec"
sign_up_in_docuseal_console_to_upgrade_on_premises_app_is_completely_standalone_console_is_used_only_to_manage_your_license: "Sign up in Intébec Console to upgrade. On-premises app is completely standalone, Console is used only to manage your license."
unlock_with_docuseal_pro: "Unlock with Intébec Pro"
activate_with_docuseal_pro: "Activate with Intébec Pro"
send_signature_request_emails_without_limits_with_docuseal_pro: "Send signature request emails without limits with Intébec Pro"
unlock_more_user_roles_with_docuseal_pro: "Unlock more user roles with Intébec Pro."
docuseal_support: "Intébec Support"
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://intebec.ca/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Click here</a> to learn more about user roles and permissions.'
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: "On a scale of 1 to 10, how satisfied are you with the Intébec product?"
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: "Your Pro Plan has been suspended due to unpaid invoices. You can update your payment details to settle the invoice and continue using Intébec or cancel your subscription."
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'This submission has multiple signers, which prevents the use of a sharing link as it''s unclear which signer is responsible for specific fields. To resolve this, follow this <a href="https://intebec.ca/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> to define the default signer details.'
welcome_to_docuseal: "Welcome to Intébec"
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: "Your email could not be reached. This may happen if there was a typo in your address or if your mailbox is not available. Please contact support@intebec.ca to log in."
add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings: "Add a unique Signature ID and timestamp to each signature for audit and traceability purposes along with the timestamp. Part of Intébec's 21 CFR Part 11 compliance settings."
require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings: "Require signers to provide a reason for signing before completing their signature (e.g., approvals, certifications). Part of Intébec's 21 CFR Part 11 compliance settings."
onboarding:
support_description: "You can use our self-service AI assistant or email us at support@intebec.ca if you have any questions."
fr:
docuseal_trusted_signature: "Signature de confiance Intébec"
sent_with_docuseal_pro_html: 'Envoyé avec <a href="%{product_url}">Intébec Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: 'Afficher l''attribution « Envoyé avec <span class="link">Intébec Pro</span> » dans les courriels'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Signez des documents avec un certificat de confiance fourni par Intébec. Vos documents et données ne sont jamais partagés avec Intébec. Une somme de contrôle PDF est fournie pour générer une signature de confiance."
by_creating_an_account_you_agree_to_our_html: 'En créant un compte, vous acceptez notre <a target="_blank" href="https://intebec.ca/privacy">Politique de confidentialité</a> et nos <a target="_blank" href="https://intebec.ca/terms">Conditions d''utilisation</a>.'
connect_salesforce_account_to_integrate_with_docuseal: "Connectez votre compte Salesforce pour l'intégrer à Intébec"
sign_up_in_docuseal_console_to_upgrade_on_premises_app_is_completely_standalone_console_is_used_only_to_manage_your_license: "Inscrivez-vous dans la console Intébec pour effectuer la mise à niveau. L'application sur site est entièrement autonome, la console est utilisée uniquement pour gérer votre licence."
unlock_with_docuseal_pro: "Débloquer avec Intébec Pro"
activate_with_docuseal_pro: "Activer avec Intébec Pro"
send_signature_request_emails_without_limits_with_docuseal_pro: "Envoyez des courriels de demande de signature sans limites avec Intébec Pro"
unlock_more_user_roles_with_docuseal_pro: "Débloquez plus de rôles utilisateurs avec Intébec Pro."
docuseal_support: "Soutien Intébec"
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://intebec.ca/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Cliquez ici</a> pour en savoir plus sur les rôles et les permissions des utilisateurs.'
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: "Sur une échelle de 1 à 10, à quel point êtes-vous satisfait du produit Intébec?"
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: "Votre plan Pro a été suspendu en raison de factures impayées. Vous pouvez mettre à jour vos informations de paiement pour régler la facture et continuer à utiliser Intébec ou annuler votre abonnement."
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Cette soumission comporte plusieurs signataires, ce qui empêche l''utilisation d''un lien de partage car on ne sait pas quel signataire est responsable de quels champs. Pour résoudre ce problème, suivez ce <a href="https://intebec.ca/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> pour définir les détails du signataire par défaut.'
welcome_to_docuseal: "Bienvenue sur Intébec"
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: "Votre courriel n'a pas pu être atteint. Cela peut arriver s'il y a une faute de frappe dans votre adresse ou si votre boîte de réception n'est pas disponible. Veuillez contacter support@intebec.ca pour vous connecter."
add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings: "Ajoutez un identifiant de signature unique et un horodatage à chaque signature à des fins d'audit et de traçabilité, ainsi que l'horodatage. Fait partie des paramètres de conformité 21 CFR Part 11 d'Intébec."
require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings: "Exiger que le signataire fournisse une raison de signature avant de terminer sa signature (ex. : approbations, certifications). Fait partie des paramètres de conformité 21 CFR Part 11 d'Intébec."
onboarding:
support_description: "Vous pouvez utiliser notre assistant IA en libre-service ou nous écrire à support@intebec.ca si vous avez des questions."
# -------------------------------------------------------------------------
# Common French overrides for base UI strings
# -------------------------------------------------------------------------
sign_in: "Connexion"
sign_out: "Déconnexion"
sign_up: "Inscription"
settings: "Paramètres"
profile: "Profil"
password: "Mot de passe"
email: "Courriel"
submit: "Soumettre"
save: "Enregistrer"
cancel: "Annuler"
delete: "Supprimer"
search: "Rechercher"
download: "Télécharger"
powered_by: "Propulsé par"
open_source_documents_software: "logiciel de signature de documents"

@ -0,0 +1,177 @@
# =============================================================================
# White-Label Configuration — Single source of truth for all branding
# =============================================================================
#
# Change the values below to rebrand the entire application for any client.
# The application reads this file once at boot (and caches it).
# After editing, restart the app (or call Whitelabel.reload! in a console).
#
# IMPORTANT: Keep the YAML keys exactly as they are — only change the values.
# This file is designed to survive upstream DocuSeal merges since it lives in
# a file that does not exist in the upstream repo.
# =============================================================================
# ---------------------------------------------------------------------------
# Brand identity
# ---------------------------------------------------------------------------
brand:
# The name shown in the navbar, page titles, emails, PDFs, PWA manifest, etc.
name: "Intébec"
# Shorter variant (used in PWA short_name, compact UI areas)
short_name: "Intébec"
# Tagline — appears alongside the brand name in page titles, meta tags, etc.
tagline: "Signature"
# Description for meta tags, PWA manifest, and default OG description
description: "Outil de signature fait par Intébec. Permet de signer des documents facilement."
# Default page titles (signed_in / signed_out)
page_title_signed_in: "Intébec Signature"
page_title_signed_out: "Intébec | Signature"
# ---------------------------------------------------------------------------
# URLs — your company's own links
# ---------------------------------------------------------------------------
urls:
# Main website (replaces docuseal.com links)
website: "https://intebec.ca"
# Support / contact email
support_email: "support@intebec.ca"
# Privacy policy & terms pages (set to null to hide links)
privacy_policy: "https://intebec.ca/privacy"
terms_of_service: "https://intebec.ca/terms"
# Social handles (set to null to hide)
twitter_url: ~
twitter_handle: ~
github_url: ~
discord_url: ~
# ---------------------------------------------------------------------------
# Email settings
# ---------------------------------------------------------------------------
email:
# "From" header: "Brand Name <address>"
from_name: "Intébec"
from_address: "info@intebec.ca"
# Attribution line at the bottom of transactional emails
# Supports basic HTML. Use %{brand} as a placeholder for the brand name.
# Use %{website} as a placeholder for the website URL.
attribution_html: 'Envoyé avec <a href="%{website}">%{brand}</a> — signature de documents sécurisée.'
# ---------------------------------------------------------------------------
# Assets — paths are relative to /public or full URLs
# ---------------------------------------------------------------------------
assets:
# Logo shown in the navbar and form banners
logo_path: "/logo.svg"
logo_width: 37
logo_height: 37
# Favicon files (place your files in /public)
favicon_svg: "/favicon.svg"
favicon_ico: "/favicon.ico"
favicon_16: "/favicon-16x16.png"
favicon_32: "/favicon-32x32.png"
favicon_96: "/favicon-96x96.png"
apple_touch_icon: "/apple-icon-180x180.png"
# Open Graph / social preview image
preview_image: "/preview.png"
# ---------------------------------------------------------------------------
# Theme — CSS custom properties injected into the DaisyUI `docuseal` theme
# Values are HSL triplets "H S% L%" to match DaisyUI conventions.
# Set a key to null to keep the compiled Tailwind default.
# ---------------------------------------------------------------------------
theme:
# Primary action colour (buttons, links, focus rings)
primary: "216 77% 52%"
primary_focus: "216 77% 44%"
primary_content: "0 0% 100%"
# Secondary colour
secondary: "220 12% 45%"
secondary_focus: "220 14% 36%"
secondary_content: "0 0% 100%"
# Accent colour
accent: "160 50% 40%"
accent_focus: "160 50% 34%"
accent_content: "0 0% 100%"
# Neutral / dark tones
neutral: "220 16% 12%"
neutral_focus: "220 16% 8%"
neutral_content: "0 0% 100%"
# Surfaces / backgrounds
base_100: "0 0% 100%"
base_200: "220 14% 96%"
base_300: "220 12% 93%"
base_content: "220 14% 10%"
# Functional colours
info: "205 80% 50%"
success: "154 55% 38%"
warning: "38 88% 48%"
error: "0 72% 50%"
# Border radius & misc
rounded_btn: "1.9rem"
tab_border: "2px"
tab_radius: ".5rem"
# ---------------------------------------------------------------------------
# PDF / Audit trail branding
# ---------------------------------------------------------------------------
pdf:
# Text embedded in PDF digital signatures (use %{name} for the signer name)
sign_reason: "Signed by %{name} with Intébec"
# Audit trail footer text
audit_trail_footer: "Signed with Intébec"
# PDF Creator metadata
creator: "Intébec"
# E-sign certificate default name
cert_name: "Intébec Self-Host Autogenerated"
# ---------------------------------------------------------------------------
# PWA (Progressive Web App) manifest
# ---------------------------------------------------------------------------
pwa:
description: "Intébec is a secure platform for digital document signing and processing."
theme_color: "#FAF7F4"
background_color: "#FAF7F4"
# ---------------------------------------------------------------------------
# Webhook user-agent string
# ---------------------------------------------------------------------------
webhooks:
user_agent: "Intébec Webhook"
# ---------------------------------------------------------------------------
# Feature flags — toggle upstream DocuSeal features
# ---------------------------------------------------------------------------
features:
# Show the "Star on GitHub" button in the navbar
show_github_button: false
# Show "Powered by …" attribution on public signing pages
show_powered_by: true
# Text for the "powered by" line (null = use brand name)
powered_by_text: ~
# Show the "Ask AI / ChatGPT" link in the user menu
show_ai_link: false
# Show the Discord link
show_discord_link: false

@ -3,7 +3,8 @@ services:
depends_on:
postgres:
condition: service_healthy
image: docuseal/docuseal:latest
build: .
image: intebec/docuseal:latest
ports:
- 3000:3000
volumes:
@ -15,7 +16,7 @@ services:
postgres:
image: postgres:18
volumes:
- './pg_data:/var/lib/postgresql/18/docker'
- "./pg_data:/var/lib/postgresql/18/docker"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

@ -0,0 +1,152 @@
# White-Label Configuration Guide
## Overview
This fork of DocuSeal uses a **centralised white-label configuration system** that lets you rebrand the entire application for any client by editing a single YAML file. No need to create a separate repo for each client.
## Architecture
```
config/whitelabel.yml ← Single source of truth (brand, URLs, theme, PDF, features)
lib/whitelabel.rb ← Ruby module that loads + exposes the config
config/initializers/whitelabel.rb ← Patches Docuseal module at boot so existing code uses new values
app/helpers/whitelabel_helper.rb ← View helper (use `wl.xxx` in any ERB template)
config/locales/whitelabel.yml ← Locale overrides for branded i18n keys (EN + FR)
public/intebec.css ← CSS theme overrides (DaisyUI custom properties)
```
## How to Rebrand for a New Client
### 1. Edit `config/whitelabel.yml`
Change the values under each section:
| Section | What it controls |
| ---------- | ---------------------------------------------- |
| `brand` | Name, tagline, description, page titles |
| `urls` | Website, support email, privacy/terms, socials |
| `email` | From address, email attribution |
| `assets` | Logo, favicons, preview image |
| `theme` | DaisyUI colour palette (HSL values) |
| `pdf` | Signing reason, audit trail footer, cert name |
| `pwa` | PWA manifest name, colours |
| `webhooks` | User-Agent string |
| `features` | Toggle GitHub button, AI link, powered-by text |
### 2. Replace Asset Files
Put your client's files in `/public`:
- `logo.svg` — navbar + form logo
- `favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`, `favicon-96x96.png`
- `favicon.svg` — SVG favicon
- `apple-icon-180x180.png` — iOS home screen icon
- `preview.png` — Open Graph social preview
### 3. Update Theme Colours
Two options:
**Option A** — Edit `theme:` in `config/whitelabel.yml` (DaisyUI tokens, HSL format):
```yaml
theme:
primary: "216 77% 52%"
secondary: "220 12% 45%"
accent: "160 50% 40%"
```
**Option B** — Edit `public/intebec.css` for fine-grained CSS overrides.
### 4. Update Locale Overrides
Edit `config/locales/whitelabel.yml` to change branded text in both English and French. This file **overrides** the base `config/locales/i18n.yml` — Rails merges them automatically.
### 5. Restart
After editing, restart the app:
```bash
docker compose down && docker compose up -d --build
```
Or in a Rails console:
```ruby
Whitelabel.reload!
```
## Usage in Code
### In ERB views
```erb
<%= wl.brand_name %>
<%= wl.logo_path %>
<%= wl.support_email %>
<%= wl.page_title(signed_in: true) %>
```
### In Ruby (controllers, mailers, lib)
```ruby
Whitelabel.brand_name # => "Intébec"
Whitelabel.website_url # => "https://intebec.ca"
Whitelabel.email_from # => "Intébec <info@intebec.ca>"
Whitelabel.sign_reason("John") # => "Signed by John with Intébec"
Whitelabel.theme(:primary) # => "216 77% 52%"
```
### In JavaScript
Brand values are available via `<meta>` tags in the page head:
```javascript
document.querySelector('meta[name="brand-name"]').content; // "Intébec"
document.querySelector('meta[name="brand-website-url"]').content; // "https://intebec.ca"
```
## Upstream Merge Strategy
This system is designed to minimise merge conflicts with the upstream DocuSeal repo:
1. **New files** (no conflicts): `config/whitelabel.yml`, `lib/whitelabel.rb`, `config/initializers/whitelabel.rb`, `app/helpers/whitelabel_helper.rb`, `config/locales/whitelabel.yml`
2. **Patched files** (potential conflicts, but isolated changes):
- `lib/docuseal.rb` — only added a comment block; the `product_name` method is overridden at runtime
- View templates — changes are surgical (replacing one hardcoded string with a `Whitelabel.xxx` call)
3. **Untouched internal identifiers**: `data-theme="docuseal"`, `Docuseal` module name, `#docuseal_modal_container`, `docuseal_clipboard` localStorage keys — all kept as-is for compatibility
### When Upstream Adds New Branded Content
1. Check if new views/lib files have hardcoded "DocuSeal" text
2. Replace with `Whitelabel.brand_name` or `wl.brand_name`
3. If it's an i18n key, add the override to `config/locales/whitelabel.yml`
## Feature Flags
Toggle upstream features without removing code:
```yaml
features:
show_github_button: false # Hide "Star on GitHub"
show_powered_by: true # Show/hide "Powered by" on signing pages
show_ai_link: false # Hide "Ask AI" in user menu
show_discord_link: false # Hide Discord link
```
## File Reference
| File | Purpose | Upstream risk |
| ---------------------------------------- | ------------------ | ----------------------- |
| `config/whitelabel.yml` | All brand config | New file — zero risk |
| `lib/whitelabel.rb` | Config loader | New file — zero risk |
| `config/initializers/whitelabel.rb` | Boot-time patching | New file — zero risk |
| `app/helpers/whitelabel_helper.rb` | View helper | New file — zero risk |
| `config/locales/whitelabel.yml` | i18n overrides | New file — zero risk |
| `public/intebec.css` | Theme CSS | Custom file — zero risk |
| `lib/docuseal.rb` | Added comment | Low risk — comment only |
| `app/views/shared/_logo.html.erb` | Dynamic logo path | Low risk |
| `app/views/shared/_meta.html.erb` | Dynamic meta tags | Low risk |
| `app/views/shared/_title.html.erb` | Dynamic brand name | Low risk |
| `app/views/layouts/application.html.erb` | Added meta tags | Low risk |
| `app/views/layouts/form.html.erb` | Added meta tags | Low risk |

@ -2,6 +2,9 @@
module Docuseal
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
# NOTE: These constants are kept for backward compatibility with upstream
# DocuSeal code. User-visible values are overridden at runtime by the
# Whitelabel module (see config/whitelabel.yml + config/initializers/whitelabel.rb).
PRODUCT_URL = 'https://www.docuseal.com'
PRODUCT_EMAIL_URL = ENV.fetch('PRODUCT_EMAIL_URL', PRODUCT_URL)
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze

@ -1,7 +1,7 @@
# frozen_string_literal: true
module SendWebhookRequest
USER_AGENT = 'DocuSeal.com Webhook'
USER_AGENT = Whitelabel.webhook_user_agent
LOCALHOSTS = DownloadUtils::LOCALHOSTS

@ -43,7 +43,7 @@ module Submissions
io = StringIO.new
document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
document.trailer.info[:Creator] = Whitelabel.pdf_creator
if pkcs
sign_params = {
@ -506,7 +506,7 @@ module Submissions
end
def sign_reason
'Signed with DocuSeal.com'
Whitelabel.audit_trail_footer
end
def select_attachments(submitter)
@ -528,8 +528,8 @@ module Submissions
def add_logo(column, _submission = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
column.formatted_text([{ text: 'DocuSeal',
link: Docuseal::PRODUCT_EMAIL_URL }],
column.formatted_text([{ text: Whitelabel.brand_name,
link: Whitelabel.website_url }],
font_size: 20,
font: [FONT_NAME, { variant: :bold }],
width: 100,

@ -37,7 +37,7 @@ module Submissions
bold_italic: FONT_BOLD_NAME
}.freeze
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
SIGN_REASON = "Signed by %<name>s with #{Whitelabel.brand_name}"
RTL_REGEXP = TextUtils::RTL_REGEXP
@ -952,7 +952,7 @@ module Submissions
end
def info_creator
"#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
Whitelabel.pdf_creator
end
def detached_signature?(_submitter)

@ -2,27 +2,29 @@
module Submitters
module FormConfigs
DEFAULT_KEYS = [AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::WITH_FIELD_LABELS_KEY,
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
module_function
def default_keys
[AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::WITH_FIELD_LABELS_KEY,
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])]
end
def call(submitter, keys = [])
configs = submitter.submission.account.account_configs.where(key: DEFAULT_KEYS + keys)
configs = submitter.submission.account.account_configs.where(key: default_keys + keys)
completed_button = find_safe_value(configs, AccountConfig::FORM_COMPLETED_BUTTON_KEY) || {}
completed_message = find_safe_value(configs, AccountConfig::FORM_COMPLETED_MESSAGE_KEY) || {}

@ -0,0 +1,247 @@
# frozen_string_literal: true
# =============================================================================
# Whitelabel — Centralised brand configuration loader
# =============================================================================
# Reads config/whitelabel.yml once at boot and exposes every value through
# simple accessor methods. All view helpers, mailers, PDF generators and
# other call-sites should use Whitelabel.xxx instead of hard-coding strings.
#
# Usage examples:
# Whitelabel.brand_name # => "Intébec"
# Whitelabel.support_email # => "support@intebec.ca"
# Whitelabel.theme(:primary) # => "216 77% 52%"
# Whitelabel.email_from # => "Intébec <info@intebec.ca>"
# Whitelabel.sign_reason("John") # => "Signed by John with Intébec"
#
# After editing config/whitelabel.yml, call Whitelabel.reload! or restart.
# =============================================================================
require 'yaml'
module Whitelabel
CONFIG_PATH = Rails.root.join('config', 'whitelabel.yml').freeze
class << self
# -----------------------------------------------------------------------
# Core loader
# -----------------------------------------------------------------------
def config
@config ||= load_config
end
def reload!
@config = load_config
end
# -----------------------------------------------------------------------
# Brand identity
# -----------------------------------------------------------------------
def brand_name
config.dig('brand', 'name') || 'DocuSeal'
end
def brand_short_name
config.dig('brand', 'short_name') || brand_name
end
def tagline
config.dig('brand', 'tagline') || ''
end
def description
config.dig('brand', 'description') || ''
end
def page_title(signed_in: false)
key = signed_in ? 'page_title_signed_in' : 'page_title_signed_out'
config.dig('brand', key) || brand_name
end
# -----------------------------------------------------------------------
# URLs
# -----------------------------------------------------------------------
def website_url
config.dig('urls', 'website') || 'https://intebec.ca'
end
def support_email
config.dig('urls', 'support_email') || 'support@intebec.ca'
end
def privacy_policy_url
config.dig('urls', 'privacy_policy')
end
def terms_url
config.dig('urls', 'terms_of_service')
end
def twitter_url
config.dig('urls', 'twitter_url')
end
def twitter_handle
config.dig('urls', 'twitter_handle')
end
def github_url
config.dig('urls', 'github_url')
end
def discord_url
config.dig('urls', 'discord_url')
end
# -----------------------------------------------------------------------
# Email
# -----------------------------------------------------------------------
def email_from
name = config.dig('email', 'from_name') || brand_name
addr = config.dig('email', 'from_address') || support_email
"#{name} <#{addr}>"
end
def email_attribution_html
raw = config.dig('email', 'attribution_html') || ''
raw.gsub('%{brand}', brand_name).gsub('%{website}', website_url)
end
# -----------------------------------------------------------------------
# Assets
# -----------------------------------------------------------------------
def logo_path
config.dig('assets', 'logo_path') || '/logo.svg'
end
def logo_width
config.dig('assets', 'logo_width') || 37
end
def logo_height
config.dig('assets', 'logo_height') || 37
end
def favicon_svg
config.dig('assets', 'favicon_svg') || '/favicon.svg'
end
def favicon_ico
config.dig('assets', 'favicon_ico') || '/favicon.ico'
end
def favicon_16
config.dig('assets', 'favicon_16') || '/favicon-16x16.png'
end
def favicon_32
config.dig('assets', 'favicon_32') || '/favicon-32x32.png'
end
def favicon_96
config.dig('assets', 'favicon_96') || '/favicon-96x96.png'
end
def apple_touch_icon
config.dig('assets', 'apple_touch_icon') || '/apple-icon-180x180.png'
end
def preview_image
config.dig('assets', 'preview_image') || '/preview.png'
end
# -----------------------------------------------------------------------
# Theme — returns HSL triplets for DaisyUI / CSS custom properties
# -----------------------------------------------------------------------
def theme(key)
config.dig('theme', key.to_s)
end
# -----------------------------------------------------------------------
# PDF / Audit trail
# -----------------------------------------------------------------------
def sign_reason(name)
template = config.dig('pdf', 'sign_reason') || "Signed by %{name} with #{brand_name}"
template.gsub('%{name}', name.to_s)
end
def audit_trail_footer
config.dig('pdf', 'audit_trail_footer') || "Signed with #{brand_name}"
end
def pdf_creator
creator = config.dig('pdf', 'creator') || brand_name
"#{creator} (#{website_url})"
end
def cert_name
config.dig('pdf', 'cert_name') || "#{brand_name} Self-Host Autogenerated"
end
# -----------------------------------------------------------------------
# PWA
# -----------------------------------------------------------------------
def pwa_description
config.dig('pwa', 'description') || "#{brand_name} is a secure platform for digital document signing."
end
def pwa_theme_color
config.dig('pwa', 'theme_color') || '#FAF7F4'
end
def pwa_background_color
config.dig('pwa', 'background_color') || '#FAF7F4'
end
# -----------------------------------------------------------------------
# Webhooks
# -----------------------------------------------------------------------
def webhook_user_agent
config.dig('webhooks', 'user_agent') || "#{brand_name} Webhook"
end
# -----------------------------------------------------------------------
# Feature flags
# -----------------------------------------------------------------------
def show_github_button?
config.dig('features', 'show_github_button') == true
end
def show_powered_by?
config.dig('features', 'show_powered_by') != false
end
def powered_by_text
config.dig('features', 'powered_by_text') || brand_name
end
def show_ai_link?
config.dig('features', 'show_ai_link') == true
end
def show_discord_link?
config.dig('features', 'show_discord_link') == true
end
private
def load_config
return {} unless CONFIG_PATH.exist?
YAML.safe_load_file(CONFIG_PATH, permitted_classes: [Symbol]) || {}
rescue Psych::SyntaxError => e
Rails.logger.error("[Whitelabel] Failed to parse #{CONFIG_PATH}: #{e.message}")
{}
end
end
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save