1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Auto logout after specific time (#6558)

* Add i18n strings

* Finish Auto timeout

* Fix linter

* Fix copies

* Add unit test to Advanced Tab component

* Add back actions and container

* Add basic test to ensure container completeness

* No zero, fix linters

* restrict negative in input
This commit is contained in:
Chi Kei Chan 2019-05-08 11:57:21 -07:00 committed by GitHub
parent 0497d209b2
commit 56ed189aeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 188 additions and 6 deletions

View File

@ -154,6 +154,12 @@
"attributions": { "attributions": {
"message": "Attributions" "message": "Attributions"
}, },
"autoLogoutTimeLimit": {
"message": "Auto-Logout Timer (minutes)"
},
"autoLogoutTimeLimitDescription": {
"message": "Set the number of idle time in minutes before Metamask automatically log out"
},
"available": { "available": {
"message": "Available" "message": "Available"
}, },

5
package-lock.json generated
View File

@ -32939,6 +32939,11 @@
"react-icon-base": "2.1.0" "react-icon-base": "2.1.0"
} }
}, },
"react-idle-timer": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-4.2.5.tgz",
"integrity": "sha512-8B/OwjG8E/DTx1fHYKTpZ4cnCbL9+LOc5I9t8SYe8tbEkP14KChiYg0xPIuyRpO33wUZHcgmQl93CVePaDhmRA=="
},
"react-input-autosize": { "react-input-autosize": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.1.2.tgz", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.1.2.tgz",

View File

@ -155,6 +155,7 @@
"react-dnd-html5-backend": "^7.4.4", "react-dnd-html5-backend": "^7.4.4",
"react-dom": "^15.6.2", "react-dom": "^15.6.2",
"react-hyperscript": "^3.0.0", "react-hyperscript": "^3.0.0",
"react-idle-timer": "^4.2.5",
"react-inspector": "^2.3.0", "react-inspector": "^2.3.0",
"react-markdown": "^3.0.0", "react-markdown": "^3.0.0",
"react-media": "^1.8.0", "react-media": "^1.8.0",

View File

@ -3,9 +3,10 @@ import PropTypes from 'prop-types'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Route, Switch, withRouter, matchPath } from 'react-router-dom' import { Route, Switch, withRouter, matchPath } from 'react-router-dom'
import { compose } from 'recompose' import { compose } from 'recompose'
import actions from '../../store/actions' import actions, {hideSidebar, hideWarning, lockMetamask} from '../../store/actions'
import log from 'loglevel' import log from 'loglevel'
import { getMetaMaskAccounts, getNetworkIdentifier } from '../../selectors/selectors' import IdleTimer from 'react-idle-timer'
import {getMetaMaskAccounts, getNetworkIdentifier, preferencesSelector} from '../../selectors/selectors'
// init // init
import FirstTimeFlow from '../first-time-flow' import FirstTimeFlow from '../first-time-flow'
@ -98,7 +99,9 @@ class Routes extends Component {
} }
renderRoutes () { renderRoutes () {
return ( const { autoLogoutTimeLimit, lockMetamask } = this.props
const routes = (
<Switch> <Switch>
<Route path={LOCK_ROUTE} component={Lock} exact /> <Route path={LOCK_ROUTE} component={Lock} exact />
<Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} /> <Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} />
@ -116,6 +119,19 @@ class Routes extends Component {
<Authenticated path={DEFAULT_ROUTE} component={Home} exact /> <Authenticated path={DEFAULT_ROUTE} component={Home} exact />
</Switch> </Switch>
) )
if (autoLogoutTimeLimit > 0) {
return (
<IdleTimer
onIdle={lockMetamask}
timeout={autoLogoutTimeLimit * 1000 * 60}
>
{routes}
</IdleTimer>
)
}
return routes
} }
onInitializationUnlockPage () { onInitializationUnlockPage () {
@ -322,6 +338,7 @@ Routes.propTypes = {
networkDropdownOpen: PropTypes.bool, networkDropdownOpen: PropTypes.bool,
showNetworkDropdown: PropTypes.func, showNetworkDropdown: PropTypes.func,
hideNetworkDropdown: PropTypes.func, hideNetworkDropdown: PropTypes.func,
lockMetamask: PropTypes.func,
history: PropTypes.object, history: PropTypes.object,
location: PropTypes.object, location: PropTypes.object,
dispatch: PropTypes.func, dispatch: PropTypes.func,
@ -344,6 +361,7 @@ Routes.propTypes = {
t: PropTypes.func, t: PropTypes.func,
providerId: PropTypes.string, providerId: PropTypes.string,
providerRequests: PropTypes.array, providerRequests: PropTypes.array,
autoLogoutTimeLimit: PropTypes.number,
} }
function mapStateToProps (state) { function mapStateToProps (state) {
@ -358,6 +376,7 @@ function mapStateToProps (state) {
} = appState } = appState
const accounts = getMetaMaskAccounts(state) const accounts = getMetaMaskAccounts(state)
const { autoLogoutTimeLimit = 0 } = preferencesSelector(state)
const { const {
identities, identities,
@ -409,6 +428,7 @@ function mapStateToProps (state) {
Qr: state.appState.Qr, Qr: state.appState.Qr,
welcomeScreenSeen: state.metamask.welcomeScreenSeen, welcomeScreenSeen: state.metamask.welcomeScreenSeen,
providerId: getNetworkIdentifier(state), providerId: getNetworkIdentifier(state),
autoLogoutTimeLimit,
// state needed to get account dropdown temporarily rendering from app bar // state needed to get account dropdown temporarily rendering from app bar
identities, identities,
@ -427,6 +447,11 @@ function mapDispatchToProps (dispatch, ownProps) {
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
lockMetamask: () => {
dispatch(lockMetamask())
dispatch(hideWarning())
dispatch(hideSidebar())
},
} }
} }

View File

@ -24,6 +24,8 @@ export default class AdvancedTab extends PureComponent {
setAdvancedInlineGasFeatureFlag: PropTypes.func, setAdvancedInlineGasFeatureFlag: PropTypes.func,
advancedInlineGas: PropTypes.bool, advancedInlineGas: PropTypes.bool,
showFiatInTestnets: PropTypes.bool, showFiatInTestnets: PropTypes.bool,
autoLogoutTimeLimit: PropTypes.number,
setAutoLogoutTimeLimit: PropTypes.func.isRequired,
setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
} }
@ -355,6 +357,48 @@ export default class AdvancedTab extends PureComponent {
) )
} }
renderAutoLogoutTimeLimit () {
const { t } = this.context
const {
autoLogoutTimeLimit,
setAutoLogoutTimeLimit,
} = this.props
return (
<div className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{ t('autoLogoutTimeLimit') }</span>
<div className="settings-page__content-description">
{ t('autoLogoutTimeLimitDescription') }
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<TextField
type="number"
id="autoTimeout"
placeholder="5"
value={this.state.autoLogoutTimeLimit}
defaultValue={autoLogoutTimeLimit}
onChange={e => this.setState({ autoLogoutTimeLimit: Math.max(Number(e.target.value), 0) })}
fullWidth
margin="dense"
min={0}
/>
<button
className="button btn-primary settings-tab__rpc-save-button"
onClick={() => {
setAutoLogoutTimeLimit(this.state.autoLogoutTimeLimit)
}}
>
{ t('save') }
</button>
</div>
</div>
</div>
)
}
renderContent () { renderContent () {
const { warning } = this.props const { warning } = this.props
@ -368,6 +412,7 @@ export default class AdvancedTab extends PureComponent {
{ this.renderAdvancedGasInputInline() } { this.renderAdvancedGasInputInline() }
{ this.renderHexDataOptIn() } { this.renderHexDataOptIn() }
{ this.renderShowConversionInTestnets() } { this.renderShowConversionInTestnets() }
{ this.renderAutoLogoutTimeLimit() }
</div> </div>
) )
} }

View File

@ -8,10 +8,11 @@ import {
setFeatureFlag, setFeatureFlag,
showModal, showModal,
setShowFiatConversionOnTestnetsPreference, setShowFiatConversionOnTestnetsPreference,
setAutoLogoutTimeLimit,
} from '../../../store/actions' } from '../../../store/actions'
import {preferencesSelector} from '../../../selectors/selectors' import {preferencesSelector} from '../../../selectors/selectors'
const mapStateToProps = state => { export const mapStateToProps = state => {
const { appState: { warning }, metamask } = state const { appState: { warning }, metamask } = state
const { const {
featureFlags: { featureFlags: {
@ -19,17 +20,18 @@ const mapStateToProps = state => {
advancedInlineGas, advancedInlineGas,
} = {}, } = {},
} = metamask } = metamask
const { showFiatInTestnets } = preferencesSelector(state) const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state)
return { return {
warning, warning,
sendHexData, sendHexData,
advancedInlineGas, advancedInlineGas,
showFiatInTestnets, showFiatInTestnets,
autoLogoutTimeLimit,
} }
} }
const mapDispatchToProps = dispatch => { export const mapDispatchToProps = dispatch => {
return { return {
setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)), setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)),
@ -39,6 +41,9 @@ const mapDispatchToProps = dispatch => {
setShowFiatConversionOnTestnetsPreference: value => { setShowFiatConversionOnTestnetsPreference: value => {
return dispatch(setShowFiatConversionOnTestnetsPreference(value)) return dispatch(setShowFiatConversionOnTestnetsPreference(value))
}, },
setAutoLogoutTimeLimit: value => {
return dispatch(setAutoLogoutTimeLimit(value))
},
} }
} }

View File

@ -0,0 +1,44 @@
import React from 'react'
import assert from 'assert'
import sinon from 'sinon'
import { shallow } from 'enzyme'
import AdvancedTab from '../advanced-tab.component'
import TextField from '../../../../components/ui/text-field'
describe('AdvancedTab Component', () => {
it('should render correctly', () => {
const root = shallow(
<AdvancedTab />,
{
context: {
t: s => `_${s}`,
},
}
)
assert.equal(root.find('.settings-page__content-row').length, 8)
})
it('should update autoLogoutTimeLimit', () => {
const setAutoLogoutTimeLimitSpy = sinon.spy()
const root = shallow(
<AdvancedTab
setAutoLogoutTimeLimit={setAutoLogoutTimeLimitSpy}
/>,
{
context: {
t: s => `_${s}`,
},
}
)
const autoTimeout = root.find('.settings-page__content-row').last()
const textField = autoTimeout.find(TextField)
textField.props().onChange({ target: { value: 1440 } })
assert.equal(root.state().autoLogoutTimeLimit, 1440)
autoTimeout.find('button').simulate('click')
assert.equal(setAutoLogoutTimeLimitSpy.args[0][0], 1440)
})
})

View File

@ -0,0 +1,46 @@
import assert from 'assert'
import { mapStateToProps, mapDispatchToProps } from '../advanced-tab.container'
const defaultState = {
appState: {
warning: null,
},
metamask: {
featureFlags: {
sendHexData: false,
advancedInlineGas: false,
},
preferences: {
autoLogoutTimeLimit: 0,
showFiatInTestnets: false,
useNativeCurrencyAsPrimaryCurrency: true,
},
},
}
describe('AdvancedTab Container', () => {
it('should map state to props correctly', () => {
const props = mapStateToProps(defaultState)
const expected = {
warning: null,
sendHexData: false,
advancedInlineGas: false,
showFiatInTestnets: false,
autoLogoutTimeLimit: 0,
}
assert.deepEqual(props, expected)
})
it('should map dispatch to props correctly', () => {
const props = mapDispatchToProps(() => 'mockDispatch')
assert.ok(typeof props.setHexDataFeatureFlag === 'function')
assert.ok(typeof props.setRpcTarget === 'function')
assert.ok(typeof props.displayWarning === 'function')
assert.ok(typeof props.showResetAccountConfirmationModal === 'function')
assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function')
assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function')
assert.ok(typeof props.setAutoLogoutTimeLimit === 'function')
})
})

View File

@ -316,6 +316,7 @@ var actions = {
UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', UPDATE_PREFERENCES: 'UPDATE_PREFERENCES',
setUseNativeCurrencyAsPrimaryCurrencyPreference, setUseNativeCurrencyAsPrimaryCurrencyPreference,
setShowFiatConversionOnTestnetsPreference, setShowFiatConversionOnTestnetsPreference,
setAutoLogoutTimeLimit,
// Migration of users to new UI // Migration of users to new UI
setCompletedUiMigration, setCompletedUiMigration,
@ -2439,6 +2440,10 @@ function setShowFiatConversionOnTestnetsPreference (value) {
return setPreference('showFiatInTestnets', value) return setPreference('showFiatInTestnets', value)
} }
function setAutoLogoutTimeLimit (value) {
return setPreference('autoLogoutTimeLimit', value)
}
function setCompletedOnboarding () { function setCompletedOnboarding () {
return async dispatch => { return async dispatch => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())