jam/src/components/Settings.jsx
Thebora Kompanioni cac503fb35
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
2022-06-28 13:14:50 +02:00

303 lines
12 KiB
JavaScript

import React, { useEffect, useState, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Sprite from './Sprite'
import Seedphrase from './Seedphrase'
import ToggleSwitch from './ToggleSwitch'
import Alert from './Alert'
import { ConfirmModal } from './Modal'
import { useSettings, useSettingsDispatch } from '../context/SettingsContext'
import { useCurrentWallet } from '../context/WalletContext'
import { useServiceInfo } from '../context/ServiceInfoContext'
import { SATS, BTC, walletDisplayName } from '../utils'
import * as Api from '../libs/JmWalletApi'
import { routes } from '../constants/routes'
import languages from '../i18n/languages'
import styles from './Settings.module.css'
function SeedModal({ show = false, onHide }) {
const { t } = useTranslation()
const currentWallet = useCurrentWallet()
const [revealSeed, setRevealSeed] = useState(false)
const [seedError, setSeedError] = useState(false)
const [seed, setSeed] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const loadSeed = async () => {
setIsLoading(true)
try {
const { name: walletName, token } = currentWallet
const res = await Api.getWalletSeed({ walletName, token })
const { seedphrase } = await (res.ok ? res.json() : Api.Helper.throwError(res))
setIsLoading(false)
setSeed(seedphrase)
} catch (e) {
setIsLoading(false)
setSeedError(true)
}
}
if (show) {
loadSeed()
}
}, [show, currentWallet])
return (
<rb.Modal size="lg" show={show} onHide={onHide} keyboard={false} centered={true} animation={true}>
<rb.Modal.Header closeButton>
<rb.Modal.Title>{walletDisplayName(currentWallet.name)}</rb.Modal.Title>
</rb.Modal.Header>
<rb.Modal.Body>
<>
{isLoading && (
<div className="d-flex justify-content-center align-items-center">
<rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" />
</div>
)}
{seedError && (
<div className="text-danger" style={{ marginLeft: '1rem' }}>
{t('settings.error_loading_seed_failed')}
</div>
)}
{seed && (
<>
<div className="mb-4">{t('settings.seed_modal_info_text')}</div>
<rb.Row className="justify-content-center align-items-center">
<rb.Col xs={12} md={10} className="mb-4">
<Seedphrase seedphrase={seed} centered={true} isBlurred={!revealSeed} />
</rb.Col>
</rb.Row>
<div className="d-flex justify-content-center align-items-center">
<div className="mb-2">
<ToggleSwitch
label={t('settings.reveal_seed')}
toggledOn={revealSeed}
onToggle={(isToggled) => setRevealSeed(isToggled)}
/>
</div>
</div>
</>
)}
</>
</rb.Modal.Body>
</rb.Modal>
)
}
export default function Settings({ stopWallet }) {
const [showingSeed, setShowingSeed] = useState(false)
const [lockingWallet, setLockingWallet] = useState(false)
const [showConfirmLockModal, setShowConfirmLockModal] = useState(null)
const [alert, setAlert] = useState(null)
const { t } = useTranslation()
const settings = useSettings()
const settingsDispatch = useSettingsDispatch()
const { i18n } = useTranslation()
const currentWallet = useCurrentWallet()
const navigate = useNavigate()
const serviceInfo = useServiceInfo()
const setTheme = (theme) => {
if (window.JM.THEMES.includes(theme)) {
document.documentElement.setAttribute(window.JM.THEME_ROOT_ATTR, theme)
settingsDispatch({ theme })
}
}
const isSats = settings.unit === SATS
const isLightTheme = settings.theme === window.JM.THEMES[0]
const lockWallet = useCallback(
async ({ force, destination }) => {
if (!force && (serviceInfo.coinjoinInProgress || serviceInfo.makerRunning)) {
setShowConfirmLockModal({ destination })
return
}
setLockingWallet(true)
try {
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)
}
} catch (e) {
setLockingWallet(false)
setAlert({ variant: 'danger', dismissible: false, message: e.message })
}
},
[currentWallet, stopWallet, navigate, serviceInfo]
)
return (
<div className={styles.settings}>
<div className="d-flex flex-column gap-3">
<div className={styles['section-title']}>{t('settings.title')}</div>
{alert && <Alert {...alert} />}
<ConfirmModal
isShown={showConfirmLockModal}
title={t('wallets.wallet_preview.modal_lock_wallet_title')}
body={
(serviceInfo?.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={() => setShowConfirmLockModal(null)}
onConfirm={() => {
setShowConfirmLockModal(null)
lockWallet({ force: true, destination: showConfirmLockModal?.destination })
}}
/>
<div className={styles['settings-group-container']}>
<rb.Button
variant="outline-dark"
className={styles['settings-btn']}
onClick={() => settingsDispatch({ showBalance: !settings.showBalance })}
>
<Sprite symbol={settings.showBalance ? 'hide' : 'show'} width="24" height="24" />
{settings.showBalance ? t('settings.hide_balance') : t('settings.show_balance')}
</rb.Button>
<rb.Button
variant="outline-dark"
className={styles['settings-btn']}
onClick={() => settingsDispatch({ unit: isSats ? BTC : SATS })}
>
<Sprite symbol={isSats ? BTC : SATS} width="24" height="24" />
{isSats ? t('settings.use_btc') : t('settings.use_sats')}
</rb.Button>
<rb.Button
variant="outline-dark"
className={styles['settings-btn']}
onClick={(e) => setTheme(isLightTheme ? window.JM.THEMES[1] : window.JM.THEMES[0])}
>
<Sprite symbol={isLightTheme ? window.JM.THEMES[0] : window.JM.THEMES[1]} width="24" height="24" />
{isLightTheme ? t('settings.use_dark_theme') : t('settings.use_light_theme')}
</rb.Button>
<rb.Button
variant="outline-dark"
className={styles['settings-btn']}
onClick={(e) => settingsDispatch({ useAdvancedWalletMode: !settings.useAdvancedWalletMode })}
>
<Sprite symbol={settings.useAdvancedWalletMode ? 'wand' : 'console'} width="24" height="24" />
{settings.useAdvancedWalletMode ? t('settings.use_normal_mode') : t('settings.use_dev_mode')}
</rb.Button>
<rb.Dropdown>
<rb.Dropdown.Toggle variant="outline-dark" className={styles['settings-btn']}>
<Sprite symbol="globe" width="24" height="24" />
{languages.find((lng) => lng.key === (i18n.resolvedLanguage || i18n.language))?.description ||
languages[0].description}
</rb.Dropdown.Toggle>
<rb.Dropdown.Menu variant={settings.theme === 'light' ? 'light' : 'dark'}>
{languages.map((lng, index) => {
return (
<rb.Dropdown.Item key={index} onClick={() => i18n.changeLanguage(lng.key)}>
{lng.description}
</rb.Dropdown.Item>
)
})}
<rb.Dropdown.Item
href="https://github.com/joinmarket-webui/joinmarket-webui/tree/master/src/i18n/README.md"
target="_blank"
rel="noopener noreferrer"
>
{t('settings.text_help_translate')}
</rb.Dropdown.Item>
</rb.Dropdown.Menu>
</rb.Dropdown>
</div>
<div className={styles['section-title']}>{t('settings.section_title_wallet')}</div>
<div className={styles['settings-group-container']}>
<rb.Button variant="outline-dark" className={styles['settings-btn']} onClick={(e) => setShowingSeed(true)}>
<Sprite symbol="mnemonic" width="24" height="24" />
{showingSeed ? t('settings.hide_seed') : t('settings.show_seed')}
</rb.Button>
{showingSeed && <SeedModal show={showingSeed} onHide={() => setShowingSeed(false)} />}
<rb.Button
variant="outline-dark"
className={styles['settings-btn']}
onClick={() => lockWallet({ force: false, destination: routes.walletList })}
>
{lockingWallet ? (
<>
<rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="mx-1" />
{t('settings.button_locking_wallet')}
</>
) : (
<>
<Sprite symbol="lock" width="24" height="24" />
{t('settings.button_lock_wallet')}
</>
)}
</rb.Button>
<Link to={routes.walletList} className={`btn btn-outline-dark ${styles['settings-btn']}`}>
<Sprite symbol="wallet" width="24" height="24" />
{t('settings.button_switch_wallet')}
</Link>
<rb.Button
variant="outline-dark"
className={styles['settings-btn']}
onClick={() => lockWallet({ force: false, destination: routes.createWallet })}
>
{lockingWallet ? (
<>
<rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="mx-1" />
{t('settings.button_locking_wallet')}
</>
) : (
<>
<Sprite symbol="plus" width="24" height="24" />
{t('settings.button_create_wallet')}
</>
)}
</rb.Button>
</div>
<div className={styles['section-title']}>{t('settings.section_title_community')}</div>
<div className={styles['settings-links']}>
<a
href="https://github.com/joinmarket-webui/joinmarket-webui"
target="_blank"
rel="noopener noreferrer"
className="link-dark"
>
<div className="d-flex align-items-center">
<Sprite symbol="github" width="24" height="24" className="me-2 p-1" />
{t('settings.github')}
</div>
</a>
<a href="https://t.me/JoinMarketWebUI" target="_blank" rel="noopener noreferrer" className="link-dark">
<div className="d-flex align-items-center">
<Sprite symbol="telegram" width="24" height="24" className="me-2 p-1" />
{t('settings.telegram')}
</div>
</a>
<a href="https://twitter.com/joinmarket" target="_blank" rel="noopener noreferrer" className="link-dark">
<div className="d-flex align-items-center">
<Sprite symbol="twitter" width="24" height="24" className="me-2 p-1" />
{t('settings.jm_twitter')}
</div>
</a>
</div>
</div>
</div>
)
}