UNPKG

onfido-sdk-ui

Version:

JavaScript SDK view layer for Onfido identity verification

261 lines (226 loc) 8.94 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 { uploadDocument, uploadLivePhoto, uploadLiveVideo } 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 { trackException, trackComponentAndMode, appendToTracking, sendEvent } from '../../Tracker' import { localised } from '../../locales' const RetakeAction = localised(({retakeAction, translate}) => <Button onClick={retakeAction} className={style.retake} variants={["outline"]} > {translate('confirm.redo')} </Button> ) const ConfirmAction = localised(({confirmAction, translate, error}) => <Button className={style["btn-primary"]} variants={["primary"]} onClick={confirmAction}> { error.type === 'warn' ? translate('confirm.continue') : translate('confirm.confirm') } </Button> ) const Actions = ({retakeAction, confirmAction, error}) => <div className={style.actionsContainer}> <div className={classNames( style.actions, {[style.error]: error.type === 'error'} )}> <RetakeAction {...{retakeAction}} /> { error.type === 'error' ? null : <ConfirmAction {...{confirmAction, error}} /> } </div> </div> const Previews = localised(({capture, retakeAction, confirmAction, error, method, documentType, translate, isFullScreen}) => { const methodNamespace = method === 'face' ? `confirm.face.${capture.variant}` : `confirm.${method}` const title = translate(`${methodNamespace}.title`) const altTag = translate(`${methodNamespace}.alt`) const enlargedAltTag = translate(`${methodNamespace}.enlarged_alt`) const subTitle = 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} subTitle={subTitle} smaller={true} className={style.title}/> } <CaptureViewer {...{ capture, method, isFullScreen, altTag, enlargedAltTag }} /> { !isFullScreen && <Actions {...{retakeAction, confirmAction, error}} /> } </div> ) }) const chainMultiframeUpload = (snapshot, selfie, token, 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, token, () => uploadLivePhoto({ file: { blob, filename }, sdkMetadata }, token, onSuccess, onError ), onError ) } class Confirm extends Component { constructor(props){ super(props) this.state = { uploadInProgress: false, error: {}, 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 = ({status, response}) => { let errorKey; if (this.props.mobileFlow && status === 401) { return this.props.crossDeviceClientError() } else if (status === 422) { errorKey = this.onfidoErrorReduce(response.error) } else { trackException(`${status} - ${response}`) errorKey = 'SERVER_ERROR' } this.setState({uploadInProgress: false}) this.setError(errorKey) } 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 }) const warnings = apiResponse.sdk_warnings if (warnings && !warnings.detect_glare.valid) { this.setState({uploadInProgress: false}) this.onGlareWarning() } else { // wait a tick to ensure the action completes before progressing setTimeout(nextStep, 0) } } handleSelfieUpload = ({snapshot, ...selfie }, token) => { // if snapshot is present, it needs to be uploaded together with the user initiated selfie if (snapshot) { chainMultiframeUpload(snapshot, selfie, token, 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 }, token, this.onApiSuccess, this.onApiError ) } } uploadCaptureToOnfido = () => { const {capture, method, side, token, documentType, language} = this.props this.startTime = performance.now() sendEvent('Starting upload', {method}) this.setState({uploadInProgress: true}) const {blob, documentType: type, variant, challengeData, sdkMetadata} = capture this.setState({ capture }) if (method === 'document') { const isPoA = poaDocumentTypes.includes(documentType) 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 data = { file: blob, type, side, validations, ...issuingCountry} uploadDocument(data, token, this.onApiSuccess, this.onApiError) } else if (method === 'face') { if (variant === 'video') { const data = { challengeData, blob, language, sdkMetadata} uploadLiveVideo(data, 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} 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}