2026-01-02 16:07:04 +02:00
< 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"
2026-01-06 11:11:49 +02:00
class = "float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
2026-01-02 16:07:04 +02:00
>
2026-01-06 11:11:49 +02:00
< span > Question { { currentQuestionIndex + 1 } } / { { questions . length } } < / span >
< span
v-if = "timeLeftSeconds !== null"
class = "block"
>
Time left : { { formattedTimeLeft } }
< / span >
2026-01-02 16:07:04 +02:00
< / span >
< / label >
< div
v-if = "field.description"
dir = "auto"
class = "mb-4 px-1 field-description-text"
>
< MarkdownContent :string = "field.description" / >
< / div >
2026-01-05 13:16:19 +02:00
< div
v-if = "isRequiredFieldEmpty"
class = "px-1 field-description-text"
>
{ { t ( 'complete_all_required_fields_to_proceed_with_identity_verification' ) } }
< / div >
2026-01-02 16:07:04 +02:00
< div
v-if = "error"
class = "mb-4 text-center"
>
< div class = "text-xl mb-4" >
{ { error } }
< / div >
< button
2026-05-31 09:09:57 +03:00
type = "button"
2026-01-02 16:07:04 +02:00
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" >
2026-01-06 11:11:49 +02:00
< p class = "font-semibold mb-4 text-lg" >
{ { currentQuestion . prompt } }
< / p >
2026-01-02 16:07:04 +02:00
< div class = "space-y-3.5 mx-auto" >
< div
v-for = "(answer, index) in currentQuestion.answers"
2026-01-05 13:16:19 +02:00
:key = "`${currentQuestion.id}_${index}`"
2026-01-02 16:07:04 +02:00
>
< label
2026-01-05 13:16:19 +02:00
:for = "`${currentQuestion.id}_${index}`"
2026-01-02 16:07:04 +02:00
class = "flex items-center space-x-3 radio-label"
>
< input
2026-01-05 13:16:19 +02:00
:id = "`${currentQuestion.id}_${index}`"
2026-01-02 16:07:04 +02:00
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 >
2026-01-05 13:16:19 +02:00
< div v-else-if = "!error && !isRequiredFieldEmpty" >
2026-01-02 16:07:04 +02:00
< 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
} ,
2026-01-05 13:16:19 +02:00
submitter : {
type : Object ,
required : true
} ,
2026-01-02 16:07:04 +02:00
submitterSlug : {
type : String ,
required : true
} ,
2026-01-05 13:16:19 +02:00
emptyValueRequiredStep : {
type : Object ,
required : false ,
default : null
} ,
2026-01-02 16:07:04 +02:00
values : {
type : Object ,
required : true
}
} ,
emits : [ 'submit' ] ,
data ( ) {
return {
isLoading : false ,
isSubmitting : false ,
questions : null ,
currentQuestionIndex : 0 ,
token : null ,
2026-01-05 13:16:19 +02:00
reference : null ,
2026-01-02 16:07:04 +02:00
answers : { } ,
error : null ,
2026-01-06 11:11:49 +02:00
timeLeftSeconds : null ,
countdownIntervalId : null ,
2026-01-02 16:07:04 +02:00
form : {
fn : '' ,
ln : '' ,
addr : '' ,
city : '' ,
state : '' ,
zip : '' ,
dob : '' ,
ssn : '' ,
phone : '' ,
email : ''
}
}
} ,
computed : {
currentQuestion ( ) {
return this . questions ? this . questions [ this . currentQuestionIndex ] : null
} ,
2026-01-05 13:16:19 +02:00
isRequiredFieldEmpty ( ) {
return this . emptyValueRequiredStep && this . emptyValueRequiredStep [ 0 ] !== this . field
} ,
2026-01-06 11:11:49 +02:00
kbaTimeLimitSeconds ( ) {
return 90
} ,
2026-01-02 16:07:04 +02:00
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
2026-01-06 11:11:49 +02:00
} ,
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' ) } `
2026-01-02 16:07:04 +02:00
}
} ,
2026-01-06 11:11:49 +02:00
beforeUnmount ( ) {
this . clearCountdown ( )
} ,
2026-01-02 16:07:04 +02:00
methods : {
2026-01-06 11:11:49 +02:00
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. `
} ,
2026-01-02 16:07:04 +02:00
nextQuestion ( ) {
if ( this . isLastQuestion ) {
this . $emit ( 'submit' )
} else {
this . currentQuestionIndex ++
}
} ,
restartKba ( ) {
2026-01-06 11:11:49 +02:00
this . clearCountdown ( )
2026-01-02 16:07:04 +02:00
this . questions = null
this . token = null
2026-01-05 13:16:19 +02:00
this . reference = null
2026-01-02 16:07:04 +02:00
this . answers = { }
this . currentQuestionIndex = 0
this . error = null
} ,
async startKba ( ) {
this . isLoading = true
this . error = null
2026-01-06 11:11:49 +02:00
this . clearCountdown ( )
2026-01-02 16:07:04 +02:00
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 )
} )
const data = await resp . json ( )
2026-01-30 18:17:47 +02:00
if ( ! resp . ok ) throw new Error ( data . error || 'Failed to start KBA' )
2026-01-02 16:07:04 +02:00
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
2026-01-05 13:16:19 +02:00
this . reference = data . meta . reference
2026-01-02 16:07:04 +02:00
this . questions . forEach ( q => {
this . answers [ q . id ] = null
} )
2026-01-06 11:11:49 +02:00
this . startCountdown ( )
2026-01-02 16:07:04 +02:00
} else {
throw new Error ( 'Invalid KBA response' )
}
} catch ( e ) {
this . error = e . message
} finally {
this . isLoading = false
}
} ,
async submit ( ) {
2026-01-06 11:11:49 +02:00
this . clearCountdown ( )
2026-01-02 16:07:04 +02:00
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 ,
2026-01-05 13:16:19 +02:00
reference : this . reference ,
2026-01-02 16:07:04 +02:00
submitter _slug : this . submitterSlug
} ) ,
headers : { 'Content-Type' : 'application/json' }
} )
const data = await resp . json ( )
if ( data . result ? . action !== 'PASS' ) {
2026-01-29 15:55:15 +02:00
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.'
}
2026-01-02 16:07:04 +02:00
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 >