pull/572/head
Pete Matsyburka 1 month ago
parent c55036a1b1
commit b10ed46ccc

@ -17,7 +17,7 @@ class SubmissionsController < ApplicationController
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper', 'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot', 'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check', 'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
'payment' => 'credit_card', 'verification' => 'id' 'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
}.freeze }.freeze
def show def show

@ -159,6 +159,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null, withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
withFieldsDetection: this.dataset.withFieldsDetection === 'true', withFieldsDetection: this.dataset.withFieldsDetection === 'true',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',

@ -60,6 +60,11 @@
class="object-contain mx-auto" class="object-contain mx-auto"
:src="stamp.url" :src="stamp.url"
> >
<img
v-else-if="field.type === 'kba' && kba"
class="object-contain mx-auto"
:src="kba.url"
>
<div <div
v-else-if="field.type === 'signature' && signature" v-else-if="field.type === 'signature' && signature"
class="flex justify-between h-full gap-1 overflow-hidden w-full" class="flex justify-between h-full gap-1 overflow-hidden w-full"
@ -272,7 +277,7 @@
<script> <script>
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId, IconUserScan } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@ -391,7 +396,8 @@ export default {
stamp: this.t('stamp'), stamp: this.t('stamp'),
payment: this.t('payment'), payment: this.t('payment'),
phone: this.t('phone'), phone: this.t('phone'),
verification: this.t('verify_id') verification: this.t('verify_id'),
kba: this.t('kba')
} }
}, },
strikethroughWidth () { strikethroughWidth () {
@ -454,7 +460,8 @@ export default {
multiple: IconChecks, multiple: IconChecks,
phone: IconPhoneCheck, phone: IconPhoneCheck,
payment: IconCreditCard, payment: IconCreditCard,
verification: IconId verification: IconId,
kba: IconUserScan,
} }
}, },
image () { image () {
@ -471,6 +478,13 @@ export default {
return null return null
} }
}, },
kba () {
if (this.field.type === 'kba') {
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
signature () { signature () {
if (this.field.type === 'signature') { if (this.field.type === 'signature') {
return this.attachmentsIndex[this.modelValue] return this.attachmentsIndex[this.modelValue]

@ -465,6 +465,18 @@
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="!isSubmitting && submitStep()"
/> />
<KbaStep
v-else-if="currentField.type === 'kba'"
ref="currentStep"
:key="currentField.uuid"
:locale="language?.toLowerCase() || browserLanguage"
:submitter="submitter"
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:values="values"
@submit="!isSubmitting && submitStep()"
/>
<VerificationStep <VerificationStep
v-else-if="currentField.type === 'verification'" v-else-if="currentField.type === 'verification'"
ref="currentStep" ref="currentStep"
@ -480,7 +492,7 @@
/> />
</div> </div>
<div <div
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]" v-if="(currentField.type !== 'payment' && currentField.type !== 'verification' && currentField.type !== 'kba') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'" :class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
> >
<button <button
@ -569,6 +581,7 @@ import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step' import PhoneStep from './phone_step'
import PaymentStep from './payment_step' import PaymentStep from './payment_step'
import VerificationStep from './verification_step' import VerificationStep from './verification_step'
import KbaStep from './kba_step'
import TextStep from './text_step' import TextStep from './text_step'
import NumberStep from './number_step' import NumberStep from './number_step'
import DateStep from './date_step' import DateStep from './date_step'
@ -613,6 +626,7 @@ export default {
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
VerificationStep, VerificationStep,
KbaStep,
InviteForm, InviteForm,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
@ -1004,7 +1018,7 @@ export default {
const verificationFields = [] const verificationFields = []
const sortedFields = this.fields.reduce((acc, f) => { const sortedFields = this.fields.reduce((acc, f) => {
if (f.type === 'verification') { if (f.type === 'verification' || f.type === 'kba') {
verificationFields.push(f) verificationFields.push(f)
} else if (!f.readonly) { } else if (!f.readonly) {
acc.push(f) acc.push(f)
@ -1401,7 +1415,7 @@ export default {
const submitStep = this.currentStep const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
? this.$refs.currentStep.submit ? this.$refs.currentStep.submit
: () => Promise.resolve({}) : () => Promise.resolve({})

@ -1,4 +1,5 @@
const en = { const en = {
kba: 'KBA',
please_upload_an_image_file: 'Please upload an image file', 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.',
@ -100,6 +101,7 @@ const en = {
} }
const es = { const es = {
kba: 'KBA',
please_upload_an_image_file: 'Por favor, sube un archivo de imagen', 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.',
@ -201,6 +203,7 @@ const es = {
} }
const it = { const it = {
kba: 'KBA',
please_upload_an_image_file: 'Per favore carica un file immagine', 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à.",
@ -302,6 +305,7 @@ const it = {
} }
const de = { const de = {
kba: 'KBA',
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch', 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: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.', complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
@ -403,6 +407,7 @@ const de = {
} }
const fr = { const fr = {
kba: 'KBA',
please_upload_an_image_file: 'Veuillez téléverser un fichier image', please_upload_an_image_file: 'Veuillez téléverser un fichier image',
must_be_characters_length: 'Doit comporter {number} caractères', must_be_characters_length: 'Doit comporter {number} caractères',
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.", complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
@ -504,6 +509,7 @@ const fr = {
} }
const pl = { const pl = {
kba: 'KBA',
please_upload_an_image_file: 'Proszę przesłać plik obrazu', 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.',
@ -605,6 +611,7 @@ const pl = {
} }
const uk = { const uk = {
kba: 'KBA',
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення', 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: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
@ -706,6 +713,7 @@ const uk = {
} }
const cs = { const cs = {
kba: 'KBA',
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor', 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.',
@ -807,6 +815,7 @@ const cs = {
} }
const pt = { const pt = {
kba: 'KBA',
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem', 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.',
@ -908,6 +917,7 @@ const pt = {
} }
const he = { const he = {
kba: 'KBA',
please_upload_an_image_file: 'אנא העלה קובץ תמונה', 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: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
@ -1009,6 +1019,7 @@ const he = {
} }
const nl = { const nl = {
kba: 'KBA',
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand', 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.',
@ -1110,6 +1121,7 @@ const nl = {
} }
const ar = { const ar = {
kba: 'KBA',
please_upload_an_image_file: 'يرجى تحميل ملف صورة', 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: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
@ -1211,6 +1223,7 @@ const ar = {
} }
const ko = { const ko = {
kba: 'KBA',
please_upload_an_image_file: '이미지 파일을 업로드해 주세요', 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: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
@ -1312,6 +1325,7 @@ const ko = {
} }
const ja = { const ja = {
kba: 'KBA',
please_upload_an_image_file: '画像ファイルをアップロードしてください', 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: '本人確認を進めるには、すべての必須項目を入力してください。',

@ -0,0 +1,490 @@
<template>
<label
v-if="!error"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
<span
v-if="questions"
class="float-right text-base font-normal text-neutral-500 mt-1 whitespace-nowrap"
>
Question {{ currentQuestionIndex + 1 }} / {{ questions.length }}
</span>
</label>
<div
v-if="field.description"
dir="auto"
class="mb-4 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
<div
v-if="error"
class="mb-4 text-center"
>
<div class="text-xl mb-4">
{{ error }}
</div>
<button
class="base-button w-full flex justify-center submit-form-button"
@click="restartKba"
>
{{ questions ? 'Restart KBA' : 'Retry' }}
</button>
</div>
<div
v-if="isLoading"
class="w-full flex space-x-2 justify-center mb-2"
>
<IconInnerShadowTop
width="40"
class="animate-spin h-10"
/>
</div>
<div v-else-if="questions && !error">
<form @submit.prevent="nextQuestion">
<div class="mb-6 px-1">
<p class="font-semibold mb-4 text-lg">{{ currentQuestion.prompt }}</p>
<div class="space-y-3.5 mx-auto">
<div
v-for="(answer, index) in currentQuestion.answers"
:key="answer.text"
>
<label
:for="`${currentQuestion.id}_${answer.text}`"
class="flex items-center space-x-3 radio-label"
>
<input
:id="`${currentQuestion.id}_${answer.text}`"
v-model="answers[currentQuestion.id]"
type="radio"
:name="currentQuestion.id"
:value="answer.text"
class="base-radio !h-7 !w-7"
required
>
<span class="text-xl">{{ answer.text }}</span>
</label>
</div>
</div>
</div>
<div class="mt-6">
<button
type="submit"
class="base-button w-full flex justify-center submit-form-button"
:disabled="isSubmitting || !answers[currentQuestion.id]"
>
<span class="flex">
<IconInnerShadowTop
v-if="isSubmitting"
class="mr-1 animate-spin"
/>
<span>
{{ isLastQuestion ? (isSubmitting ? t('submitting_') : t('complete')) : t('next') }}
</span><span
v-if="isSubmitting"
class="w-6 flex justify-start mr-1"
><span>...</span></span>
</span>
</button>
</div>
</form>
</div>
<div v-else-if="!error">
<form @submit.prevent="startKba">
<div class="grid grid-cols-6 gap-x-2 md:gap-x-4 md:gap-y-2 mb-4">
<div class="col-span-3">
<label
for="kba_fn"
class="label text-sm md:text-base"
>First Name</label>
<input
id="kba_fn"
v-model="form.fn"
type="text"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
<div class="col-span-3">
<label
for="kba_ln"
class="label text-sm md:text-base"
>Last Name</label>
<input
id="kba_ln"
v-model="form.ln"
type="text"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
<div class="col-span-6">
<label
for="kba_addr"
class="label text-sm md:text-base"
>Address</label>
<input
id="kba_addr"
v-model="form.addr"
type="text"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
<div class="col-span-2">
<label
for="kba_city"
class="label text-sm md:text-base"
>City</label>
<input
id="kba_city"
v-model="form.city"
type="text"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
<div class="col-span-2">
<label
for="kba_state"
class="label text-sm md:text-base"
>State</label>
<select
id="kba_state"
v-model="form.state"
class="select select-bordered !h-10 min-h-[2.5rem] w-full bg-white font-normal !text-base"
required
>
<option
value=""
disabled
>
Select State
</option>
<option
v-for="state in states"
:key="state.code"
:value="state.code"
>
{{ state.name }}
</option>
</select>
</div>
<div class="col-span-2">
<label
for="kba_zip"
class="label text-sm md:text-base"
>Zip</label>
<input
id="kba_zip"
v-model="form.zip"
type="text"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
<div class="col-span-3">
<label
for="kba_phone"
class="label text-sm md:text-base"
>Phone</label>
<input
id="kba_phone"
v-model="form.phone"
type="text"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
<div class="col-span-3">
<label
for="kba_email"
class="label text-sm md:text-base"
>Email</label>
<input
id="kba_email"
v-model="form.email"
type="email"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
<div class="col-span-3">
<label
for="kba_dob"
class="label text-sm md:text-base"
>DOB</label>
<input
id="kba_dob"
v-model="form.dob"
type="date"
class="input input-bordered !h-10 md:w-full bg-white"
required
>
</div>
<div class="col-span-3">
<label
for="kba_ssn"
class="label text-sm md:text-base"
>SSN</label>
<input
id="kba_ssn"
v-model="form.ssn"
type="text"
class="input input-bordered !h-10 w-full bg-white"
required
>
</div>
</div>
<div class="mt-6">
<button
type="submit"
class="base-button w-full flex justify-center submit-form-button"
:disabled="isLoading"
>
<span class="flex">
<IconInnerShadowTop
v-if="isLoading"
class="mr-1 animate-spin"
/>
<span>
{{ isLoading ? 'Loading...' : 'Start Verification' }}
</span><span
v-if="isLoading"
class="w-6 flex justify-start mr-1"
><span>...</span></span>
</span>
</button>
</div>
</form>
</div>
</template>
<script>
import MarkdownContent from './markdown_content'
import { IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'KbaStep',
components: {
MarkdownContent,
IconInnerShadowTop
},
inject: ['baseUrl', 't'],
props: {
field: {
type: Object,
required: true
},
submitterSlug: {
type: String,
required: true
},
values: {
type: Object,
required: true
}
},
emits: ['submit'],
data () {
return {
isLoading: false,
isSubmitting: false,
questions: null,
currentQuestionIndex: 0,
token: null,
answers: {},
error: null,
form: {
fn: '',
ln: '',
addr: '',
city: '',
state: '',
zip: '',
dob: '',
ssn: '',
phone: '',
email: ''
}
}
},
computed: {
currentQuestion () {
return this.questions ? this.questions[this.currentQuestionIndex] : null
},
states () {
return [
{ code: 'AL', name: 'Alabama' },
{ code: 'AK', name: 'Alaska' },
{ code: 'AZ', name: 'Arizona' },
{ code: 'AR', name: 'Arkansas' },
{ code: 'CA', name: 'California' },
{ code: 'CO', name: 'Colorado' },
{ code: 'CT', name: 'Connecticut' },
{ code: 'DE', name: 'Delaware' },
{ code: 'DC', name: 'District of Columbia' },
{ code: 'FL', name: 'Florida' },
{ code: 'GA', name: 'Georgia' },
{ code: 'HI', name: 'Hawaii' },
{ code: 'ID', name: 'Idaho' },
{ code: 'IL', name: 'Illinois' },
{ code: 'IN', name: 'Indiana' },
{ code: 'IA', name: 'Iowa' },
{ code: 'KS', name: 'Kansas' },
{ code: 'KY', name: 'Kentucky' },
{ code: 'LA', name: 'Louisiana' },
{ code: 'ME', name: 'Maine' },
{ code: 'MD', name: 'Maryland' },
{ code: 'MA', name: 'Massachusetts' },
{ code: 'MI', name: 'Michigan' },
{ code: 'MN', name: 'Minnesota' },
{ code: 'MS', name: 'Mississippi' },
{ code: 'MO', name: 'Missouri' },
{ code: 'MT', name: 'Montana' },
{ code: 'NE', name: 'Nebraska' },
{ code: 'NV', name: 'Nevada' },
{ code: 'NH', name: 'New Hampshire' },
{ code: 'NJ', name: 'New Jersey' },
{ code: 'NM', name: 'New Mexico' },
{ code: 'NY', name: 'New York' },
{ code: 'NC', name: 'North Carolina' },
{ code: 'ND', name: 'North Dakota' },
{ code: 'OH', name: 'Ohio' },
{ code: 'OK', name: 'Oklahoma' },
{ code: 'OR', name: 'Oregon' },
{ code: 'PA', name: 'Pennsylvania' },
{ code: 'RI', name: 'Rhode Island' },
{ code: 'SC', name: 'South Carolina' },
{ code: 'SD', name: 'South Dakota' },
{ code: 'TN', name: 'Tennessee' },
{ code: 'TX', name: 'Texas' },
{ code: 'UT', name: 'Utah' },
{ code: 'VT', name: 'Vermont' },
{ code: 'VA', name: 'Virginia' },
{ code: 'WA', name: 'Washington' },
{ code: 'WV', name: 'West Virginia' },
{ code: 'WI', name: 'Wisconsin' },
{ code: 'WY', name: 'Wyoming' }
]
},
isLastQuestion () {
return this.questions && this.currentQuestionIndex === this.questions.length - 1
}
},
methods: {
nextQuestion () {
if (this.isLastQuestion) {
this.$emit('submit')
} else {
this.currentQuestionIndex++
}
},
restartKba () {
this.questions = null
this.token = null
this.answers = {}
this.currentQuestionIndex = 0
this.error = null
},
async startKba () {
this.isLoading = true
this.error = null
try {
const payload = { ...this.form, submitter_slug: this.submitterSlug }
if (payload.dob) {
payload.dob = payload.dob.replace(/-/g, '')
}
if (payload.ssn) {
payload.ssn = payload.ssn.replace(/\D/g, '')
}
if (payload.phone) {
payload.phone = payload.phone.replace(/^\+1/, '')
}
const resp = await fetch(this.baseUrl + '/api/kba', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (!resp.ok) throw new Error('Failed to start KBA')
const data = await resp.json()
if (data.result && data.result.action === 'FAIL') {
if (data.result.detail === 'NO MATCH') {
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
}
throw new Error(data.result.detail || 'KBA Start Failed')
}
if (data.output && data.output.questions && data.output.questions.questions) {
this.questions = data.output.questions.questions
this.token = data.continuations.questions.template.token
this.questions.forEach(q => {
this.answers[q.id] = null
})
} else {
throw new Error('Invalid KBA response')
}
} catch (e) {
this.error = e.message
} finally {
this.isLoading = false
}
},
async submit () {
this.isSubmitting = true
this.error = null
const formattedAnswers = Object.keys(this.answers).reduce((acc, key) => {
acc[key] = [this.answers[key]]
return acc
}, {})
try {
const resp = await fetch(this.baseUrl + `/api/kba/${this.field.uuid}`, {
method: 'PUT',
body: JSON.stringify({
token: this.token,
answers: formattedAnswers,
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
})
const data = await resp.json()
if (data.result?.action !== 'PASS') {
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
throw new Error('Knowledge Based Authentication Failed')
}
if (!resp.ok) {
this.error = 'Failed to submit answers'
throw new Error('Failed to submit answers')
}
return resp
} finally {
this.isSubmitting = false
}
}
}
}
</script>

@ -583,6 +583,7 @@ export default {
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withVerification: this.withVerification, withVerification: this.withVerification,
withKba: this.withKba,
withPayment: this.withPayment, withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected, isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula, withFormula: this.withFormula,
@ -794,6 +795,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
withKba: {
type: Boolean,
required: false,
default: null
},
withPayment: { withPayment: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -1482,7 +1488,7 @@ export default {
} else if (type === 'image') { } else if (type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (type === 'signature' || type === 'stamp' || type === 'verification') { } else if (type === 'signature' || type === 'stamp' || type === 'verification' || type === 'kba') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2 area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (type === 'initials') { } else if (type === 'initials') {
@ -1717,7 +1723,7 @@ export default {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
} }
} else if (fieldType === 'signature' || fieldType === 'stamp' || fieldType === 'verification') { } else if (fieldType === 'signature' || fieldType === 'stamp' || fieldType === 'verification' || fieldType === 'kba') {
baseArea = { baseArea = {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2 h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2

@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted" v-for="(icon, type) in fieldIconsSorted"
:key="type" :key="type"
> >
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"> <li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') || (withKba || type != 'kba'))">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@ -51,11 +51,11 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough, IconUserScan } from '@tabler/icons-vue'
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'], inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 't', 'fieldTypes'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@ -114,7 +114,8 @@ export default {
stamp: this.t('stamp'), stamp: this.t('stamp'),
payment: this.t('payment'), payment: this.t('payment'),
phone: this.t('phone'), phone: this.t('phone'),
verification: this.t('verify_id') verification: this.t('verify_id'),
kba: this.t('kba')
} }
}, },
fieldLabels () { fieldLabels () {
@ -134,7 +135,8 @@ export default {
stamp: this.t('stamp_field'), stamp: this.t('stamp_field'),
payment: this.t('payment_field'), payment: this.t('payment_field'),
phone: this.t('phone_field'), phone: this.t('phone_field'),
verification: this.t('verify_id') verification: this.t('verify_id'),
kba: this.t('kba')
} }
}, },
fieldIcons () { fieldIcons () {
@ -157,7 +159,8 @@ export default {
stamp: IconRubberStamp, stamp: IconRubberStamp,
payment: IconCreditCard, payment: IconCreditCard,
phone: IconPhoneCheck, phone: IconPhoneCheck,
verification: IconId verification: IconId,
kba: IconUserScan,
} }
}, },
skipTypes () { skipTypes () {

@ -114,7 +114,7 @@
:key="type" :key="type"
> >
<button <button
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))" v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))"
:id="`${type}_type_field_button`" :id="`${type}_type_field_button`"
draggable="true" draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item" class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
@ -122,7 +122,7 @@
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'" :class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart($event, { type: type })" @dragstart="onDragstart($event, { type: type })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)" @click="['file', 'payment', 'verification', 'kba'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
> >
<div <div
class="flex items-console transition-all cursor-grab h-full absolute left-0" class="flex items-console transition-all cursor-grab h-full absolute left-0"
@ -283,7 +283,7 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'], inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
props: { props: {
fields: { fields: {
type: Array, type: Array,

@ -1,4 +1,5 @@
const en = { const en = {
kba: 'KBA',
analyzing_: 'Analyzing...', analyzing_: 'Analyzing...',
download: 'Download', download: 'Download',
downloading_: 'Downloading...', downloading_: 'Downloading...',
@ -188,6 +189,7 @@ const en = {
} }
const es = { const es = {
kba: 'KBA',
autodetect_fields: 'Autodetectar campos', autodetect_fields: 'Autodetectar campos',
analyzing_: 'Analizando...', analyzing_: 'Analizando...',
download: 'Descargar', download: 'Descargar',
@ -377,6 +379,7 @@ const es = {
} }
const it = { const it = {
kba: 'KBA',
autodetect_fields: 'Rileva campi', autodetect_fields: 'Rileva campi',
analyzing_: 'Analisi...', analyzing_: 'Analisi...',
download: 'Scarica', download: 'Scarica',
@ -566,6 +569,7 @@ const it = {
} }
const pt = { const pt = {
kba: 'KBA',
autodetect_fields: 'Detectar campos', autodetect_fields: 'Detectar campos',
analyzing_: 'Analisando...', analyzing_: 'Analisando...',
download: 'Baixar', download: 'Baixar',
@ -755,6 +759,7 @@ const pt = {
} }
const fr = { const fr = {
kba: 'KBA',
autodetect_fields: 'Détecter les champs', autodetect_fields: 'Détecter les champs',
analyzing_: 'Analyse...', analyzing_: 'Analyse...',
download: 'Télécharger', download: 'Télécharger',
@ -944,6 +949,7 @@ const fr = {
} }
const de = { const de = {
kba: 'KBA',
autodetect_fields: 'Felder erkennen', autodetect_fields: 'Felder erkennen',
analyzing_: 'Analysiere...', analyzing_: 'Analysiere...',
download: 'Download', download: 'Download',
@ -1133,6 +1139,7 @@ const de = {
} }
const nl = { const nl = {
kba: 'KBA',
autodetect_fields: 'Velden detecteren', autodetect_fields: 'Velden detecteren',
analyzing_: 'Analyseren...', analyzing_: 'Analyseren...',
download: 'Downloaden', download: 'Downloaden',

@ -59,7 +59,7 @@
v-for="(icon, type) in fieldIconsSorted" v-for="(icon, type) in fieldIconsSorted"
:key="type" :key="type"
> >
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"> <li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@ -89,7 +89,7 @@ export default {
IconPlus, IconPlus,
IconX IconX
}, },
inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'], inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor', 't'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,

@ -40,10 +40,17 @@ class ProcessSubmitterCompletionJob
complete_verification_events, sms_events = complete_verification_events, sms_events =
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification]) submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
.partition { |e| e.event_type == 'complete_verification' } .partition { |e| e.event_type == 'complete_verification' || e.event_type == 'complete_kba' }
complete_verification_event = complete_verification_events.first complete_verification_event = complete_verification_events.first
verification_method =
if complete_verification_event&.event_type == 'complete_kba'
'kba'
elsif complete_verification_event
complete_verification_event.data['method']
end
completed_submitter.assign_attributes( completed_submitter.assign_attributes(
submission_id: submitter.submission_id, submission_id: submitter.submission_id,
account_id: submission.account_id, account_id: submission.account_id,
@ -51,7 +58,7 @@ class ProcessSubmitterCompletionJob
template_id: submission.template_id, template_id: submission.template_id,
source: submission.source, source: submission.source,
sms_count: sms_events.sum { |e| e.data['segments'] || 1 }, sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
verification_method: complete_verification_event&.data&.dig('method'), verification_method:,
completed_at: submitter.completed_at completed_at: submitter.completed_at
) )

@ -56,7 +56,9 @@ class SubmissionEvent < ApplicationRecord
email_verified: 'email_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', start_kba: 'start_kba',
complete_kba: 'complete_kba',
fail_kba: 'fail_kba',
view_form: 'view_form', view_form: 'view_form',
invite_party: 'invite_party', invite_party: 'invite_party',
complete_form: 'complete_form', complete_form: 'complete_form',

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10 9a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M8 16a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

@ -33,7 +33,7 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% elsif field['type'].in?(['image', 'initials', 'stamp']) && attachments_index[value].image? %> <% elsif field['type'].in?(['image', 'initials', 'stamp', 'kba']) && attachments_index[value].image? %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment', 'image']) %> <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<autosize-field></autosize-field> <autosize-field></autosize-field>

@ -262,7 +262,7 @@
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
</div> </div>
<% elsif field['type'].in?(['image', 'stamp']) && attachments_index[value].image? %> <% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment', 'image']) %> <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">

@ -677,6 +677,9 @@ en: &en
payment_field: Payment Field payment_field: Payment Field
phone_field: Phone Field phone_field: Phone Field
verification_field: Identity Verification verification_field: Identity Verification
knowledge_based_authentication: Knowledge Based Authentication
kba_field: KBA
passed: Passed
identity_verification: Identity verification identity_verification: Identity verification
paid_price: 'Paid %{price}' paid_price: 'Paid %{price}'
verified: Verified verified: Verified
@ -910,6 +913,9 @@ en: &en
complete_form_by_html: '<b>Submission completed</b> by %{submitter_name}' complete_form_by_html: '<b>Submission completed</b> by %{submitter_name}'
start_verification_by_html: '<b>Identity verification started</b> by %{submitter_name}' start_verification_by_html: '<b>Identity verification started</b> by %{submitter_name}'
complete_verification_by_html: '<b>Identity verification completed</b> by %{submitter_name} with %{provider}' complete_verification_by_html: '<b>Identity verification completed</b> by %{submitter_name} with %{provider}'
start_kba_by_html: '<b>KBA started</b> by %{submitter_name}'
complete_kba_by_html: '<b>KBA completed</b> by %{submitter_name}'
fail_kba_by_html: '<b>KBA failed</b> by %{submitter_name}'
api_complete_form_by_html: '<b>Submission completed via API</b> by %{submitter_name}' api_complete_form_by_html: '<b>Submission completed via API</b> by %{submitter_name}'
import_list: import_list:
select_worksheet: Select Worksheet select_worksheet: Select Worksheet
@ -992,6 +998,9 @@ en: &en
range_without_total: "%{from}-%{to} events" range_without_total: "%{from}-%{to} events"
es: &es es: &es
knowledge_based_authentication: Autenticación basada en el conocimiento
kba_field: KBA
passed: Aprobado
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Las plantillas que requieren autenticación en dos pasos por correo electrónico o teléfono no se pueden usar mediante un enlace compartido. templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Las plantillas que requieren autenticación en dos pasos por correo electrónico o teléfono no se pueden usar mediante un enlace compartido.
make_owner: Hacer propietario make_owner: Hacer propietario
billing: Facturación billing: Facturación
@ -1883,6 +1892,9 @@ es: &es
api_complete_form_by_html: '<b>Envío completado vía API</b> por %{submitter_name}' api_complete_form_by_html: '<b>Envío completado vía API</b> por %{submitter_name}'
start_verification_by_html: '<b>Verificación de identidad iniciada</b> por %{submitter_name}' start_verification_by_html: '<b>Verificación de identidad iniciada</b> por %{submitter_name}'
complete_verification_by_html: '<b>Verificación de identidad completada</b> por %{submitter_name} con %{provider}' complete_verification_by_html: '<b>Verificación de identidad completada</b> por %{submitter_name} con %{provider}'
start_kba_by_html: '<b>KBA iniciado</b> por %{submitter_name}'
complete_kba_by_html: '<b>KBA completado</b> por %{submitter_name}'
fail_kba_by_html: '<b>KBA fallido</b> por %{submitter_name}'
import_list: import_list:
select_worksheet: Seleccionar hoja de cálculo select_worksheet: Seleccionar hoja de cálculo
open: Abrir open: Abrir
@ -1964,6 +1976,9 @@ es: &es
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
it: &it it: &it
knowledge_based_authentication: Autenticazione basata sulla conoscenza
kba_field: KBA
passed: Superato
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: I modelli che richiedono 2FA via email o telefono non possono essere utilizzati tramite un link condiviso. templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: I modelli che richiedono 2FA via email o telefono non possono essere utilizzati tramite un link condiviso.
make_owner: Rendi proprietario make_owner: Rendi proprietario
billing: Fatturazione billing: Fatturazione
@ -2856,6 +2871,9 @@ it: &it
api_complete_form_by_html: '<b>Invio completato tramite API</b> da %{submitter_name}' api_complete_form_by_html: '<b>Invio completato tramite API</b> da %{submitter_name}'
start_verification_by_html: "<b>Verifica dell'identità iniziata</b> da %{submitter_name}" start_verification_by_html: "<b>Verifica dell'identità iniziata</b> da %{submitter_name}"
complete_verification_by_html: "<b>Verifica dell'identità completata</b> da %{submitter_name} con %{provider}" complete_verification_by_html: "<b>Verifica dell'identità completata</b> da %{submitter_name} con %{provider}"
start_kba_by_html: "<b>KBA avviata</b> da %{submitter_name}"
complete_kba_by_html: "<b>KBA completata</b> da %{submitter_name}"
fail_kba_by_html: "<b>KBA non riuscita</b> da %{submitter_name}"
import_list: import_list:
select_worksheet: Seleziona il foglio di lavoro select_worksheet: Seleziona il foglio di lavoro
open: Apri open: Apri
@ -2937,6 +2955,9 @@ it: &it
range_without_total: "%{from}-%{to} eventi" range_without_total: "%{from}-%{to} eventi"
fr: &fr fr: &fr
knowledge_based_authentication: Authentification basée sur la connaissance
kba_field: KBA
passed: Réussi
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Les modèles nécessitant une authentification à deux facteurs par e-mail ou téléphone ne peuvent pas être utilisés via un lien partagé. templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Les modèles nécessitant une authentification à deux facteurs par e-mail ou téléphone ne peuvent pas être utilisés via un lien partagé.
make_owner: Rendre propriétaire make_owner: Rendre propriétaire
billing: Facturation billing: Facturation
@ -3825,6 +3846,9 @@ fr: &fr
start_verification_by_html: "<b>Vérification didentité démarrée</b> par %{submitter_name}" start_verification_by_html: "<b>Vérification didentité démarrée</b> par %{submitter_name}"
complete_verification_by_html: "<b>Vérification didentité terminée</b> par %{submitter_name} avec %{provider}" complete_verification_by_html: "<b>Vérification didentité terminée</b> par %{submitter_name} avec %{provider}"
api_complete_form_by_html: "<b>Soumission terminée via API</b> par %{submitter_name}" api_complete_form_by_html: "<b>Soumission terminée via API</b> par %{submitter_name}"
start_kba_by_html: "<b>KBA démarré</b> par %{submitter_name}"
complete_kba_by_html: "<b>KBA terminé</b> par %{submitter_name}"
fail_kba_by_html: "<b>KBA échoué</b> par %{submitter_name}"
import_list: import_list:
select_worksheet: Sélectionner une feuille select_worksheet: Sélectionner une feuille
open: Ouvrir open: Ouvrir
@ -3906,6 +3930,9 @@ fr: &fr
range_without_total: "%{from}-%{to} événements" range_without_total: "%{from}-%{to} événements"
pt: &pt pt: &pt
knowledge_based_authentication: Autenticação baseada em conhecimento
kba_field: KBA
passed: Aprovado
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Modelos que exigem 2FA por e-mail ou telefone não podem ser usados por meio de um link compartilhado. templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Modelos que exigem 2FA por e-mail ou telefone não podem ser usados por meio de um link compartilhado.
make_owner: Tornar proprietário make_owner: Tornar proprietário
billing: Pagamentos billing: Pagamentos
@ -4797,6 +4824,9 @@ pt: &pt
start_verification_by_html: '<b>Verificação de identidade iniciada</b> por %{submitter_name}' start_verification_by_html: '<b>Verificação de identidade iniciada</b> por %{submitter_name}'
complete_verification_by_html: '<b>Verificação de identidade concluída</b> por %{submitter_name} com %{provider}' complete_verification_by_html: '<b>Verificação de identidade concluída</b> por %{submitter_name} com %{provider}'
api_complete_form_by_html: '<b>Submissão concluída via API</b> por %{submitter_name}' api_complete_form_by_html: '<b>Submissão concluída via API</b> por %{submitter_name}'
start_kba_by_html: '<b>KBA iniciada</b> por %{submitter_name}'
complete_kba_by_html: '<b>KBA concluída</b> por %{submitter_name}'
fail_kba_by_html: '<b>KBA reprovada</b> por %{submitter_name}'
import_list: import_list:
select_worksheet: Selecionar planilha select_worksheet: Selecionar planilha
open: Abrir open: Abrir
@ -4878,6 +4908,9 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos" range_without_total: "%{from}-%{to} eventos"
de: &de de: &de
knowledge_based_authentication: Wissensbasierte Authentifizierung
kba_field: KBA
passed: Bestanden
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Vorlagen, die eine Zwei-Faktor-Authentifizierung per E-Mail oder Telefon erfordern, können nicht über einen geteilten Link verwendet werden. templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Vorlagen, die eine Zwei-Faktor-Authentifizierung per E-Mail oder Telefon erfordern, können nicht über einen geteilten Link verwendet werden.
make_owner: Eigentümer machen make_owner: Eigentümer machen
billing: Abrechnung billing: Abrechnung
@ -5769,6 +5802,9 @@ de: &de
start_verification_by_html: '<b>Identitätsüberprüfung gestartet</b> von %{submitter_name}' start_verification_by_html: '<b>Identitätsüberprüfung gestartet</b> von %{submitter_name}'
complete_verification_by_html: '<b>Identitätsüberprüfung abgeschlossen</b> von %{submitter_name} mit %{provider}' complete_verification_by_html: '<b>Identitätsüberprüfung abgeschlossen</b> von %{submitter_name} mit %{provider}'
api_complete_form_by_html: '<b>Einreichung über API abgeschlossen</b> von %{submitter_name}' api_complete_form_by_html: '<b>Einreichung über API abgeschlossen</b> von %{submitter_name}'
start_kba_by_html: '<b>KBA gestartet</b> von %{submitter_name}'
complete_kba_by_html: '<b>KBA abgeschlossen</b> von %{submitter_name}'
fail_kba_by_html: '<b>KBA fehlgeschlagen</b> von %{submitter_name}'
import_list: import_list:
select_worksheet: Arbeitsblatt auswählen select_worksheet: Arbeitsblatt auswählen
open: Öffnen open: Öffnen
@ -6238,6 +6274,9 @@ he:
too_many_requests_try_again_later: יותר מדי בקשות. נסה שוב מאוחר יותר. too_many_requests_try_again_later: יותר מדי בקשות. נסה שוב מאוחר יותר.
nl: &nl nl: &nl
knowledge_based_authentication: Kennisgebaseerde authenticatie
kba_field: KBA
passed: Geslaagd
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Sjablonen waarvoor e-mail- of telefoon-2FA vereist is, kunnen niet via een gedeelde link worden gebruikt. templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Sjablonen waarvoor e-mail- of telefoon-2FA vereist is, kunnen niet via een gedeelde link worden gebruikt.
make_owner: Eigenaar maken make_owner: Eigenaar maken
billing: Facturatie billing: Facturatie
@ -7126,6 +7165,9 @@ nl: &nl
start_verification_by_html: "<b>Identiteitsverificatie gestart</b> door %{submitter_name}" start_verification_by_html: "<b>Identiteitsverificatie gestart</b> door %{submitter_name}"
complete_verification_by_html: "<b>Identiteitsverificatie voltooid</b> door %{submitter_name} met %{provider}" complete_verification_by_html: "<b>Identiteitsverificatie voltooid</b> door %{submitter_name} met %{provider}"
api_complete_form_by_html: "<b>Inzending via API voltooid</b> door %{submitter_name}" api_complete_form_by_html: "<b>Inzending via API voltooid</b> door %{submitter_name}"
start_kba_by_html: "<b>KBA gestart</b> door %{submitter_name}"
complete_kba_by_html: "<b>KBA voltooid</b> door %{submitter_name}"
fail_kba_by_html: "<b>KBA mislukt</b> door %{submitter_name}"
import_list: import_list:
select_worksheet: Werkblad selecteren select_worksheet: Werkblad selecteren
open: Openen open: Openen

@ -265,6 +265,11 @@ module Submissions
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?
end end
is_kba_passed =
submission.template_fields.any? do |e|
e['type'] == 'kba' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
end
info_rows = [ info_rows = [
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
@ -288,6 +293,9 @@ module Submissions
is_id_verified && { is_id_verified && {
text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n" text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n"
}, },
is_kba_passed && {
text: "#{I18n.t('knowledge_based_authentication')}: #{I18n.t('passed')}\n"
},
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" }, completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" }, completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" }, completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
@ -353,7 +361,7 @@ module Submissions
text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left, text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left,
line_spacing: 1.3, padding: [0, 0, 2, 0] line_spacing: 1.3, padding: [0, 0, 2, 0]
), ),
if field['type'].in?(%w[image signature initials stamp]) && if field['type'].in?(%w[image signature initials stamp kba]) &&
(attachment = submitter.attachments.find { |a| a.uuid == value }) && (attachment = submitter.attachments.find { |a| a.uuid == value }) &&
attachment.image? attachment.image?

@ -434,7 +434,7 @@ module Submissions
height: image.height * scale height: image.height * scale
) )
end end
when 'image', 'signature', 'initials', 'stamp' when 'image', 'signature', 'initials', 'stamp', 'kba'
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
image = image =

@ -111,7 +111,7 @@ module Submitters
end end
def fetch_field_value(field, value, attachments_index, expires_at: nil) def fetch_field_value(field, value, attachments_index, expires_at: nil)
if field['type'].in?(%w[image signature initials stamp payment]) if field['type'].in?(%w[image signature initials stamp payment kba])
rails_storage_proxy_url(attachments_index[value], expires_at:) rails_storage_proxy_url(attachments_index[value], expires_at:)
elsif field['type'] == 'file' elsif field['type'] == 'file'
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) } Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) }

Loading…
Cancel
Save