Skip to content

Commit

Permalink
Add a settings dialog, with "dangerous mode" setting
Browse files Browse the repository at this point in the history
Also, in modals, improve handling of danger, and add delays.
  • Loading branch information
csillag committed Sep 27, 2022
1 parent 6a0320a commit cdf1ccb
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 9 deletions.
80 changes: 71 additions & 9 deletions src/app/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import { createContext, useCallback, useContext, useState } from 'react'
import React from 'react'
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { Box, Button, Layer, Heading, Paragraph } from 'grommet'
import { useTranslation } from 'react-i18next'
import { Alert, Checkmark, Close } from 'grommet-icons/icons'
import { AlertBox } from '../AlertBox'
import { selectAllowDangerousSetting } from '../SettingsDialog/slice/selectors'
import { useSelector } from 'react-redux'

interface Modal {
title: string
description: string
handleConfirm: () => void

/**
* Is this a dangerous operation?
*
* If marked as such, it will only be possible to execute it if the wallet is configured to run in dangerous mode.
*
* It also automatically implies a mandatory waiting time of 10 sec, unless specified otherwise.
*/
isDangerous: boolean

/**
* How long does the user have to wait before he can actually confirm the action?
*/
mustWaitSecs?: number
}

interface ModalContainerProps {
Expand All @@ -32,27 +49,72 @@ const ModalContainer = ({ modal, closeModal }: ModalContainerProps) => {
modal.handleConfirm()
closeModal()
}, [closeModal, modal])
const { isDangerous, mustWaitSecs } = modal
const allowDangerous = useSelector(selectAllowDangerousSetting)
const forbidden = isDangerous && !allowDangerous
const waitingTime = forbidden
? 0 // If the action is forbidden, there is nothing to wait for
: isDangerous
? mustWaitSecs ?? 10 // For dangerous actions, we require 10 seconds of waiting, unless specified otherwise.
: mustWaitSecs ?? 0 // For normal, non-dangerous operations, just use what was specified

const [secsLeft, setSecsLeft] = useState(0)

useEffect(() => {
if (waitingTime) {
setSecsLeft(waitingTime)
const stopCounting = () => window.clearInterval(interval)
const interval = window.setInterval(
() =>
setSecsLeft(seconds => {
const remains = seconds - 1
if (!remains) stopCounting()
return remains
}),
1000,
)
return stopCounting
}
}, [waitingTime])

return (
<Layer modal onEsc={closeModal} onClickOutside={closeModal} background="background-front">
<Box margin="medium">
<Heading size="small">{modal.title}</Heading>
<Paragraph fill>{modal.description}</Paragraph>
{forbidden && (
<AlertBox color={'status-error'}>
{t(
'dangerMode.youDontWantThis',
"You most probably don't want to do this, so I won't allow it. If you really do, then please enable the 'dangerous mode' in wallet settings, and try again.",
)}
</AlertBox>
)}
{isDangerous && allowDangerous && (
<AlertBox color={'status-warning'}>
{t(
'dangerMode.youCanButDoYouWant',
"You most probably shouldn't do this, but since you have specifically enabled 'dangerous mode' in wallet settings, we won't stop you.",
)}
</AlertBox>
)}
<Box direction="row" gap="small" alignSelf="end" pad={{ top: 'large' }}>
<Button
label={t('common.cancel', 'Cancel')}
onClick={closeModal}
secondary
icon={<Close size="18px" />}
/>
<Button
label={t('common.confirm', 'Confirm')}
onClick={confirm}
disabled={modal.isDangerous}
primary={modal.isDangerous}
color={modal.isDangerous ? 'status-error' : ''}
icon={modal.isDangerous ? <Alert size="18px" /> : <Checkmark size="18px" />}
/>
{!forbidden && (
<Button
label={t('common.confirm', 'Confirm') + (secsLeft ? ` (${secsLeft})` : '')}
onClick={confirm}
disabled={!!secsLeft}
primary={modal.isDangerous}
color={modal.isDangerous ? 'status-error' : ''}
icon={modal.isDangerous ? <Alert size="18px" /> : <Checkmark size="18px" />}
/>
)}
</Box>
</Box>
</Layer>
Expand Down
22 changes: 22 additions & 0 deletions src/app/components/SettingsButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useState } from 'react'
import { SidebarButton } from '../Sidebar'
import { Configure } from 'grommet-icons/icons'
import { useTranslation } from 'react-i18next'
import { SettingsDialog } from '../SettingsDialog'

export const SettingsButton = () => {
const [layerVisibility, setLayerVisibility] = useState(false)
const { t } = useTranslation()
return (
<>
<SidebarButton
icon={<Configure />}
label={t('menu.settings', 'Settings')}
onClick={() => {
setLayerVisibility(true)
}}
/>
{layerVisibility && <SettingsDialog closeHandler={() => setLayerVisibility(false)} />}
</>
)
}
80 changes: 80 additions & 0 deletions src/app/components/SettingsDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { ResponsiveLayer } from '../ResponsiveLayer'
import { Box, Button, Heading, Paragraph, RadioButtonGroup, ResponsiveContext } from 'grommet'
import { useDispatch, useSelector } from 'react-redux'
import { selectAllowDangerousSetting } from './slice/selectors'
import { Threats } from 'grommet-icons'
import { settingsActions } from './slice'

interface SettingsDialogProps {
closeHandler: () => void
}

export const SettingsDialog = (props: SettingsDialogProps) => {
const { t } = useTranslation()
const size = useContext(ResponsiveContext)

const dispatch = useDispatch()
const dangerousMode = useSelector(selectAllowDangerousSetting)

return (
<ResponsiveLayer
onClickOutside={props.closeHandler}
onEsc={props.closeHandler}
animation="slide"
background="background-front"
modal
>
<Box pad={{ vertical: 'small' }} margin="medium" width={size === 'small' ? 'auto' : '700px'}>
<Heading size="1" margin={{ vertical: 'small' }}>
{t('settings.dialog.title', 'Wallet settings')}
</Heading>
<Paragraph fill>
{t(
'settings.dialog.description',
'This is where you can configure the behavior of the Oasis Wallet.',
)}
</Paragraph>
<Box
gap="small"
pad={{ vertical: 'medium', right: 'small' }}
overflow={{ vertical: 'auto' }}
height={{ max: '400px' }}
>
<Paragraph fill>
<strong>
{t(
'dangerMode.description',
'Dangerous mode: should the wallet let the user shoot himself in the foot?',
)}
</strong>
</Paragraph>
<RadioButtonGroup
name="doc"
options={[
{
value: false,
label: t('dangerMode.off', 'Off - Refuse to execute nonsensical actions'),
},
{
value: true,
label: (
<span>
{t('dangerMode.on', "On - Allow executing nonsensical actions. Don't blame Oasis!")}{' '}
<Threats size={'large'} />
</span>
),
},
]}
value={dangerousMode}
onChange={event => dispatch(settingsActions.setAllowDangerous(event.target.value === 'true'))}
/>
</Box>
<Box align="end" pad={{ top: 'medium' }}>
<Button primary label={t('settings.dialog.close', 'Close')} onClick={props.closeHandler} />
</Box>
</Box>
</ResponsiveLayer>
)
}
22 changes: 22 additions & 0 deletions src/app/components/SettingsDialog/slice/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from 'utils/@reduxjs/toolkit'

import { SettingsState } from './types'

export const initialState: SettingsState = {
allowDangerous: false,
}

const slice = createSlice({
name: 'settings',
initialState,
reducers: {
setAllowDangerous(state, action: PayloadAction<boolean>) {
state.allowDangerous = action.payload
},
},
})

export const { actions: settingsActions } = slice

export default slice.reducer
8 changes: 8 additions & 0 deletions src/app/components/SettingsDialog/slice/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createSelector } from '@reduxjs/toolkit'

import { RootState } from 'types'
import { initialState } from '.'

const selectSlice = (state: RootState) => state.settings || initialState

export const selectAllowDangerousSetting = createSelector([selectSlice], settings => settings.allowDangerous)
3 changes: 3 additions & 0 deletions src/app/components/SettingsDialog/slice/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SettingsState {
allowDangerous: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,39 @@ exports[`<Navigation /> should match snapshot 1`] = `
<div
class="c6"
/>
<button
aria-label="menu.settings"
class="c13"
type="button"
>
<div
class="c14"
>
<svg
aria-label="Configure"
class="c10"
viewBox="0 0 24 24"
>
<path
d="M16 15c4.009-.065 7-3.033 7-7 0-3.012-.997-2.015-2-1-.991.98-3 3-3 3l-4-4s2.02-2.009 3-3c1.015-1.003 1.015-2-1-2-3.967 0-6.947 2.991-7 7 .042.976 0 3 0 3-1.885 1.897-4.34 4.353-6 6-2.932 2.944 1.056 6.932 4 4 1.65-1.662 4.113-4.125 6-6 0 0 2.024-.042 3 0z"
fill="none"
stroke="#000"
stroke-width="2"
/>
</svg>
<div
class="c11"
/>
<span
class="c5"
>
menu.settings
</span>
</div>
</button>
<div
class="c6"
/>
<a
aria-label="GitHub"
href="https://github.com/oasisprotocol/oasis-wallet-web"
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Language } from '../../../styles/theme/icons/language/Language'
import { ThemeSwitcher } from '../ThemeSwitcher'
import logotype from '../../../../public/logo192.png'
import { languageLabels } from '../../../locales/i18n'
import { SettingsButton } from '../SettingsButton'

const SidebarTooltip = (props: { children: React.ReactNode; isActive: boolean; label: string }) => {
const size = useContext(ResponsiveContext)
Expand Down Expand Up @@ -211,6 +212,7 @@ const SidebarFooter = (props: SidebarFooterProps) => {
</Menu>
</Box>
</SidebarTooltip>
<SettingsButton />
<SidebarButton
icon={<Github />}
label="GitHub"
Expand Down
15 changes: 15 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@
"newMnemonic": "Generate a new mnemonic",
"thisIsYourPhrase": "This is your mnemonic"
},
"dangerMode": {
"description": "Dangerous mode: should the wallet let the user shoot himself in the foot?",
"off": "Off - Refuse to execute nonsensical actions",
"on": "On - Allow executing nonsensical actions. Don't blame Oasis!",
"youCanButDoYouWant": "You most probably shouldn't do this, but since you have specifically enabled 'dangerous mode' in wallet settings, we won't stop you.",
"youDontWantThis": "You most probably don't want to do this, so I won't allow it. If you really do, then please enable the 'dangerous mode' in wallet settings, and try again."
},
"delegations": {
"activeDelegations": "Active delegations",
"debondingDelegations": "Debonding delegations",
Expand Down Expand Up @@ -193,6 +200,7 @@
"menu": {
"closeWallet": "Close wallet",
"home": "Home",
"settings": "Settings",
"stake": "Stake",
"wallet": "Wallet"
},
Expand Down Expand Up @@ -229,6 +237,13 @@
"showPrivateKey": "Show private key"
}
},
"settings": {
"dialog": {
"close": "Close",
"description": "This is where you can configure the behavior of the Oasis Wallet.",
"title": "Wallet settings"
}
},
"theme": {
"darkMode": "Dark mode",
"lightMode": "Light mode"
Expand Down
2 changes: 2 additions & 0 deletions src/store/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import stakingReducer from 'app/state/staking'
import transactionReducer from 'app/state/transaction'
import walletReducer from 'app/state/wallet'
import themeReducer from 'styles/theme/slice'
import settingReducer from 'app/components/SettingsDialog/slice'

export function createReducer() {
const rootReducer = combineReducers({
Expand All @@ -24,6 +25,7 @@ export function createReducer() {
theme: themeReducer,
transaction: transactionReducer,
wallet: walletReducer,
settings: settingReducer,
})

return rootReducer
Expand Down
2 changes: 2 additions & 0 deletions src/types/RootState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TransactionState } from 'app/state/transaction/types'
import { ImportAccountsState } from 'app/state/importaccounts/types'
import { StakingState } from 'app/state/staking/types'
import { FatalErrorState } from 'app/state/fatalerror/types'
import { SettingsState } from '../app/components/SettingsDialog/slice/types'
// [IMPORT NEW CONTAINERSTATE ABOVE] < Needed for generating containers seamlessly

export interface RootState {
Expand All @@ -20,6 +21,7 @@ export interface RootState {
importAccounts: ImportAccountsState
staking: StakingState
fatalError: FatalErrorState
settings: SettingsState
// [INSERT NEW REDUCER KEY ABOVE] < Needed for generating containers seamlessly
}

Expand Down

0 comments on commit cdf1ccb

Please sign in to comment.