Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/components/layout/AppNavbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react'
import { useState, type PropsWithChildren } from 'react'
import type { SessionResponse } from '@joinmarket-webui/joinmarket-api-ts/jm'
import type { TFunction } from 'i18next'
import {
Expand All @@ -12,7 +12,9 @@ import {
} 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'
Expand All @@ -21,6 +23,7 @@ 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'

const WithActivityIndicator = ({ active, children }: PropsWithChildren<{ active: boolean }>) => {
Expand Down Expand Up @@ -137,6 +140,9 @@ 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
Expand All @@ -155,6 +161,24 @@ export function AppNavbar({

const rescanningRoute = rescanInfo?.rescanning !== true ? undefined : routes.rescan

const handleLockWallet = async () => {
try {
setIsLockingWallet(true)
await onLockWallet(navigate, t)
} finally {
setIsLockingWallet(false)
setShowLockWalletDialog(false)
}
}

const onClickLockWallet = () => {
if (showLockWalletConfirmation) {
setShowLockWalletDialog(true)
} else {
handleLockWallet()
Comment on lines +174 to +178
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onClickLockWallet function should be async and should await the call to handleLockWallet() on line 178 to ensure proper error handling. Currently, if handleLockWallet() fails when the confirmation is disabled, the error would be silently ignored. The SettingsPage implementation correctly uses await for this call.

Suggested change
const onClickLockWallet = () => {
if (showLockWalletConfirmation) {
setShowLockWalletDialog(true)
} else {
handleLockWallet()
const onClickLockWallet = async () => {
if (showLockWalletConfirmation) {
setShowLockWalletDialog(true)
} else {
await handleLockWallet()

Copilot uses AI. Check for mistakes.
}
}
Comment on lines +164 to +180
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is significant code duplication between this implementation and the identical logic in SettingsPage.tsx (lines 62-78). Consider extracting the handleLockWallet and onClickLockWallet functions into a shared custom hook (e.g., useWalletLocking) to reduce duplication and ensure consistent behavior across both components.

Copilot uses AI. Check for mistakes.

return (
<header className="light:bg-gray-100 light:text-black flex items-center justify-between bg-[#23262b] px-4 py-2 text-white transition-colors duration-300">
<WalletPreview
Expand Down Expand Up @@ -228,7 +252,7 @@ export function AppNavbar({
className="hidden sm:flex"
variant="ghost-navbar"
size="icon"
onClick={async () => await onLockWallet(navigate, t)}
onClick={onClickLockWallet}
aria-label={t('settings.button_lock_wallet')}
title={t('settings.button_lock_wallet')}
>
Expand All @@ -246,6 +270,12 @@ export function AppNavbar({
</Button>
{sidebarTrigger}
</div>
<LockWalletConfirmDialog
open={showLockWalletDialog}
onOpenChange={setShowLockWalletDialog}
onConfirm={handleLockWallet}
isLocking={isLockingWallet}
/>
</header>
)
}
76 changes: 76 additions & 0 deletions src/components/settings/LockWalletConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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<ComponentProps<typeof Dialog>, '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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t('wallets.wallet_preview.modal_lock_wallet_title')}</DialogTitle>
<DialogDescription>
{t('wallets.wallet_preview.modal_lock_wallet_alternative_action_text')}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
{makerRunning && (
<Alert variant="warning">
<AlertTriangleIcon className="size-4" />
<AlertTitle>{t('wallets.wallet_preview.modal_lock_wallet_title')}</AlertTitle>
<AlertDescription>{t('wallets.wallet_preview.modal_lock_wallet_maker_running_text')}</AlertDescription>
</Alert>
)}
{coinjoinInProgress && (
<Alert variant="warning">
<AlertTriangleIcon className="size-4" />
<AlertTitle>{t('wallets.wallet_preview.modal_lock_wallet_title')}</AlertTitle>
<AlertDescription>
{t('wallets.wallet_preview.modal_lock_wallet_coinjoin_in_progress_text')}
</AlertDescription>
</Alert>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isLocking}>
{t('global.cancel')}
</Button>
<Button variant="destructive" onClick={onConfirm} disabled={isLocking}>
{isLocking ? t('wallets.wallet_preview.button_locking') : t('wallets.wallet_preview.button_lock')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
27 changes: 26 additions & 1 deletion src/components/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ArrowLeftRightIcon,
LockKeyholeIcon,
BookKeyIcon,
ShieldAlertIcon,
} from 'lucide-react'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
Expand All @@ -34,6 +35,7 @@ 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'

Expand All @@ -52,6 +54,7 @@ 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)
Expand All @@ -62,6 +65,15 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps)
await onLockWallet(navigate, t)
} finally {
setIsLockingWallet(false)
setShowLockWalletDialog(false)
}
}

const onClickLockWallet = async () => {
if (jamSettings.state.showLockWalletConfirmation) {
setShowLockWalletDialog(true)
} else {
await handleLockWallet()
}
}

Expand Down Expand Up @@ -144,10 +156,17 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps)
<SettingItem
icon={LockKeyholeIcon}
title={t('settings.button_lock_wallet')}
action={handleLockWallet}
action={onClickLockWallet}
disabled={isLockingWallet}
/>
<Separator className="opacity-50" />
<SettingSwitch
icon={ShieldAlertIcon}
title={t('settings.show_lock_confirmation')}
checked={jamSettings.state.showLockWalletConfirmation}
onCheckedChange={(checked) => jamSettings.update({ showLockWalletConfirmation: checked })}
/>
<Separator className="opacity-50" />
<SettingsLink icon={ArrowLeftRightIcon} title={t('settings.button_switch_wallet')} to={routes.switchWallet} />
<Separator className="opacity-50" />
<SettingsLink icon={PackageSearchIcon} title={t('settings.rescan_chain')} to={routes.rescan} />
Expand Down Expand Up @@ -255,6 +274,12 @@ export const SettingsPage = ({ walletFileName, onLockWallet }: SettingPageProps)
)}

<FeeLimitDialog walletFileName={walletFileName} open={showFeeLimitDialog} onOpenChange={setShowFeeLimitDialog} />
<LockWalletConfirmDialog
open={showLockWalletDialog}
onOpenChange={setShowLockWalletDialog}
onConfirm={handleLockWallet}
isLocking={isLockingWallet}
/>
{hashedPassword && (
<>
<SeedPhraseDialog
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
"reveal_seed": "Reveal seed phrase",
"show_logs": "Show logs",
"show_fee_config": "Adjust fee limits",
"show_lock_confirmation": "Ask confirmation before locking",
"button_lock_wallet": "Lock wallet",
"button_locking_wallet": "Locking...",
"button_switch_wallet": "Switch wallet",
Expand Down
2 changes: 2 additions & 0 deletions src/store/jamSettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type JamSettings = {
privateMode: boolean
currencyUnit: Currency
cheatsheetForceOpenAt?: number
showLockWalletConfirmation: boolean
}

interface JamSettingsStoreState {
Expand All @@ -21,6 +22,7 @@ const initial: JamSettings = {
privateMode: false,
currencyUnit: 'sats',
cheatsheetForceOpenAt: undefined,
showLockWalletConfirmation: true,
}

export const jamSettingsStore = createStore<JamSettingsStoreState>()(
Expand Down
Loading