UNPKG

onfido-sdk-ui

Version:

JavaScript SDK view layer for Onfido identity verification

333 lines (281 loc) 9.9 kB
import { h, Component } from 'preact' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import io from 'socket.io-client' import createHistory from 'history/createBrowserHistory' import URLSearchParams from 'url-search-params' import { componentsList } from './StepComponentMap' import StepsRouter from './StepsRouter' import { themeWrap } from '../Theme' import Spinner from '../Spinner' import GenericError from '../crossDevice/GenericError' import { unboundActions } from '../../core' import { isDesktop } from '../utils' import { jwtExpired } from '../utils/jwt' import { getWoopraCookie, setWoopraCookie, trackException } from '../../Tracker' import { LocaleProvider } from '../../locales' const history = createHistory() const restrictedXDevice = process.env.RESTRICTED_XDEVICE_FEATURE_ENABLED const Router = (props) =>{ const RouterComponent = props.options.mobileFlow ? CrossDeviceMobileRouter : MainRouter return <RouterComponent {...props} allowCrossDeviceFlow={!props.options.mobileFlow && isDesktop}/> } // Wrap components with theme that include navigation and footer const WrappedSpinner = themeWrap(Spinner) const WrappedError = themeWrap(GenericError) class CrossDeviceMobileRouter extends Component { constructor(props) { super(props) // Some environments put the link ID in the query string so they can serve // the cross device flow without running nginx const searchParams = new URLSearchParams(window.location.search) const roomId = window.location.pathname.substring(3) || searchParams.get('link_id').substring(2) this.state = { token: null, steps: null, step: null, socket: io(process.env.DESKTOP_SYNC_URL, {autoConnect: false}), roomId, crossDeviceError: false, loading: true } if (restrictedXDevice && isDesktop) { return this.setError('FORBIDDEN_CLIENT_ERROR') } this.state.socket.on('config', this.setConfig(props.actions)) this.state.socket.on('connect', () => { this.state.socket.emit('join', {roomId: this.state.roomId}) }) this.state.socket.open() this.requestConfig() } configTimeoutId = null pingTimeoutId = null componentDidMount() { this.state.socket.on('custom disconnect', this.onDisconnect) this.state.socket.on('disconnect pong', this.onDisconnectPong) } componentWillUnmount() { this.clearConfigTimeout() this.clearPingTimeout() this.state.socket.close() } sendMessage = (event, payload) => { const roomId = this.state.roomId this.state.socket.emit('message', {roomId, event, payload}) } requestConfig = () => { this.sendMessage('get config') this.clearConfigTimeout() this.configTimeoutId = setTimeout(() => { if (this.state.loading) this.setError() }, 10000) } clearConfigTimeout = () => this.configTimeoutId && clearTimeout(this.configTimeoutId) clearPingTimeout = () => { if (this.pingTimeoutId) { clearTimeout(this.pingTimeoutId) this.pingTimeoutId = null } } setConfig = (actions) => (data) => { const {token, steps, language, documentType, step: userStepIndex,clientStepIndex, woopraCookie} = data setWoopraCookie(woopraCookie) if (!token) { console.error('Desktop did not send token') trackException('Desktop did not send token') return this.setError() } if (jwtExpired(token)) { console.error('Desktop token has expired') trackException(`Token has expired: ${token}`) return this.setError() } const isFaceStep = steps[clientStepIndex].type === "face" this.setState( { token, steps, step: isFaceStep ? clientStepIndex : userStepIndex, stepIndexType: isFaceStep ? 'client' : 'user', crossDeviceError: false, language }, // Temporary fix for https://github.com/valotas/preact-context/issues/20 // Once a fix is released, it should be done in CX-2571 () => this.setState({ loading: false }) ) actions.setDocumentType(documentType) actions.acceptTerms() } setError = (name='GENERIC_CLIENT_ERROR') => { this.setState({crossDeviceError: { name }, loading: false}) } onDisconnect = () => { this.pingTimeoutId = setTimeout(this.setError, 3000) this.sendMessage('disconnect ping') } onDisconnectPong = () => this.clearPingTimeout() sendClientSuccess = () => { this.state.socket.off('custom disconnect', this.onDisconnect) const { faceCapture } = this.props const data = faceCapture ? {faceCapture: {blob: null, ...faceCapture}} : {} this.sendMessage('client success', data) } render = () => { const { language } = this.state return ( <LocaleProvider language={language}> { this.state.loading ? <WrappedSpinner disableNavigation={true} /> : this.state.crossDeviceError ? <WrappedError disableNavigation={true} error={this.state.crossDeviceError} /> : <HistoryRouter {...this.props} {...this.state} sendClientSuccess={this.sendClientSuccess} crossDeviceClientError={this.setError} /> } </LocaleProvider> ) } } class MainRouter extends Component { constructor(props) { super(props) this.state = { crossDeviceInitialStep: null, } } mobileConfig = () => { const {documentType, options} = this.props const {steps, token, language} = options const woopraCookie = getWoopraCookie() return {steps, token, language, documentType, woopraCookie, step: this.state.crossDeviceInitialStep, clientStepIndex:this.state.crossDeviceInitialClientStep} } onFlowChange = ( newFlow, newStep, previousFlow, {userStepIndex,clientStepIndex}) => { if (newFlow === "crossDeviceSteps"){ this.setState({ crossDeviceInitialStep: userStepIndex, crossDeviceInitialClientStep: clientStepIndex }) } } render = (props) => <HistoryRouter {...props} steps={props.options.steps} onFlowChange={this.onFlowChange} mobileConfig={this.mobileConfig()} /> } const findFirstIndex = (componentsList, clientStepIndex) => Array.findIndex(componentsList, ({stepIndex})=> stepIndex === clientStepIndex) class HistoryRouter extends Component { constructor(props) { super(props) const componentsList = this.buildComponentsList({flow:'captureSteps'},this.props) const stepIndex = this.props.stepIndexType === "client" ? findFirstIndex(componentsList, this.props.step || 0) : this.props.step || 0 this.state = { flow: 'captureSteps', step: stepIndex, initialStep: stepIndex, } this.unlisten = history.listen(this.onHistoryChange) this.setStepIndex(this.state.step, this.state.flow) } onHistoryChange = ({state:historyState}) => { this.setState({...historyState}) } componentWillUnmount () { this.unlisten() } getStepType = step => { const componentList = this.componentsList() return componentList[step] ? componentList[step].step.type : null } disableNavigation = () => { return this.initialStep() || this.getStepType(this.state.step) === 'complete' } initialStep = () => this.state.initialStep === this.state.step && this.state.flow === 'captureSteps' changeFlowTo = (newFlow, newStep = 0, excludeStepFromHistory = false) => { const {flow: previousFlow, step: previousUserStepIndex} = this.state if (previousFlow === newFlow) return const previousUserStep = this.componentsList()[previousUserStepIndex] this.props.onFlowChange(newFlow, newStep, previousFlow, { userStepIndex: previousUserStepIndex, clientStepIndex: previousUserStep.stepIndex, clientStep: previousUserStep } ) this.setStepIndex(newStep, newFlow, excludeStepFromHistory) } nextStep = () => { const {step: currentStep} = this.state const componentsList = this.componentsList() const newStepIndex = currentStep + 1 if (componentsList.length === newStepIndex) { this.triggerOnComplete() } else { this.setStepIndex(newStepIndex) } } triggerOnComplete = () => { const { variant } = this.props.faceCapture || {} const data = variant ? {face: {variant}} : {} this.props.options.events.emit('complete', data) } previousStep = () => { const {step: currentStep} = this.state this.setStepIndex(currentStep - 1) } back = () => { history.goBack() } setStepIndex = (newStepIndex, newFlow, excludeStepFromHistory) => { const {flow:currentFlow} = this.state const newState = { step: newStepIndex, flow: newFlow || currentFlow, } if (excludeStepFromHistory) { this.setState(newState) } else { const path = `${location.pathname}${location.search}${location.hash}` history.push(path, newState) } } componentsList = () => this.buildComponentsList(this.state, this.props) buildComponentsList = ({flow}, {documentType, steps, options: {mobileFlow}}) => componentsList({flow, documentType, steps, mobileFlow}); render = (props) => <StepsRouter {...props} componentsList={this.componentsList()} step={this.state.step} disableNavigation={this.disableNavigation()} changeFlowTo={this.changeFlowTo} nextStep={this.nextStep} previousStep={this.previousStep} back={this.back} />; } HistoryRouter.defaultProps = { onFlowChange: ()=>{}, stepIndexType: 'user' } function mapStateToProps(state) { return { ...state.globals, faceCapture: state.captures.face, } } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators(unboundActions, dispatch) } } export default connect(mapStateToProps, mapDispatchToProps)(Router)