From 97b4c64c77ec84b4d87577329455ecb42fe28957 Mon Sep 17 00:00:00 2001 From: Adam Hesch Date: Sat, 19 Jul 2025 23:04:28 -0500 Subject: [PATCH] feat: add company logo upload and custom PDF signature field functionality --- .../personalization_settings_controller.rb | 18 +++++++ app/models/account.rb | 15 ++++++ .../_logo_form.html.erb | 52 ++++++++++++++++++- .../personalization_settings/show.html.erb | 7 ++- config/locales/i18n.yml | 4 ++ config/routes.rb | 2 +- .../generate_combined_attachment.rb | 27 +++++++++- .../generate_result_attachments.rb | 36 +++++++++++-- 8 files changed, 150 insertions(+), 11 deletions(-) diff --git a/app/controllers/personalization_settings_controller.rb b/app/controllers/personalization_settings_controller.rb index 9812aaee..30789d14 100644 --- a/app/controllers/personalization_settings_controller.rb +++ b/app/controllers/personalization_settings_controller.rb @@ -16,6 +16,24 @@ class PersonalizationSettingsController < ApplicationController def show authorize!(:read, AccountConfig) + @account = current_account + end + + def update + authorize!(:update, current_account) + @account = current_account + + if @account.update(account_params) + redirect_to '/settings/personalization', notice: 'Logo has been updated successfully.' + else + render :show + end + end + + private + + def account_params + params.require(:account).permit(:logo, :remove_logo) end def create diff --git a/app/models/account.rb b/app/models/account.rb index cbf157e5..bb5215bf 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -21,6 +21,21 @@ class Account < ApplicationRecord attribute :uuid, :string, default: -> { SecureRandom.uuid } has_many :users, dependent: :destroy + has_one_attached :logo do |attachable| + attachable.variant :thumb, resize_to_limit: [100, 100] + end + + attr_accessor :remove_logo + + after_save :purge_logo, if: :remove_logo? + + def remove_logo? + remove_logo.to_s == '1' + end + + def purge_logo + logo.purge + end has_many :encrypted_configs, dependent: :destroy has_many :account_configs, dependent: :destroy has_many :email_messages, dependent: :destroy diff --git a/app/views/personalization_settings/_logo_form.html.erb b/app/views/personalization_settings/_logo_form.html.erb index fc6f3ac7..099a6fc5 100644 --- a/app/views/personalization_settings/_logo_form.html.erb +++ b/app/views/personalization_settings/_logo_form.html.erb @@ -1 +1,51 @@ -<%= render 'logo_placeholder' %> +
+ <%= form.label :logo, class: 'block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2' %> + +
+
+ +
+
+ <%= t('company_logo') %> +
+
+
+
+ <% if @account.logo.attached? %> +
+
+ <%= image_tag @account.logo.variant(:thumb), class: "w-16 h-16 object-contain border border-gray-300 rounded" %> +
+
+

Current logo

+
+
+ <% end %> + +
+ <%= form.label :logo, "Upload Logo", class: "label" %> + <%= form.file_field :logo, accept: "image/png,image/jpeg,image/gif,image/svg+xml", class: "file-input file-input-bordered w-full" %> +
+ + <%= t('personalization_settings.logo_form.logo_help_html').html_safe %> + +
+
+ + <% if @account.logo.attached? %> +
+ +
+ <% end %> + +
+ <%= form.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> +
+
+
+
+
+
diff --git a/app/views/personalization_settings/show.html.erb b/app/views/personalization_settings/show.html.erb index 438da311..87f5149b 100644 --- a/app/views/personalization_settings/show.html.erb +++ b/app/views/personalization_settings/show.html.erb @@ -9,10 +9,9 @@ <%= render 'documents_copy_email_form' %> <%= render 'submitter_completed_email_form' %> -

- <%= t('company_logo') %> -

- <%= render 'logo_form' %> + <%= form_with(model: @account, url: '/settings/personalization', method: :patch, html: { class: 'space-y-8' }) do |form| %> + <%= render 'logo_form', form: form %> + <% end %>

<%= t('submission_form') %>

diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 71e846ab..a4d473b5 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -298,6 +298,10 @@ en: &en attach_audit_log_pdf: Attach audit log PDF attach_documents: Attach documents settings_have_been_saved: Settings have been saved. + personalization_settings: + logo_form: + remove_logo: Remove logo + logo_help_html: The logo will be displayed in emails and on the signing page. Use a transparent PNG for best results. display_your_company_name_and_logo_when_signing_documents: Display your company name and logo when signing documents. profile: Profile signature: Signature diff --git a/config/routes.rb b/config/routes.rb index 9d958fa3..50f31213 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -178,7 +178,7 @@ Rails.application.routes.draw do defaults: { status: :archived } resources :integration_users, only: %i[index], path: 'users/:status', controller: 'users', defaults: { status: :integration } - resource :personalization, only: %i[show create], controller: 'personalization_settings' + resource :personalization, only: %i[show create update], controller: 'personalization_settings' resources :api, only: %i[index create], controller: 'api_settings' resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do post :resend diff --git a/lib/submissions/generate_combined_attachment.rb b/lib/submissions/generate_combined_attachment.rb index 715e7a18..e983a720 100644 --- a/lib/submissions/generate_combined_attachment.rb +++ b/lib/submissions/generate_combined_attachment.rb @@ -22,9 +22,27 @@ module Submissions pdf.config['font.map'] = GenerateResultAttachments::PDFA_FONT_MAP end + # Always create a custom signature field to replace the default watermark, regardless of formal signing + sig_field = pdf.acro_form(create: true).create_signature_field("DocuSealSignature-#{SecureRandom.hex(4)}") + # The widget is placed on the page, but the appearance stream is what matters. + # We make it very small and out of the way. + widget = sig_field.create_widget(pdf.pages.first, Rect: [0, 0, 5, 5]) + appearance = widget.create_appearance + canvas = appearance.canvas + logo = submitter.account.logo + + if logo.attached? + logo.blob.open do |tempfile| + canvas.image(tempfile.path, at: [2, 1], height: 38) + end + end + + pdf.pages.first[:Annots] = [] unless pdf.pages.first[:Annots].respond_to?(:<<) + if pkcs sign_params = { reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter), + signature: sig_field, **Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url) } @@ -32,7 +50,14 @@ module Submissions Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params) else - pdf.write(io, incremental: true, validate: true) + # Even without formal signing, use the custom signature field to prevent default watermark + begin + pdf.sign(io, signature: sig_field) + rescue StandardError => e + # Fallback to regular write if signing fails + Rollbar.error(e) if defined?(Rollbar) + pdf.write(io, incremental: true, validate: true) + end end ActiveStorage::Attachment.create!( diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 3eeb2265..f2cb9fe9 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -627,14 +627,30 @@ module Submissions sign_reason = fetch_sign_reason(submitter) + # Always create a custom signature field to replace the default watermark, regardless of formal signing + sig_field = pdf.acro_form(create: true).create_signature_field("DocuSealSignature-#{SecureRandom.hex(4)}") + # The widget is placed on the page, but the appearance stream is what matters. + # We make it very small and out of the way. + widget = sig_field.create_widget(pdf.pages.first, Rect: [0, 0, 5, 5]) + appearance = widget.create_appearance + canvas = appearance.canvas + logo = submitter.account.logo + + if logo.attached? + logo.blob.open do |tempfile| + canvas.image(tempfile.path, at: [2, 1], height: 38) + end + end + + pdf.pages.first[:Annots] = [] unless pdf.pages.first[:Annots].respond_to?(:<<) + if sign_reason && pkcs sign_params = { reason: sign_reason, + signature: sig_field, **build_signing_params(submitter, pkcs, tsa_url) } - pdf.pages.first[:Annots] = [] unless pdf.pages.first[:Annots].respond_to?(:<<) - begin pdf.sign(io, write_options: { validate: false }, **sign_params) rescue HexaPDF::MalformedPDFError => e @@ -645,12 +661,24 @@ module Submissions maybe_enable_ltv(io, sign_params) else + # Even without formal signing, use the custom signature field to prevent default watermark begin - pdf.write(io, incremental: true, validate: false) + pdf.sign(io, signature: sig_field, write_options: { validate: false }) rescue HexaPDF::MalformedPDFError => e Rollbar.error(e) if defined?(Rollbar) - pdf.write(io, incremental: false, validate: false) + pdf.sign(io, signature: sig_field, write_options: { validate: false, incremental: false }) + rescue StandardError => e + # Fallback to regular write if signing fails + Rollbar.error(e) if defined?(Rollbar) + + begin + pdf.write(io, incremental: true, validate: false) + rescue HexaPDF::MalformedPDFError => e + Rollbar.error(e) if defined?(Rollbar) + + pdf.write(io, incremental: false, validate: false) + end end end