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 12 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
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
- Internal: Fixed the `tearDown` method to clear the onComplete callback functions.
Expand Down
27 changes: 19 additions & 8 deletions src/components/Capture/capture.js
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 @@ -23,6 +24,7 @@ class Capture extends Component {
uploadFallback: false,
error: null,
hasWebcam: hasWebcamStartupValue,
privacyTermsAccepted: props.termsAccepted
}
}

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

acceptTerms = () => {
this.props.actions.acceptTerms()
this.setState({privacyTermsAccepted: true})
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this a prop already?

}

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

render ({useWebcam, ...other}) {
render ({useWebcam, back, i18n, ...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}}/>
<div>
Copy link
Contributor

Choose a reason for hiding this comment

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

This wrapping div looks unnecessary, no?

{ !this.state.privacyTermsAccepted ?
<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}}/> }
</div>
)
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/components/NavigationBar/style.css
@@ -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.
50 changes: 50 additions & 0 deletions src/components/PrivacyStatement/index.js
@@ -0,0 +1,50 @@
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'

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}>
{i18n.t('privacy.small_print_p1')}
<a href='https://onfido.com/termofuse' target='_blank'>{i18n.t('privacy.terms_link')}</a>
{i18n.t('privacy.small_print_p2')}
<a href='https://onfido.com/privacy' target='_blank'>{i18n.t('privacy.privacy_link')}</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a really brittle way of doing i18n

You're assuming that any language you want to translate to will fit into the structure you've written here. But you might get a language where it sounds way more natural to order this as "Onfido Terms of Use you are agreeing to" (Japanese is like that, I think)

We're handling this elsewhere by explicitly storing i18n as markdown, and parsing that. But it's fairly heavyweight, we can just get away with it for now. You might want to either make an explicit fragment class, or trust your translations and use dangerouslySetInnerHtml through a translation wrapper (as sort of discussed here)

But also feel free to just kick the bucket down with this one. It'll definitely bite you at some point though, so bear it 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.

That's a very good point. I will have look at fixing this tomorrow.

</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
84 changes: 84 additions & 0 deletions src/components/PrivacyStatement/style.css
@@ -0,0 +1,84 @@
@import (less) "../Theme/constants.css";

.privacy {
height: calc(~"100% - 112px");
Copy link
Contributor

Choose a reason for hiding this comment

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

calc isn't a less mixin, or anything - it's a native CSS function

You should just be able to do calc(100% - 112px), right?

Copy link
Contributor

Choose a reason for hiding this comment

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

what do we need calc for?

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 need it because the privacy component has an absolute positioning and we want to make sure that its height never covers the footer so it has a 100% height minus the height (including paddings/margins) of the footer.
@stephencookdev If I use calc(100% - 112px) it gets computed as height: calc(-12%);

Copy link
Contributor

Choose a reason for hiding this comment

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

so we can't use padding instead? Also, the computation is a bit weird, does padding have a height os 12%?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, calc does not work as expected with LESS that's why I'm using the following syntax calc(~"100% - 112px"); see this issue.
I cannot use a padding but I could get rid of absolute positioning and give it a fixed height

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a comment for now as this behaviour is also used in the confirm screen where there is already a comment explaining why we are using calc. This will have to be refactored as part of the header/footer positioning ticket

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;
}

.list {
font-size: 16px;
line-height: 24px;
text-align: left;
padding-left: 64px;
position: relative;
@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;
}
}
4 changes: 2 additions & 2 deletions src/components/Router/index.js
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
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
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
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
12 changes: 12 additions & 0 deletions src/locales/en.js
Expand Up @@ -130,6 +130,18 @@ 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_p1: 'By continuing, you agree to the ',
terms_link: 'Onfido Terms of Use',
small_print_p2: ' and understand that your information, including your facial identifiers, will be processed in accordance with the ',
privacy_link: 'Onfido Privacy Policy',
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
12 changes: 12 additions & 0 deletions src/locales/es.js
Expand Up @@ -130,6 +130,18 @@ 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_p1: 'Al continuar, usted está de acuerdo con los ',
terms_link: 'Términos de Uso de Onfido',
small_print_p2: ' y comprende que su información, incluidos sus identificadores faciales, se procesará de acuerdo con la ',
privacy_link: 'Política de privacidad de Onfido',
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
8 changes: 8 additions & 0 deletions test/features/page_objects/sdk.rb
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
13 changes: 13 additions & 0 deletions test/features/sdk_flow.feature
Expand Up @@ -162,3 +162,16 @@ Feature: SDK File Upload Tests
When I press esc key
When I click on modal_button ()
Then I should see page_title ()

Scenario Outline: I should be able to decline privacy terms
Given I initiate the verification process with <locale>
Then I should see 3 document_select_buttons ()
When I click on passport ()
Then page_title should include translation for "privacy.title"
When I click on decline_privacy_terms ()
Then I can navigate back to the previous page with title "document_selector.title"

Examples:
| locale |
| |
| es |
13 changes: 10 additions & 3 deletions test/features/step_definitions/sdk_steps.rb
Expand Up @@ -2,9 +2,15 @@

i18n = I18nHelper.new

Given(/^I verify with (passport|identity_card|drivers_license)(?:| with (.+)?)$/) do |document_type, locale|
Given(/^I initiate the verification process with(?: (.+)?)?$/) do |locale|
i18n.load_locale(locale)
steps %Q{
Given I navigate to the SDK with "#{locale}"
Then I click on primary_button (SDK)
}
end

Given(/^I verify with (passport|identity_card|drivers_license)(?: with (.+)?)?$/) do |document_type, locale|
if document_type == 'passport'
key = 'capture.passport.front.title'
elsif document_type == 'identity_card'
Expand All @@ -14,10 +20,11 @@
end

steps %Q{
Given I navigate to the SDK with "#{locale}"
When I click on primary_button (SDK)
Given I initiate the verification process with #{locale}
Then I should see 3 document_select_buttons ()
When I click on #{document_type} ()
Then page_title should include translation for "privacy.title"
When I click on confirm_privacy_terms ()
Then page_title should include translation for "#{key}"
And cross_device_header should include translation for "cross_device.switch_device.header"
}
Expand Down