mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-20 13:28:21 +02:00
refactor: move lock/unlock requests from Wallet to Wallets (#351)
* refactor: move lock/unlock wallet api calls from child component to parent * refactor: control Wallet component appearance from parent * fix: prevent state update on an unmounted component on Settings page
This commit is contained in:
parent
a958ba6d88
commit
cac503fb35
6 changed files with 517 additions and 434 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { Route, Routes, Navigate } from 'react-router-dom'
|
||||
import * as rb from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
|
@ -38,7 +38,7 @@ export default function App() {
|
|||
const [showBetaWarning, setShowBetaWarning] = useState(false)
|
||||
const [showCheatsheet, setShowCheatsheet] = useState(false)
|
||||
|
||||
const cheatsheetEnabled = currentWallet
|
||||
const cheatsheetEnabled = useMemo(() => !!currentWallet, [currentWallet])
|
||||
|
||||
const startWallet = useCallback(
|
||||
(name, token) => {
|
||||
|
|
@ -122,7 +122,10 @@ export default function App() {
|
|||
{!sessionConnectionError && (
|
||||
<>
|
||||
<Route element={<Layout />}>
|
||||
<Route path={routes.home} element={<Wallets startWallet={startWallet} stopWallet={stopWallet} />} />
|
||||
<Route
|
||||
path={routes.home}
|
||||
element={<Wallets currentWallet={currentWallet} startWallet={startWallet} stopWallet={stopWallet} />}
|
||||
/>
|
||||
{currentWallet && (
|
||||
<>
|
||||
<Route path={routes.jam} element={<Jam />} />
|
||||
|
|
|
|||
|
|
@ -124,14 +124,13 @@ export default function Settings({ stopWallet }) {
|
|||
const { name: walletName, token } = currentWallet
|
||||
const res = await Api.getWalletLock({ walletName, token })
|
||||
|
||||
setLockingWallet(false)
|
||||
if (res.ok || res.status === 401) {
|
||||
stopWallet()
|
||||
navigate(destination)
|
||||
} else {
|
||||
await Api.Helper.throwError(res)
|
||||
}
|
||||
|
||||
setLockingWallet(false)
|
||||
navigate(destination)
|
||||
} catch (e) {
|
||||
setLockingWallet(false)
|
||||
setAlert({ variant: 'danger', dismissible: false, message: e.message })
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import React, { useState, useCallback, useEffect } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Formik } from 'formik'
|
||||
import * as rb from 'react-bootstrap'
|
||||
import { ConfirmModal } from './Modal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { walletDisplayName } from '../utils'
|
||||
import * as Api from '../libs/JmWalletApi'
|
||||
import { TabActivityIndicator, JoiningIndicator } from './ActivityIndicators'
|
||||
import { routes } from '../constants/routes'
|
||||
import styles from './Wallet.module.css'
|
||||
|
|
@ -104,146 +102,17 @@ const WalletUnlockForm = ({ walletName, unlockWallet }) => {
|
|||
|
||||
export default function Wallet({
|
||||
name,
|
||||
noneActive,
|
||||
lockWallet,
|
||||
unlockWallet,
|
||||
isActive,
|
||||
hasToken,
|
||||
makerRunning,
|
||||
coinjoinInProgress,
|
||||
currentWallet,
|
||||
startWallet,
|
||||
stopWallet,
|
||||
setAlert,
|
||||
...props
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [showLockConfirmModal, setShowLockConfirmModal] = useState(false)
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const unlockWallet = useCallback(
|
||||
async (walletName, password) => {
|
||||
if (currentWallet) {
|
||||
setAlert({
|
||||
variant: 'warning',
|
||||
dismissible: false,
|
||||
message:
|
||||
currentWallet.name === name
|
||||
? // unlocking same wallet
|
||||
t('wallets.wallet_preview.alert_wallet_already_unlocked', { walletName: walletDisplayName(name) })
|
||||
: // unlocking another wallet while one is already unlocked
|
||||
t('wallets.wallet_preview.alert_other_wallet_unlocked', { walletName: walletDisplayName(name) }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setAlert(null)
|
||||
try {
|
||||
const res = await Api.postWalletUnlock({ walletName }, { password })
|
||||
const body = await (res.ok ? res.json() : Api.Helper.throwError(res))
|
||||
|
||||
const { walletname: unlockedWalletName, token } = body
|
||||
startWallet(unlockedWalletName, token)
|
||||
navigate(routes.wallet)
|
||||
} catch (e) {
|
||||
const message = e.message.replace('Wallet', walletName)
|
||||
setAlert({ variant: 'danger', dismissible: false, message })
|
||||
}
|
||||
},
|
||||
[currentWallet, name, setAlert, startWallet, t, navigate]
|
||||
)
|
||||
|
||||
const lockWallet = useCallback(
|
||||
async (lockableWalletName, { confirmed = false }) => {
|
||||
if (!currentWallet || currentWallet.name !== lockableWalletName) {
|
||||
setAlert({
|
||||
variant: 'warning',
|
||||
dismissible: false,
|
||||
message: currentWallet
|
||||
? // locking another wallet while active one is still unlocked
|
||||
t('wallets.wallet_preview.alert_other_wallet_unlocked', {
|
||||
walletName: walletDisplayName(currentWallet.name),
|
||||
})
|
||||
: // locking without active wallet
|
||||
t('wallets.wallet_preview.alert_wallet_already_locked', {
|
||||
walletName: walletDisplayName(lockableWalletName),
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const needsLockConfirmation = !confirmed && (coinjoinInProgress || makerRunning)
|
||||
if (needsLockConfirmation) {
|
||||
setShowLockConfirmModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
setAlert(null)
|
||||
|
||||
try {
|
||||
const { name: walletName, token } = currentWallet
|
||||
|
||||
const res = await Api.getWalletLock({ walletName, token })
|
||||
|
||||
// On status OK or UNAUTHORIZED, stop the wallet and clear all local
|
||||
// information. The token might have become invalid or another one might have been
|
||||
// issued for the same wallet, etc.
|
||||
// In any case, the user has no access to the wallet anymore.
|
||||
if (res.ok || res.status === 401) {
|
||||
stopWallet()
|
||||
}
|
||||
|
||||
const body = await (res.ok ? res.json() : Api.Helper.throwError(res))
|
||||
const { walletname: lockedWalletName, already_locked } = body
|
||||
|
||||
setAlert({
|
||||
variant: already_locked ? 'warning' : 'success',
|
||||
dismissible: false,
|
||||
message: already_locked
|
||||
? t('wallets.wallet_preview.alert_wallet_already_locked', {
|
||||
walletName: walletDisplayName(lockedWalletName),
|
||||
})
|
||||
: t('wallets.wallet_preview.alert_wallet_locked_successfully', {
|
||||
walletName: walletDisplayName(lockedWalletName),
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
setAlert({ variant: 'danger', dismissible: false, message: e.message })
|
||||
}
|
||||
},
|
||||
[currentWallet, coinjoinInProgress, makerRunning, setAlert, stopWallet, t]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentWallet) {
|
||||
setShowLockConfirmModal(false)
|
||||
}
|
||||
}, [currentWallet])
|
||||
|
||||
const onLockConfirmed = useCallback(async () => {
|
||||
if (!currentWallet) return
|
||||
|
||||
setShowLockConfirmModal(false)
|
||||
await lockWallet(currentWallet.name, { confirmed: true })
|
||||
}, [currentWallet, lockWallet])
|
||||
|
||||
const showLockOptions = isActive && hasToken
|
||||
const showUnlockOptions = noneActive || (isActive && !hasToken) || (!hasToken && !makerRunning && !coinjoinInProgress)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmModal
|
||||
isShown={showLockConfirmModal}
|
||||
title={t('wallets.wallet_preview.modal_lock_wallet_title')}
|
||||
body={
|
||||
(makerRunning
|
||||
? t('wallets.wallet_preview.modal_lock_wallet_maker_running_text')
|
||||
: t('wallets.wallet_preview.modal_lock_wallet_coinjoin_in_progress_text')) +
|
||||
' ' +
|
||||
t('wallets.wallet_preview.modal_lock_wallet_alternative_action_text')
|
||||
}
|
||||
onCancel={() => setShowLockConfirmModal(false)}
|
||||
onConfirm={onLockConfirmed}
|
||||
/>
|
||||
<rb.Card {...props}>
|
||||
<rb.Card.Body>
|
||||
<div className="w-100 d-flex justify-content-between align-items-center flex-wrap py-1">
|
||||
|
|
@ -269,11 +138,11 @@ export default function Wallet({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{showLockOptions ? (
|
||||
{lockWallet ? (
|
||||
<WalletLockForm walletName={name} lockWallet={lockWallet} />
|
||||
) : (
|
||||
<div className={`w-100 mt-3 mt-md-0 ${styles['wallet-password-input']}`}>
|
||||
{showUnlockOptions && <WalletUnlockForm walletName={name} unlockWallet={unlockWallet} />}
|
||||
{unlockWallet && <WalletUnlockForm walletName={name} unlockWallet={unlockWallet} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react'
|
||||
import { render, screen, waitFor, waitForElementToBeRemoved } from '../testUtils'
|
||||
import { render, screen, waitFor } from '../testUtils'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import user from '@testing-library/user-event'
|
||||
import * as apiMock from '../libs/JmWalletApi'
|
||||
|
|
@ -10,45 +10,31 @@ import Wallet from './Wallet'
|
|||
jest.mock('../libs/JmWalletApi', () => ({
|
||||
...jest.requireActual('../libs/JmWalletApi'),
|
||||
getSession: jest.fn(),
|
||||
postWalletUnlock: jest.fn(),
|
||||
getWalletLock: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockedNavigate = jest.fn()
|
||||
jest.mock('react-router-dom', () => {
|
||||
return {
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}
|
||||
})
|
||||
|
||||
describe('<Wallet />', () => {
|
||||
const dummyWalletName = 'dummy.jmdat'
|
||||
const dummyToken = 'dummyToken'
|
||||
const dummyPassword = 'correct horse battery staple'
|
||||
|
||||
const mockStartWallet = jest.fn()
|
||||
const mockStopWallet = jest.fn()
|
||||
const mockSetAlert = jest.fn()
|
||||
const mockUnlockWallet = jest.fn()
|
||||
const mockLockWallet = jest.fn()
|
||||
|
||||
const setup = ({
|
||||
name,
|
||||
lockWallet = undefined,
|
||||
unlockWallet = undefined,
|
||||
isActive = false,
|
||||
hasToken = false,
|
||||
makerRunning = false,
|
||||
coinjoinInProgress = false,
|
||||
currentWallet = null,
|
||||
}) => {
|
||||
render(
|
||||
<Wallet
|
||||
name={name}
|
||||
lockWallet={lockWallet}
|
||||
unlockWallet={unlockWallet}
|
||||
isActive={isActive}
|
||||
hasToken={hasToken}
|
||||
makerRunning={makerRunning}
|
||||
coinjoinInProgress={coinjoinInProgress}
|
||||
currentWallet={currentWallet}
|
||||
startWallet={mockStartWallet}
|
||||
stopWallet={mockStopWallet}
|
||||
setAlert={mockSetAlert}
|
||||
makerRunning={makerRunning}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -63,29 +49,23 @@ describe('<Wallet />', () => {
|
|||
|
||||
expect(screen.getByText(walletDisplayName(dummyWalletName))).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('wallets.wallet_preview.placeholder_password')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_unlock')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_open')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_lock')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should unlock inactive wallet successfully', async () => {
|
||||
apiMock.postWalletUnlock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ walletname: dummyWalletName, token: dummyToken }),
|
||||
})
|
||||
|
||||
await act(async () => setup({ name: dummyWalletName }))
|
||||
await act(async () => setup({ name: dummyWalletName, unlockWallet: mockUnlockWallet }))
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_open')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_lock')).not.toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
user.type(
|
||||
screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password'),
|
||||
'correct horse battery staple'
|
||||
)
|
||||
user.paste(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password'), dummyPassword)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -96,49 +76,34 @@ describe('<Wallet />', () => {
|
|||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
|
||||
})
|
||||
|
||||
expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, dummyToken)
|
||||
expect(mockedNavigate).toHaveBeenCalledWith('/wallet')
|
||||
expect(mockUnlockWallet).toHaveBeenCalledWith(dummyWalletName, dummyPassword)
|
||||
})
|
||||
|
||||
it('should add alert if unlocking of inactive wallet fails', async () => {
|
||||
const apiErrorMessage = 'ANY_ERROR_MESSAGE with template <Wallet>'
|
||||
apiMock.postWalletUnlock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ message: apiErrorMessage }),
|
||||
})
|
||||
it('should provide ability to unlock active wallet (i.e. when auth token is missing)', () => {
|
||||
act(() =>
|
||||
setup({
|
||||
name: dummyWalletName,
|
||||
isActive: true,
|
||||
unlockWallet: mockUnlockWallet,
|
||||
})
|
||||
)
|
||||
|
||||
act(() => setup({ name: dummyWalletName }))
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
user.type(
|
||||
screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password'),
|
||||
'correct horse battery staple'
|
||||
)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
const unlockWalletButton = screen.getByText('wallets.wallet_preview.button_unlock')
|
||||
user.click(unlockWalletButton)
|
||||
|
||||
await waitFor(() => screen.findByText(/wallets.wallet_preview.button_unlocking/))
|
||||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
|
||||
})
|
||||
|
||||
expect(mockStartWallet).not.toHaveBeenCalled()
|
||||
expect(mockedNavigate).not.toHaveBeenCalled()
|
||||
expect(mockSetAlert).toHaveBeenCalledWith({
|
||||
variant: 'danger',
|
||||
dismissible: false,
|
||||
message: apiErrorMessage.replace('Wallet', dummyWalletName),
|
||||
})
|
||||
expect(screen.getByText(walletDisplayName(dummyWalletName))).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_open')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_lock')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render active wallet without errors', () => {
|
||||
act(() => setup({ name: dummyWalletName, isActive: true, hasToken: true }))
|
||||
act(() =>
|
||||
setup({
|
||||
name: dummyWalletName,
|
||||
isActive: true,
|
||||
lockWallet: mockLockWallet,
|
||||
})
|
||||
)
|
||||
|
||||
expect(screen.getByText(walletDisplayName(dummyWalletName))).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
|
||||
|
|
@ -149,17 +114,11 @@ describe('<Wallet />', () => {
|
|||
})
|
||||
|
||||
it('should lock active wallet successfully', async () => {
|
||||
apiMock.getWalletLock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ walletname: dummyWalletName, already_locked: false }),
|
||||
})
|
||||
|
||||
act(() =>
|
||||
setup({
|
||||
name: dummyWalletName,
|
||||
isActive: true,
|
||||
hasToken: true,
|
||||
currentWallet: { name: dummyWalletName, token: dummyToken },
|
||||
lockWallet: mockLockWallet,
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -174,97 +133,7 @@ describe('<Wallet />', () => {
|
|||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
|
||||
})
|
||||
|
||||
expect(mockStopWallet).toHaveBeenCalled()
|
||||
expect(mockSetAlert).toHaveBeenCalledWith({
|
||||
variant: 'success',
|
||||
dismissible: false,
|
||||
message: `wallets.wallet_preview.alert_wallet_locked_successfully`,
|
||||
})
|
||||
})
|
||||
|
||||
it('should add alert if locking active wallet fails', async () => {
|
||||
const apiErrorMessage = 'ANY_ERROR_MESSAGE'
|
||||
apiMock.getWalletLock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500, // any other error status than 401
|
||||
json: () => Promise.resolve({ message: apiErrorMessage }),
|
||||
})
|
||||
|
||||
act(() =>
|
||||
setup({
|
||||
name: dummyWalletName,
|
||||
isActive: true,
|
||||
hasToken: true,
|
||||
currentWallet: { name: dummyWalletName, token: dummyToken },
|
||||
})
|
||||
)
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
|
||||
await waitFor(() => screen.findByText(/wallets.wallet_preview.button_locking/))
|
||||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
|
||||
})
|
||||
|
||||
// on errors other than 401, stopWallet should not have been called
|
||||
expect(mockStopWallet).not.toHaveBeenCalled()
|
||||
expect(mockSetAlert).toHaveBeenCalledWith({
|
||||
variant: 'danger',
|
||||
dismissible: false,
|
||||
message: apiErrorMessage,
|
||||
})
|
||||
})
|
||||
|
||||
it('should add alert but clear wallet if locking active wallet fails with UNAUTHORIZED', async () => {
|
||||
const apiErrorMessage = 'Invalid credentials.'
|
||||
apiMock.getWalletLock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ message: apiErrorMessage }),
|
||||
})
|
||||
|
||||
act(() =>
|
||||
setup({
|
||||
name: dummyWalletName,
|
||||
isActive: true,
|
||||
hasToken: true,
|
||||
currentWallet: { name: dummyWalletName, token: dummyToken },
|
||||
})
|
||||
)
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
|
||||
await waitFor(() => screen.findByText(/wallets.wallet_preview.button_locking/))
|
||||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
|
||||
})
|
||||
|
||||
// on 401 errors, stopWallet *should* have been called
|
||||
expect(mockStopWallet).toHaveBeenCalled()
|
||||
expect(mockSetAlert).toHaveBeenCalledWith({
|
||||
variant: 'danger',
|
||||
dismissible: false,
|
||||
message: apiErrorMessage,
|
||||
})
|
||||
})
|
||||
|
||||
it('should provide ability to unlock active wallet when token is missing', () => {
|
||||
act(() => setup({ name: dummyWalletName, isActive: true, hasToken: false }))
|
||||
|
||||
expect(screen.getByText(walletDisplayName(dummyWalletName))).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_open')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('wallets.wallet_preview.button_lock')).not.toBeInTheDocument()
|
||||
expect(mockLockWallet).toHaveBeenCalledWith(dummyWalletName, { confirmed: false })
|
||||
})
|
||||
|
||||
it('should display earn indicator when maker is running', () => {
|
||||
|
|
@ -280,71 +149,4 @@ describe('<Wallet />', () => {
|
|||
expect(document.querySelector('.joining-indicator')).toBeInTheDocument()
|
||||
expect(document.querySelector('.earn-indicator')).toBeNull()
|
||||
})
|
||||
|
||||
it.each`
|
||||
makerRunning | coinjoinInProgress | expectedModalBody
|
||||
${true} | ${false} | ${'wallets.wallet_preview.modal_lock_wallet_maker_running_text wallets.wallet_preview.modal_lock_wallet_alternative_action_text'}
|
||||
${false} | ${true} | ${'wallets.wallet_preview.modal_lock_wallet_coinjoin_in_progress_text wallets.wallet_preview.modal_lock_wallet_alternative_action_text'}
|
||||
`(
|
||||
'should confirm locking wallet if maker ($makerRunning) or taker ($coinjoinInProgress) is running',
|
||||
async ({ makerRunning, coinjoinInProgress, expectedModalBody }) => {
|
||||
apiMock.getWalletLock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ walletname: dummyWalletName, already_locked: false }),
|
||||
})
|
||||
|
||||
act(() =>
|
||||
setup({
|
||||
name: dummyWalletName,
|
||||
isActive: true,
|
||||
hasToken: true,
|
||||
makerRunning,
|
||||
coinjoinInProgress,
|
||||
currentWallet: { name: dummyWalletName, token: dummyToken },
|
||||
})
|
||||
)
|
||||
|
||||
// modal is initially not shown
|
||||
expect(screen.queryByText('wallets.wallet_preview.modal_lock_wallet_title')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
// click on the "lock" button
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
|
||||
// modal appeared
|
||||
expect(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title')).toBeInTheDocument()
|
||||
expect(screen.getByText(expectedModalBody)).toBeInTheDocument()
|
||||
expect(screen.getByText('modal.confirm_button_accept')).toBeInTheDocument()
|
||||
expect(screen.getByText('modal.confirm_button_reject')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
// click on the modal's "cancel" button
|
||||
const lockWalletButton = screen.getByText('modal.confirm_button_reject')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title'))
|
||||
|
||||
expect(mockStopWallet).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
|
||||
// modal appeared
|
||||
expect(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
// click on the modal's "confirm" button
|
||||
const lockWalletButton = screen.getByText('modal.confirm_button_accept')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title'))
|
||||
|
||||
expect(mockStopWallet).toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import React, { useMemo, useEffect, useCallback, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import * as rb from 'react-bootstrap'
|
||||
import Sprite from './Sprite'
|
||||
import Alert from './Alert'
|
||||
import Wallet from './Wallet'
|
||||
import PageTitle from './PageTitle'
|
||||
import { useCurrentWallet } from '../context/WalletContext'
|
||||
import { useServiceInfo, useReloadServiceInfo } from '../context/ServiceInfoContext'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { walletDisplayName } from '../utils'
|
||||
import * as Api from '../libs/JmWalletApi'
|
||||
import { routes } from '../constants/routes'
|
||||
import { ConfirmModal } from './Modal'
|
||||
|
||||
function arrayEquals(a, b) {
|
||||
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index])
|
||||
|
|
@ -24,14 +24,124 @@ function sortWallets(wallets, activeWalletName = null) {
|
|||
}
|
||||
}
|
||||
|
||||
export default function Wallets({ startWallet, stopWallet }) {
|
||||
export default function Wallets({ currentWallet, startWallet, stopWallet }) {
|
||||
const { t } = useTranslation()
|
||||
const currentWallet = useCurrentWallet()
|
||||
const navigate = useNavigate()
|
||||
const serviceInfo = useServiceInfo()
|
||||
const reloadServiceInfo = useReloadServiceInfo()
|
||||
const [walletList, setWalletList] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [alert, setAlert] = useState(null)
|
||||
const [showLockConfirmModal, setShowLockConfirmModal] = useState(false)
|
||||
|
||||
const coinjoinInProgress = useMemo(() => serviceInfo && serviceInfo.coinjoinInProgress, [serviceInfo])
|
||||
const makerRunning = useMemo(() => serviceInfo && serviceInfo.makerRunning, [serviceInfo])
|
||||
|
||||
const unlockWallet = useCallback(
|
||||
async (walletName, password) => {
|
||||
if (currentWallet) {
|
||||
setAlert({
|
||||
variant: 'warning',
|
||||
dismissible: false,
|
||||
message:
|
||||
currentWallet.name === walletName
|
||||
? // unlocking same wallet
|
||||
t('wallets.wallet_preview.alert_wallet_already_unlocked', { walletName: walletDisplayName(walletName) })
|
||||
: // unlocking another wallet while one is already unlocked
|
||||
t('wallets.wallet_preview.alert_other_wallet_unlocked', { walletName: walletDisplayName(walletName) }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setAlert(null)
|
||||
try {
|
||||
const res = await Api.postWalletUnlock({ walletName }, { password })
|
||||
const body = await (res.ok ? res.json() : Api.Helper.throwError(res))
|
||||
|
||||
const { walletname: unlockedWalletName, token } = body
|
||||
startWallet(unlockedWalletName, token)
|
||||
navigate(routes.wallet)
|
||||
} catch (e) {
|
||||
const message = e.message.replace('Wallet', walletName)
|
||||
setAlert({ variant: 'danger', dismissible: false, message })
|
||||
}
|
||||
},
|
||||
[currentWallet, setAlert, startWallet, t, navigate]
|
||||
)
|
||||
|
||||
const lockWallet = useCallback(
|
||||
async (lockableWalletName, { confirmed = false }) => {
|
||||
if (!currentWallet || currentWallet.name !== lockableWalletName) {
|
||||
setAlert({
|
||||
variant: 'warning',
|
||||
dismissible: false,
|
||||
message: currentWallet
|
||||
? // locking another wallet while active one is still unlocked
|
||||
t('wallets.wallet_preview.alert_other_wallet_unlocked', {
|
||||
walletName: walletDisplayName(currentWallet.name),
|
||||
})
|
||||
: // locking without active wallet
|
||||
t('wallets.wallet_preview.alert_wallet_already_locked', {
|
||||
walletName: walletDisplayName(lockableWalletName),
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const needsLockConfirmation = !confirmed && (coinjoinInProgress || makerRunning)
|
||||
if (needsLockConfirmation) {
|
||||
setShowLockConfirmModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
setAlert(null)
|
||||
|
||||
try {
|
||||
const { name: walletName, token } = currentWallet
|
||||
|
||||
const res = await Api.getWalletLock({ walletName, token })
|
||||
|
||||
// On status OK or UNAUTHORIZED, stop the wallet and clear all local
|
||||
// information. The token might have become invalid or another one might have been
|
||||
// issued for the same wallet, etc.
|
||||
// In any case, the user has no access to the wallet anymore.
|
||||
if (res.ok || res.status === 401) {
|
||||
stopWallet()
|
||||
}
|
||||
|
||||
const body = await (res.ok ? res.json() : Api.Helper.throwError(res))
|
||||
const { walletname: lockedWalletName, already_locked } = body
|
||||
|
||||
setAlert({
|
||||
variant: already_locked ? 'warning' : 'success',
|
||||
dismissible: false,
|
||||
message: already_locked
|
||||
? t('wallets.wallet_preview.alert_wallet_already_locked', {
|
||||
walletName: walletDisplayName(lockedWalletName),
|
||||
})
|
||||
: t('wallets.wallet_preview.alert_wallet_locked_successfully', {
|
||||
walletName: walletDisplayName(lockedWalletName),
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
setAlert({ variant: 'danger', dismissible: false, message: e.message })
|
||||
}
|
||||
},
|
||||
[currentWallet, coinjoinInProgress, makerRunning, setAlert, stopWallet, t]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentWallet) {
|
||||
setShowLockConfirmModal(false)
|
||||
}
|
||||
}, [currentWallet])
|
||||
|
||||
const onLockConfirmed = useCallback(async () => {
|
||||
if (!currentWallet) return
|
||||
|
||||
setShowLockConfirmModal(false)
|
||||
await lockWallet(currentWallet.name, { confirmed: true })
|
||||
}, [currentWallet, lockWallet])
|
||||
|
||||
useEffect(() => {
|
||||
if (walletList && serviceInfo) {
|
||||
|
|
@ -76,50 +186,71 @@ export default function Wallets({ startWallet, stopWallet }) {
|
|||
}, [currentWallet, reloadServiceInfo, t])
|
||||
|
||||
return (
|
||||
<div className="wallets">
|
||||
<PageTitle
|
||||
title={t('wallets.title')}
|
||||
subtitle={walletList?.length === 0 ? t('wallets.subtitle_no_wallets') : null}
|
||||
center={true}
|
||||
/>
|
||||
{alert && <Alert {...alert} />}
|
||||
{isLoading ? (
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
|
||||
<span>{t('wallets.text_loading')}</span>
|
||||
</div>
|
||||
) : (
|
||||
walletList?.map((wallet, index) => (
|
||||
<Wallet
|
||||
key={wallet}
|
||||
name={wallet}
|
||||
noneActive={!serviceInfo?.walletName}
|
||||
isActive={serviceInfo?.walletName === wallet}
|
||||
hasToken={currentWallet && currentWallet.token && currentWallet.name === serviceInfo?.walletName}
|
||||
makerRunning={serviceInfo?.makerRunning}
|
||||
coinjoinInProgress={serviceInfo?.coinjoinInProgress}
|
||||
currentWallet={currentWallet}
|
||||
startWallet={startWallet}
|
||||
stopWallet={stopWallet}
|
||||
setAlert={setAlert}
|
||||
className={`bg-transparent rounded-0 border-start-0 border-end-0 ${
|
||||
index === 0 ? 'border-top-1' : 'border-top-0'
|
||||
}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div className="d-flex justify-content-center">
|
||||
<Link
|
||||
to={routes.createWallet}
|
||||
className={`btn mt-4 ${walletList?.length === 0 ? 'btn-lg btn-dark' : 'btn-outline-dark'}`}
|
||||
data-testid="new-wallet-btn"
|
||||
>
|
||||
<>
|
||||
<div className="wallets">
|
||||
<PageTitle
|
||||
title={t('wallets.title')}
|
||||
subtitle={walletList?.length === 0 ? t('wallets.subtitle_no_wallets') : null}
|
||||
center={true}
|
||||
/>
|
||||
{alert && <Alert {...alert} />}
|
||||
{isLoading ? (
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<Sprite symbol="plus" width="24" height="24" className="me-2" />
|
||||
<span>{t('wallets.button_new_wallet')}</span>
|
||||
<rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
|
||||
<span>{t('wallets.text_loading')}</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
walletList?.map((wallet, index) => {
|
||||
const noneActive = !serviceInfo?.walletName
|
||||
const isActive = serviceInfo?.walletName === wallet
|
||||
const hasToken = currentWallet && currentWallet.token && currentWallet.name === serviceInfo?.walletName
|
||||
|
||||
const showLockOptions = isActive && hasToken
|
||||
const showUnlockOptions =
|
||||
noneActive || (isActive && !hasToken) || (!hasToken && !makerRunning && !coinjoinInProgress)
|
||||
return (
|
||||
<Wallet
|
||||
key={wallet}
|
||||
name={wallet}
|
||||
lockWallet={showLockOptions ? lockWallet : undefined}
|
||||
unlockWallet={showUnlockOptions ? unlockWallet : undefined}
|
||||
isActive={isActive}
|
||||
makerRunning={makerRunning}
|
||||
coinjoinInProgress={coinjoinInProgress}
|
||||
className={`bg-transparent rounded-0 border-start-0 border-end-0 ${
|
||||
index === 0 ? 'border-top-1' : 'border-top-0'
|
||||
}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
<div className="d-flex justify-content-center">
|
||||
<Link
|
||||
to={routes.createWallet}
|
||||
className={`btn mt-4 ${walletList?.length === 0 ? 'btn-lg btn-dark' : 'btn-outline-dark'}`}
|
||||
data-testid="new-wallet-btn"
|
||||
>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<Sprite symbol="plus" width="24" height="24" className="me-2" />
|
||||
<span>{t('wallets.button_new_wallet')}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
isShown={showLockConfirmModal}
|
||||
title={t('wallets.wallet_preview.modal_lock_wallet_title')}
|
||||
body={
|
||||
(makerRunning
|
||||
? t('wallets.wallet_preview.modal_lock_wallet_maker_running_text')
|
||||
: t('wallets.wallet_preview.modal_lock_wallet_coinjoin_in_progress_text')) +
|
||||
' ' +
|
||||
t('wallets.wallet_preview.modal_lock_wallet_alternative_action_text')
|
||||
}
|
||||
onCancel={() => setShowLockConfirmModal(false)}
|
||||
onConfirm={onLockConfirmed}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import { render, screen, waitForElementToBeRemoved } from '../testUtils'
|
||||
import { render, screen, waitFor, waitForElementToBeRemoved } from '../testUtils'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import user from '@testing-library/user-event'
|
||||
|
||||
import * as apiMock from '../libs/JmWalletApi'
|
||||
|
||||
|
|
@ -10,11 +11,24 @@ jest.mock('../libs/JmWalletApi', () => ({
|
|||
...jest.requireActual('../libs/JmWalletApi'),
|
||||
getSession: jest.fn(),
|
||||
getWalletAll: jest.fn(),
|
||||
postWalletUnlock: jest.fn(),
|
||||
getWalletLock: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockedNavigate = jest.fn()
|
||||
jest.mock('react-router-dom', () => {
|
||||
return {
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}
|
||||
})
|
||||
|
||||
describe('<Wallets />', () => {
|
||||
const setup = () => {
|
||||
render(<Wallets />)
|
||||
const mockStartWallet = jest.fn()
|
||||
const mockStopWallet = jest.fn()
|
||||
|
||||
const setup = ({ currentWallet = null }) => {
|
||||
render(<Wallets currentWallet={currentWallet} startWallet={mockStartWallet} stopWallet={mockStopWallet} />)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -27,7 +41,7 @@ describe('<Wallets />', () => {
|
|||
apiMock.getSession.mockResolvedValueOnce(neverResolvingPromise)
|
||||
apiMock.getWalletAll.mockResolvedValueOnce(neverResolvingPromise)
|
||||
|
||||
act(setup)
|
||||
act(() => setup({}))
|
||||
|
||||
expect(screen.getByText('wallets.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.text_loading')).toBeInTheDocument()
|
||||
|
|
@ -42,7 +56,7 @@ describe('<Wallets />', () => {
|
|||
ok: false,
|
||||
})
|
||||
|
||||
act(setup)
|
||||
act(() => setup({}))
|
||||
|
||||
expect(screen.getByText('wallets.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.button_new_wallet')).toBeInTheDocument()
|
||||
|
|
@ -69,7 +83,7 @@ describe('<Wallets />', () => {
|
|||
json: () => Promise.resolve({ wallets: [] }),
|
||||
})
|
||||
|
||||
act(setup)
|
||||
act(() => setup({}))
|
||||
|
||||
expect(screen.getByText('wallets.text_loading')).toBeInTheDocument()
|
||||
|
||||
|
|
@ -101,7 +115,7 @@ describe('<Wallets />', () => {
|
|||
json: () => Promise.resolve({ wallets: ['wallet0.jmdat', 'wallet1.jmdat'] }),
|
||||
})
|
||||
|
||||
act(setup)
|
||||
act(() => setup({}))
|
||||
|
||||
expect(screen.getByText('wallets.text_loading')).toBeInTheDocument()
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.text_loading'))
|
||||
|
|
@ -115,4 +129,269 @@ describe('<Wallets />', () => {
|
|||
expect(callToActionButton.classList.contains('btn')).toBe(true)
|
||||
expect(callToActionButton.classList.contains('btn-lg')).toBe(false)
|
||||
})
|
||||
|
||||
describe('<Wallets /> lock/unlock flow', () => {
|
||||
const dummyWalletName = 'dummy.jmdat'
|
||||
const dummyToken = 'dummyToken'
|
||||
const dummyPassword = 'correct horse battery staple'
|
||||
|
||||
it('should unlock inactive wallet successfully', async () => {
|
||||
apiMock.getSession.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
session: false,
|
||||
maker_running: false,
|
||||
coinjoin_in_process: false,
|
||||
wallet_name: 'None',
|
||||
}),
|
||||
})
|
||||
apiMock.getWalletAll.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ wallets: [dummyWalletName] }),
|
||||
})
|
||||
apiMock.postWalletUnlock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ walletname: dummyWalletName, token: dummyToken }),
|
||||
})
|
||||
|
||||
await act(async () => setup({}))
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
user.paste(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password'), dummyPassword)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
const unlockWalletButton = screen.getByText('wallets.wallet_preview.button_unlock')
|
||||
user.click(unlockWalletButton)
|
||||
|
||||
await waitFor(() => screen.findByText(/wallets.wallet_preview.button_unlocking/))
|
||||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
|
||||
})
|
||||
|
||||
expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, dummyToken)
|
||||
expect(mockedNavigate).toHaveBeenCalledWith('/wallet')
|
||||
})
|
||||
|
||||
it('should add alert if unlocking of inactive wallet fails', async () => {
|
||||
const apiErrorMessage = 'ANY_ERROR_MESSAGE with template <Wallet>'
|
||||
|
||||
apiMock.getSession.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
session: false,
|
||||
maker_running: false,
|
||||
coinjoin_in_process: false,
|
||||
wallet_name: 'None',
|
||||
}),
|
||||
})
|
||||
apiMock.getWalletAll.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ wallets: [dummyWalletName] }),
|
||||
})
|
||||
apiMock.postWalletUnlock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ message: apiErrorMessage }),
|
||||
})
|
||||
|
||||
await act(async () => setup({}))
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_inactive')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_unlock')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
user.paste(screen.getByPlaceholderText('wallets.wallet_preview.placeholder_password'), dummyPassword)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
const unlockWalletButton = screen.getByText('wallets.wallet_preview.button_unlock')
|
||||
user.click(unlockWalletButton)
|
||||
|
||||
await waitFor(() => screen.findByText(/wallets.wallet_preview.button_unlocking/))
|
||||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
|
||||
})
|
||||
|
||||
expect(mockStartWallet).not.toHaveBeenCalled()
|
||||
expect(mockedNavigate).not.toHaveBeenCalled()
|
||||
|
||||
expect(screen.getByText(apiErrorMessage.replace('Wallet', dummyWalletName))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should lock active wallet successfully', async () => {
|
||||
apiMock.getSession.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
session: true,
|
||||
maker_running: false,
|
||||
coinjoin_in_process: false,
|
||||
wallet_name: dummyWalletName,
|
||||
}),
|
||||
})
|
||||
apiMock.getWalletAll.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ wallets: [dummyWalletName] }),
|
||||
})
|
||||
apiMock.getWalletLock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ walletname: dummyWalletName, already_locked: false }),
|
||||
})
|
||||
|
||||
await act(async () =>
|
||||
setup({
|
||||
currentWallet: {
|
||||
name: dummyWalletName,
|
||||
token: dummyToken,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
|
||||
await waitFor(() => screen.findByText(/wallet_preview.button_locking/))
|
||||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
|
||||
})
|
||||
|
||||
expect(mockStopWallet).toHaveBeenCalled()
|
||||
expect(screen.getByText('wallets.wallet_preview.alert_wallet_locked_successfully')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should add alert but clear wallet if locking active wallet fails with UNAUTHORIZED', async () => {
|
||||
const apiErrorMessage = 'Invalid credentials.'
|
||||
|
||||
apiMock.getSession.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
session: true,
|
||||
maker_running: false,
|
||||
coinjoin_in_process: false,
|
||||
wallet_name: dummyWalletName,
|
||||
}),
|
||||
})
|
||||
apiMock.getWalletAll.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ wallets: [dummyWalletName] }),
|
||||
})
|
||||
apiMock.getWalletLock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ message: apiErrorMessage }),
|
||||
})
|
||||
|
||||
await act(async () =>
|
||||
setup({
|
||||
currentWallet: {
|
||||
name: dummyWalletName,
|
||||
token: dummyToken,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
expect(screen.getByText('wallets.wallet_preview.wallet_active')).toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
|
||||
await waitFor(() => screen.findByText(/wallets.wallet_preview.button_locking/))
|
||||
await waitFor(() => screen.findByText('wallets.wallet_preview.button_lock'))
|
||||
})
|
||||
|
||||
// on 401 errors, stopWallet *should* have been called
|
||||
expect(mockStopWallet).toHaveBeenCalled()
|
||||
expect(screen.getByText(apiErrorMessage)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each`
|
||||
makerRunning | coinjoinInProgress | expectedModalBody
|
||||
${true} | ${false} | ${'wallets.wallet_preview.modal_lock_wallet_maker_running_text wallets.wallet_preview.modal_lock_wallet_alternative_action_text'}
|
||||
${false} | ${true} | ${'wallets.wallet_preview.modal_lock_wallet_coinjoin_in_progress_text wallets.wallet_preview.modal_lock_wallet_alternative_action_text'}
|
||||
`(
|
||||
'should confirm locking wallet if maker ($makerRunning) or taker ($coinjoinInProgress) is running',
|
||||
async ({ makerRunning, coinjoinInProgress, expectedModalBody }) => {
|
||||
apiMock.getSession.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
session: true,
|
||||
maker_running: makerRunning,
|
||||
coinjoin_in_process: coinjoinInProgress,
|
||||
wallet_name: dummyWalletName,
|
||||
}),
|
||||
})
|
||||
apiMock.getWalletAll.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ wallets: [dummyWalletName] }),
|
||||
})
|
||||
apiMock.getWalletLock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ walletname: dummyWalletName, already_locked: false }),
|
||||
})
|
||||
|
||||
await act(async () =>
|
||||
setup({
|
||||
currentWallet: {
|
||||
name: dummyWalletName,
|
||||
token: dummyToken,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// modal is initially not shown
|
||||
expect(screen.queryByText('wallets.wallet_preview.modal_lock_wallet_title')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('wallets.wallet_preview.button_lock')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
// click on the "lock" button
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
|
||||
// modal appeared
|
||||
expect(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title')).toBeInTheDocument()
|
||||
expect(screen.getByText(expectedModalBody)).toBeInTheDocument()
|
||||
expect(screen.getByText('modal.confirm_button_accept')).toBeInTheDocument()
|
||||
expect(screen.getByText('modal.confirm_button_reject')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
// click on the modal's "cancel" button
|
||||
const lockWalletButton = screen.getByText('modal.confirm_button_reject')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title'))
|
||||
|
||||
expect(mockStopWallet).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
const lockWalletButton = screen.getByText('wallets.wallet_preview.button_lock')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
|
||||
// modal appeared
|
||||
expect(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
// click on the modal's "confirm" button
|
||||
const lockWalletButton = screen.getByText('modal.confirm_button_accept')
|
||||
user.click(lockWalletButton)
|
||||
})
|
||||
await waitForElementToBeRemoved(screen.getByText('wallets.wallet_preview.modal_lock_wallet_title'))
|
||||
|
||||
expect(mockStopWallet).toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue