diff --git a/src/App.tsx b/src/App.tsx index a73a1083c..fa0f6e62c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, useEffect, useMemo } from 'react' +import { lazy, Suspense, useEffect, useMemo, useState } from 'react' import type { PropsWithChildren } from 'react' import { lockwalletOptions } from '@joinmarket-webui/joinmarket-api-ts/@tanstack/react-query' import { token } from '@joinmarket-webui/joinmarket-api-ts/jm' @@ -44,9 +44,11 @@ import { queryClient } from '@/lib/queryClient' import { setIntervalDebounced, walletDisplayName, type WalletFileName } from '@/lib/utils' import { authStore } from '@/store/authStore' import { jamSettingsStore } from '@/store/jamSettingsStore' +import { LockWalletConfirmDialog } from './components/ui/jam/LockWalletConfirmDialog' import { Spinner } from './components/ui/spinner' import { WalletJarsDetailsPage } from './components/wallet/WalletJarsDetailsPage' import { JamSessionInfoContextProvider } from './context/JamSessionInfoContextProvider' +import { jmSessionStore } from './store/jmSessionStore' const DevSetupPage = lazy(() => import('@/components/dev/DevSetupPage')) const DevPage = lazy(() => import('@/components/dev/DevPage')) @@ -56,11 +58,23 @@ const ProtectedRoute = ({ authenticated, children }: PropsWithChildren<{ authent return authenticated ? <>{children} : } +type LockWalletDialogContext = { + open: true // always true - otherwise object is `undefined` + navigate: NavigateFunction + t: TFunction<'translation', undefined> +} + function App() { const walletFileName = useStore(authStore, (state) => state.state?.walletFileName) const hasAuthToken = useStore(authStore, (state) => state.state?.auth?.token !== undefined) const authenticated = useMemo(() => walletFileName !== undefined && hasAuthToken, [walletFileName, hasAuthToken]) const { clear: clearAuth } = useStore(authStore, (state) => state) + + const session = useStore(jmSessionStore, (state) => state.state) + + const makerRunning = session?.maker_running === true + const coinjoinInProgress = session?.coinjoin_in_process === true || (session?.schedule?.length || 0) > 0 + const client = useApiClient() const lockWalletQuery = useQuery( @@ -75,6 +89,7 @@ function App() { }, queryClient, ) + const [lockWalletDialogContext, setLockWalletDialogContext] = useState() const doOnLogout = async (navigate: NavigateFunction) => { clearAuth() @@ -84,11 +99,26 @@ function App() { const doOnLockWallet = async (navigate: NavigateFunction, t: TFunction<'translation', undefined>) => { if (!walletFileName) return + + if (makerRunning || coinjoinInProgress) { + setLockWalletDialogContext({ + open: true, + navigate, + t, + }) + } else { + await doOnLockWalletConfirm(navigate, t) + } + } + + const doOnLockWalletConfirm = async (navigate: NavigateFunction, t: TFunction<'translation', undefined>) => { + if (!walletFileName) return try { await lockWalletQuery.refetch() toast.success( t('wallets.wallet_preview.alert_wallet_locked_successfully', { walletName: walletDisplayName(walletFileName) }), ) + setLockWalletDialogContext(undefined) await doOnLogout(navigate) } catch (error: unknown) { const reason = (error instanceof Error ? error.message : undefined) || t('global.errors.reason_unknown') @@ -190,6 +220,16 @@ function App() { {walletFileName && } + {lockWalletDialogContext && ( + setLockWalletDialogContext(undefined)} + onConfirm={() => doOnLockWalletConfirm(lockWalletDialogContext?.navigate, lockWalletDialogContext?.t)} + isLocking={lockWalletQuery.isFetching} + makerRunning={makerRunning} + coinjoinInProgress={coinjoinInProgress} + /> + )} diff --git a/src/components/layout/AppNavbar.tsx b/src/components/layout/AppNavbar.tsx index 325e022ae..1224ae541 100644 --- a/src/components/layout/AppNavbar.tsx +++ b/src/components/layout/AppNavbar.tsx @@ -4,9 +4,7 @@ import type { TFunction } from 'i18next' import { LockKeyholeIcon, LogOutIcon, PackageSearchIcon, SettingsIcon, ShuffleIcon, WalletIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Link, useNavigate, type NavigateFunction } from 'react-router-dom' -import { useStore } from 'zustand' import { DevBadge } from '@/components/dev/DevBadge' -import { LockWalletConfirmDialog } from '@/components/settings/LockWalletConfirmDialog' import { Button } from '@/components/ui/button' import { ThemeToggleButton } from '@/components/ui/jam/ThemeToggleButton' import { Skeleton } from '@/components/ui/skeleton' @@ -15,7 +13,6 @@ import { isDevMode } from '@/constants/debugFeatures' import { routes } from '@/constants/routes' import type { RescanInfo } from '@/context/JamSessionInfoContext' import { cn, shortenStringMiddle } from '@/lib/utils' -import { jamSettingsStore } from '@/store/jamSettingsStore' import type { AmountSats } from '@/types/global' import { Spinner } from '../ui/spinner' @@ -133,9 +130,6 @@ export function AppNavbar({ }: AppNavbarProps) { const { t } = useTranslation() const navigate = useNavigate() - const showLockWalletConfirmation = useStore(jamSettingsStore, (state) => state.state.showLockWalletConfirmation) - const [showLockWalletDialog, setShowLockWalletDialog] = useState(false) - const [isLockingWallet, setIsLockingWallet] = useState(false) const isSidebarOpen = sidebarInfo === undefined ? false : sidebarInfo.isMobile ? sidebarInfo.openMobile : sidebarInfo.open @@ -154,21 +148,13 @@ export function AppNavbar({ const rescanningRoute = rescanInfo?.rescanning !== true ? undefined : routes.rescan - const handleLockWallet = async () => { + const [isLockingWallet, setIsLockingWallet] = useState(false) + const doOnLockWallet = async () => { try { setIsLockingWallet(true) await onLockWallet(navigate, t) } finally { setIsLockingWallet(false) - setShowLockWalletDialog(false) - } - } - - const onClickLockWallet = () => { - if (showLockWalletConfirmation) { - setShowLockWalletDialog(true) - } else { - handleLockWallet() } } @@ -245,9 +231,10 @@ export function AppNavbar({ className="hidden sm:flex" variant="ghost-navbar" size="icon" - onClick={onClickLockWallet} + onClick={doOnLockWallet} aria-label={t('settings.button_lock_wallet')} title={t('settings.button_lock_wallet')} + disabled={isLockingWallet} > @@ -263,12 +250,6 @@ export function AppNavbar({ {sidebarTrigger} - ) } diff --git a/src/components/settings/LockWalletConfirmDialog.tsx b/src/components/settings/LockWalletConfirmDialog.tsx deleted file mode 100644 index 86c173233..000000000 --- a/src/components/settings/LockWalletConfirmDialog.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { ComponentProps } from 'react' -import { AlertTriangleIcon } from 'lucide-react' -import { useTranslation } from 'react-i18next' -import { useStore } from 'zustand' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { jmSessionStore } from '@/store/jmSessionStore' -import type { WithRequiredProperty } from '@/types/global' - -type LockWalletConfirmDialogProps = WithRequiredProperty< - Omit, 'children'>, - 'open' | 'onOpenChange' -> & { - onConfirm: () => void - isLocking?: boolean -} - -export const LockWalletConfirmDialog = ({ - open, - onOpenChange, - onConfirm, - isLocking = false, -}: LockWalletConfirmDialogProps) => { - const { t } = useTranslation() - const session = useStore(jmSessionStore, (state) => state.state) - - const makerRunning = session?.maker_running === true - const coinjoinInProgress = session?.coinjoin_in_process === true - - return ( - - - - {t('wallets.wallet_preview.modal_lock_wallet_title')} - - {t('wallets.wallet_preview.modal_lock_wallet_alternative_action_text')} - - -
- {makerRunning && ( - - - {t('wallets.wallet_preview.modal_lock_wallet_title')} - {t('wallets.wallet_preview.modal_lock_wallet_maker_running_text')} - - )} - {coinjoinInProgress && ( - - - {t('wallets.wallet_preview.modal_lock_wallet_title')} - - {t('wallets.wallet_preview.modal_lock_wallet_coinjoin_in_progress_text')} - - - )} -
- - - - -
-
- ) -} diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 1483e66ab..6cb72b9b1 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -15,7 +15,6 @@ import { ArrowLeftRightIcon, LockKeyholeIcon, BookKeyIcon, - ShieldAlertIcon, } from 'lucide-react' import { useTheme } from 'next-themes' import { useTranslation } from 'react-i18next' @@ -35,7 +34,6 @@ import { jamSettingsStore } from '@/store/jamSettingsStore' import { AccountXpubsDialog } from './AccountXpubsDialog' import { FeeLimitDialog } from './FeeLimitDialog' import { LanguageSelector } from './LanguageSelector' -import { LockWalletConfirmDialog } from './LockWalletConfirmDialog' import { SeedPhraseDialog } from './SeedPhraseDialog' import { SettingItem, SettingsLink, SettingSwitch } from './SettingsItem' @@ -54,26 +52,16 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) const [showSeedDialog, setShowSeedDialog] = useState(false) const [showXpubsDialog, setShowXpubsDialog] = useState(false) const [showFeeLimitDialog, setShowFeeLimitDialog] = useState(false) - const [showLockWalletDialog, setShowLockWalletDialog] = useState(false) const hashedPassword = useStore(authStore, (state) => state.state?.hashed_password) const { isLogsEnabled } = useFeatures() - const [isLockingWallet, setIsLockingWallet] = useState(false) - const handleLockWallet = async () => { + const [isLockingWallet, setIsLockingWallet] = useState(false) + const doOnLockWallet = async () => { try { setIsLockingWallet(true) await onLockWallet(navigate, t) } finally { setIsLockingWallet(false) - setShowLockWalletDialog(false) - } - } - - const onClickLockWallet = async () => { - if (jamSettings.state.showLockWalletConfirmation) { - setShowLockWalletDialog(true) - } else { - await handleLockWallet() } } @@ -156,17 +144,10 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) - jamSettings.update({ showLockWalletConfirmation: checked })} - /> - @@ -274,12 +255,7 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps) )} - + {hashedPassword && ( <> , 'children'>, + 'open' | 'onOpenChange' +> & { + onConfirm: () => Promise + isLocking: boolean + makerRunning: boolean + coinjoinInProgress: boolean +} + +export const LockWalletConfirmDialog = ({ + open, + onOpenChange, + onConfirm, + isLocking, + makerRunning, + coinjoinInProgress, +}: LockWalletConfirmDialogProps) => { + const { t } = useTranslation() + + return ( + + + + {t('wallets.wallet_preview.modal_lock_wallet_title')} + + {makerRunning && ( + + + {t('wallets.wallet_preview.modal_lock_wallet_maker_running_text')} + + )} + {coinjoinInProgress && ( + + + + {t('wallets.wallet_preview.modal_lock_wallet_coinjoin_in_progress_text')} + + + )} +

{t('wallets.wallet_preview.modal_lock_wallet_alternative_action_text')}

+ + + + +
+
+ ) +} diff --git a/src/store/jamSettingsStore.ts b/src/store/jamSettingsStore.ts index eaaf48c76..839145794 100644 --- a/src/store/jamSettingsStore.ts +++ b/src/store/jamSettingsStore.ts @@ -8,7 +8,6 @@ export type JamSettings = { privateMode: boolean currencyUnit: Currency cheatsheetForceOpenAt?: number - showLockWalletConfirmation: boolean } interface JamSettingsStoreState { @@ -22,7 +21,6 @@ const initial: JamSettings = { privateMode: false, currencyUnit: 'sats', cheatsheetForceOpenAt: undefined, - showLockWalletConfirmation: true, } export const jamSettingsStore = createStore()( diff --git a/src/stories/jam/dialogs/LockWalletConfirmDialog.stories.tsx b/src/stories/jam/dialogs/LockWalletConfirmDialog.stories.tsx new file mode 100644 index 000000000..6ad91f65b --- /dev/null +++ b/src/stories/jam/dialogs/LockWalletConfirmDialog.stories.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react' +import type { Meta, StoryObj } from '@storybook/react-vite' +import { Button } from '@/components/ui/button' +import { LockWalletConfirmDialog } from '@/components/ui/jam/LockWalletConfirmDialog' + +const meta: Meta = { + title: 'Dialog/LockWalletConfirmDialog', + component: LockWalletConfirmDialog, + tags: ['autodocs'], + render: (args) => { + const [open, setOpen] = useState(false) + return ( + <> + + setOpen(false)} /> + + ) + }, +} +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + coinjoinInProgress: false, + makerRunning: false, + isLocking: false, + onConfirm: async () => alert('Confirm clicked!'), + }, +} + +export const MakerRunning: Story = { + args: { + coinjoinInProgress: false, + makerRunning: true, + isLocking: false, + onConfirm: async () => alert('Confirm clicked!'), + }, +} + +export const CoinjoinInProgress: Story = { + args: { + coinjoinInProgress: true, + makerRunning: false, + isLocking: false, + onConfirm: async () => alert('Confirm clicked!'), + }, +} + +export const Locking: Story = { + args: { + coinjoinInProgress: true, + makerRunning: false, + isLocking: true, + onConfirm: async () => alert('Confirm clicked!'), + }, +}