Merge from docusealco/wip

pull/502/head 2.0.7
Alex Turchyn 4 months ago committed by GitHub
commit 0741a6f485
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -63,7 +63,7 @@ RSpec/MultipleExpectations:
Max: 25 Max: 25
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 50 Max: 500
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 15 Max: 15

@ -6,12 +6,15 @@ class StartFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show completed] around_action :with_browser_locale, only: %i[show update completed]
before_action :maybe_redirect_com, only: %i[show completed] before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update before_action :load_resubmit_submitter, only: :update
before_action :load_template before_action :load_template
before_action :authorize_start!, only: :update before_action :authorize_start!, only: :update
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
@ -20,6 +23,7 @@ class StartFormController < ApplicationController
.submitters.new(account_id: @template.account_id, .submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first || uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid']) @template.submitters.first)['uuid'])
render :email_verification if params[:email_verification]
else else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@ -49,17 +53,10 @@ class StartFormController < ApplicationController
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent) @submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end end
if @submitter.errors.blank? && @submitter.save if @template.preferences['shared_link_2fa'] == true
if is_new_record handle_require_2fa(@submitter, is_new_record:)
WebhookUrls.enqueue_events(@submitter.submission, 'submission.created') elsif @submitter.errors.blank? && @submitter.save
enqueue_new_submitter_jobs(@submitter) if is_new_record
SearchEntries.enqueue_reindex(@submitter)
if @submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
'submission_id' => @submitter.submission_id)
end
end
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
@ -89,6 +86,16 @@ class StartFormController < ApplicationController
private private
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
SearchEntries.enqueue_reindex(submitter)
return unless submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
end
def load_resubmit_submitter def load_resubmit_submitter
@resubmit_submitter = @resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true']) if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
@ -123,7 +130,7 @@ class StartFormController < ApplicationController
.order(id: :desc) .order(id: :desc)
.where(declined_at: nil) .where(declined_at: nil)
.where(external_id: nil) .where(external_id: nil)
.where(ip: [nil, request.remote_ip]) .where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(find_params) .find_or_initialize_by(find_params)
@ -173,7 +180,7 @@ class StartFormController < ApplicationController
end end
def submitter_params def submitter_params
return current_user.slice(:email) if params[:selfsign] return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present? return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
@ -197,4 +204,39 @@ class StartFormController < ApplicationController
I18n.t('not_found') I18n.t('not_found')
end end
end end
def handle_require_2fa(submitter, is_new_record:)
return render :show, status: :unprocessable_entity if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
if submitter.save
enqueue_new_submitter_jobs(submitter) if is_new_record
if is_otp_verified
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
cookies.encrypted[:email_2fa_slug] =
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_entity
end
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
render :email_verification
end
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: e.message
rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end
end end

@ -0,0 +1,31 @@
# frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
def create
@template = Template.find_by!(slug: params[:slug])
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id)
Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
**redir_params
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
end
private
def submitter_params
params.require(:submitter).permit(:name, :email, :phone)
end
end

@ -12,6 +12,7 @@ class SubmissionEventsController < ApplicationController
'send_2fa_sms' => '2fa', 'send_2fa_sms' => '2fa',
'send_sms' => 'send', 'send_sms' => 'send',
'phone_verified' => 'phone_check', 'phone_verified' => 'phone_check',
'email_verified' => 'email_check',
'click_sms' => 'hand_click', 'click_sms' => 'hand_click',
'decline_form' => 'x', 'decline_form' => 'x',
'start_verification' => 'player_play', 'start_verification' => 'player_play',

@ -9,6 +9,7 @@ class SubmitFormController < ApplicationController
before_action :load_submitter, only: %i[show update completed] before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_locked_page, only: :show before_action :maybe_render_locked_page, only: :show
before_action :maybe_require_link_2fa, only: %i[show update]
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
@ -50,7 +51,7 @@ class SubmitFormController < ApplicationController
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end end
if @submitter.template&.archived_at? || @submitter.submission.archived_at? if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end end
@ -80,6 +81,15 @@ class SubmitFormController < ApplicationController
private private
def maybe_require_link_2fa
return if @submitter.submission.source != 'link'
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
redirect_to start_form_path(@submitter.submission.template.slug)
end
def maybe_render_locked_page def maybe_render_locked_page
return render :archived if @submitter.submission.template&.archived_at? || return render :archived if @submitter.submission.template&.archived_at? ||
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||

@ -26,7 +26,7 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_attach_documents completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration default_expire_at_duration shared_link_2fa
default_expire_at default_expire_at
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] +

@ -52,7 +52,7 @@ class WebhookEventsController < ApplicationController
turbo_stream.replace(helpers.dom_id(@webhook_event), turbo_stream.replace(helpers.dom_id(@webhook_event),
partial: 'event_row', partial: 'event_row',
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }), locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
turbo_stream.replace("drawer_events_#{helpers.dom_id(@webhook_event)}", turbo_stream.replace(helpers.dom_id(@webhook_event, :drawer_events),
partial: 'drawer_events', partial: 'drawer_events',
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event }) locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
] ]

@ -36,6 +36,7 @@ import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour' import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone' import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group' import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@ -107,6 +108,7 @@ safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone) safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick) safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {

@ -147,3 +147,11 @@ button[disabled] .enabled {
outline-offset: 3px; outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2); outline-color: hsl(var(--bc) / 0.2);
} }
.font-times {
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
}
.font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
}

@ -98,13 +98,13 @@ export default targetable(class extends HTMLElement {
el.classList.add('opacity-50') el.classList.add('opacity-50')
if (e.dataTransfer.files.length) { if (e.dataTransfer.files.length) {
const params = new URLSearchParams({ folder_name: el.innerText }).toString() const params = new URLSearchParams({ folder_name: el.innerText.trim() }).toString()
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`) this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
} else { } else {
const formData = new FormData() const formData = new FormData()
formData.append('name', el.innerText) formData.append('name', el.innerText.trim())
fetch(`/templates/${templateId}/folder`, { fetch(`/templates/${templateId}/folder`, {
method: 'PUT', method: 'PUT',

@ -0,0 +1,14 @@
export default class extends HTMLElement {
connectedCallback () {
this.image.addEventListener('load', (e) => {
this.image.setAttribute('width', e.target.naturalWidth)
this.image.setAttribute('height', e.target.naturalHeight)
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
})
}
get image () {
return this.querySelector('img')
}
}

@ -5,6 +5,7 @@ import DownloadButton from './elements/download_button'
import ToggleSubmit from './elements/toggle_submit' import ToggleSubmit from './elements/toggle_submit'
import FetchForm from './elements/fetch_form' import FetchForm from './elements/fetch_form'
import ScrollButtons from './elements/scroll_buttons' import ScrollButtons from './elements/scroll_buttons'
import PageContainer from './elements/page_container'
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options) const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
@ -12,6 +13,7 @@ safeRegisterElement('download-button', DownloadButton)
safeRegisterElement('toggle-submit', ToggleSubmit) safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('fetch-form', FetchForm) safeRegisterElement('fetch-form', FetchForm)
safeRegisterElement('scroll-buttons', ScrollButtons) safeRegisterElement('scroll-buttons', ScrollButtons)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('submission-form', class extends HTMLElement { safeRegisterElement('submission-form', class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.appElem = document.createElement('div') this.appElem = document.createElement('div')

@ -70,3 +70,11 @@ button[disabled] .enabled {
.base-radio { .base-radio {
@apply radio bg-white radio-sm; @apply radio bg-white radio-sm;
} }
.font-times {
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
}
.font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
}

@ -2,8 +2,8 @@
<div <div
class="flex absolute lg:text-base -outline-offset-1 field-area" class="flex absolute lg:text-base -outline-offset-1 field-area"
dir="auto" dir="auto"
:style="computedStyle" :style="[computedStyle, fontStyle]"
:class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@ -236,6 +236,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
isInlineSize: {
type: Boolean,
required: false,
default: true
},
submitter: { submitter: {
type: Object, type: Object,
required: false, required: false,
@ -348,8 +353,8 @@ export default {
} }
return { return {
'font-mono': this.field.preferences.font === 'Courier', 'font-courier': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times', 'font-times': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type), 'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type) italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
} }
@ -427,6 +432,37 @@ export default {
return [] return []
} }
}, },
fontStyle () {
let fontSize = ''
if (this.isInlineSize) {
if (this.textOverflowChars) {
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
} else {
fontSize = `${this.fontSizePx / 10}cqmin`
}
} else {
if (this.textOverflowChars) {
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
} else {
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
}
}
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
},
fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
},
lineHeight () {
return 1.3
},
fontScale () {
return 1000 / 612.0
},
ladscapeScale () {
return 8.5 / 11.0
},
computedStyle () { computedStyle () {
const { x, y, w, h } = this.area const { x, y, w, h } = this.area
@ -437,11 +473,6 @@ export default {
height: h * 100 + '%' height: h * 100 + '%'
} }
if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) { if (this.field.preferences?.color) {
style.color = this.field.preferences.color style.color = this.field.preferences.color
} }
@ -456,7 +487,7 @@ export default {
modelValue () { modelValue () {
this.$nextTick(() => { this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) { if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0 this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
} }
}) })
} }
@ -464,7 +495,7 @@ export default {
mounted () { mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) { if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0 this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
} }
}) })
}, },

@ -23,6 +23,7 @@
:area="area" :area="area"
:submittable="submittable" :submittable="submittable"
:field-index="fieldIndex" :field-index="fieldIndex"
:is-inline-size="isInlineSize"
:scroll-padding="scrollPadding" :scroll-padding="scrollPadding"
:submitter="submitter" :submitter="submitter"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
@ -110,6 +111,9 @@ export default {
} }
}, },
computed: { computed: {
isInlineSize () {
return CSS.supports('container-type: size')
},
isMobileContainer () { isMobileContainer () {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div') const container = root.body || root.querySelector('div')

@ -95,7 +95,19 @@ export default {
}, },
methods: { methods: {
onDropFiles (e) { onDropFiles (e) {
this.uploadFiles(e.dataTransfer.files) const files = Array.from(e.dataTransfer.files).filter((f) => {
if (this.accept === 'image/*') {
return f.type.startsWith('image')
} else {
return true
}
})
if (this.accept === 'image/*' && !files.length) {
alert(this.t('please_upload_an_image_file'))
} else {
this.uploadFiles(files)
}
}, },
onSelectFiles (e) { onSelectFiles (e) {
e.preventDefault() e.preventDefault()

@ -67,7 +67,7 @@
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button" class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
style="width: 96%; margin-left: 2%" style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]" @click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
> >

@ -14,6 +14,7 @@
<FieldArea <FieldArea
v-if="isMathLoaded" v-if="isMathLoaded"
:model-value="calculateFormula(field)" :model-value="calculateFormula(field)"
:is-inline-size="isInlineSize"
:field="field" :field="field"
:area="area" :area="area"
:submittable="false" :submittable="false"
@ -54,6 +55,11 @@ export default {
isMathLoaded: false isMathLoaded: false
} }
}, },
computed: {
isInlineSize () {
return CSS.supports('container-type: size')
}
},
async mounted () { async mounted () {
const { const {
create, create,

@ -1,4 +1,5 @@
const en = { const en = {
please_upload_an_image_file: 'Please upload an image file',
must_be_characters_length: 'Must be {number} characters length', must_be_characters_length: 'Must be {number} characters length',
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID', verify_id: 'Verify ID',
@ -99,6 +100,7 @@ const en = {
} }
const es = { const es = {
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
must_be_characters_length: 'Debe tener {number} caracteres de longitud', must_be_characters_length: 'Debe tener {number} caracteres de longitud',
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID', verify_id: 'Verificar ID',
@ -199,6 +201,7 @@ const es = {
} }
const it = { const it = {
please_upload_an_image_file: 'Per favore carica un file immagine',
must_be_characters_length: 'Deve essere lungo {number} caratteri', must_be_characters_length: 'Deve essere lungo {number} caratteri',
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.", complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID', verify_id: 'Verifica ID',
@ -299,6 +302,7 @@ const it = {
} }
const de = { const de = {
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
must_be_characters_length: 'Muss {number} Zeichen lang sein', must_be_characters_length: 'Muss {number} Zeichen lang sein',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.', complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
verify_id: 'ID überprüfen', verify_id: 'ID überprüfen',
@ -399,6 +403,7 @@ const de = {
} }
const fr = { const fr = {
please_upload_an_image_file: 'Veuillez télécharger un fichier image',
must_be_characters_length: 'Doit contenir {number} caractères', must_be_characters_length: 'Doit contenir {number} caractères',
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.", complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verify_id: "Vérification de l'ID", verify_id: "Vérification de l'ID",
@ -499,6 +504,7 @@ const fr = {
} }
const pl = { const pl = {
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
must_be_characters_length: 'Musi mieć długość {number} znaków', must_be_characters_length: 'Musi mieć długość {number} znaków',
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.', complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID', verify_id: 'Zweryfikuj ID',
@ -599,6 +605,7 @@ const pl = {
} }
const uk = { const uk = {
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
must_be_characters_length: 'Має містити {number} символів', must_be_characters_length: 'Має містити {number} символів',
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.", complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності', verify_id: 'Підтвердження ідентичності',
@ -699,6 +706,7 @@ const uk = {
} }
const cs = { const cs = {
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
must_be_characters_length: 'Musí mít délku {number} znaků', must_be_characters_length: 'Musí mít délku {number} znaků',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.', complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID', verify_id: 'Ověřit ID',
@ -799,6 +807,7 @@ const cs = {
} }
const pt = { const pt = {
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
must_be_characters_length: 'Deve ter {number} caracteres', must_be_characters_length: 'Deve ter {number} caracteres',
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.', complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
verify_id: 'Verificar ID', verify_id: 'Verificar ID',
@ -899,6 +908,7 @@ const pt = {
} }
const he = { const he = {
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
must_be_characters_length: 'חייב להיות באורך של {number} תווים', must_be_characters_length: 'חייב להיות באורך של {number} תווים',
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.', complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה', verify_id: 'אמת מזהה',
@ -999,6 +1009,7 @@ const he = {
} }
const nl = { const nl = {
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
must_be_characters_length: 'Moet {number} tekens lang zijn', must_be_characters_length: 'Moet {number} tekens lang zijn',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.', complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
verify_id: 'Verifiëren ID', verify_id: 'Verifiëren ID',
@ -1099,6 +1110,7 @@ const nl = {
} }
const ar = { const ar = {
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا', must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.', complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية', verify_id: 'تحقق من الهوية',
@ -1199,6 +1211,7 @@ const ar = {
} }
const ko = { const ko = {
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
must_be_characters_length: '{number}자여야 합니다', must_be_characters_length: '{number}자여야 합니다',
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.', complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인', verify_id: '아이디 확인',
@ -1299,6 +1312,7 @@ const ko = {
} }
const ja = { const ja = {
please_upload_an_image_file: '画像ファイルをアップロードしてください',
must_be_characters_length: '{number}文字でなければなりません', must_be_characters_length: '{number}文字でなければなりません',
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。', complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
verify_id: '本人確認', verify_id: '本人確認',

@ -172,7 +172,8 @@
> >
<div <div
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')" v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type), 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }" :class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
:style="fontStyle"
> >
<div <div
ref="textContainer" ref="textContainer"
@ -315,7 +316,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['template', 'selectedAreaRef', 'save', 't'], inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
props: { props: {
area: { area: {
type: Object, type: Object,
@ -385,6 +386,37 @@ export default {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels, fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
fontStyle () {
let fontSize = ''
if (this.isInlineSize) {
if (this.textOverflowChars) {
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
} else {
fontSize = `${this.fontSizePx / 10}cqmin`
}
} else {
if (this.textOverflowChars) {
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
} else {
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
}
}
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
},
fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
},
lineHeight () {
return 1.3
},
fontScale () {
return 1040 / 612.0
},
ladscapeScale () {
return 8.5 / 11.0
},
isDefaultValuePresent () { isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) { if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
return false return false
@ -413,8 +445,8 @@ export default {
'justify-center': this.field.preferences.align === 'center', 'justify-center': this.field.preferences.align === 'center',
'justify-start': this.field.preferences.align === 'left', 'justify-start': this.field.preferences.align === 'left',
'justify-end': this.field.preferences.align === 'right', 'justify-end': this.field.preferences.align === 'right',
'font-mono': this.field.preferences.font === 'Courier', 'font-courier': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times', 'font-times': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type), 'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type) italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
} }
@ -491,7 +523,7 @@ export default {
'field.default_value' () { 'field.default_value' () {
this.$nextTick(() => { this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) { if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0 this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
} }
}) })
} }
@ -499,7 +531,7 @@ export default {
mounted () { mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) { if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0 this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
} }
}) })
}, },

@ -542,6 +542,7 @@ export default {
isPaymentConnected: this.isPaymentConnected, isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula, withFormula: this.withFormula,
withConditions: this.withConditions, withConditions: this.withConditions,
isInlineSize: this.isInlineSize,
defaultDrawFieldType: this.defaultDrawFieldType, defaultDrawFieldType: this.defaultDrawFieldType,
selectedAreaRef: computed(() => this.selectedAreaRef), selectedAreaRef: computed(() => this.selectedAreaRef),
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef) fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
@ -793,6 +794,9 @@ export default {
language () { language () {
return this.locale.split('-')[0].toLowerCase() return this.locale.split('-')[0].toLowerCase()
}, },
isInlineSize () {
return CSS.supports('container-type: size')
},
isMobile () { isMobile () {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent) const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)

@ -29,6 +29,7 @@
</template> </template>
<script> <script>
import Page from './page' import Page from './page'
import { reactive } from 'vue'
export default { export default {
name: 'TemplateDocument', name: 'TemplateDocument',
@ -123,14 +124,14 @@ export default {
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
}, },
sortedPreviewImages () { sortedPreviewImages () {
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1]?.metadata || { width: 1400, height: 1812 }
return [...Array(this.numberOfPages).keys()].map((i) => { return [...Array(this.numberOfPages).keys()].map((i) => {
return this.previewImagesIndex[i] || { return this.previewImagesIndex[i] || reactive({
metadata: lazyloadMetadata, metadata: { ...lazyloadMetadata },
id: Math.random().toString(), id: Math.random().toString(),
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg` url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
} })
}) })
}, },
previewImagesIndex () { previewImagesIndex () {

@ -222,8 +222,8 @@ export default {
fonts () { fonts () {
return [ return [
{ value: null, label: 'Default' }, { value: null, label: 'Default' },
{ value: 'Times', label: 'Times', class: 'font-serif' }, { value: 'Times', label: 'Times', class: 'font-times' },
{ value: 'Courier', label: 'Courier', class: 'font-mono' } { value: 'Courier', label: 'Courier', class: 'font-courier' }
] ]
}, },
types () { types () {
@ -258,8 +258,8 @@ export default {
}, },
textClasses () { textClasses () {
return { return {
'font-mono': this.preferences.font === 'Courier', 'font-courier': this.preferences.font === 'Courier',
'font-serif': this.preferences.font === 'Times', 'font-times': this.preferences.font === 'Times',
'justify-center': this.preferences.align === 'center', 'justify-center': this.preferences.align === 'center',
'justify-start': this.preferences.align === 'left', 'justify-start': this.preferences.align === 'left',
'justify-end': this.preferences.align === 'right', 'justify-end': this.preferences.align === 'right',

@ -1,8 +1,9 @@
<template> <template>
<div <div
class="relative select-none" class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
:class="{ 'cursor-crosshair': allowDraw }" :class="{ 'cursor-crosshair': allowDraw, 'touch-none': !!drawField }"
:style="drawField ? 'touch-action: none' : ''" style="container-type: size"
:style="{ aspectRatio: `${width} / ${height}`}"
> >
<img <img
ref="image" ref="image"
@ -10,7 +11,7 @@
:src="image.url" :src="image.url"
:width="width" :width="width"
:height="height" :height="height"
class="border rounded mb-4" class="rounded"
@load="onImageLoad" @load="onImageLoad"
> >
<div <div
@ -191,8 +192,8 @@ export default {
}, },
methods: { methods: {
onImageLoad (e) { onImageLoad (e) {
e.target.setAttribute('width', e.target.naturalWidth) this.image.metadata.width = e.target.naturalWidth
e.target.setAttribute('height', e.target.naturalHeight) this.image.metadata.height = e.target.naturalHeight
}, },
setAreaRefs (el) { setAreaRefs (el) {
if (el) { if (el) {

@ -0,0 +1,13 @@
# frozen_string_literal: true
class TemplateMailer < ApplicationMailer
def otp_verification_email(template, email:)
@template = template
@otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':'))
assign_message_metadata('otp_verification_email', template)
mail(to: email, subject: I18n.t('email_verification'))
end
end

@ -42,7 +42,7 @@ class AccountConfig < ApplicationRecord
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'
WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_AUDIT_SUBMITTER_TIMEZONE_KEY = 'with_audit_submitter_timezone' WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature' REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'

@ -47,6 +47,7 @@ class SubmissionEvent < ApplicationRecord
click_email: 'click_email', click_email: 'click_email',
click_sms: 'click_sms', click_sms: 'click_sms',
phone_verified: 'phone_verified', phone_verified: 'phone_verified',
email_verified: 'email_verified',
start_form: 'start_form', start_form: 'start_form',
start_verification: 'start_verification', start_verification: 'start_verification',
complete_verification: 'complete_verification', complete_verification: 'complete_verification',

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v6" />
<path d="M3 7l9 6l9 -6" />
<path d="M15 19l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 420 B

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" wi width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8.243 7.34l-6.38 .925l-.113 .023a1 1 0 0 0 -.44 1.684l4.622 4.499l-1.09 6.355l-.013 .11a1 1 0 0 0 1.464 .944l5.706 -3l5.693 3l.1 .046a1 1 0 0 0 1.352 -1.1l-1.091 -6.355l4.624 -4.5l.078 -.085a1 1 0 0 0 -.633 -1.62l-6.38 -.926l-2.852 -5.78a1 1 0 0 0 -1.794 0l-2.853 5.78z" stroke-width="0" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 604 B

@ -2,16 +2,15 @@
if (!window.customElements.get('autosize-field')) { if (!window.customElements.get('autosize-field')) {
window.customElements.define('autosize-field', class extends HTMLElement { window.customElements.define('autosize-field', class extends HTMLElement {
connectedCallback() { connectedCallback() {
if (this.field.scrollHeight > this.field.clientHeight) { const originalFontValue = this.field.style.fontSize
this.field.classList.remove('text-[1.6vw]', 'lg:text-base');
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
if (this.field.scrollHeight > this.field.clientHeight) { if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]'); this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`;
} this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`;
if (this.field.scrollHeight > this.field.clientHeight) { if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.add('text-[0.7vw]', 'lg:text-[0.45rem]'); this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`;
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`;
} }
} }
} }

@ -1,3 +1,6 @@
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline"> <a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<img alt="GitHub Repo stars" src="https://www.docuseal.com/github-badge.svg" style="height: 22px"> <span class="flex items-center justify-between space-x-0.5 font-extrabold">
<%= svg_icon('start', class: 'h-3 w-3') %>
<span>9k</span>
</span>
</a> </a>

@ -5,7 +5,7 @@
<%= render 'shared/title' %> <%= render 'shared/title' %>
</a> </a>
<span class="hidden sm:inline"> <span class="hidden sm:inline">
<%= render 'shared/github' %> <%= render 'shared/github' if request.path.starts_with?('/settings') %>
</span> </span>
</div> </div>
<% if signed_in? %> <% if signed_in? %>
@ -79,7 +79,7 @@
</div> </div>
<% else %> <% else %>
<div class="flex space-x-2"> <div class="flex space-x-2">
<% if request.path != new_user_session_path %> <% if request.path != new_user_session_path && request.path != setup_index_path %>
<%= link_to new_user_session_path({ lang: params[:lang] }.compact_blank), class: 'font-medium text-lg' do %> <%= link_to new_user_session_path({ lang: params[:lang] }.compact_blank), class: 'font-medium text-lg' do %>
<span class="flex items-center justify-center space-x-1"> <span class="flex items-center justify-center space-x-1">
<%= svg_icon('login', class: 'w-6 h-6') %> <%= svg_icon('login', class: 'w-6 h-6') %>

@ -1,8 +1,8 @@
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
<%= t('upgrade') %>
<% end %>
<% if signed_in? && current_user != true_user %> <% if signed_in? && current_user != true_user %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<%= render 'shared/test_alert' %> <%= render 'shared/test_alert' %>
<% elsif request.path.starts_with?('/settings') %>
<%= link_to "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
<%= t('upgrade') %>
<% end %> <% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span> <span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<% end %>

@ -66,7 +66,7 @@
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= t('plans') %> <%= t('plans') %>
<span class="badge badge-warning"><%= t('new') %></span> <span class="badge badge-warning"><%= t('pro') %></span>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>

@ -0,0 +1,53 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% 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 %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
<p dir="auto" class="text-sm"><%= t('invited_by_html', name: @template.account.name) %></p>
</div>
</div>
</div>
</div>
<div>
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
</div>
<%= form_for '', url: start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0">
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %>
<div class="flex justify-between items-center mt-1">
<span>
<% if flash[:alert] %>
<span class="text-red-500">
<%= flash[:alert] %>
</span>
<% elsif flash[:notice] %>
<%= flash[:notice] %>
<% end %>
</span>
<span>
<label for="resend_code" id="resend_label" class="link"><%= t(:re_send_code) %></label>
</span>
</div>
</div>
<toggle-submit dir="auto" class="form-control">
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
</toggle-submit>
<% end %>
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
</div>
</div>

@ -3,7 +3,8 @@
<% color = field.dig('preferences', 'color') %> <% color = field.dig('preferences', 'color') %>
<% font = field.dig('preferences', 'font') %> <% font = field.dig('preferences', 'font') %>
<% font_type = field.dig('preferences', 'font_type') %> <% font_type = field.dig('preferences', 'font_type') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
<% if field['type'] == 'signature' %> <% if field['type'] == 'signature' %>
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %> <% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>"> <div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">

@ -1,6 +1,7 @@
<% if params[:controller] == 'submissions_preview' %> <% if params[:controller] == 'submissions_preview' %>
<%= render 'submissions/preview_tags' %> <%= render 'submissions/preview_tags' %>
<% end %> <% end %>
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
<% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %> <% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %>
<div style="max-width: 1600px" class="mx-auto pl-4"> <div style="max-width: 1600px" class="mx-auto pl-4">
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100"> <div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
@ -94,11 +95,11 @@
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %> <% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %> <% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.first&.metadata || {} %> <% lazyload_metadata = document.preview_images.first&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %> <% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %> <% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative"> <page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
<img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>"> <img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= width %>" class="rounded" height="<%= height %>">
<div class="top-0 bottom-0 left-0 right-0 absolute"> <div class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %> <% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %> <%= render 'submissions/annotation', annot: %>
@ -110,18 +111,18 @@
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %> <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
<span class="group"> <span class="group">
<span class="hidden group-hover:inline"> <span class="hidden group-hover:inline">
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
</span> </span>
<span class="group-hover:hidden"> <span class="group-hover:hidden">
<%= render 'submissions/value', area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
</span> </span>
</span> </span>
<% else %> <% else %>
<%= render 'submissions/value', area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
</div> </page-container>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>

@ -7,6 +7,7 @@
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %> <% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %> <% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %> <% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
<div style="max-height: -webkit-fill-available;"> <div style="max-height: -webkit-fill-available;">
<div id="scrollbox"> <div id="scrollbox">
<div class="mx-auto block pb-72" style="max-width: 1000px"> <div class="mx-auto block pb-72" style="max-width: 1000px">
@ -66,11 +67,11 @@
<div id="document-<%= document.uuid %>"> <div id="document-<%= document.uuid %>">
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %> <% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.last&.metadata || {} %> <% lazyload_metadata = document.preview_images.last&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %> <% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %> <% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
<div class="relative my-4 shadow-md"> <page-container class="block relative my-4 shadow-md" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>"> <img loading="lazy" src="<%= page.url %>" width="<%= width %>" height="<%= height %>">
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute"> <div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
<% if annots = document_annots_index[index] %> <% if annots = document_annots_index[index] %>
<%= render 'submit_form/annotations', annots: %> <%= render 'submit_form/annotations', annots: %>
@ -83,10 +84,10 @@
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %> <% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %> <%= render 'submissions/value', font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
<% end %> <% end %>
</div> </div>
</div> </page-container>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>

@ -0,0 +1,3 @@
<p><%= t('your_verification_code_to_access_the_name', name: @template.name) %></p>
<p><b><%= @otp_code %></b></p>
<p><%= t('please_reply_to_this_email_if_you_didnt_request_this') %></p>

@ -41,5 +41,15 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
<label for="template_preferences_shared_link_2fa" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</label>
<% end %>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>

@ -1,4 +1,4 @@
<div id="drawer_events_<%= dom_id(webhook_event) %>"> <div id="<%= dom_id(webhook_event, :drawer_events) %>">
<ol class="relative border-s border-base-300 space-y-6 ml-3"> <ol class="relative border-s border-base-300 space-y-6 ml-3">
<% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %> <% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %>
<% if webhook_event.status == 'error' %> <% if webhook_event.status == 'error' %>

@ -20,6 +20,7 @@ en: &en
language_ko: 한국어 language_ko: 한국어
language_ja: 日本語 language_ja: 日本語
hi_there: Hi there hi_there: Hi there
pro: Pro
thanks: Thanks thanks: Thanks
private: Private private: Private
default_parties: Default parties default_parties: Default parties
@ -769,6 +770,15 @@ en: &en
there_are_no_events: There are no events there_are_no_events: There are no events
resend: Resend resend: Resend
next_attempt_in_time_in_words: Next attempt in %{time_in_words} next_attempt_in_time_in_words: Next attempt in %{time_in_words}
request_email_otp_verification_with_shared_link: Request email OTP verification with shared link
sms_rate_limit_exceeded: SMS rate limit exceeded
invalid_phone_number: Invalid phone number
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Please contact the requester to specify your phone number for two-factor authentication.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: We've sent a one-time verification code to your email address. Please enter the code below to continue.
re_send_code: Re-send Code
email_verification: Email verification
your_verification_code_to_access_the_name: 'Your verification code to access the "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Please reply to this email if you didn't request this.
submission_sources: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@ -786,6 +796,7 @@ en: &en
click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}' click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}'
click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}' click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}'
phone_verified_by_html: '<b>Phone verified</b> by %{submitter_name}' phone_verified_by_html: '<b>Phone verified</b> by %{submitter_name}'
email_verified_by_html: '<b>Email verified</b> by %{submitter_name}'
start_form_by_html: '<b>Submission started</b> by %{submitter_name}' start_form_by_html: '<b>Submission started</b> by %{submitter_name}'
view_form_by_html: '<b>Form viewed</b> by %{submitter_name}' view_form_by_html: '<b>Form viewed</b> by %{submitter_name}'
invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}' invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}'
@ -874,6 +885,7 @@ en: &en
range_without_total: "%{from}-%{to} events" range_without_total: "%{from}-%{to} events"
es: &es es: &es
pro: Pro
default_parties: Partes predeterminadas default_parties: Partes predeterminadas
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
require_all_recipients: Requerir a todos los destinatarios require_all_recipients: Requerir a todos los destinatarios
@ -1624,6 +1636,15 @@ es: &es
there_are_no_events: No hay eventos there_are_no_events: No hay eventos
resend: Reenviar resend: Reenviar
next_attempt_in_time_in_words: Próximo intento en %{time_in_words} next_attempt_in_time_in_words: Próximo intento en %{time_in_words}
request_email_otp_verification_with_shared_link: Solicitar verificación OTP por correo electrónico con enlace compartido
sms_rate_limit_exceeded: Límite de SMS excedido
invalid_phone_number: Número de teléfono inválido
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Contacte al solicitante para especificar su número para la autenticación de dos factores.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Enviamos un código de verificación único a su correo electrónico. Ingréselo a continuación para continuar.
re_send_code: Reenviar código
email_verification: Verificación por correo electrónico
your_verification_code_to_access_the_name: 'Su código de verificación para acceder a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Por favor, responda este correo si no solicitó esto.
submission_sources: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@ -1641,6 +1662,7 @@ es: &es
click_email_by_html: '<b>Enlace del correo electrónico clicado</b> por %{submitter_name}' click_email_by_html: '<b>Enlace del correo electrónico clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Enlace del SMS clicado</b> por %{submitter_name}' click_sms_by_html: '<b>Enlace del SMS clicado</b> por %{submitter_name}'
phone_verified_by_html: '<b>Teléfono verificado</b> por %{submitter_name}' phone_verified_by_html: '<b>Teléfono verificado</b> por %{submitter_name}'
email_verified_by_html: '<b>Correo electrónico verificado</b> por %{submitter_name}'
start_form_by_html: '<b>Envío iniciado</b> por %{submitter_name}' start_form_by_html: '<b>Envío iniciado</b> por %{submitter_name}'
view_form_by_html: '<b>Formulario visto</b> por %{submitter_name}' view_form_by_html: '<b>Formulario visto</b> por %{submitter_name}'
invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}' invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}'
@ -1729,6 +1751,7 @@ es: &es
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
it: &it it: &it
pro: Pro
default_parties: Parti predefiniti default_parties: Parti predefiniti
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token" authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
require_all_recipients: Richiedi tutti i destinatari require_all_recipients: Richiedi tutti i destinatari
@ -2477,6 +2500,16 @@ it: &it
there_are_no_events: Nessun evento there_are_no_events: Nessun evento
resend: Invia di nuovo resend: Invia di nuovo
next_attempt_in_time_in_words: Prossimo tentativo tra %{time_in_words} next_attempt_in_time_in_words: Prossimo tentativo tra %{time_in_words}
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: "Questo modello ha più parti, il che impedisce l'uso di un link di condivisione perché non è chiaro quale parte sia responsabile di campi specifici. Per risolvere, definisci i dettagli predefiniti della parte."
request_email_otp_verification_with_shared_link: "Richiedi la verifica OTP tramite email con link condiviso"
sms_rate_limit_exceeded: Limite SMS superato
invalid_phone_number: Numero di telefono non valido
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Contatta il richiedente per specificare il tuo numero per l'autenticazione a due fattori.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Abbiamo inviato un codice di verifica una tantum alla tua email. Inseriscilo qui sotto per continuare.
re_send_code: Invia di nuovo il codice
email_verification: Verifica email
your_verification_code_to_access_the_name: 'Il tuo codice per accedere a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Rispondi a questa email se non hai richiesto questo.
submission_sources: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@ -2494,6 +2527,7 @@ it: &it
click_email_by_html: "<b>Link dell'e-mail cliccato</b> da %{submitter_name}" click_email_by_html: "<b>Link dell'e-mail cliccato</b> da %{submitter_name}"
click_sms_by_html: "<b>Link dell'SMS cliccato</b> da %{submitter_name}" click_sms_by_html: "<b>Link dell'SMS cliccato</b> da %{submitter_name}"
phone_verified_by_html: '<b>Telefono verificato</b> da %{submitter_name}' phone_verified_by_html: '<b>Telefono verificato</b> da %{submitter_name}'
email_verified_by_html: '<b>Email verificata</b> da %{submitter_name}'
start_form_by_html: '<b>Invio iniziato</b> da %{submitter_name}' start_form_by_html: '<b>Invio iniziato</b> da %{submitter_name}'
view_form_by_html: '<b>Modulo visualizzato</b> da %{submitter_name}' view_form_by_html: '<b>Modulo visualizzato</b> da %{submitter_name}'
invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}' invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}'
@ -2582,6 +2616,7 @@ it: &it
range_without_total: "%{from}-%{to} eventi" range_without_total: "%{from}-%{to} eventi"
fr: &fr fr: &fr
pro: Pro
default_parties: Parties par défaut default_parties: Parties par défaut
authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton
require_all_recipients: Exiger tous les destinataires require_all_recipients: Exiger tous les destinataires
@ -3333,6 +3368,16 @@ fr: &fr
there_are_no_events: Aucun événement there_are_no_events: Aucun événement
resend: Renvoyer resend: Renvoyer
next_attempt_in_time_in_words: Nouvelle tentative dans %{time_in_words} next_attempt_in_time_in_words: Nouvelle tentative dans %{time_in_words}
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: "Ce modèle contient plusieurs parties, ce qui empêche l'utilisation d'un lien de partage car il n'est pas clair quelle partie est responsable de certains champs. Pour résoudre cela, définissez les détails de la partie par défaut."
request_email_otp_verification_with_shared_link: "Demander une vérification OTP par e-mail avec un lien de partage"
sms_rate_limit_exceeded: Limite de SMS dépassée
invalid_phone_number: Numéro de téléphone invalide
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Veuillez contacter l'expéditeur pour spécifier votre numéro pour l'authentification à deux facteurs.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Un code de vérification unique a été envoyé à votre adresse email. Veuillez le saisir ci-dessous pour continuer.
re_send_code: Renvoyer le code
email_verification: Vérification de l'email
your_verification_code_to_access_the_name: 'Votre code pour accéder à "%{name}" :'
please_reply_to_this_email_if_you_didnt_request_this: Veuillez répondre à cet email si vous n'avez pas fait cette demande.
submission_sources: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@ -3350,6 +3395,7 @@ fr: &fr
click_email_by_html: "<b>Lien de l'e-mail cliqué</b> par %{submitter_name}" click_email_by_html: "<b>Lien de l'e-mail cliqué</b> par %{submitter_name}"
click_sms_by_html: '<b>Lien du SMS cliqué</b> par %{submitter_name}' click_sms_by_html: '<b>Lien du SMS cliqué</b> par %{submitter_name}'
phone_verified_by_html: '<b>Téléphone vérifié</b> par %{submitter_name}' phone_verified_by_html: '<b>Téléphone vérifié</b> par %{submitter_name}'
email_verified_by_html: '<b>Email vérifié</b> par %{submitter_name}'
start_form_by_html: '<b>Soumission commencée</b> par %{submitter_name}' start_form_by_html: '<b>Soumission commencée</b> par %{submitter_name}'
view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}' view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}'
invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}' invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}'
@ -3438,6 +3484,7 @@ fr: &fr
range_without_total: "%{from} à %{to} événements" range_without_total: "%{from} à %{to} événements"
pt: &pt pt: &pt
pro: Pro
default_parties: Partes padrão default_parties: Partes padrão
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
require_all_recipients: Exigir todos os destinatários require_all_recipients: Exigir todos os destinatários
@ -4188,6 +4235,15 @@ pt: &pt
there_are_no_events: Nenhum evento there_are_no_events: Nenhum evento
resend: Reenviar resend: Reenviar
next_attempt_in_time_in_words: Próxima tentativa em %{time_in_words} next_attempt_in_time_in_words: Próxima tentativa em %{time_in_words}
request_email_otp_verification_with_shared_link: Solicitar verificação de OTP por e-mail com link compartilhado
sms_rate_limit_exceeded: Limite de SMS excedido
invalid_phone_number: Número de telefone inválido
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Entre em contato com o solicitante para especificar seu número para autenticação de dois fatores.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Enviamos um código de verificação único para seu e-mail. Insira-o abaixo para continuar.
re_send_code: Reenviar código
email_verification: Verificação de e-mail
your_verification_code_to_access_the_name: 'Seu código de verificação para acessar "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Responda a este e-mail se você não solicitou isso.
submission_sources: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@ -4205,6 +4261,7 @@ pt: &pt
click_email_by_html: '<b>Link do e-mail clicado</b> por %{submitter_name}' click_email_by_html: '<b>Link do e-mail clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Link do SMS clicado</b> por %{submitter_name}' click_sms_by_html: '<b>Link do SMS clicado</b> por %{submitter_name}'
phone_verified_by_html: '<b>Telefone verificado</b> por %{submitter_name}' phone_verified_by_html: '<b>Telefone verificado</b> por %{submitter_name}'
email_verified_by_html: '<b>Email verificado</b> por %{submitter_name}'
start_form_by_html: '<b>Submissão iniciada</b> por %{submitter_name}' start_form_by_html: '<b>Submissão iniciada</b> por %{submitter_name}'
view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}' view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}'
invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}' invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}'
@ -4294,6 +4351,7 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
de: &de de: &de
pro: Pro
default_parties: Standardparteien default_parties: Standardparteien
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
require_all_recipients: Alle Empfänger erforderlich require_all_recipients: Alle Empfänger erforderlich
@ -5044,6 +5102,15 @@ de: &de
there_are_no_events: Keine Ereignisse vorhanden there_are_no_events: Keine Ereignisse vorhanden
resend: Erneut senden resend: Erneut senden
next_attempt_in_time_in_words: Nächster Versuch in %{time_in_words} next_attempt_in_time_in_words: Nächster Versuch in %{time_in_words}
request_email_otp_verification_with_shared_link: Fordern Sie die E-Mail-OTP-Verifizierung mit Freigabelink an
sms_rate_limit_exceeded: SMS-Limit überschritten
invalid_phone_number: Ungültige Telefonnummer
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Kontaktieren Sie den Absender, um Ihre Telefonnummer für die Zwei-Faktor-Authentifizierung anzugeben.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Wir haben einen einmaligen Verifizierungscode an Ihre E-Mail-Adresse gesendet. Bitte geben Sie ihn unten ein, um fortzufahren.
re_send_code: Code erneut senden
email_verification: E-Mail-Verifizierung
your_verification_code_to_access_the_name: 'Ihr Verifizierungscode für den Zugriff auf "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Antworten Sie auf diese E-Mail, wenn Sie dies nicht angefordert haben.
submission_sources: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
@ -5061,6 +5128,7 @@ de: &de
click_email_by_html: '<b>E-Mail-Link angeklickt</b> von %{submitter_name}' click_email_by_html: '<b>E-Mail-Link angeklickt</b> von %{submitter_name}'
click_sms_by_html: '<b>SMS-Link angeklickt</b> von %{submitter_name}' click_sms_by_html: '<b>SMS-Link angeklickt</b> von %{submitter_name}'
phone_verified_by_html: '<b>Telefon verifiziert</b> von %{submitter_name}' phone_verified_by_html: '<b>Telefon verifiziert</b> von %{submitter_name}'
email_verified_by_html: '<b>Email verifiziert</b> von %{submitter_name}'
start_form_by_html: '<b>Einreichung gestartet</b> von %{submitter_name}' start_form_by_html: '<b>Einreichung gestartet</b> von %{submitter_name}'
view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}' view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}'
invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}' invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}'
@ -5227,6 +5295,15 @@ pl:
select_data_residency: Wybierz lokalizację danych select_data_residency: Wybierz lokalizację danych
company_name: Nazwa firmy company_name: Nazwa firmy
optional: opcjonalne optional: opcjonalne
submit: Prześlij
sms_rate_limit_exceeded: Przekroczono limit wiadomości SMS
invalid_phone_number: Nieprawidłowy numer telefonu
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Skontaktuj się z nadawcą, aby podać numer telefonu do uwierzytelniania dwuskładnikowego.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Wysłaliśmy jednorazowy kod weryfikacyjny na Twój adres e-mail. Wprowadź go poniżej, aby kontynuować.
re_send_code: Wyślij ponownie kod
email_verification: Weryfikacja e-mail
your_verification_code_to_access_the_name: 'Twój kod weryfikacyjny do uzyskania dostępu do "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Odpowiedz na ten e-mail, jeśli nie prosiłeś o to.
uk: uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@ -5307,6 +5384,15 @@ uk:
select_data_residency: Виберіть місце зберігання даних select_data_residency: Виберіть місце зберігання даних
company_name: Назва компанії company_name: Назва компанії
optional: необов’язково optional: необов’язково
submit: Надіслати
sms_rate_limit_exceeded: Перевищено ліміт SMS
invalid_phone_number: Невірний номер телефону
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Зв’яжіться з відправником, щоб вказати номер телефону для двофакторної автентифікації.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Ми надіслали одноразовий код підтвердження на вашу електронну пошту. Введіть його нижче, щоб продовжити.
re_send_code: Надіслати код повторно
email_verification: Підтвердження електронної пошти
your_verification_code_to_access_the_name: 'Ваш код доступу до "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Відповідайте на цей лист, якщо ви цього не запитували.
cs: cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@ -5387,6 +5473,15 @@ cs:
select_data_residency: Vyberte umístění dat select_data_residency: Vyberte umístění dat
company_name: Název společnosti company_name: Název společnosti
optional: volitelné optional: volitelné
submit: Odeslat
sms_rate_limit_exceeded: Překročena hranice SMS
invalid_phone_number: Neplatné telefonní číslo
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Kontaktujte odesílatele kvůli zadání vašeho čísla pro dvoufázové ověření.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Poslali jsme jednorázový ověřovací kód na váš e-mail. Zadejte ho níže pro pokračování.
re_send_code: Znovu odeslat kód
email_verification: Ověření e-mailu
your_verification_code_to_access_the_name: 'Váš ověřovací kód pro přístup k "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Odpovězte na tento e-mail, pokud jste o to nežádali.
he: he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@ -5467,6 +5562,15 @@ he:
select_data_residency: בחר מיקום נתונים select_data_residency: בחר מיקום נתונים
company_name: שם החברה company_name: שם החברה
optional: אופציונלי optional: אופציונלי
submit: 'שלח'
sms_rate_limit_exceeded: 'חריגה ממגבלת SMS'
invalid_phone_number: 'מספר טלפון לא תקין'
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 'פנה לשולח כדי לציין את מספרך לאימות דו-שלבי.'
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 'שלחנו קוד אימות חד-פעמי לדוא"ל שלך. הזן את הקוד למטה כדי להמשיך.'
re_send_code: 'שלח קוד מחדש'
email_verification: 'אימות דוא"ל'
your_verification_code_to_access_the_name: 'קוד האימות שלך לגישה ל-%{name}:'
please_reply_to_this_email_if_you_didnt_request_this: 'השב למייל זה אם לא ביקשת זאת.'
nl: nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
@ -5547,6 +5651,15 @@ nl:
select_data_residency: Selecteer gegevenslocatie select_data_residency: Selecteer gegevenslocatie
company_name: Bedrijfsnaam company_name: Bedrijfsnaam
optional: optioneel optional: optioneel
submit: Verzenden
sms_rate_limit_exceeded: SMS-limiet overschreden
invalid_phone_number: Ongeldig telefoonnummer
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Neem contact op met de aanvrager om uw nummer voor twee-factor-authenticatie op te geven.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: We hebben een eenmalige verificatiecode naar je e-mailadres gestuurd. Voer de code hieronder in om verder te gaan.
re_send_code: Code opnieuw verzenden
email_verification: E-mailverificatie
your_verification_code_to_access_the_name: 'Je verificatiecode voor toegang tot "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Reageer op deze e-mail als je dit niet hebt aangevraagd.
ar: ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@ -5627,6 +5740,15 @@ ar:
select_data_residency: اختر موقع تخزين البيانات select_data_residency: اختر موقع تخزين البيانات
company_name: اسم الشركة company_name: اسم الشركة
optional: اختياري optional: اختياري
submit: 'إرسال'
sms_rate_limit_exceeded: 'تم تجاوز حد رسائل SMS'
invalid_phone_number: 'رقم الهاتف غير صالح'
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 'يرجى الاتصال بالمرسل لتحديد رقم هاتفك للمصادقة الثنائية.'
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 'أرسلنا رمز تحقق لمرة واحدة إلى بريدك الإلكتروني. الرجاء إدخاله أدناه للمتابعة.'
re_send_code: 'إعادة إرسال الرمز'
email_verification: 'التحقق من البريد الإلكتروني'
your_verification_code_to_access_the_name: 'رمز التحقق الخاص بك للوصول إلى "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: 'يرجى الرد على هذا البريد إذا لم تطلب ذلك.'
ko: ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함 require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@ -5707,6 +5829,15 @@ ko:
select_data_residency: 데이터 저장 위치 선택 select_data_residency: 데이터 저장 위치 선택
company_name: 회사 이름 company_name: 회사 이름
optional: 선택 사항 optional: 선택 사항
submit: 제출
sms_rate_limit_exceeded: SMS 제한 초과
invalid_phone_number: 잘못된 전화번호입니다
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 이중 인증을 위해 전화번호를 지정하려면 요청자에게 문의하세요.
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 일회용 인증 코드를 이메일로 보냈습니다. 계속하려면 아래에 입력하세요.
re_send_code: 코드 재전송
email_verification: 이메일 인증
your_verification_code_to_access_the_name: '"%{name}"에 액세스하기 위한 인증 코드:'
please_reply_to_this_email_if_you_didnt_request_this: 요청하지 않았다면 이 이메일에 회신하세요.
ja: ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です require_phone_2fa_to_open: 電話による2段階認証が必要です
@ -5787,6 +5918,15 @@ ja:
select_data_residency: データ保存場所を選択 select_data_residency: データ保存場所を選択
company_name: 会社名 company_name: 会社名
optional: 任意 optional: 任意
submit: 送信
sms_rate_limit_exceeded: SMSの送信制限を超えました
invalid_phone_number: 無効な電話番号です
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 二要素認証のため、リクエスターに電話番号を指定してください。
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: ワンタイム認証コードをメールアドレスに送信しました。続行するには、以下にコードを入力してください。
re_send_code: コードを再送信
email_verification: メール認証
your_verification_code_to_access_the_name: '"%{name}"へのアクセスコード:'
please_reply_to_this_email_if_you_didnt_request_this: このリクエストを行っていない場合は、このメールに返信してください。
en-US: en-US:
<<: *en <<: *en

@ -133,6 +133,7 @@ Rails.application.routes.draw do
end end
resource :resubmit_form, controller: 'start_form', only: :update resource :resubmit_form, controller: 'start_form', only: :update
resources :start_form_email_2fa_send, only: :create
resources :submit_form, only: %i[], path: '' do resources :submit_form, only: %i[], path: '' do
get :success, on: :collection get :success, on: :collection

@ -88,6 +88,10 @@ module Docuseal
true true
end end
def pdf_format
@pdf_format ||= ENV['PDF_FORMAT'].to_s.downcase
end
def trusted_certs def trusted_certs
@trusted_certs ||= @trusted_certs ||=
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64| ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64|

@ -0,0 +1,27 @@
# frozen_string_literal: true
module EmailVerificationCodes
DRIFT_BEHIND = 5.minutes
module_function
def generate(value)
totp = ROTP::TOTP.new(build_totp_secret(value))
totp.at(Time.current)
end
def verify(code, value)
totp = ROTP::TOTP.new(build_totp_secret(value))
totp.verify(code, drift_behind: DRIFT_BEHIND)
end
def build_totp_secret(value)
ROTP::Base32.encode(
Digest::SHA1.digest(
[Rails.application.secret_key_base, value].join(':')
)
)
end
end

@ -1,6 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module PdfUtils module PdfUtils
DEFAULT_DPI = 72
US_LETTER_W = DEFAULT_DPI * 8.5
module_function module_function
def encrypted?(data, password: nil) def encrypted?(data, password: nil)

@ -132,12 +132,14 @@ module SearchEntries
return if submitter.email.blank? && submitter.phone.blank? && submitter.name.blank? return if submitter.email.blank? && submitter.phone.blank? && submitter.name.blank?
email_phone_name = [ email_phone_name = [
[submitter.email.to_s, submitter.email.to_s.split('@').last].join(' ').delete("\0"), submitter.email.to_s.then { |e| [e, e.split('@').last] }.join(' ').delete("\0"),
[submitter.phone.to_s.gsub(/\D/, ''), submitter.phone.to_s.then { |e| [e.gsub(/\D/, ''), e.gsub(PhoneCodes::REGEXP, '').gsub(/\D/, '')] }
submitter.phone.to_s.gsub(PhoneCodes::REGEXP, '').gsub(/\D/, '')].uniq.join(' ').delete("\0"), .uniq.join(' ').delete("\0"),
TextUtils.transliterate(submitter.name).delete("\0") TextUtils.transliterate(submitter.name).delete("\0")
] ]
values_string = build_submitter_values_string(submitter)
sql = SearchEntry.sanitize_sql_array( sql = SearchEntry.sanitize_sql_array(
[ [
"SELECT setweight(to_tsvector(?), 'A') || setweight(to_tsvector(?), 'B') || "SELECT setweight(to_tsvector(?), 'A') || setweight(to_tsvector(?), 'B') ||
@ -145,9 +147,7 @@ module SearchEntries
setweight(to_tsvector('simple', ?), 'A') || setweight(to_tsvector('simple', ?), 'A') ||
setweight(to_tsvector('simple', ?), 'B') || setweight(to_tsvector('simple', ?), 'B') ||
setweight(to_tsvector('simple', ?), 'C') as ngram".squish, setweight(to_tsvector('simple', ?), 'C') as ngram".squish,
*email_phone_name, *email_phone_name, values_string, *email_phone_name
build_submitter_values_string(submitter),
*email_phone_name
] ]
) )
@ -155,6 +155,9 @@ module SearchEntries
entry.account_id = submitter.account_id entry.account_id = submitter.account_id
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
add_hyphens(entry, values_string)
entry.ngram = build_ngram(ngram) entry.ngram = build_ngram(ngram)
return if entry.tsvector.blank? return if entry.tsvector.blank?
@ -189,11 +192,7 @@ module SearchEntries
entry.account_id = template.account_id entry.account_id = template.account_id
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/) add_hyphens(entry, text)
hyphens.uniq.each_with_index do |item, index|
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
end
entry.ngram = build_ngram(ngram) entry.ngram = build_ngram(ngram)
@ -220,11 +219,7 @@ module SearchEntries
entry.account_id = submission.account_id entry.account_id = submission.account_id
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/) add_hyphens(entry, text)
hyphens.uniq.each_with_index do |item, index|
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
end
entry.ngram = build_ngram(ngram) entry.ngram = build_ngram(ngram)
@ -239,6 +234,16 @@ module SearchEntries
retry retry
end end
def add_hyphens(entry, text)
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
hyphens.uniq.each_with_index do |item, index|
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
end
entry
end
def build_ngram(ngram) def build_ngram(ngram)
ngrams = ngrams =
ngram.split(/\s(?=')/).each_with_object([]) do |item, acc| ngram.split(/\s(?=')/).each_with_object([]) do |item, acc|

@ -81,7 +81,12 @@ module Submissions
end end
composer = HexaPDF::Composer.new(skip_page_creation: true) composer = HexaPDF::Composer.new(skip_page_creation: true)
composer.document.task(:pdfa) if FONT_NAME == 'GoNotoKurrent'
if Docuseal.pdf_format == 'pdf/a-3b'
composer.document.task(:pdfa, level: '3b')
elsif FONT_NAME == 'GoNotoKurrent'
composer.document.task(:pdfa)
end
composer.document.config['font.map'] = { composer.document.config['font.map'] = {
'Helvetica' => { 'Helvetica' => {
@ -108,17 +113,16 @@ module Submissions
configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY, configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::WITH_AUDIT_SUBMITTER_TIMEZONE_KEY]) AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY])
last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at) last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false
with_audit_submitter_timezone = with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
configs.find { |c| c.key == AccountConfig::WITH_AUDIT_SUBMITTER_TIMEZONE_KEY }&.value == true
timezone = account.timezone timezone = account.timezone
timezone = last_submitter.timezone || account.timezone if with_audit_submitter_timezone timezone = last_submitter.timezone || account.timezone if with_submitter_timezone
composer.page_style(:default, page_size:) do |canvas, style| composer.page_style(:default, page_size:) do |canvas, style|
box = canvas.context.box(:media) box = canvas.context.box(:media)
@ -240,11 +244,18 @@ module Submissions
click_email_event = click_email_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.click_email? } submission.submission_events.find { |e| e.submitter_id == submitter.id && e.click_email? }
verify_email_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.phone_verified? }
is_phone_verified = is_phone_verified =
submission.template_fields.any? do |e| submission.template_fields.any? do |e|
e['type'] == 'phone' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present? e['type'] == 'phone' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
end end
verify_phone_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.phone_verified? }
is_id_verified = is_id_verified =
submission.template_fields.any? do |e| submission.template_fields.any? do |e|
e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present? e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
@ -266,10 +277,10 @@ module Submissions
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
submitter.email && click_email_event && { submitter.email && (click_email_event || verify_email_event) && {
text: "#{I18n.t('email_verification')}: #{I18n.t('verified')}\n" text: "#{I18n.t('email_verification')}: #{I18n.t('verified')}\n"
}, },
submitter.phone && is_phone_verified && { submitter.phone && (is_phone_verified || verify_phone_event) && {
text: "#{I18n.t('phone_verification')}: #{I18n.t('verified')}\n" text: "#{I18n.t('phone_verification')}: #{I18n.t('verified')}\n"
}, },
is_id_verified && { is_id_verified && {

@ -17,6 +17,11 @@ module Submissions
pdf.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})" pdf.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
if Docuseal.pdf_format == 'pdf/a-3b'
pdf.task(:pdfa, level: '3b')
pdf.config['font.map'] = GenerateResultAttachments::PDFA_FONT_MAP
end
if pkcs if pkcs
sign_params = { sign_params = {
reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter), reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter),

@ -13,10 +13,12 @@ module Submissions
end end
configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID]) AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten) pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten)
@ -28,7 +30,8 @@ module Submissions
submitters.preload(attachments_attachments: :blob).each_with_index do |s, index| submitters.preload(attachments_attachments: :blob).each_with_index do |s, index|
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index, GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
with_signature_id:, is_flatten:, with_headings: index.zero?) with_signature_id:, is_flatten:, with_headings: index.zero?,
with_submitter_timezone:)
end end
template = submission.template template = submission.template

@ -30,6 +30,13 @@ module Submissions
bold_italic: FONT_BOLD_ITALIC_NAME bold_italic: FONT_BOLD_ITALIC_NAME
}.freeze }.freeze
PDFA_FONT_VARIANS = {
none: FONT_NAME,
bold: FONT_BOLD_NAME,
italic: FONT_NAME,
bold_italic: FONT_BOLD_NAME
}.freeze
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com' SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
RTL_REGEXP = TextUtils::RTL_REGEXP RTL_REGEXP = TextUtils::RTL_REGEXP
@ -43,11 +50,18 @@ module Submissions
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING' TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
DEFAULT_FONTS = %w[Times Helvetica Courier].freeze DEFAULT_FONTS = %w[Times Helvetica Courier].freeze
FONTS_LINE_HEIGHT = { FONTS_LINE_HEIGHT = {
'Times' => 1.4, 'Times' => 1.5,
'Helvetica' => 1.4, 'Helvetica' => 1.5,
'Courier' => 1.6 'Courier' => 1.6
}.freeze }.freeze
PDFA_FONT_MAP = {
FONT_NAME => PDFA_FONT_VARIANS,
'Helvetica' => PDFA_FONT_VARIANS,
'Times' => PDFA_FONT_VARIANS,
'Courier' => PDFA_FONT_VARIANS
}.freeze
MISSING_GLYPH_REPLACE = { MISSING_GLYPH_REPLACE = {
'▪' => '-', '▪' => '-',
'✔️' => 'V', '✔️' => 'V',
@ -124,10 +138,12 @@ module Submissions
def generate_pdfs(submitter) def generate_pdfs(submitter)
configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID]) AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten) pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten)
@ -171,10 +187,12 @@ module Submissions
end end
end end
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:) fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:,
with_submitter_timezone:)
end end
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil) def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
with_submitter_timezone: false)
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center) cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
attachments_data_cache = {} attachments_data_cache = {}
@ -274,10 +292,13 @@ module Submissions
reason_string = reason_string =
I18n.with_locale(locale) do I18n.with_locale(locale) do
timezone = submitter.account.timezone
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
"#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \ "#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \ "#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
"#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \ "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
"#{TimeUtils.timezone_abbr(submitter.account.timezone, attachment.created_at)}" "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
end end
reason_text = HexaPDF::Layout::TextFragment.create(reason_string, reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
@ -599,6 +620,11 @@ module Submissions
pdf.trailer.info[:Creator] = info_creator pdf.trailer.info[:Creator] = info_creator
if Docuseal.pdf_format == 'pdf/a-3b'
pdf.task(:pdfa, level: '3b')
pdf.config['font.map'] = PDFA_FONT_MAP
end
sign_reason = fetch_sign_reason(submitter) sign_reason = fetch_sign_reason(submitter)
if sign_reason && pkcs if sign_reason && pkcs

@ -11,6 +11,9 @@ module Submitters
'values' => 'D' 'values' => 'D'
}.freeze }.freeze
UnableToSendCode = Class.new(StandardError)
InvalidOtp = Class.new(StandardError)
module_function module_function
def search(current_user, submitters, keyword) def search(current_user, submitters, keyword)
@ -194,4 +197,27 @@ module Submitters
"#{filename}.#{blob.filename.extension}" "#{filename}.#{blob.filename.extension}"
end end
def send_shared_link_email_verification_code(submitter, request:)
RateLimit.call("send-otp-code-#{request.remote_ip}", limit: 2, ttl: 45.seconds, enabled: true)
TemplateMailer.otp_verification_email(submitter.submission.template, email: submitter.email).deliver_later!
rescue RateLimit::LimitApproached
Rollbar.warning("Limit verification code for template: #{submitter.submission.template.id}") if defined?(Rollbar)
raise UnableToSendCode, I18n.t('too_many_attempts')
end
def verify_link_otp!(otp, submitter)
return false if otp.blank?
RateLimit.call("verify-2fa-code-#{Digest::MD5.base64digest(submitter.email)}",
limit: 2, ttl: 45.seconds, enabled: true)
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
raise InvalidOtp, I18n.t(:invalid_code) unless EmailVerificationCodes.verify(otp, link_2fa_key)
true
end
end end

@ -15,6 +15,7 @@ module Templates
MAX_NUMBER_OF_PAGES_PROCESSED = 15 MAX_NUMBER_OF_PAGES_PROCESSED = 15
MAX_FLATTEN_FILE_SIZE = 20.megabytes MAX_FLATTEN_FILE_SIZE = 20.megabytes
GENERATE_PREVIEW_SIZE_LIMIT = 50.megabytes GENERATE_PREVIEW_SIZE_LIMIT = 50.megabytes
US_LETTER_SIZE = { 'width' => MAX_WIDTH, 'height' => 1812 }.freeze
module_function module_function

@ -6,7 +6,7 @@
"@babel/plugin-transform-runtime": "7.21.4", "@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.5", "@babel/preset-env": "7.21.5",
"@babel/runtime": "7.21.5", "@babel/runtime": "7.21.5",
"@eid-easy/eideasy-widget": "^2.159.0", "@eid-easy/eideasy-widget": "^2.163.4",
"@github/catalyst": "^2.0.0-beta", "@github/catalyst": "^2.0.0-beta",
"@hotwired/turbo": "https://github.com/docusealco/turbo#main", "@hotwired/turbo": "https://github.com/docusealco/turbo#main",
"@hotwired/turbo-rails": "^7.3.0", "@hotwired/turbo-rails": "^7.3.0",

@ -0,0 +1,9 @@
# frozen_string_literal: true
class TemplateMailerPreview < ActionMailer::Preview
def otp_verification_email
template = Template.active.last
TemplateMailer.otp_verification_email(template, email: 'john.doe@example.com')
end
end

@ -161,7 +161,6 @@ RSpec.describe 'Signing Form' do
expect(field_value(submitter, 'Cell code')).to eq '123' expect(field_value(submitter, 'Cell code')).to eq '123'
end end
# rubocop:disable RSpec/ExampleLength
it 'completes the form when name, email, and phone are required' do it 'completes the form when name, email, and phone are required' do
template.update(preferences: { link_form_fields: %w[email name phone] }) template.update(preferences: { link_form_fields: %w[email name phone] })
@ -250,7 +249,93 @@ RSpec.describe 'Signing Form' do
expect(field_value(submitter, 'Attachment')).to be_present expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123' expect(field_value(submitter, 'Cell code')).to eq '123'
end end
# rubocop:enable RSpec/ExampleLength
it 'completes the form when identity verification with a 2FA code is enabled', sidekiq: :inline do
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
value: GenerateCertificate.call.transform_values(&:to_pem))
template.update(preferences: { link_form_fields: %w[email name], shared_link_2fa: true })
visit start_form_path(slug: template.slug)
fill_in 'Email', with: 'john.dou@example.com'
fill_in 'Name', with: 'John Doe'
expect do
click_button 'Start'
end.to change { ActionMailer::Base.deliveries.count }.by(1)
email = ActionMailer::Base.deliveries.last
code = email.body.encoded[%r{<b>(.*?)</b>}, 1]
fill_in 'one_time_code', with: code
click_button 'Submit'
fill_in 'First Name', with: 'John'
click_button 'next'
fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d')
click_button 'next'
check 'Do you agree?'
click_button 'next'
choose 'Boy'
click_button 'next'
draw_canvas
click_button 'next'
fill_in 'House number', with: '123'
click_button 'next'
%w[Red Blue].each { |color| check color }
click_button 'next'
select 'Male', from: 'Gender'
click_button 'next'
draw_canvas
click_button 'next'
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
click_button 'next'
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
click_button 'next'
fill_in 'Cell code', with: '123'
click_on 'Complete'
expect(page).to have_button('Download')
expect(page).to have_content('Document has been signed!')
submitter = template.submissions.last.submitters.last
expect(submitter.email).to eq('john.dou@example.com')
expect(submitter.name).to eq('John Doe')
expect(submitter.ip).to eq('127.0.0.1')
expect(submitter.ua).to be_present
expect(submitter.opened_at).to be_present
expect(submitter.completed_at).to be_present
expect(submitter.declined_at).to be_nil
expect(field_value(submitter, 'First Name')).to eq 'John'
expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d')
expect(field_value(submitter, 'Do you agree?')).to be_truthy
expect(field_value(submitter, 'First child')).to eq 'Boy'
expect(field_value(submitter, 'Signature')).to be_present
expect(field_value(submitter, 'House number')).to eq 123
expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue')
expect(field_value(submitter, 'Gender')).to eq 'Male'
expect(field_value(submitter, 'Initials')).to be_present
expect(field_value(submitter, 'Avatar')).to be_present
expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123'
end
end end
context 'when the submitter form link is opened' do context 'when the submitter form link is opened' do

@ -1035,22 +1035,22 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@eid-easy/eideasy-browser-client@2.124.0": "@eid-easy/eideasy-browser-client@2.127.0":
version "2.124.0" version "2.127.0"
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.124.0.tgz#940ccb9d4d853f0bd32e49cc455f2c7b13f3bbd1" resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.127.0.tgz#d1b09e3634e94d7e32f593209a63f962fd326731"
integrity sha512-hZuUeg0CcyvgJSseRBQixRStAIr58bulmNcikcASBo6z8wv+/R8nAnUoE7qdNW1l4ZyYxLmVOwDs6+W+FHy6hQ== integrity sha512-2iosVqkF1C0hQWc6TVBe1SK7b/z8ZnvHnulGXfQf6VrrpEJMgnzK95h6LFDqDQyetfIwEGGoeOiUij2hYA1ZPA==
dependencies: dependencies:
axios "1.8.2" axios "1.8.2"
jsencrypt "3.2.1" jsencrypt "3.2.1"
lodash "^4.17.21" lodash "^4.17.21"
serialize-error "^9.1.1" serialize-error "^9.1.1"
"@eid-easy/eideasy-widget@^2.159.0": "@eid-easy/eideasy-widget@^2.163.4":
version "2.159.0" version "2.163.4"
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.159.0.tgz#f2291dad292bd5f7941496f0db0aa0a180226b6b" resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.163.4.tgz#4a8ada2c61f032527a08f9f8d9d1adf3fa7b5334"
integrity sha512-527uCNrN5MVY/PaOUoZ3J2XZ0C+xt6057sJ4xSO0/FPYj2cxOn+qyCJODQjOhjBL7GcnpShWPpYAfv1OIStYwQ== integrity sha512-7OQ1bm+KSG99wUI6szXT25Icq67CEQRK38VGj8fynW0ctqJTiFXRop7dqgR9JXLlJ1s1Z++El7igYxph7Dq5Aw==
dependencies: dependencies:
"@eid-easy/eideasy-browser-client" "2.124.0" "@eid-easy/eideasy-browser-client" "2.127.0"
core-js "^3.8.3" core-js "^3.8.3"
i18n-iso-countries "^6.7.0" i18n-iso-countries "^6.7.0"
lodash.defaultsdeep "^4.6.1" lodash.defaultsdeep "^4.6.1"

Loading…
Cancel
Save