Skip to content

Commit cdf1ccb

Browse files
committed
Add a settings dialog, with "dangerous mode" setting
Also, in modals, improve handling of danger, and add delays.
1 parent 6a0320a commit cdf1ccb

File tree

11 files changed

+260
-9
lines changed

11 files changed

+260
-9
lines changed

src/app/components/Modal/index.tsx

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
1-
import { createContext, useCallback, useContext, useState } from 'react'
1+
import React from 'react'
2+
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
23
import { Box, Button, Layer, Heading, Paragraph } from 'grommet'
34
import { useTranslation } from 'react-i18next'
45
import { Alert, Checkmark, Close } from 'grommet-icons/icons'
6+
import { AlertBox } from '../AlertBox'
7+
import { selectAllowDangerousSetting } from '../SettingsDialog/slice/selectors'
8+
import { useSelector } from 'react-redux'
59

610
interface Modal {
711
title: string
812
description: string
913
handleConfirm: () => void
14+
15+
/**
16+
* Is this a dangerous operation?
17+
*
18+
* If marked as such, it will only be possible to execute it if the wallet is configured to run in dangerous mode.
19+
*
20+
* It also automatically implies a mandatory waiting time of 10 sec, unless specified otherwise.
21+
*/
1022
isDangerous: boolean
23+
24+
/**
25+
* How long does the user have to wait before he can actually confirm the action?
26+
*/
27+
mustWaitSecs?: number
1128
}
1229

1330
interface ModalContainerProps {
@@ -32,27 +49,72 @@ const ModalContainer = ({ modal, closeModal }: ModalContainerProps) => {
3249
modal.handleConfirm()
3350
closeModal()
3451
}, [closeModal, modal])
52+
const { isDangerous, mustWaitSecs } = modal
53+
const allowDangerous = useSelector(selectAllowDangerousSetting)
54+
const forbidden = isDangerous && !allowDangerous
55+
const waitingTime = forbidden
56+
? 0 // If the action is forbidden, there is nothing to wait for
57+
: isDangerous
58+
? mustWaitSecs ?? 10 // For dangerous actions, we require 10 seconds of waiting, unless specified otherwise.
59+
: mustWaitSecs ?? 0 // For normal, non-dangerous operations, just use what was specified
60+
61+
const [secsLeft, setSecsLeft] = useState(0)
62+
63+
useEffect(() => {
64+
if (waitingTime) {
65+
setSecsLeft(waitingTime)
66+
const stopCounting = () => window.clearInterval(interval)
67+
const interval = window.setInterval(
68+
() =>
69+
setSecsLeft(seconds => {
70+
const remains = seconds - 1
71+
if (!remains) stopCounting()
72+
return remains
73+
}),
74+
1000,
75+
)
76+
return stopCounting
77+
}
78+
}, [waitingTime])
3579

3680
return (
3781
<Layer modal onEsc={closeModal} onClickOutside={closeModal} background="background-front">
3882
<Box margin="medium">
3983
<Heading size="small">{modal.title}</Heading>
4084
<Paragraph fill>{modal.description}</Paragraph>
85+
{forbidden && (
86+
<AlertBox color={'status-error'}>
87+
{t(
88+
'dangerMode.youDontWantThis',
89+
"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.",
90+
)}
91+
</AlertBox>
92+
)}
93+
{isDangerous && allowDangerous && (
94+
<AlertBox color={'status-warning'}>
95+
{t(
96+
'dangerMode.youCanButDoYouWant',
97+
"You most probably shouldn't do this, but since you have specifically enabled 'dangerous mode' in wallet settings, we won't stop you.",
98+
)}
99+
</AlertBox>
100+
)}
41101
<Box direction="row" gap="small" alignSelf="end" pad={{ top: 'large' }}>
42102
<Button
43103
label={t('common.cancel', 'Cancel')}
44104
onClick={closeModal}
45105
secondary
46106
icon={<Close size="18px" />}
47107
/>
48-
<Button
49-
label={t('common.confirm', 'Confirm')}
50-
onClick={confirm}
51-
disabled={modal.isDangerous}
52-
primary={modal.isDangerous}
53-
color={modal.isDangerous ? 'status-error' : ''}
54-
icon={modal.isDangerous ? <Alert size="18px" /> : <Checkmark size="18px" />}
55-
/>
108+
{!forbidden && (
109+
<Button
110+
label={t('common.confirm', 'Confirm') + (secsLeft ? ` (${secsLeft})` : '')}
111+
onClick={confirm}
112+
disabled={!!secsLeft}
113+
primary={modal.isDangerous}
114+
color={modal.isDangerous ? 'status-error' : ''}
115+
icon={modal.isDangerous ? <Alert size="18px" /> : <Checkmark size="18px" />}
116+
/>
117+
)}
56118
</Box>
57119
</Box>
58120
</Layer>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React, { useState } from 'react'
2+
import { SidebarButton } from '../Sidebar'
3+
import { Configure } from 'grommet-icons/icons'
4+
import { useTranslation } from 'react-i18next'
5+
import { SettingsDialog } from '../SettingsDialog'
6+
7+
export const SettingsButton = () => {
8+
const [layerVisibility, setLayerVisibility] = useState(false)
9+
const { t } = useTranslation()
10+
return (
11+
<>
12+
<SidebarButton
13+
icon={<Configure />}
14+
label={t('menu.settings', 'Settings')}
15+
onClick={() => {
16+
setLayerVisibility(true)
17+
}}
18+
/>
19+
{layerVisibility && <SettingsDialog closeHandler={() => setLayerVisibility(false)} />}
20+
</>
21+
)
22+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useContext } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { ResponsiveLayer } from '../ResponsiveLayer'
4+
import { Box, Button, Heading, Paragraph, RadioButtonGroup, ResponsiveContext } from 'grommet'
5+
import { useDispatch, useSelector } from 'react-redux'
6+
import { selectAllowDangerousSetting } from './slice/selectors'
7+
import { Threats } from 'grommet-icons'
8+
import { settingsActions } from './slice'
9+
10+
interface SettingsDialogProps {
11+
closeHandler: () => void
12+
}
13+
14+
export const SettingsDialog = (props: SettingsDialogProps) => {
15+
const { t } = useTranslation()
16+
const size = useContext(ResponsiveContext)
17+
18+
const dispatch = useDispatch()
19+
const dangerousMode = useSelector(selectAllowDangerousSetting)
20+
21+
return (
22+
<ResponsiveLayer
23+
onClickOutside={props.closeHandler}
24+
onEsc={props.closeHandler}
25+
animation="slide"
26+
background="background-front"
27+
modal
28+
>
29+
<Box pad={{ vertical: 'small' }} margin="medium" width={size === 'small' ? 'auto' : '700px'}>
30+
<Heading size="1" margin={{ vertical: 'small' }}>
31+
{t('settings.dialog.title', 'Wallet settings')}
32+
</Heading>
33+
<Paragraph fill>
34+
{t(
35+
'settings.dialog.description',
36+
'This is where you can configure the behavior of the Oasis Wallet.',
37+
)}
38+
</Paragraph>
39+
<Box
40+
gap="small"
41+
pad={{ vertical: 'medium', right: 'small' }}
42+
overflow={{ vertical: 'auto' }}
43+
height={{ max: '400px' }}
44+
>
45+
<Paragraph fill>
46+
<strong>
47+
{t(
48+
'dangerMode.description',
49+
'Dangerous mode: should the wallet let the user shoot himself in the foot?',
50+
)}
51+
</strong>
52+
</Paragraph>
53+
<RadioButtonGroup
54+
name="doc"
55+
options={[
56+
{
57+
value: false,
58+
label: t('dangerMode.off', 'Off - Refuse to execute nonsensical actions'),
59+
},
60+
{
61+
value: true,
62+
label: (
63+
<span>
64+
{t('dangerMode.on', "On - Allow executing nonsensical actions. Don't blame Oasis!")}{' '}
65+
<Threats size={'large'} />
66+
</span>
67+
),
68+
},
69+
]}
70+
value={dangerousMode}
71+
onChange={event => dispatch(settingsActions.setAllowDangerous(event.target.value === 'true'))}
72+
/>
73+
</Box>
74+
<Box align="end" pad={{ top: 'medium' }}>
75+
<Button primary label={t('settings.dialog.close', 'Close')} onClick={props.closeHandler} />
76+
</Box>
77+
</Box>
78+
</ResponsiveLayer>
79+
)
80+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { PayloadAction } from '@reduxjs/toolkit'
2+
import { createSlice } from 'utils/@reduxjs/toolkit'
3+
4+
import { SettingsState } from './types'
5+
6+
export const initialState: SettingsState = {
7+
allowDangerous: false,
8+
}
9+
10+
const slice = createSlice({
11+
name: 'settings',
12+
initialState,
13+
reducers: {
14+
setAllowDangerous(state, action: PayloadAction<boolean>) {
15+
state.allowDangerous = action.payload
16+
},
17+
},
18+
})
19+
20+
export const { actions: settingsActions } = slice
21+
22+
export default slice.reducer
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createSelector } from '@reduxjs/toolkit'
2+
3+
import { RootState } from 'types'
4+
import { initialState } from '.'
5+
6+
const selectSlice = (state: RootState) => state.settings || initialState
7+
8+
export const selectAllowDangerousSetting = createSelector([selectSlice], settings => settings.allowDangerous)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface SettingsState {
2+
allowDangerous: boolean
3+
}

src/app/components/Sidebar/__tests__/__snapshots__/index.test.tsx.snap

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,39 @@ exports[`<Navigation /> should match snapshot 1`] = `
672672
<div
673673
class="c6"
674674
/>
675+
<button
676+
aria-label="menu.settings"
677+
class="c13"
678+
type="button"
679+
>
680+
<div
681+
class="c14"
682+
>
683+
<svg
684+
aria-label="Configure"
685+
class="c10"
686+
viewBox="0 0 24 24"
687+
>
688+
<path
689+
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"
690+
fill="none"
691+
stroke="#000"
692+
stroke-width="2"
693+
/>
694+
</svg>
695+
<div
696+
class="c11"
697+
/>
698+
<span
699+
class="c5"
700+
>
701+
menu.settings
702+
</span>
703+
</div>
704+
</button>
705+
<div
706+
class="c6"
707+
/>
675708
<a
676709
aria-label="GitHub"
677710
href="https://github.com/oasisprotocol/oasis-wallet-web"

src/app/components/Sidebar/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Language } from '../../../styles/theme/icons/language/Language'
2222
import { ThemeSwitcher } from '../ThemeSwitcher'
2323
import logotype from '../../../../public/logo192.png'
2424
import { languageLabels } from '../../../locales/i18n'
25+
import { SettingsButton } from '../SettingsButton'
2526

2627
const SidebarTooltip = (props: { children: React.ReactNode; isActive: boolean; label: string }) => {
2728
const size = useContext(ResponsiveContext)
@@ -211,6 +212,7 @@ const SidebarFooter = (props: SidebarFooterProps) => {
211212
</Menu>
212213
</Box>
213214
</SidebarTooltip>
215+
<SettingsButton />
214216
<SidebarButton
215217
icon={<Github />}
216218
label="GitHub"

src/locales/en/translation.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@
119119
"newMnemonic": "Generate a new mnemonic",
120120
"thisIsYourPhrase": "This is your mnemonic"
121121
},
122+
"dangerMode": {
123+
"description": "Dangerous mode: should the wallet let the user shoot himself in the foot?",
124+
"off": "Off - Refuse to execute nonsensical actions",
125+
"on": "On - Allow executing nonsensical actions. Don't blame Oasis!",
126+
"youCanButDoYouWant": "You most probably shouldn't do this, but since you have specifically enabled 'dangerous mode' in wallet settings, we won't stop you.",
127+
"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."
128+
},
122129
"delegations": {
123130
"activeDelegations": "Active delegations",
124131
"debondingDelegations": "Debonding delegations",
@@ -193,6 +200,7 @@
193200
"menu": {
194201
"closeWallet": "Close wallet",
195202
"home": "Home",
203+
"settings": "Settings",
196204
"stake": "Stake",
197205
"wallet": "Wallet"
198206
},
@@ -229,6 +237,13 @@
229237
"showPrivateKey": "Show private key"
230238
}
231239
},
240+
"settings": {
241+
"dialog": {
242+
"close": "Close",
243+
"description": "This is where you can configure the behavior of the Oasis Wallet.",
244+
"title": "Wallet settings"
245+
}
246+
},
232247
"theme": {
233248
"darkMode": "Dark mode",
234249
"lightMode": "Light mode"

src/store/reducers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import stakingReducer from 'app/state/staking'
1212
import transactionReducer from 'app/state/transaction'
1313
import walletReducer from 'app/state/wallet'
1414
import themeReducer from 'styles/theme/slice'
15+
import settingReducer from 'app/components/SettingsDialog/slice'
1516

1617
export function createReducer() {
1718
const rootReducer = combineReducers({
@@ -24,6 +25,7 @@ export function createReducer() {
2425
theme: themeReducer,
2526
transaction: transactionReducer,
2627
wallet: walletReducer,
28+
settings: settingReducer,
2729
})
2830

2931
return rootReducer

0 commit comments

Comments
 (0)