feat: add company logo upload and custom PDF signature field functionality

pull/500/head
Adam Hesch 3 months ago
parent f2f1745526
commit 97b4c64c77

@ -16,6 +16,24 @@ class PersonalizationSettingsController < ApplicationController
def show def show
authorize!(:read, AccountConfig) 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 end
def create def create

@ -21,6 +21,21 @@ class Account < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
has_many :users, dependent: :destroy 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 :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy has_many :account_configs, dependent: :destroy
has_many :email_messages, dependent: :destroy has_many :email_messages, dependent: :destroy

@ -1 +1,51 @@
<%= render 'logo_placeholder' %> <div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<%= form.label :logo, class: 'block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2' %>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<div class="collapse collapse-plus bg-base-200">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
<%= t('company_logo') %>
</div>
</div>
<div class="collapse-content">
<div class="space-y-4">
<% if @account.logo.attached? %>
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<%= image_tag @account.logo.variant(:thumb), class: "w-16 h-16 object-contain border border-gray-300 rounded" %>
</div>
<div>
<p class="text-sm text-gray-600">Current logo</p>
</div>
</div>
<% end %>
<div class="form-control">
<%= 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" %>
<div class="label">
<span class="label-text-alt text-gray-500">
<%= t('personalization_settings.logo_form.logo_help_html').html_safe %>
</span>
</div>
</div>
<% if @account.logo.attached? %>
<div class="form-control">
<label class="label cursor-pointer justify-start space-x-2">
<%= form.check_box :remove_logo, class: "checkbox" %>
<span class="label-text"><%= t('personalization_settings.logo_form.remove_logo') %></span>
</label>
</div>
<% end %>
<div class="form-control pt-2">
<%= form.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
</div>
</div>
</div>
</div>
</div>

@ -9,10 +9,9 @@
<%= render 'documents_copy_email_form' %> <%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %> <%= render 'submitter_completed_email_form' %>
</div> </div>
<p class="text-4xl font-bold mb-4 mt-8"> <%= form_with(model: @account, url: '/settings/personalization', method: :patch, html: { class: 'space-y-8' }) do |form| %>
<%= t('company_logo') %> <%= render 'logo_form', form: form %>
</p> <% end %>
<%= render 'logo_form' %>
<p class="text-4xl font-bold mb-4 mt-8"> <p class="text-4xl font-bold mb-4 mt-8">
<%= t('submission_form') %> <%= t('submission_form') %>
</p> </p>

@ -298,6 +298,10 @@ en: &en
attach_audit_log_pdf: Attach audit log PDF attach_audit_log_pdf: Attach audit log PDF
attach_documents: Attach documents attach_documents: Attach documents
settings_have_been_saved: Settings have been saved. 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. display_your_company_name_and_logo_when_signing_documents: Display your company name and logo when signing documents.
profile: Profile profile: Profile
signature: Signature signature: Signature

@ -178,7 +178,7 @@ Rails.application.routes.draw do
defaults: { status: :archived } defaults: { status: :archived }
resources :integration_users, only: %i[index], path: 'users/:status', controller: 'users', resources :integration_users, only: %i[index], path: 'users/:status', controller: 'users',
defaults: { status: :integration } 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 :api, only: %i[index create], controller: 'api_settings'
resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
post :resend post :resend

@ -22,9 +22,27 @@ module Submissions
pdf.config['font.map'] = GenerateResultAttachments::PDFA_FONT_MAP pdf.config['font.map'] = GenerateResultAttachments::PDFA_FONT_MAP
end 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 if pkcs
sign_params = { sign_params = {
reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter), reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter),
signature: sig_field,
**Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url) **Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url)
} }
@ -32,7 +50,14 @@ module Submissions
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params) Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
else 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 end
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(

@ -627,14 +627,30 @@ module Submissions
sign_reason = fetch_sign_reason(submitter) 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 if sign_reason && pkcs
sign_params = { sign_params = {
reason: sign_reason, reason: sign_reason,
signature: sig_field,
**build_signing_params(submitter, pkcs, tsa_url) **build_signing_params(submitter, pkcs, tsa_url)
} }
pdf.pages.first[:Annots] = [] unless pdf.pages.first[:Annots].respond_to?(:<<)
begin begin
pdf.sign(io, write_options: { validate: false }, **sign_params) pdf.sign(io, write_options: { validate: false }, **sign_params)
rescue HexaPDF::MalformedPDFError => e rescue HexaPDF::MalformedPDFError => e
@ -645,12 +661,24 @@ module Submissions
maybe_enable_ltv(io, sign_params) maybe_enable_ltv(io, sign_params)
else else
# Even without formal signing, use the custom signature field to prevent default watermark
begin begin
pdf.write(io, incremental: true, validate: false) pdf.sign(io, signature: sig_field, write_options: { validate: false })
rescue HexaPDF::MalformedPDFError => e rescue HexaPDF::MalformedPDFError => e
Rollbar.error(e) if defined?(Rollbar) 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
end end

Loading…
Cancel
Save