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

feat(protocol-designer): implement selective redux persistence #2436

Merged
merged 2 commits into from
Oct 8, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions protocol-designer/src/analytics/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @flow
import {initializeAnalytics, shutdownAnalytics} from './integrations'

export type SetOptIn = {
type: 'SET_OPT_IN',
payload: boolean,
}

const _setOptIn = (payload: $PropertyType<SetOptIn, 'payload'>): SetOptIn => {
// side effects
if (payload) {
initializeAnalytics()
} else {
shutdownAnalytics()
}

return {
type: 'SET_OPT_IN',
payload,
}
}

export const optIn = () => _setOptIn(true)
export const optOut = () => _setOptIn(false)
56 changes: 10 additions & 46 deletions protocol-designer/src/analytics/index.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,14 @@
/* eslint-disable */
// @flow
import * as actions from './actions'
import * as selectors from './selectors'
import {rootReducer, type RootState} from './reducers'

export const shutdownAnalytics = () => {
if (window[window['_fs_namespace']]) { window[window['_fs_namespace']].shutdown() }
delete window[window['_fs_namespace']]
export {
actions,
selectors,
rootReducer,
}

export const optIn = () => {
try {
window.localStorage.setItem('optedInToAnalytics', true)
} catch(e) {
console.error('attempted to persist analytics preference in localStorage, but failed with error: ', e)
return false
}
return true
}

export const optOut = () => {
try {
window.localStorage.setItem('optedInToAnalytics', false)
} catch(e) {
console.error('attempted to persist analytics preference in localStorage, but failed with error: ', e)
return false
}
return true
}

export const getHasOptedIn = () => (
JSON.parse(window.localStorage.getItem('optedInToAnalytics'))
)

// NOTE: this code snippet is distributed by FullStory and formatting has been maintained
window['_fs_debug'] = false;
window['_fs_host'] = 'fullstory.com';
window['_fs_org'] = process.env.OT_PD_FULLSTORY_ORG;
window['_fs_namespace'] = 'FS';

export const initializeAnalytics = () => {
(function(m,n,e,t,l,o,g,y){
if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;}
g=m[e]=function(a,b){g.q?g.q.push([a,b]):g._api(a,b);};g.q=[];
o=n.createElement(t);o.async=1;o.src='https://'+_fs_host+'/s/fs.js';
y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);
g.identify=function(i,v){g(l,{uid:i});if(v)g(l,v)};g.setUserVars=function(v){g(l,v)};g.event=function(i,v){g('event',{n:i,p:v})};
g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)};
g.consent=function(a){g("consent",!arguments.length||a)};
g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};
g.clearUserCookie=function(){};
})(window,document,window['_fs_namespace'],'script','user');
export type {
RootState,
}
26 changes: 26 additions & 0 deletions protocol-designer/src/analytics/integrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable */
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this file all copied from the previous analytics/index.js


export const shutdownAnalytics = () => {
if (window[window['_fs_namespace']]) { window[window['_fs_namespace']].shutdown() }
delete window[window['_fs_namespace']]
}

// NOTE: this code snippet is distributed by FullStory and formatting has been maintained
window['_fs_debug'] = false;
window['_fs_host'] = 'fullstory.com';
window['_fs_org'] = process.env.OT_PD_FULLSTORY_ORG;
window['_fs_namespace'] = 'FS';

export const initializeAnalytics = () => {
(function(m,n,e,t,l,o,g,y){
if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;}
g=m[e]=function(a,b){g.q?g.q.push([a,b]):g._api(a,b);};g.q=[];
o=n.createElement(t);o.async=1;o.src='https://'+_fs_host+'/s/fs.js';
y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);
g.identify=function(i,v){g(l,{uid:i});if(v)g(l,v)};g.setUserVars=function(v){g(l,v)};g.event=function(i,v){g('event',{n:i,p:v})};
g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)};
g.consent=function(a){g("consent",!arguments.length||a)};
g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};
g.clearUserCookie=function(){};
})(window,document,window['_fs_namespace'],'script','user');
}
23 changes: 23 additions & 0 deletions protocol-designer/src/analytics/reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @flow
import {combineReducers} from 'redux'
import {handleActions} from 'redux-actions'
import {rehydrate} from '../persist'

import type {SetOptIn} from './actions'

type OptInState = boolean | null
const optInInitialState = null
const hasOptedIn = handleActions({
SET_OPT_IN: (state: OptInState, action: SetOptIn): OptInState => action.payload,
REHYDRATE_PERSISTED: () => rehydrate('analytics.hasOptedIn', optInInitialState),
}, optInInitialState)

const _allReducers = {
hasOptedIn,
}

export type RootState = {
hasOptedIn: OptInState,
}

export const rootReducer = combineReducers(_allReducers)
4 changes: 4 additions & 0 deletions protocol-designer/src/analytics/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @flow
import type {BaseState} from '../types'

export const getHasOptedIn = (state: BaseState) => state.analytics.hasOptedIn
96 changes: 53 additions & 43 deletions protocol-designer/src/components/SettingsPage/Privacy.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,66 @@
// @flow
import React from 'react'
// import {connect} from 'react-redux'
import {connect} from 'react-redux'
import i18n from '../../localization'
import {Card, ToggleButton} from '@opentrons/components'
// import type {BaseState} from '../types'
import styles from './SettingsPage.css'
import {
optIn,
optOut,
getHasOptedIn,
shutdownAnalytics,
initializeAnalytics,
actions as analyticsActions,
selectors as analyticsSelectors,
} from '../../analytics'
import type {BaseState} from '../../types'

type State = {optInToggleValue: boolean}
class Privacy extends React.Component<*, State> {
state: State = {optInToggleValue: getHasOptedIn()}
toggleAnalyticsOptInValue = () => {
const hasOptedIn = getHasOptedIn()
if (hasOptedIn) {
shutdownAnalytics()
if (optOut()) this.setState({optInToggleValue: false})
} else {
initializeAnalytics()
if (optIn()) this.setState({optInToggleValue: true})
}
return true
type Props = {
hasOptedIn: boolean | null,
toggleOptedIn: () => mixed,
}

type SP = {
hasOptedIn: $PropertyType<Props, 'hasOptedIn'>,
}

function Privacy (props: Props) {
const {hasOptedIn, toggleOptedIn} = props
return (
<div className={styles.card_wrapper}>
<Card title={i18n.t('card.title.privacy')}>
<div className={styles.toggle_row}>
<p className={styles.toggle_label}>{i18n.t('card.toggle.share_session')}</p>
<ToggleButton
className={styles.toggle_button}
toggledOn={Boolean(hasOptedIn)}
onClick={toggleOptedIn} />
</div>
<div className={styles.body_wrapper}>
<p className={styles.card_body}>{i18n.t('card.body.reason_for_collecting_data')}</p>
<ul className={styles.card_point_list}>
<li>{i18n.t('card.body.data_collected_is_internal')}</li>
{/* TODO: BC 2018-09-26 uncomment when only using fullstory <li>{i18n.t('card.body.data_only_from_pd')}</li> */}
<li>{i18n.t('card.body.opt_out_of_data_collection')}</li>
</ul>
</div>
</Card>
</div>
)
}

function mapStateToProps (state: BaseState): SP {
return {
hasOptedIn: analyticsSelectors.getHasOptedIn(state),
}
}

function mergeProps (stateProps: SP, dispatchProps: {dispatch: Dispatch<*>}): Props {
const {dispatch} = dispatchProps
const {hasOptedIn} = stateProps

render () {
return (
<div className={styles.card_wrapper}>
<Card title={i18n.t('card.title.privacy')}>
<div className={styles.toggle_row}>
<p className={styles.toggle_label}>{i18n.t('card.toggle.share_session')}</p>
<ToggleButton
className={styles.toggle_button}
toggledOn={this.state.optInToggleValue}
onClick={this.toggleAnalyticsOptInValue} />
</div>
<div className={styles.body_wrapper}>
<p className={styles.card_body}>{i18n.t('card.body.reason_for_collecting_data')}</p>
<ul className={styles.card_point_list}>
<li>{i18n.t('card.body.data_collected_is_internal')}</li>
{/* TODO: BC 2018-09-26 uncomment when only using fullstory <li>{i18n.t('card.body.data_only_from_pd')}</li> */}
<li>{i18n.t('card.body.opt_out_of_data_collection')}</li>
</ul>
</div>
</Card>
</div>
)
const _toggleOptedIn = hasOptedIn
? analyticsActions.optOut
: analyticsActions.optIn
return {
...stateProps,
toggleOptedIn: () => dispatch(_toggleOptedIn()),
}
}

export default Privacy
export default connect(mapStateToProps, null, mergeProps)(Privacy)
111 changes: 52 additions & 59 deletions protocol-designer/src/components/modals/AnalyticsModal.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,67 @@
// @flow
import * as React from 'react'
import {connect} from 'react-redux'
import cx from 'classnames'
import { AlertModal } from '@opentrons/components'
import i18n from '../../localization'
import modalStyles from './modal.css'
import settingsStyles from '../SettingsPage/SettingsPage.css'
import {
initializeAnalytics,
shutdownAnalytics,
optIn,
optOut,
getHasOptedIn,
actions as analyticsActions,
selectors as analyticsSelectors,
} from '../../analytics'
import type {BaseState} from '../../types'

type State = {isAnalyticsModalOpen: boolean}
type Props = {
hasOptedIn: boolean | null,
optIn: () => mixed,
optOut: () => mixed,
}

class AnalyticsModal extends React.Component<*, State> {
constructor () {
super()
const hasOptedIn = getHasOptedIn()
let initialState = {isAnalyticsModalOpen: false}
if (!!process.env.OT_PD_FULLSTORY_ORG && hasOptedIn === null) { // NOTE: only null if never set and has env variable
initialState = {isAnalyticsModalOpen: true}
} else if (hasOptedIn === true) {
initializeAnalytics()
} else {
// sanity check: there shouldn't be an analytics session, but shutdown just in case if user opted out
shutdownAnalytics()
}
this.state = initialState
}
handleCloseAnalyticsModal = () => {
this.setState({isAnalyticsModalOpen: false})
}
render () {
if (!this.state.isAnalyticsModalOpen) return null
return (
<AlertModal
className={cx(modalStyles.modal)}
buttons={[
{
onClick: () => {
this.handleCloseAnalyticsModal()
optOut()
shutdownAnalytics() // sanity check, there shouldn't be an analytics instance yet
},
children: i18n.t('button.no'),
},
{
onClick: () => {
this.handleCloseAnalyticsModal()
optIn()
initializeAnalytics()
},
children: i18n.t('button.yes'),
},
]}>
<h3>{i18n.t('card.toggle.share_session')}</h3>
<div className={settingsStyles.body_wrapper}>
<p className={settingsStyles.card_body}>{i18n.t('card.body.reason_for_collecting_data')}</p>
<ul className={settingsStyles.card_point_list}>
<li>{i18n.t('card.body.data_collected_is_internal')}</li>
{/* TODO: BC 2018-09-26 uncomment when only using fullstory <li>{i18n.t('card.body.data_only_from_pd')}</li> */}
<li>{i18n.t('card.body.opt_out_of_data_collection')}</li>
</ul>
</div>
</AlertModal>
type SP = {
hasOptedIn: $PropertyType<Props, 'hasOptedIn'>,
}

type DP = $Diff<Props, SP>

function AnalyticsModal (props: Props) {
const {hasOptedIn, optIn, optOut} = props
if (hasOptedIn !== null) return null
return (
<AlertModal
className={cx(modalStyles.modal)}
buttons={[
{
onClick: optOut,
children: i18n.t('button.no'),
},
{
onClick: optIn,
children: i18n.t('button.yes'),
},
]}>
<h3>{i18n.t('card.toggle.share_session')}</h3>
<div className={settingsStyles.body_wrapper}>
<p className={settingsStyles.card_body}>{i18n.t('card.body.reason_for_collecting_data')}</p>
<ul className={settingsStyles.card_point_list}>
<li>{i18n.t('card.body.data_collected_is_internal')}</li>
{/* TODO: BC 2018-09-26 uncomment when only using fullstory <li>{i18n.t('card.body.data_only_from_pd')}</li> */}
<li>{i18n.t('card.body.opt_out_of_data_collection')}</li>
</ul>
</div>
</AlertModal>
)
}

function mapStateToProps (state: BaseState): SP {
return {hasOptedIn: analyticsSelectors.getHasOptedIn(state)}
}

)
function mapDispatchToProps (dispatch: Dispatch<*>): DP {
return {
optIn: () => dispatch(analyticsActions.optIn()),
optOut: () => dispatch(analyticsActions.optOut()),
}
}

export default AnalyticsModal
export default connect(mapStateToProps, mapDispatchToProps)(AnalyticsModal)
Loading