UNPKG

onfido-sdk-ui

Version:

JavaScript SDK view layer for Onfido identity verification

581 lines (519 loc) 15.7 kB
import type { LocaleConfig, SupportedLanguages } from '~types/locales' import type { DocumentTypes, DocumentTypeConfig, StepConfig, StepTypes, } from '~types/steps' import type { ServerRegions, SdkOptions, SdkResponse } from '~types/sdk' import type { UICustomizationOptions } from '~types/ui-customisation-options' import type { ApplicantData, DecoupleResponseOptions, StringifiedBoolean, } from './types' import customUIConfig from './custom-ui-config.json' import testDarkCobrandLogo from './assets/onfido-logo.svg' import testLightCobrandLogo from './assets/onfido-logo-light.svg' import sampleCompanyLogo from './assets/sample-logo.svg' export type QueryParams = { countryCode?: StringifiedBoolean createCheck?: StringifiedBoolean disableAnalytics?: StringifiedBoolean forceCrossDevice?: StringifiedBoolean hideOnfidoLogo?: StringifiedBoolean language?: 'customTranslations' | SupportedLanguages customWelcomeScreenCopy?: StringifiedBoolean link_id?: string docVideo?: StringifiedBoolean faceVideo?: StringifiedBoolean multiDocWithInvalidPresetCountry?: StringifiedBoolean multiDocWithPresetCountry?: StringifiedBoolean multiDocWithBooleanValues?: StringifiedBoolean noCompleteStep?: StringifiedBoolean oneDoc?: DocumentTypes oneDocWithCountrySelection?: StringifiedBoolean oneDocWithPresetCountry?: StringifiedBoolean poa?: StringifiedBoolean region?: string shouldCloseOnOverlayClick?: StringifiedBoolean showCobrand?: StringifiedBoolean showLogoCobrand?: StringifiedBoolean showUserConsent?: StringifiedBoolean showAuth?: StringifiedBoolean smsNumber?: StringifiedBoolean snapshotInterval?: string uploadFallback?: StringifiedBoolean useHistory?: StringifiedBoolean useLiveDocumentCapture?: StringifiedBoolean useMemoryHistory?: StringifiedBoolean useModal?: StringifiedBoolean useMultipleSelfieCapture?: StringifiedBoolean useUploader?: StringifiedBoolean useWebcam?: StringifiedBoolean customisedUI?: StringifiedBoolean useCustomizedApiRequests?: StringifiedBoolean decoupleResponse?: DecoupleResponseOptions photoCaptureFallback?: StringifiedBoolean showUserAnalyticsEvents?: StringifiedBoolean excludeSmsCrossDeviceOption?: StringifiedBoolean singleCrossDeviceOption?: StringifiedBoolean invalidCrossDeviceAlternativeMethods?: StringifiedBoolean crossDeviceClientIntroCustomProductName?: StringifiedBoolean crossDeviceClientIntroCustomProductLogo?: StringifiedBoolean autoFocusOnInitialScreenTitle?: StringifiedBoolean } export type CheckData = { applicantId?: string sdkFlowCompleted: boolean } export type UIConfigs = { darkBackground: boolean iframeWidth: string iframeHeight: string tearDown: boolean } const SAMPLE_LOCALE: LocaleConfig = { locale: 'en', phrases: { 'welcome.title': 'My custom title' }, mobilePhrases: { 'capture.driving_licence.back.instructions': 'Custom instructions', }, } export const queryParamToValueString = window.location.search .slice(1) .split('&') .reduce((acc: QueryParams, cur: string) => { const [key, value] = cur.split('=') return { ...acc, [key]: value } }, {}) const getPreselectedDocumentTypes = (): Partial< Record<DocumentTypes, DocumentTypeConfig> > => { const preselectedDocumentType = queryParamToValueString.oneDoc if (preselectedDocumentType) { return { [preselectedDocumentType]: true, } } if (queryParamToValueString.oneDocWithCountrySelection === 'true') { return { driving_licence: true, } } if (queryParamToValueString.oneDocWithPresetCountry === 'true') { return { driving_licence: { country: 'ESP', }, } } if (queryParamToValueString.multiDocWithPresetCountry === 'true') { return { driving_licence: { country: 'ESP', }, national_identity_card: { country: 'MYS', }, residence_permit: { country: null, }, } } if (queryParamToValueString.multiDocWithInvalidPresetCountry === 'true') { return { driving_licence: { country: 'ES', }, national_identity_card: { country: 'XYZ', }, } } if (queryParamToValueString.multiDocWithBooleanValues === 'true') { return { driving_licence: true, national_identity_card: true, } } return {} } export const getInitSdkOptions = (): SdkOptions => { const linkId = queryParamToValueString.link_id as string if (linkId) { return { mobileFlow: true, roomId: linkId.substring(2), } } const language = queryParamToValueString.language === 'customTranslations' ? SAMPLE_LOCALE : queryParamToValueString.language const steps: Array<StepConfig> = [] if (queryParamToValueString.customWelcomeScreenCopy === 'true') { steps.push({ type: 'welcome', options: { title: 'Open your new bank account', descriptions: [ 'To open a bank account, we will need to verify your identity.', 'It will only take a couple of minutes.', ], nextButton: 'Verify Identity', }, }) } else { steps.push({ type: 'welcome' }) } if (queryParamToValueString.showAuth === 'true') { steps.push({ type: 'auth', options: { retries: 10 } }) } if (queryParamToValueString.showUserConsent === 'true') { steps.push({ type: 'userConsent' }) } if (queryParamToValueString.poa === 'true') { steps.push({ type: 'poa' }) } steps.push({ type: 'document', options: { useLiveDocumentCapture: queryParamToValueString.useLiveDocumentCapture === 'true', uploadFallback: queryParamToValueString.uploadFallback !== 'false', useWebcam: queryParamToValueString.useWebcam === 'true', documentTypes: getPreselectedDocumentTypes(), showCountrySelection: queryParamToValueString.oneDocWithCountrySelection === 'true', forceCrossDevice: queryParamToValueString.forceCrossDevice === 'true', requestedVariant: queryParamToValueString.docVideo === 'true' ? 'video' : 'standard', }, }) steps.push({ type: 'face', options: { useUploader: queryParamToValueString.useUploader === 'true', uploadFallback: queryParamToValueString.uploadFallback !== 'false', useMultipleSelfieCapture: queryParamToValueString.useMultipleSelfieCapture !== 'false', photoCaptureFallback: queryParamToValueString.photoCaptureFallback !== 'false', requestedVariant: queryParamToValueString.faceVideo === 'true' ? 'video' : 'standard', }, }) if (queryParamToValueString.noCompleteStep !== 'true') { steps.push({ type: 'complete' }) } const smsNumberCountryCode = queryParamToValueString.countryCode ? { smsNumberCountryCode: queryParamToValueString.countryCode } : {} const hideOnfidoLogo = queryParamToValueString.hideOnfidoLogo === 'true' const cobrand = queryParamToValueString.showCobrand === 'true' ? { text: '[COMPANY/PRODUCT NAME]' } : undefined const logoCobrand = queryParamToValueString.showLogoCobrand === 'true' ? { lightLogoSrc: testLightCobrandLogo, darkLogoSrc: testDarkCobrandLogo } : undefined const useCustomizedApiRequests = queryParamToValueString.useCustomizedApiRequests === 'true' let decoupleCallbacks = {} if (queryParamToValueString.decoupleResponse === 'success') { const successResponse = Promise.resolve({ onfidoSuccessResponse: { id: '123-456-789', }, }) decoupleCallbacks = { onSubmitDocument: () => successResponse, onSubmitSelfie: () => successResponse, onSubmitVideo: () => successResponse, } } else if (queryParamToValueString.decoupleResponse === 'error') { const errorResponse = { status: 422, response: JSON.stringify({ error: { message: 'There was a validation error on this request', type: 'validation_error', fields: { detect_glare: ['glare found in image'] }, }, }), } decoupleCallbacks = { onSubmitDocument: () => Promise.reject(errorResponse), onSubmitSelfie: () => Promise.reject(errorResponse), onSubmitVideo: () => Promise.reject(errorResponse), } } else if (queryParamToValueString.decoupleResponse === 'onfido') { const response = Promise.resolve({ continueWithOnfidoSubmission: true }) decoupleCallbacks = { onSubmitDocument: () => response, onSubmitSelfie: () => response, onSubmitVideo: () => response, } } let visibleCrossDeviceMethods if (queryParamToValueString.excludeSmsCrossDeviceOption === 'true') { visibleCrossDeviceMethods = ['copy_link', 'qr_code'] } if (queryParamToValueString.singleCrossDeviceOption === 'true') { visibleCrossDeviceMethods = ['sms'] } if (queryParamToValueString.invalidCrossDeviceAlternativeMethods === 'true') { visibleCrossDeviceMethods = ['copy', 'qrCode', 'sms'] } const customUI = queryParamToValueString.customisedUI === 'true' ? customUIConfig : undefined const crossDeviceClientIntroProductName = queryParamToValueString.crossDeviceClientIntroCustomProductName === 'true' ? 'for a [COMPANY/PRODUCT NAME] loan' : undefined const crossDeviceClientIntroProductLogoSrc = queryParamToValueString.crossDeviceClientIntroCustomProductLogo === 'true' ? sampleCompanyLogo : undefined let autoFocusOnInitialScreenTitle = true if (queryParamToValueString.autoFocusOnInitialScreenTitle) { autoFocusOnInitialScreenTitle = queryParamToValueString.autoFocusOnInitialScreenTitle === 'true' } return { useModal: queryParamToValueString.useModal === 'true', shouldCloseOnOverlayClick: queryParamToValueString.shouldCloseOnOverlayClick !== 'true', language, disableAnalytics: queryParamToValueString.disableAnalytics === 'true', useMemoryHistory: queryParamToValueString.useMemoryHistory === 'true', steps, mobileFlow: false, userDetails: { smsNumber: queryParamToValueString.smsNumber, }, enterpriseFeatures: { hideOnfidoLogo, cobrand, logoCobrand, useCustomizedApiRequests, ...decoupleCallbacks, }, customUI: customUI as UICustomizationOptions, crossDeviceClientIntroProductName, crossDeviceClientIntroProductLogoSrc, ...smsNumberCountryCode, _crossDeviceLinkMethods: visibleCrossDeviceMethods, autoFocusOnInitialScreenTitle, } } export const commonSteps: Record<string, Array<StepTypes | StepConfig>> = { standard: [], faceVideo: [ 'welcome', 'document', { type: 'face', options: { requestedVariant: 'video' }, }, 'complete', ], poa: ['welcome', 'poa', 'complete'], 'no welcome': ['document', 'face', 'complete'], 'no complete': ['welcome', 'document', 'face'], 'upload fallback': [ 'welcome', { type: 'document', options: { useWebcam: false, }, }, { type: 'face', options: { uploadFallback: true, }, }, 'complete', ], 'document autocapture (BETA)': [ 'welcome', { type: 'document', options: { useWebcam: true, }, }, 'face', 'complete', ], 'document live capture (BETA)': [ 'welcome', { type: 'document', options: { useLiveDocumentCapture: true, }, }, 'face', 'complete', ], 'force cross device (docs)': [ 'welcome', { type: 'document', options: { forceCrossDevice: true, }, }, 'face', 'complete', ], 'no upload fallback': [ 'welcome', { type: 'document', options: { useWebcam: false, }, }, { type: 'face', options: { uploadFallback: false, }, }, 'complete', ], 'no snapshot': [ 'welcome', 'document', { type: 'face', options: { useMultipleSelfieCapture: false, }, }, 'complete', ], } export const commonVisibleCrossDeviceLinkOptions: Record<string, string[]> = { smsOnly: ['sms'], copyLinkOnly: ['copy_link'], qrCodeOnly: ['qr_code'], excludeSms: ['copy_link', 'qr_code'], reorderOptions: ['sms', 'copy_link', 'qr_code'], invalidOptions: ['copy', 'qrCode', 'sms'], } export const commonLanguages: Record< string, SupportedLanguages | LocaleConfig > = { en: 'en', es: 'es', de: 'de', fr: 'fr', nl: 'nl', custom: { phrases: { 'welcome.title': 'My custom title' }, mobilePhrases: { 'capture.driving_licence.back.instructions': 'Custom instructions', }, }, } export const commonRegions: ServerRegions[] = ['EU', 'US', 'CA'] export const getTokenFactoryUrl = (region: ServerRegions): string => { if (region === 'US' && process.env.US_JWT_FACTORY) { return process.env.US_JWT_FACTORY } if (region === 'CA' && process.env.CA_JWT_FACTORY) { return process.env.CA_JWT_FACTORY } if (region === 'EU' && process.env.JWT_FACTORY) { return process.env.JWT_FACTORY } throw new Error('No JWT_FACTORY env provided') } const buildTokenRequestParams = (applicantData?: ApplicantData): string => { if (!applicantData) { return '' } return Object.entries(applicantData) .filter(([, value]) => value) .map((pair) => pair.join('=')) .join('&') } export const getToken = ( hasPreview: boolean, url: string, applicantData: ApplicantData | undefined, eventEmitter: MessagePort | undefined, onSuccess: (token: string, applicantId: string) => void ): void => { const request = new XMLHttpRequest() request.open( 'GET', [url, buildTokenRequestParams(applicantData)].join('?'), true ) request.setRequestHeader( 'Authorization', `BASIC ${process.env.SDK_TOKEN_FACTORY_SECRET}` ) request.onload = () => { if (request.status >= 200 && request.status < 400) { const data = JSON.parse(request.responseText) if (hasPreview && eventEmitter) { eventEmitter.postMessage({ type: 'UPDATE_CHECK_DATA', payload: { applicantId: data.applicant_id, }, }) } onSuccess(data.message, data.applicant_id) } } request.send() } export const createCheckIfNeeded = ( tokenUrl?: string, applicantId?: string, submittedData?: SdkResponse ): void => { // Don't create check if createCheck flag isn't present if (!queryParamToValueString.createCheck || !tokenUrl || !applicantId) { return } const { poa, docVideo, faceVideo } = queryParamToValueString const request = new XMLHttpRequest() request.open('POST', tokenUrl.replace('sdk_token', 'check'), true) request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') request.setRequestHeader( 'Authorization', `BASIC ${process.env.SDK_TOKEN_FACTORY_SECRET}` ) request.onload = () => { if (request.status >= 200 && request.status < 400) { const data = JSON.parse(request.responseText) console.log('Check created!', data) } } const documentIds = docVideo ? submittedData?.document_video?.media_uuids : undefined const documentReport = docVideo ? 'document_video_capture' : 'document' const body = { applicant_id: applicantId, report_names: [ poa ? 'proof_of_address' : documentReport, faceVideo ? 'facial_similarity_video' : 'facial_similarity_photo', ], document_ids: documentIds, // api_version: docVideo ? 'v4' : 'v3', } request.send(JSON.stringify(body)) }