Skip to content

Commit 39da44e

Browse files
committed
Refactor Modal so that
- The state is handled in Redux - No more ModalProvider - pop-up in pop-up is now supported (recursively)
1 parent f29b336 commit 39da44e

File tree

12 files changed

+139
-122
lines changed

12 files changed

+139
-122
lines changed

src/app/components/AddEscrowForm/__tests__/index.test.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { render, screen } from '@testing-library/react'
22
import userEvent from '@testing-library/user-event'
3-
import { ModalProvider } from 'app/components/Modal'
3+
import { ModalContainer } from 'app/components/Modal'
44
import { Validator } from 'app/state/staking/types'
55
import * as React from 'react'
66
import { Provider } from 'react-redux'
@@ -12,9 +12,8 @@ import { AddEscrowForm } from '..'
1212
const renderComponent = (store: any, address: string, validatorStatus: Validator['status']) =>
1313
render(
1414
<Provider store={store}>
15-
<ModalProvider>
16-
<AddEscrowForm validatorAddress={address} validatorStatus={validatorStatus} validatorRank={21} />
17-
</ModalProvider>
15+
<AddEscrowForm validatorAddress={address} validatorStatus={validatorStatus} validatorRank={21} />
16+
<ModalContainer />
1817
</Provider>,
1918
)
2019

src/app/components/AddEscrowForm/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* AddEscrowForm
44
*
55
*/
6-
import { useModal } from 'app/components/Modal'
6+
import { Modal } from '../Modal/slice/types'
7+
import { modalActions } from '../Modal/slice'
78
import { parseRoseStringToBaseUnitString } from 'app/lib/helpers'
89
import { selectMinStaking } from 'app/state/network/selectors'
910
import { Validator } from 'app/state/staking/types'
@@ -24,7 +25,7 @@ interface Props {
2425

2526
export const AddEscrowForm = memo((props: Props) => {
2627
const { t } = useTranslation()
27-
const { launchModal } = useModal()
28+
const launchModal = (modal: Modal) => dispatch(modalActions.launch(modal))
2829
const { error, success } = useSelector(selectTransaction)
2930
const isTop20 = props.validatorRank <= 20
3031
const [showNotice, setShowNotice] = useState(isTop20)

src/app/components/Modal/index.tsx

Lines changed: 17 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
1+
import { useEffect, useState } from 'react'
22
import { Box, Button, Layer, Paragraph } from 'grommet'
33
import { useTranslation } from 'react-i18next'
44
import { Alert, Checkmark, Close, Configure } from 'grommet-icons'
@@ -7,81 +7,32 @@ import { AlertBox } from '../AlertBox'
77
import { selectAllowDangerousSetting } from '../SettingsDialog/slice/selectors'
88
import { useDispatch, useSelector } from 'react-redux'
99
import { settingsActions } from '../SettingsDialog/slice'
10+
import { selectCurrentModal } from './slice/selectors'
11+
import { modalActions } from './slice'
1012

11-
interface Modal {
12-
title: string
13-
description: string
14-
handleConfirm: () => void
15-
16-
/**
17-
* Is this a dangerous operation?
18-
*
19-
* If marked as such, it will only be possible to execute it if the wallet is configured to run in dangerous mode.
20-
*
21-
* It also automatically implies a mandatory waiting time of 10 sec, unless specified otherwise.
22-
*/
23-
isDangerous: boolean
24-
25-
/**
26-
* How long does the user have to wait before he can actually confirm the action?
27-
*/
28-
mustWaitSecs?: number
29-
}
30-
31-
interface ModalContainerProps {
32-
modal: Modal
33-
closeModal: () => void
34-
hideModal: () => void
35-
}
36-
37-
interface ModalContextProps {
38-
/**
39-
* Show a new modal
40-
*/
41-
launchModal: (modal: Modal) => void
42-
43-
/**
44-
* Close the current modal
45-
*/
46-
closeModal: () => void
47-
48-
/**
49-
* Hide the current modal (with the intention of showing in again later)
50-
*/
51-
hideModal: () => void
52-
53-
/**
54-
* Show the previously hidden modal again
55-
*/
56-
showModal: () => void
57-
}
58-
59-
interface ModalProviderProps {
60-
children: React.ReactNode
61-
}
62-
63-
const ModalContext = createContext<ModalContextProps>({} as ModalContextProps)
64-
65-
const ModalContainer = ({ modal, closeModal, hideModal }: ModalContainerProps) => {
13+
const ModalContainer = () => {
6614
const dispatch = useDispatch()
6715
const { t } = useTranslation()
68-
const confirm = useCallback(() => {
69-
modal.handleConfirm()
70-
closeModal()
71-
}, [closeModal, modal])
72-
const { isDangerous, mustWaitSecs } = modal
16+
const modal = useSelector(selectCurrentModal)
7317
const allowDangerous = useSelector(selectAllowDangerousSetting)
18+
const [secsLeft, setSecsLeft] = useState(0)
19+
const closeModal = () => dispatch(modalActions.close())
20+
const confirm = () => {
21+
modal?.handleConfirm()
22+
dispatch(modalActions.close())
23+
}
24+
const { isDangerous, mustWaitSecs } = modal || {}
25+
7426
const forbidden = isDangerous && !allowDangerous
7527
const waitingTime = forbidden
7628
? 0 // If the action is forbidden, there is nothing to wait for
7729
: isDangerous
7830
? mustWaitSecs ?? 10 // For dangerous actions, we require 10 seconds of waiting, unless specified otherwise.
7931
: mustWaitSecs ?? 0 // For normal, non-dangerous operations, just use what was specified
8032

81-
const [secsLeft, setSecsLeft] = useState(0)
8233
const openSettings = () => {
83-
hideModal()
84-
setTimeout(() => dispatch(settingsActions.setOpen(true)), 100)
34+
dispatch(modalActions.stash())
35+
dispatch(settingsActions.setOpen(true))
8536
}
8637

8738
useEffect(() => {
@@ -101,6 +52,7 @@ const ModalContainer = ({ modal, closeModal, hideModal }: ModalContainerProps) =
10152
}
10253
}, [waitingTime])
10354

55+
if (!modal) return null
10456
return (
10557
<Layer modal onEsc={closeModal} onClickOutside={closeModal} background="background-front">
10658
<Box margin="medium">
@@ -148,45 +100,4 @@ const ModalContainer = ({ modal, closeModal, hideModal }: ModalContainerProps) =
148100
)
149101
}
150102

151-
const ModalProvider = (props: ModalProviderProps) => {
152-
const [modal, setModal] = useState<Modal | null>(null)
153-
const [hiddenModal, setHiddenModal] = useState<Modal | null>(null)
154-
const closeModal = useCallback(() => {
155-
setModal(null)
156-
}, [])
157-
const hideModal = useCallback(() => {
158-
if (!modal) {
159-
throw new Error("You can't call hideModal if no model is shown!")
160-
}
161-
setHiddenModal(modal)
162-
setModal(null)
163-
}, [modal])
164-
const showModal = useCallback(() => {
165-
if (modal) {
166-
throw new Error("You can't call showModal when a modal is already visible!")
167-
}
168-
if (!hiddenModal) {
169-
return
170-
}
171-
setModal(hiddenModal)
172-
setHiddenModal(null)
173-
}, [modal, hiddenModal])
174-
175-
return (
176-
<ModalContext.Provider value={{ closeModal, launchModal: setModal, hideModal, showModal }}>
177-
{props.children}
178-
{modal && <ModalContainer modal={modal} closeModal={closeModal} hideModal={hideModal} />}
179-
</ModalContext.Provider>
180-
)
181-
}
182-
183-
const useModal = () => {
184-
const context = useContext(ModalContext)
185-
if (context === undefined) {
186-
throw new Error('useModal must be used within a ModalProvider')
187-
}
188-
189-
return context
190-
}
191-
192-
export { ModalProvider, useModal }
103+
export { ModalContainer }
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { PayloadAction } from '@reduxjs/toolkit'
2+
import { createSlice } from 'utils/@reduxjs/toolkit'
3+
4+
import { Modal, ModalState } from './types'
5+
6+
export const initialState: ModalState = {
7+
current: null,
8+
stash: [],
9+
}
10+
11+
const slice = createSlice({
12+
name: 'modal',
13+
initialState,
14+
reducers: {
15+
/**
16+
* Show a new modal
17+
*/
18+
launch(state, action: PayloadAction<Modal>) {
19+
if (state.current) {
20+
state.stash.push(state.current)
21+
}
22+
state.current = action.payload
23+
},
24+
25+
/**
26+
* Close the current modal
27+
*/
28+
close(state) {
29+
state.current = state.stash.pop() || null
30+
},
31+
32+
/**
33+
* Hide the current modal (with the intention of showing in again later)
34+
*
35+
* The semantics is the same as with git stash.
36+
*/
37+
stash(state) {
38+
if (!state.current) {
39+
throw new Error("You can't call hideModal if no model is shown!")
40+
}
41+
state.stash.push(state.current)
42+
state.current = null
43+
},
44+
45+
/**
46+
* Show the previously hidden modal again.
47+
*
48+
* The semantics is the same as with `git stash pop`.
49+
*/
50+
stashPop(state) {
51+
if (state.current) {
52+
throw new Error("You can't call showModal when a modal is already visible!")
53+
}
54+
const latest = state.stash.pop()
55+
if (!latest) return
56+
state.current = latest
57+
},
58+
},
59+
})
60+
61+
export const { actions: modalActions } = slice
62+
63+
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.modal || initialState
7+
8+
export const selectCurrentModal = createSelector([selectSlice], settings => settings.current)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export interface Modal {
2+
title: string
3+
description: string
4+
handleConfirm: () => void
5+
6+
/**
7+
* Is this a dangerous operation?
8+
*
9+
* If marked as such, it will only be possible to execute it if the wallet is configured to run in dangerous mode.
10+
*
11+
* It also automatically implies a mandatory waiting time of 10 sec, unless specified otherwise.
12+
*/
13+
isDangerous: boolean
14+
15+
/**
16+
* How long does the user have to wait before he can actually confirm the action?
17+
*/
18+
mustWaitSecs?: number
19+
}
20+
21+
export interface ModalState {
22+
current: Modal | null
23+
stash: Modal[]
24+
}

src/app/components/SettingsButton/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@ import { SettingsDialog } from '../SettingsDialog'
66
import { selectIsSettingsDialogOpen } from '../SettingsDialog/slice/selectors'
77
import { useDispatch, useSelector } from 'react-redux'
88
import { settingsActions } from '../SettingsDialog/slice'
9-
import { useModal } from '../Modal'
9+
import { modalActions } from '../Modal/slice'
1010

1111
export const SettingsButton = () => {
1212
const dispatch = useDispatch()
1313
const layerVisibility = useSelector(selectIsSettingsDialogOpen)
1414
const { t } = useTranslation()
15-
const { showModal } = useModal()
1615
const setLayerVisibility = (value: boolean) => dispatch(settingsActions.setOpen(value))
1716
const closeSettings = () => {
18-
setLayerVisibility(false)
19-
showModal()
17+
dispatch(settingsActions.setOpen(false))
18+
dispatch(modalActions.stashPop())
2019
}
2120
return (
2221
<>

src/app/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { AccountPage } from './pages/AccountPage'
2020
import { CreateWalletPage } from './pages/CreateWalletPage'
2121
import { HomePage } from './pages/HomePage'
2222
import { OpenWalletPage } from './pages/OpenWalletPage'
23-
import { ModalProvider } from './components/Modal'
23+
import { ModalContainer } from './components/Modal'
2424
import { useRouteRedirects } from './useRouteRedirects'
2525

2626
const AppMain = styled(Main)`
@@ -33,7 +33,7 @@ export function App() {
3333
const size = useContext(ResponsiveContext)
3434

3535
return (
36-
<ModalProvider>
36+
<>
3737
<Helmet
3838
titleTemplate="%s - Oasis Wallet"
3939
defaultTitle="Oasis Wallet"
@@ -57,6 +57,7 @@ export function App() {
5757
</AppMain>
5858
</Box>
5959
</Box>
60-
</ModalProvider>
60+
<ModalContainer />
61+
</>
6162
)
6263
}

src/app/pages/AccountPage/Features/SendTransaction/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TransactionStatus } from 'app/components/TransactionStatus'
2-
import { useModal } from 'app/components/Modal'
2+
import { modalActions } from '../../../../components/Modal/slice'
33
import { transactionActions } from 'app/state/transaction'
44
import { selectTransaction } from 'app/state/transaction/selectors'
55
import { selectValidators } from 'app/state/staking/selectors'
@@ -9,6 +9,7 @@ import { useEffect, useState } from 'react'
99
import { useTranslation } from 'react-i18next'
1010
import { useDispatch, useSelector } from 'react-redux'
1111
import { parseRoseStringToBaseUnitString } from 'app/lib/helpers'
12+
import { Modal } from '../../../../components/Modal/slice/types'
1213

1314
export interface SendTransactionProps {
1415
isAddressInWallet: boolean
@@ -21,7 +22,7 @@ export function SendTransaction(props: SendTransactionProps) {
2122

2223
const { t } = useTranslation()
2324
const dispatch = useDispatch()
24-
const { launchModal } = useModal()
25+
const launchModal = (modal: Modal) => dispatch(modalActions.launch(modal))
2526
const { error, success } = useSelector(selectTransaction)
2627
const validators = useSelector(selectValidators)
2728
const [recipient, setRecipient] = useState('')

src/store/configureStore.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ export function configureAppStore(state?: Partial<RootState>) {
2424

2525
const store = configureStore({
2626
reducer: createReducer(),
27-
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(middlewares),
27+
middleware: getDefaultMiddleware =>
28+
getDefaultMiddleware({
29+
serializableCheck: {
30+
ignoredActionPaths: ['payload.handleConfirm'],
31+
ignoredPaths: ['modal.current.handleConfirm', 'modal.stash'],
32+
},
33+
}).concat(middlewares),
2834
devTools:
2935
/* istanbul ignore next line */
3036
process.env.NODE_ENV !== 'production',

0 commit comments

Comments
 (0)