diff --git a/src/app/__tests__/__snapshots__/index.test.tsx.snap b/src/app/__tests__/__snapshots__/index.test.tsx.snap index 00d2482a64..c5c4c86d5e 100644 --- a/src/app/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/__tests__/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` should render and match the snapshot 1`] = ` - + should render and match the snapshot 1`] = ` - + + `; diff --git a/src/app/components/AddEscrowForm/__tests__/index.test.tsx b/src/app/components/AddEscrowForm/__tests__/index.test.tsx index fbe3955cec..8c72486ee5 100644 --- a/src/app/components/AddEscrowForm/__tests__/index.test.tsx +++ b/src/app/components/AddEscrowForm/__tests__/index.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { ModalProvider } from 'app/components/Modal' +import { ModalContainer } from 'app/components/Modal' import { Validator } from 'app/state/staking/types' import * as React from 'react' import { Provider } from 'react-redux' @@ -12,9 +12,8 @@ import { AddEscrowForm } from '..' const renderComponent = (store: any, address: string, validatorStatus: Validator['status']) => render( - - - + + , ) diff --git a/src/app/components/AddEscrowForm/index.tsx b/src/app/components/AddEscrowForm/index.tsx index 54447266dc..b9738bf736 100644 --- a/src/app/components/AddEscrowForm/index.tsx +++ b/src/app/components/AddEscrowForm/index.tsx @@ -3,7 +3,8 @@ * AddEscrowForm * */ -import { useModal } from 'app/components/Modal' +import { Modal } from '../Modal/slice/types' +import { modalActions } from '../Modal/slice' import { parseRoseStringToBaseUnitString } from 'app/lib/helpers' import { selectMinStaking } from 'app/state/network/selectors' import { Validator } from 'app/state/staking/types' @@ -24,7 +25,7 @@ interface Props { export const AddEscrowForm = memo((props: Props) => { const { t } = useTranslation() - const { launchModal } = useModal() + const launchModal = (modal: Modal) => dispatch(modalActions.launch(modal)) const { error, success } = useSelector(selectTransaction) const isTop20 = props.validatorRank <= 20 const [showNotice, setShowNotice] = useState(isTop20) diff --git a/src/app/components/Modal/index.tsx b/src/app/components/Modal/index.tsx index 31ba5f9275..4182a9915f 100644 --- a/src/app/components/Modal/index.tsx +++ b/src/app/components/Modal/index.tsx @@ -1,44 +1,79 @@ -import { createContext, useCallback, useContext, useState } from 'react' +import { useEffect, useState } from 'react' import { Box, Button, Layer, Paragraph } from 'grommet' import { useTranslation } from 'react-i18next' -import { Alert, Checkmark, Close } from 'grommet-icons' +import { Alert, Checkmark, Close, Configure } from 'grommet-icons' import { ModalHeader } from 'app/components/Header' +import { AlertBox } from '../AlertBox' +import { selectAllowDangerousSetting } from '../SettingsDialog/slice/selectors' +import { useDispatch, useSelector } from 'react-redux' +import { settingsActions } from '../SettingsDialog/slice' +import { selectCurrentModal } from './slice/selectors' +import { modalActions } from './slice' -interface Modal { - title: string - description: string - handleConfirm: () => void - isDangerous: boolean -} - -interface ModalContainerProps { - modal: Modal - closeModal: () => void -} - -interface ModalContextProps { - launchModal: (modal: Modal) => void - closeModal: () => void -} +const ModalContainer = () => { + const dispatch = useDispatch() + const { t } = useTranslation() + const modal = useSelector(selectCurrentModal) + const allowDangerous = useSelector(selectAllowDangerousSetting) + const [secsLeft, setSecsLeft] = useState(0) + const closeModal = () => dispatch(modalActions.close()) + const confirm = () => { + modal?.handleConfirm() + dispatch(modalActions.close()) + } + const { isDangerous, mustWaitSecs } = modal || {} -interface ModalProviderProps { - children: React.ReactNode -} + 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 ModalContext = createContext({} as ModalContextProps) + const openSettings = () => { + dispatch(modalActions.stash()) + dispatch(settingsActions.setOpen(true)) + } -const ModalContainer = ({ modal, closeModal }: ModalContainerProps) => { - const { t } = useTranslation() - const confirm = useCallback(() => { - modal.handleConfirm() - closeModal() - }, [closeModal, modal]) + 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]) + if (!modal) return null return ( {modal.title} {modal.description} + {forbidden && ( + + {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.", + )} + + )} + {isDangerous && allowDangerous && ( + + {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.", + )} + + )} +
{ const size = useContext(ResponsiveContext) @@ -211,6 +212,7 @@ const SidebarFooter = (props: SidebarFooterProps) => { + } label="GitHub" diff --git a/src/app/index.tsx b/src/app/index.tsx index 20fb36d776..62c0f6efff 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -20,7 +20,7 @@ import { AccountPage } from './pages/AccountPage' import { CreateWalletPage } from './pages/CreateWalletPage' import { HomePage } from './pages/HomePage' import { OpenWalletPage } from './pages/OpenWalletPage' -import { ModalProvider } from './components/Modal' +import { ModalContainer } from './components/Modal' import { useRouteRedirects } from './useRouteRedirects' const AppMain = styled(Main)` @@ -33,7 +33,7 @@ export function App() { const size = useContext(ResponsiveContext) return ( - + <> - + + ) } diff --git a/src/app/pages/AccountPage/Features/SendTransaction/index.tsx b/src/app/pages/AccountPage/Features/SendTransaction/index.tsx index 3a2b2d1a12..2ac2cc17c7 100644 --- a/src/app/pages/AccountPage/Features/SendTransaction/index.tsx +++ b/src/app/pages/AccountPage/Features/SendTransaction/index.tsx @@ -1,5 +1,5 @@ import { TransactionStatus } from 'app/components/TransactionStatus' -import { useModal } from 'app/components/Modal' +import { modalActions } from '../../../../components/Modal/slice' import { transactionActions } from 'app/state/transaction' import { selectTransaction } from 'app/state/transaction/selectors' import { selectValidators } from 'app/state/staking/selectors' @@ -9,6 +9,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { parseRoseStringToBaseUnitString } from 'app/lib/helpers' +import { Modal } from '../../../../components/Modal/slice/types' export interface SendTransactionProps { isAddressInWallet: boolean @@ -21,7 +22,7 @@ export function SendTransaction(props: SendTransactionProps) { const { t } = useTranslation() const dispatch = useDispatch() - const { launchModal } = useModal() + const launchModal = (modal: Modal) => dispatch(modalActions.launch(modal)) const { error, success } = useSelector(selectTransaction) const validators = useSelector(selectValidators) const [recipient, setRecipient] = useState('') diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 74123c7725..469999d02a 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -121,6 +121,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", @@ -198,6 +205,7 @@ "menu": { "closeWallet": "Close wallet", "home": "Home", + "settings": "Settings", "stake": "Stake", "wallet": "Wallet" }, @@ -234,6 +242,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" diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts index 88a5a710a0..ede5a0e2b0 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -24,7 +24,13 @@ export function configureAppStore(state?: Partial) { const store = configureStore({ reducer: createReducer(), - middleware: getDefaultMiddleware => getDefaultMiddleware().concat(middlewares), + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + serializableCheck: { + ignoredActionPaths: ['payload.handleConfirm'], + ignoredPaths: ['modal.current.handleConfirm', 'modal.stash'], + }, + }).concat(middlewares), devTools: /* istanbul ignore next line */ process.env.NODE_ENV !== 'production', diff --git a/src/store/reducers.ts b/src/store/reducers.ts index eb5435e3cb..ccbd57890e 100644 --- a/src/store/reducers.ts +++ b/src/store/reducers.ts @@ -12,6 +12,8 @@ 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 modalReducer from 'app/components/Modal/slice' +import settingReducer from 'app/components/SettingsDialog/slice' export function createReducer() { const rootReducer = combineReducers({ @@ -24,6 +26,8 @@ export function createReducer() { theme: themeReducer, transaction: transactionReducer, wallet: walletReducer, + modal: modalReducer, + settings: settingReducer, }) return rootReducer diff --git a/src/types/RootState.ts b/src/types/RootState.ts index f3e356a3f7..ed31727099 100644 --- a/src/types/RootState.ts +++ b/src/types/RootState.ts @@ -8,6 +8,8 @@ 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 { ModalState } from '../app/components/Modal/slice/types' // [IMPORT NEW CONTAINERSTATE ABOVE] < Needed for generating containers seamlessly export interface RootState { @@ -20,6 +22,8 @@ export interface RootState { importAccounts: ImportAccountsState staking: StakingState fatalError: FatalErrorState + modal: ModalState + settings: SettingsState // [INSERT NEW REDUCER KEY ABOVE] < Needed for generating containers seamlessly }