mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
597 lines
17 KiB
Vue
597 lines
17 KiB
Vue
<template>
|
|
<label
|
|
v-if="!error"
|
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
|
>
|
|
<span class="flex items-end">
|
|
<MarkdownContent
|
|
v-if="field.title"
|
|
:string="field.title"
|
|
/>
|
|
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
|
<span
|
|
class="tooltip tooltip-bottom ml-2 mb-1"
|
|
data-tip="To verify your identity, the details you enter are checked against third-party records, which may include credit bureau data, and used to generate questions based on those records. Only these identity details are shared with the identity verification provider; this document and your tax return information are not shared with it or with any credit reporting company. If the check is recorded on your credit report, it appears as a soft inquiry, which does not affect your credit score and is not shown to lenders. This check does not give the sender of this document or the IRS access to your credit report."
|
|
>
|
|
<IconInfoCircle class="w-4 h-4 text-neutral-500" />
|
|
</span>
|
|
</span>
|
|
<span
|
|
v-if="questions"
|
|
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
|
|
>
|
|
<span>Question {{ currentQuestionIndex + 1 }} / {{ questions.length }}</span>
|
|
<span
|
|
v-if="timeLeftSeconds !== null"
|
|
class="block"
|
|
>
|
|
Time left: {{ formattedTimeLeft }}
|
|
</span>
|
|
</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="isRequiredFieldEmpty"
|
|
class="px-1 field-description-text"
|
|
>
|
|
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
|
</div>
|
|
<div
|
|
v-if="error"
|
|
class="mb-4 text-center"
|
|
>
|
|
<div class="text-xl mb-4">
|
|
{{ error }}
|
|
</div>
|
|
<button
|
|
type="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="`${currentQuestion.id}_${index}`"
|
|
>
|
|
<label
|
|
:for="`${currentQuestion.id}_${index}`"
|
|
class="flex items-center space-x-3 radio-label"
|
|
>
|
|
<input
|
|
:id="`${currentQuestion.id}_${index}`"
|
|
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 && !isRequiredFieldEmpty">
|
|
<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, IconInfoCircle } from '@tabler/icons-vue'
|
|
|
|
export default {
|
|
name: 'KbaStep',
|
|
components: {
|
|
MarkdownContent,
|
|
IconInnerShadowTop,
|
|
IconInfoCircle
|
|
},
|
|
inject: ['baseUrl', 't'],
|
|
props: {
|
|
field: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
submitter: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
submitterSlug: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
emptyValueRequiredStep: {
|
|
type: Object,
|
|
required: false,
|
|
default: null
|
|
},
|
|
values: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
fetchOptions: {
|
|
type: Object,
|
|
required: false,
|
|
default: () => ({})
|
|
}
|
|
},
|
|
emits: ['submit'],
|
|
data () {
|
|
return {
|
|
isLoading: false,
|
|
isSubmitting: false,
|
|
questions: null,
|
|
currentQuestionIndex: 0,
|
|
token: null,
|
|
reference: null,
|
|
answers: {},
|
|
error: null,
|
|
timeLeftSeconds: null,
|
|
countdownIntervalId: null,
|
|
form: {
|
|
fn: '',
|
|
ln: '',
|
|
addr: '',
|
|
city: '',
|
|
state: '',
|
|
zip: '',
|
|
dob: '',
|
|
ssn: '',
|
|
phone: '',
|
|
email: ''
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
currentQuestion () {
|
|
return this.questions ? this.questions[this.currentQuestionIndex] : null
|
|
},
|
|
isRequiredFieldEmpty () {
|
|
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
|
},
|
|
kbaTimeLimitSeconds () {
|
|
return 90
|
|
},
|
|
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
|
|
},
|
|
formattedTimeLeft () {
|
|
if (this.timeLeftSeconds === null) return ''
|
|
|
|
const minutes = Math.floor(this.timeLeftSeconds / 60)
|
|
const seconds = this.timeLeftSeconds % 60
|
|
|
|
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
|
}
|
|
},
|
|
beforeUnmount () {
|
|
this.clearCountdown()
|
|
},
|
|
methods: {
|
|
clearCountdown () {
|
|
if (this.countdownIntervalId) {
|
|
clearInterval(this.countdownIntervalId)
|
|
this.countdownIntervalId = null
|
|
}
|
|
|
|
this.timeLeftSeconds = null
|
|
},
|
|
startCountdown () {
|
|
this.clearCountdown()
|
|
|
|
this.timeLeftSeconds = this.kbaTimeLimitSeconds
|
|
|
|
this.countdownIntervalId = setInterval(() => {
|
|
if (this.timeLeftSeconds === null) return
|
|
|
|
this.timeLeftSeconds -= 1
|
|
|
|
if (this.timeLeftSeconds <= 0) {
|
|
this.handleTimeout()
|
|
}
|
|
}, 1000)
|
|
},
|
|
handleTimeout () {
|
|
this.clearCountdown()
|
|
|
|
this.questions = null
|
|
this.token = null
|
|
this.reference = null
|
|
this.answers = {}
|
|
this.currentQuestionIndex = 0
|
|
this.error = `Knowledge Based Authentication timed out. You only have ${this.kbaTimeLimitSeconds} seconds to complete the verification. Please retry.`
|
|
},
|
|
nextQuestion () {
|
|
if (this.isLastQuestion) {
|
|
this.$emit('submit')
|
|
} else {
|
|
this.currentQuestionIndex++
|
|
}
|
|
},
|
|
restartKba () {
|
|
this.clearCountdown()
|
|
|
|
this.questions = null
|
|
this.token = null
|
|
this.reference = null
|
|
this.answers = {}
|
|
this.currentQuestionIndex = 0
|
|
this.error = null
|
|
},
|
|
async startKba () {
|
|
this.isLoading = true
|
|
this.error = null
|
|
this.clearCountdown()
|
|
|
|
try {
|
|
const payload = { ...this.form }
|
|
|
|
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 + `/s/${this.submitterSlug}/kba`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
...this.fetchOptions,
|
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
|
})
|
|
|
|
const data = await resp.json()
|
|
|
|
if (!resp.ok) throw new Error(data.error || 'Failed to start KBA')
|
|
|
|
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.reference = data.meta.reference
|
|
|
|
this.questions.forEach(q => {
|
|
this.answers[q.id] = null
|
|
})
|
|
|
|
this.startCountdown()
|
|
} else {
|
|
throw new Error('Invalid KBA response')
|
|
}
|
|
} catch (e) {
|
|
this.error = e.message
|
|
} finally {
|
|
this.isLoading = false
|
|
}
|
|
},
|
|
async submit () {
|
|
this.clearCountdown()
|
|
|
|
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 + `/s/${this.submitterSlug}/kba/${this.field.uuid}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
token: this.token,
|
|
answers: formattedAnswers,
|
|
reference: this.reference
|
|
}),
|
|
...this.fetchOptions,
|
|
headers: { 'Content-Type': 'application/json', ...this.fetchOptions.headers }
|
|
})
|
|
|
|
const data = await resp.json()
|
|
|
|
if (data.result?.action !== 'PASS') {
|
|
if (data.result?.issues?.length) {
|
|
this.error = `Knowledge Based Authentication Failed - make sure you provide correct details for the Knowledge Based authentication: ${data.result.issues.join(', ')}`
|
|
} else {
|
|
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>
|