Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/privacy statement cx 1711 #316

Merged
merged 20 commits into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This project adheres to the Node [default version scheme](https://docs.npmjs.com

### Added
- Public: Added `onModalRequestClose` options, which is a callback that fires when the user attempts to close the modal.
- UI: Added legal documentation view before first document capture to assist client with user privacy notifications and compliance.

### Fixed
- Public: Fixed `complete` step to allow string customization at initialization time.
Expand Down
2 changes: 1 addition & 1 deletion dist/onfido.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/style.css

Large diffs are not rendered by default.

25 changes: 16 additions & 9 deletions src/components/Capture/capture.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import randomId from '../utils/randomString'
import { Uploader } from '../Uploader'
import Camera from '../Camera'
import Title from '../Title'
import PrivacyStatement from '../PrivacyStatement'
import { functionalSwitch, isDesktop, checkIfHasWebcam } from '../utils'
import { canvasToBase64Images } from '../utils/canvas.js'
import { base64toBlob, fileToBase64, isOfFileType, fileToLossyBase64Image } from '../utils/file.js'
Expand All @@ -22,7 +23,7 @@ class Capture extends Component {
this.state = {
uploadFallback: false,
error: null,
hasWebcam: hasWebcamStartupValue,
hasWebcam: hasWebcamStartupValue
}
}

Expand All @@ -43,6 +44,10 @@ class Capture extends Component {
if (allInvalid) this.onFileGeneralError()
}

acceptTerms = () => {
this.props.actions.acceptTerms()
}

checkWebcamSupport = () => {
checkIfHasWebcam( hasWebcam => this.setState({hasWebcam}) )
}
Expand Down Expand Up @@ -196,16 +201,18 @@ class Capture extends Component {
this.setState({error: null})
}

render ({useWebcam, ...other}) {
render ({useWebcam, back, i18n, termsAccepted, ...other}) {
const useCapture = (!this.state.uploadFallback && useWebcam && isDesktop && this.state.hasWebcam)
return (
<CaptureMode {...{useCapture,
onScreenshot: this.onScreenshot,
onUploadFallback: this.onUploadFallback,
onImageSelected: this.onImageFileSelected,
onWebcamError: this.onWebcamError,
error: this.state.error,
...other}}/>
!termsAccepted ?
<PrivacyStatement {...{i18n, back, acceptTerms: this.acceptTerms, ...other}}/> :
<CaptureMode {...{useCapture, i18n,
onScreenshot: this.onScreenshot,
onUploadFallback: this.onUploadFallback,
onImageSelected: this.onImageFileSelected,
onWebcamError: this.onWebcamError,
error: this.state.error,
...other}}/>
)
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/components/Modal/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import style from './style.css'
import ReactModal from 'react-modal'
import { h, Component } from 'preact'
import theme from '../Theme/style.css'
import { getCSSMilisecsValue, wrapWithClass } from '../utils'

const MODAL_ANIMATION_DURATION = getCSSMilisecsValue(style.modal_animation_duration)

const WrapperContent = ({children}) =>
wrapWithClass(style.content, wrapWithClass(theme.step, children))
wrapWithClass(style.content, children)

const Wrapper = ({children}) =>
wrapWithClass(style.inner, <WrapperContent>{children}</WrapperContent>)
Expand Down
7 changes: 3 additions & 4 deletions src/components/NavigationBar/style.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
@import (less) "../Theme/constants.css";

.navigation {
float: left;
position: absolute;
top: 16px;
margin-left: 16px;
margin: 0 16px;
margin-top: -16px;
text-align: left;
@media (--small-viewport) {
margin-left: 12px;
margin-top: -32px;
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/components/PrivacyStatement/assets/tick-grey.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions src/components/PrivacyStatement/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { h, Component} from 'preact'

import theme from '../Theme/style.css'
import style from './style.css'
import Title from '../Title'
import {preventDefaultOnClick} from '../utils'
import {sendScreen} from '../../Tracker'
import {parseI18nWithXmlTags} from '../../locales'

const externalUrls = {
terms: process.env.ONFIDO_TERMS_URL,
privacy: process.env.ONFIDO_PRIVACY_URL
}

class PrivacyStatement extends Component {
componentDidMount() {
sendScreen(['privacy'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't just use the tracking component wrapper for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can but the screen name on woopra will be appended to the capture component one, resulting in something like screen_document_front_capture_privacy and we just want screen_privacy

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should have as an option to get rid of the prefixes

}

render({i18n, back, acceptTerms}) {
const title = i18n.t('privacy.title')
return (
<div className={style.privacy}>
<Title {...{title}} />
<div className={`${theme.thickWrapper} ${style.content}`}>
<ul className={style.list}>
<li className={style.item}>{i18n.t('privacy.item_1')}</li>
<li className={style.item}>{i18n.t('privacy.item_2')}</li>
<li className={style.item}>{i18n.t('privacy.item_3')}</li>
</ul>

<div>
<div className={style.smallPrint}>
{ parseI18nWithXmlTags(i18n, 'privacy.small_print', tagElement => (
<a href={externalUrls[tagElement.type]} target='_blank'>{tagElement.text}</a>
))}
</div>
<div className={style.actions}>
<button onClick={preventDefaultOnClick(back)}
className={`${theme.btn} ${style.decline}`}>
{i18n.t('privacy.decline')}
</button>
<button href='#' className={`${theme.btn} ${theme["btn-primary"]} ${style.primary}`}
onClick={preventDefaultOnClick(acceptTerms)}>
{i18n.t('privacy.continue')}
</button>
</div>
</div>
</div>
</div>
)
}
}

export default PrivacyStatement
94 changes: 94 additions & 0 deletions src/components/PrivacyStatement/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
@import (less) "../Theme/constants.css";

.privacy {
/*
The height is calculated by subtracting 112px (parent's margin top 48px + parent's margin bottom (64px)) from the height of its parent.
This will allow consistent spacing between its children when using flexbox
and for the actions to always sit above the "powered-by-onfido" logo.
*/
height: calc(~"100% - 112px");
width: 100%;
position: absolute;
}

.content {
height: calc(~"100% - 112px");
display: flex;
justify-content: space-around;
flex-direction: column;
color: #0F2536;
}

.actions {
display: flex;
justify-content: space-between;
flex-direction: row;
margin-bottom: 24px;
@media (--small-viewport) {
margin-bottom: 0;
}
}

.list {
font-size: 16px;
line-height: 24px;
text-align: left;
padding-left: 64px;
position: relative;
margin-bottom: 0;
@media (--small-viewport) {
padding-left: 32px;
}
}

.item {
list-style: none;
margin-bottom: 16px;
}

.item:before{
content: '';
display: inline-block;
height: 16px;
width: 32px;
background-image: url('assets/tick-grey.svg');
background-repeat: no-repeat;
position: relative;
margin-left: -32px;
@media (--small-viewport) {
margin-left: -32px;
}
}

.smallPrint {
text-align: left;
font-size: 11px;
font-weight: 400;
line-height: 18px;
margin-bottom: 24px;
}

.smallPrint a {
color: #0F2536;
margin-top: 10px;
margin-bottom: 10px;
}

.decline {
width: 136px;
border: 1px solid #8A9293;
border-radius: 28px;
background-color: transparent;
color: #585E5F;
line-height: 16px;
@media (--small-viewport) {
width: 112px;
padding: 0px 16px;
}
}

.primary {
@media (--small-viewport) {
width: 152px;
}
}
3 changes: 2 additions & 1 deletion src/components/Router/StepsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { h, Component } from 'preact'
import {sendScreen} from '../../Tracker'
import {wrapArray} from '../utils/array'
import NavigationBar from '../NavigationBar'
import theme from '../Theme/style.css'

class StepsRouter extends Component {
constructor(props) {
Expand All @@ -21,7 +22,7 @@ class StepsRouter extends Component {
const componentBlob = this.currentComponent()
const CurrentComponent = componentBlob.component
return (
<div>
<div className={theme.step}>
{!this.props.disableBackNavigation && <NavigationBar back={this.props.back} i18n={this.props.i18n} />}
<CurrentComponent
{...{...componentBlob.step.options, ...globalUserOptions, ...otherProps}}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ class CrossDeviceMobileRouter extends Component {
sendError(`Token has expired: ${token}`)
return this.setError()
}
this.setState({token, steps, step, loading: false})
this.setState({i18n: initializeI18n(language)})
this.setState({token, steps, step, loading: false, i18n: initializeI18n(language)})
actions.setDocumentType(documentType)
actions.acceptTerms()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a comment on why we are accepting this. also, if the UI order changes and privacy shows after the cross device can be started, this will introduce a bug, which is something to bear in mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, you will never be able to reach cross device if you don't accept the privacy terms, even if you change the steps order. The acceptance criteria didn't say that the privacy screen should not be showed for face capture so I am double checking with @seewah what's the expected behaviour

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After our conversation this morning, we agreed on showing the privacy screen also for the face step. Given the current implementation, the terms will always be accepted when the user reaches the cross device client. I could send the termsAccepted prop to the cross device client and trigger this action once we have received it but it seems redundant to me because a user cannot reach the cross device flow without accepting terms first.

}

setError = () =>
Expand Down
1 change: 1 addition & 0 deletions src/core/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const SET_SOCKET = 'SET_SOCKET'
export const SET_MOBILE_NUMBER = 'SET_MOBILE_NUMBER'
export const SET_CLIENT_SUCCESS = 'SET_CLIENT_SUCCESS'
export const MOBILE_CONNECTED = 'MOBILE_CONNECTED'
export const ACCEPT_TERMS = 'ACCEPT_TERMS'

export const CAPTURE_CREATE = 'CAPTURE_CREATE'
export const CAPTURE_VALIDATE = 'CAPTURE_VALIDATE'
Expand Down
6 changes: 6 additions & 0 deletions src/core/store/actions/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ export function mobileConnected(payload) {
payload
}
}

export function acceptTerms() {
return {
type: constants.ACCEPT_TERMS
}
}
3 changes: 3 additions & 0 deletions src/core/store/reducers/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const initialState = {
sms: {number: null, valid: false},
clientSuccess: false,
i18n: null,
termsAccepted: false
}


Expand All @@ -24,6 +25,8 @@ export default function globals(state = initialState, action) {
return {...state, clientSuccess: action.payload}
case constants.MOBILE_CONNECTED:
return {...state, mobileConnected: action.payload}
case constants.ACCEPT_TERMS:
return {...state, termsAccepted: true}
default:
return state
}
Expand Down
9 changes: 9 additions & 0 deletions src/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ export const en = {
},
tips: 'Tips',
},
privacy: {
title: 'You\'ll have to upload a photo of your identity document',
item_1: 'All your document details must be visible',
item_2: 'Your document must be in colour',
item_3: 'Avoid light reflections',
small_print: 'By continuing, you agree to the <terms>Onfido Terms of Use</terms> and understand that your information, including your facial identifiers, will be processed in accordance with the <privacy>Onfido Privacy Policy</privacy>',
decline: 'Decline',
continue: 'Continue'
},
errors: {
invalid_capture: {message:'No document detected', instruction: 'Make sure all the document is in the photo'},
invalid_type: {message: 'File not uploading', instruction: 'Try using another file type'},
Expand Down
9 changes: 9 additions & 0 deletions src/locales/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ export const es = {
},
tips: 'Tips',
},
privacy: {
title: 'Tendrá que subir una foto de su documento de identidad',
item_1: 'Todos los datos de su documento tienen que ser visibles',
item_2: 'El documento debe ser en color',
item_3: 'Evite los reflejos',
small_print: 'Al continuar, usted está de acuerdo con los <terms>Términos de Uso de Onfido</terms> y comprende que su información, incluidos sus identificadores faciales, se procesará de acuerdo con la <privacy>Política de privacidad de Onfido</privacy>',
decline: 'Declinar',
continue: 'Continuar'
},
errors: {
invalid_capture: {message:'Documento no detectado', instruction: 'Asegúrese de que todo el documento esté en la foto'},
invalid_type: {message: 'Archivo no cargado', instruction: 'Intenta usar otro tipo de archivo'},
Expand Down
10 changes: 10 additions & 0 deletions src/locales/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,13 @@ export const initializeI18n = (language) => {
if (!language) return polyglot
return overrideTranslations(language, polyglot) || polyglot
}

export const parseI18nWithXmlTags = (i18n, translationKey, handleTag) => {
const str = i18n.t(translationKey)
const parser = new DOMParser();
const stringToXml = parser.parseFromString(`<l>${str}</l>`, 'application/xml')
const xmlToNodesArray = Array.from(stringToXml.firstChild.childNodes)
return xmlToNodesArray.map(
node => node.nodeType === document.TEXT_NODE ? node.textContent : handleTag({type: node.tagName, text: node.textContent})
)
}
8 changes: 8 additions & 0 deletions test/features/page_objects/sdk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ def confirm
@driver.find_element(:css, '.onfido-sdk-ui-Confirm-actions > .onfido-sdk-ui-Theme-btn-primary')
end

def confirm_privacy_terms
@driver.find_element(:css, '.onfido-sdk-ui-PrivacyStatement-primary')
end

def decline_privacy_terms
@driver.find_element(:css, '.onfido-sdk-ui-PrivacyStatement-decline')
end

def page_title
@driver.find_element(:css, '.onfido-sdk-ui-Title-title')
end
Expand Down