UNPKG

velocity-verification

Version:

JavaScript SDK view layer for identity verification

257 lines (227 loc) 9.57 kB
import { h, Component } from 'preact' import { connect } from 'react-redux' import style from './style.css' import theme from '../Theme/style.css' import classNames from 'classnames' // import { isOfMimeType } from '~utils/blob' import { cleanFalsy } from '~utils/array' import { uploadLivePhoto, uploadLiveVideo, uploadDocToCore } from '~utils/onfidoApi' import CaptureViewer from './CaptureViewer' // import { poaDocumentTypes } from '../DocumentSelector/documentTypes' import Button from '../Button' import Error from '../Error' import Spinner from '../Spinner' import PageTitle from '../PageTitle' import { trackComponentAndMode, appendToTracking, sendEvent } from '../../Tracker' import { localised } from '../../locales' const RetakeAction = localised(({retakeAction, translate}) => <Button onClick={retakeAction} className={style['btn-secondary']} variants={['secondary']} > {translate('confirm.redo')} </Button> ) const ConfirmAction = localised(({ confirmAction, isUploading, translate, error }) => <Button className={style['btn-primary']} variants={['primary']} onClick={confirmAction} disabled={isUploading} > { error.type === 'warn' ? translate('confirm.continue') : translate('confirm.confirm') } </Button> ) const Actions = ({ retakeAction, confirmAction, isUploading, error }) => ( <div className={style.actionsContainer}> <div className={classNames( style.actions, {[style.error]: error.type === 'error'} )}> <RetakeAction {...{retakeAction}} /> { error.type === 'error' ? null : <ConfirmAction {...{ confirmAction, isUploading, error }} /> } </div> </div> ) const Previews = localised( ({ capture, retakeAction, confirmAction, error, method, documentType, translate, isFullScreen, isUploading }) => { const methodNamespace = method === 'face' ? `confirm.face.${capture.variant}` : `confirm.${method}` const title = translate(`${methodNamespace}.title`) const imageAltTag = translate(`${methodNamespace}.alt`) const videoAriaLabel = translate('accessibility.replay_video') const message = method === 'face' ? translate(`confirm.face.${capture.variant}.message`) : translate(`confirm.${documentType}.message`) return ( <div className={classNames(style.previewsContainer, theme.fullHeightContainer, { [style.previewsContainerIsFullScreen]: isFullScreen, })}> { isFullScreen ? null : error.type ? <Error {...{error, withArrow: true, role: "alert", focusOnMount: false}} /> : <PageTitle title={title} smaller={true} className={style.title}/> } <CaptureViewer {...{ capture, method, isFullScreen, imageAltTag, videoAriaLabel }} /> {!isFullScreen && <div> <p className={style.message}> {message} </p> <Actions {...{ retakeAction, confirmAction, isUploading, error }} /> </div> } </div> ) } ) const chainMultiframeUpload = (snapshot, selfie, token, url, onSuccess, onError) => { const snapshotData = { file: { blob: snapshot.blob, filename: snapshot.filename }, sdkMetadata: snapshot.sdkMetadata, snapshot: true, advanced_validation: false } const { blob, filename, sdkMetadata } = selfie // try to upload snapshot first, if success upload selfie, else handle error uploadLivePhoto( snapshotData, url, token, () => uploadLivePhoto({ file: { blob, filename }, sdkMetadata }, url, token, onSuccess, onError), onError ) } class Confirm extends Component { constructor(props) { super(props) this.state = { uploadInProgress: false, error: {type:''}, capture: null } } onGlareWarning = () => { this.setWarning('GLARE_DETECTED') } setError = name => this.setState({ error: { name, type: 'error' } }) setWarning = name => this.setState({ error: { name, type: 'warn' } }) onfidoErrorFieldMap = ([key, val]) => { if (key === 'document_detection') return 'INVALID_CAPTURE' // on corrupted PDF or other unsupported file types if (key === 'file') return 'INVALID_TYPE' // hit on PDF/invalid file type submission for face detection if (key === 'attachment' || key === 'attachment_content_type') return 'UNSUPPORTED_FILE' if (key === 'face_detection') { return val[0].indexOf('Multiple faces') === -1 ? 'NO_FACE_ERROR' : 'MULTIPLE_FACES_ERROR' } } onfidoErrorReduce = ({ fields }) => { const [first] = Object.entries(fields).map(this.onfidoErrorFieldMap) return first } onApiError = () => { this.setError('SERVER_ERROR') this.setState({ uploadInProgress: false }) } onApiSuccess = apiResponse => { const { method, nextStep, actions } = this.props const { capture } = this.state const duration = Math.round(performance.now() - this.startTime) sendEvent('Completed upload', { duration, method }) actions.setCaptureMetadata({ capture, apiResponse }) // wait a tick to ensure the action completes before progressing setTimeout(nextStep, 0) } handleSelfieUpload = ({ snapshot, ...selfie }, token) => { const url = this.props.urls.VELOCITY_API_URL // if snapshot is present, it needs to be uploaded together with the user initiated selfie if (snapshot) { sendEvent('Starting multiframe selfie upload') chainMultiframeUpload(snapshot, selfie, token, url, this.onApiSuccess, this.onApiError) } else { const { blob, filename, sdkMetadata } = selfie // filename is only present for images taken via webcam. // Captures that have been taken via the Uploader component do not have filename // and the blob is a File type const filePayload = filename ? { blob, filename } : blob uploadLivePhoto({ file: filePayload, sdkMetadata }, url, token, this.onApiSuccess, this.onApiError) } } toBase64 = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = (error) => reject(error); }) uploadCaptureToOnfido = async () => { const { urls, capture, method, side, token, language, coreRequest, Method, verification, verification_submission_id} = this.props const url = urls.VELOCITY_API_URL this.startTime = performance.now() sendEvent('Starting upload', { method }) this.setState({ uploadInProgress: true }) const { blob, documentType, variant, challengeData, sdkMetadata } = capture this.setState({ capture }) if (method === 'document') { // const isPoA = poaDocumentTypes.includes(poaDocumentType) // const shouldDetectGlare = !isOfMimeType(['pdf'], blob) && !isPoA // const shouldDetectDocument = !isPoA // const validations = { // ...(shouldDetectDocument ? { detect_document: 'error' } : {}), // ...(shouldDetectGlare ? { detect_glare: 'warn' } : {}) // } // const issuingCountry = isPoA ? { issuing_country: this.props.country || 'GBR' } : {} const file = blob.type ? blob : blob.blob const base64 = await this.toBase64(file) const documentName = side ? `${documentType}_${side}` : documentType const data = { [documentName]: base64 } const mobileUpload = verification_submission_id ? true : false const id = mobileUpload ? verification_submission_id : verification.id await uploadDocToCore(coreRequest, Method, data, documentName, id, mobileUpload, this.onApiError, this.onApiSuccess) // uploadDocument(data, url, token, this.onApiSuccess, this.onApiError) } else if (method === 'face') { if (variant === 'video') { const data = { challengeData, blob, language, sdkMetadata } uploadLiveVideo(data, url, token, this.onApiSuccess, this.onApiError) } else { this.handleSelfieUpload(capture, token) } } } onConfirm = () => { this.state.error.type === 'warn' ? this.props.nextStep() : this.uploadCaptureToOnfido() } render = ({ capture, previousStep, method, documentType, isFullScreen }) => this.state.uploadInProgress ? ( <Spinner /> ) : ( <Previews isFullScreen={isFullScreen} capture={capture} retakeAction={previousStep} confirmAction={this.onConfirm} isUploading={this.state.uploadInProgress} error={this.state.error} method={method} documentType={documentType} /> ) } const captureKey = (...args) => cleanFalsy(args).join('_') const mapStateToProps = (state, { method, side }) => ({ capture: state.captures[captureKey(method, side)], isFullScreen: state.globals.isFullScreen }) const TrackedConfirmComponent = trackComponentAndMode(Confirm, 'confirmation', 'error') const MapConfirm = connect(mapStateToProps)(localised(TrackedConfirmComponent)) const DocumentFrontWrapper = props => <MapConfirm {...props} method="document" side="front" /> const DocumentBackWrapper = props => <MapConfirm {...props} method="document" side="back" /> const BaseFaceConfirm = props => <MapConfirm {...props} method="face" /> const DocumentFrontConfirm = appendToTracking(DocumentFrontWrapper, 'front') const DocumentBackConfirm = appendToTracking(DocumentBackWrapper, 'back') const SelfieConfirm = appendToTracking(BaseFaceConfirm, 'selfie') const VideoConfirm = appendToTracking(BaseFaceConfirm, 'video') export { DocumentFrontConfirm, DocumentBackConfirm, SelfieConfirm, VideoConfirm }