feat(ui): Wallet menu overlay — info, xpub, and password-protected seed reveal

Add a Wallet menu item (between File and Settings) that opens an in-app
overlay showing wallet metadata: file name, network, type (HD seed / xprv /
WIF / watch-only), script kind, derivation path, master fingerprint, and
account xpub.

Includes a seed section: if the wallet has no seed, a note is shown; if it
has one and is unencrypted the seed is revealed directly; if encrypted the
user must enter the wallet file password before the mnemonic is displayed.
The revealed seed is shown in a danger-coloured bordered box with a warning.
Closing the overlay (backdrop, Close button, or Esc) always clears the
password input and hides the seed.

Also adds the private-key password-prompt overlay and its localization keys
(shared UI scaffolding used by the following commit).
This commit is contained in:
2026-06-21 22:27:31 +02:00
parent 5d6ff07f0b
commit 4a97172346
4 changed files with 360 additions and 7 deletions
+205 -6
View File
@@ -37,6 +37,9 @@
<MenuItem Header="{Binding Loc[menu.file.close]}" Command="{Binding CloseWalletCommand}"
IsEnabled="{Binding IsWalletOpen}"/>
</MenuItem>
<MenuItem Header="{Binding Loc[menu.wallet]}"
Command="{Binding OpenWalletInfoCommand}"
IsEnabled="{Binding IsWalletOpen}"/>
<MenuItem Header="{Binding Loc[menu.settings]}"
Command="{Binding OpenSettingsCommand}"/>
<MenuItem Header="{Binding Loc[menu.help]}"
@@ -47,6 +50,9 @@
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" Margin="8,4"
IsVisible="{Binding IsMobile}">
<Button Content="{Binding Loc[menu.wallet]}"
Command="{Binding OpenWalletInfoCommand}"
IsEnabled="{Binding IsWalletOpen}"/>
<Button Content="{Binding Loc[menu.settings]}" Command="{Binding OpenSettingsCommand}"/>
<Button Content="{Binding Loc[menu.help]}" Command="{Binding OpenHelpCommand}"/>
</StackPanel>
@@ -943,12 +949,25 @@
</StackPanel>
<!-- Private key (only when available) -->
<StackPanel Spacing="4" IsVisible="{Binding HasPrivKey}">
<TextBlock Text="{Binding Loc[addr.privkey]}" FontSize="11" Foreground="{DynamicResource DangerBrush}"/>
<SelectableTextBlock Text="{Binding PrivKey}"
FontFamily="monospace" FontSize="12"
Foreground="{DynamicResource DangerBrush}"
TextWrapping="Wrap"/>
<StackPanel Spacing="8" IsVisible="{Binding HasPrivKey}">
<TextBlock Text="{Binding Loc[addr.privkey]}" FontSize="11"
Foreground="{DynamicResource DangerBrush}"/>
<!-- Not yet revealed: single reveal button -->
<Button IsVisible="{Binding !$parent[UserControl].((vm:MainWindowViewModel)DataContext).IsAddressPrivKeyRevealed}"
Content="{Binding Loc[walletinfo.seed.reveal]}"
Command="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).RequestPrivKeyRevealCommand}"/>
<!-- Revealed: key text + hide button -->
<StackPanel IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsAddressPrivKeyRevealed}"
Spacing="6">
<SelectableTextBlock Text="{Binding PrivKey}"
FontFamily="monospace" FontSize="12"
Foreground="{DynamicResource DangerBrush}"
TextWrapping="Wrap"/>
<Button Content="{Binding Loc[walletinfo.seed.hide]}"
Command="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).HideAddressPrivKeyCommand}"/>
</StackPanel>
</StackPanel>
<Button Content="{Binding Loc[addr.close]}"
@@ -959,6 +978,38 @@
</Border>
</Border>
<!-- ============ PRIVATE KEY PASSWORD PROMPT ============ -->
<!-- Appears on top of the address detail overlay when the wallet is encrypted. -->
<Border Grid.Row="0" Grid.RowSpan="3"
Background="{DynamicResource ScrimBrush}"
IsVisible="{Binding IsPrivKeyPromptOpen}">
<Border Background="{DynamicResource OverlayCardBrush}"
BorderBrush="{DynamicResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="8"
MaxWidth="360" Margin="16"
HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Margin="24" Spacing="14">
<TextBlock Text="{Binding Loc[addr.privkey.prompt.title]}"
FontSize="16" FontWeight="Bold"/>
<TextBlock Text="{Binding Loc[addr.privkey.prompt.desc]}"
FontSize="12" TextWrapping="Wrap"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<TextBox Text="{Binding PrivKeyPromptPassword}"
PasswordChar="●"
PlaceholderText="{Binding Loc[wiz.open.placeholder]}"/>
<TextBlock Text="{Binding PrivKeyPromptError}"
Foreground="{DynamicResource DangerBrush}" FontSize="12"
IsVisible="{Binding PrivKeyPromptError,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Content="{Binding Loc[addr.close]}"
Command="{Binding CancelPrivKeyPromptCommand}"/>
<Button Content="{Binding Loc[walletinfo.seed.reveal]}" Classes="accent"
Command="{Binding ConfirmPrivKeyPasswordCommand}"/>
</StackPanel>
</StackPanel>
</Border>
</Border>
<!-- ============ TRANSACTION DETAIL OVERLAY ============ -->
<!-- In-app overlay (like the address overlay): appears immediately with a
spinner; data arrives from the server in the background; instant close. -->
@@ -1267,6 +1318,154 @@
</Border>
</Border>
<!-- ============ WALLET INFO OVERLAY ============ -->
<Border Grid.Row="0" Grid.RowSpan="3"
Background="{DynamicResource ScrimBrush}"
Tapped="OnWalletInfoOverlayBackdropTapped"
IsVisible="{Binding IsWalletInfoOpen}">
<Border Background="{DynamicResource OverlayCardBrush}"
BorderBrush="{DynamicResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="8"
MaxWidth="500" Margin="16"
HorizontalAlignment="Center" VerticalAlignment="Center">
<ScrollViewer MaxHeight="620">
<StackPanel Margin="24" Spacing="14">
<TextBlock Text="{Binding Loc[walletinfo.title]}"
FontSize="18" FontWeight="Bold"/>
<!-- File name -->
<StackPanel Spacing="3">
<TextBlock Text="{Binding Loc[walletinfo.file]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<SelectableTextBlock Text="{Binding WalletInfoFileName}"
FontFamily="monospace" FontSize="13"/>
</StackPanel>
<!-- Network -->
<StackPanel Spacing="3">
<TextBlock Text="{Binding Loc[walletinfo.network]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<TextBlock Text="{Binding WalletInfoNetwork}" FontSize="13"/>
</StackPanel>
<!-- Wallet type -->
<StackPanel Spacing="3">
<TextBlock Text="{Binding Loc[walletinfo.type]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<TextBlock Text="{Binding WalletInfoType}" FontSize="13"/>
</StackPanel>
<!-- Script kind -->
<StackPanel Spacing="3">
<TextBlock Text="{Binding Loc[walletinfo.script]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<TextBlock Text="{Binding WalletInfoScriptKind}" FontSize="13"/>
</StackPanel>
<!-- Derivation path (only if present) -->
<StackPanel Spacing="3"
IsVisible="{Binding WalletInfoDerivPath,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding Loc[walletinfo.derivpath]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<SelectableTextBlock Text="{Binding WalletInfoDerivPath}"
FontFamily="monospace" FontSize="13"/>
</StackPanel>
<!-- Master fingerprint (only if present) -->
<StackPanel Spacing="3"
IsVisible="{Binding WalletInfoFingerprint,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding Loc[walletinfo.fingerprint]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<SelectableTextBlock Text="{Binding WalletInfoFingerprint}"
FontFamily="monospace" FontSize="13"/>
</StackPanel>
<!-- Account xpub (only if present) -->
<StackPanel Spacing="3"
IsVisible="{Binding WalletInfoXpub,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding Loc[walletinfo.xpub]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<SelectableTextBlock Text="{Binding WalletInfoXpub}"
FontFamily="monospace" FontSize="11"
TextWrapping="Wrap"/>
</StackPanel>
<!-- BIP39 passphrase indicator -->
<StackPanel Spacing="3" IsVisible="{Binding WalletInfoHasPassphrase}">
<TextBlock Text="{Binding Loc[walletinfo.passphrase]}" FontSize="11"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<TextBlock Text="{Binding Loc[walletinfo.passphrase.set]}"
FontSize="13" Foreground="{DynamicResource TextSecondaryBrush}"/>
</StackPanel>
<!-- Separator -->
<Border Height="1" Background="{DynamicResource BorderSubtleBrush}" Margin="0,4"/>
<!-- Seed section -->
<TextBlock Text="{Binding Loc[walletinfo.seed.section]}"
FontSize="13" FontWeight="SemiBold"/>
<!-- No seed wallet -->
<TextBlock IsVisible="{Binding !WalletInfoHasSeed}"
Text="{Binding Loc[walletinfo.seed.noseed]}"
Foreground="{DynamicResource TextSecondaryBrush}"
FontSize="12" TextWrapping="Wrap"/>
<!-- Seed wallet: needs password -->
<StackPanel IsVisible="{Binding WalletInfoHasSeed}" Spacing="10">
<!-- Not yet revealed + password needed -->
<StackPanel IsVisible="{Binding WalletInfoSeedNeedsPassword}" Spacing="8">
<StackPanel IsVisible="{Binding !IsWalletInfoSeedRevealed}" Spacing="8">
<TextBlock Text="{Binding Loc[walletinfo.seed.password]}"
FontSize="12" TextWrapping="Wrap"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<TextBox Text="{Binding WalletInfoSeedPasswordInput}"
PasswordChar="●"
PlaceholderText="{Binding Loc[wiz.open.placeholder]}"/>
<TextBlock Text="{Binding WalletInfoSeedError}"
Foreground="{DynamicResource DangerBrush}" FontSize="12"
IsVisible="{Binding WalletInfoSeedError,
Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Button Content="{Binding Loc[walletinfo.seed.reveal]}"
Command="{Binding RevealSeedCommand}"/>
</StackPanel>
</StackPanel>
<!-- Not yet revealed + no password needed -->
<Button IsVisible="{Binding !WalletInfoSeedNeedsPassword}"
Content="{Binding Loc[walletinfo.seed.reveal]}"
Command="{Binding RevealSeedCommand}"
IsEnabled="{Binding !IsWalletInfoSeedRevealed}"/>
<!-- Revealed seed -->
<StackPanel IsVisible="{Binding IsWalletInfoSeedRevealed}" Spacing="8">
<Border BorderBrush="{DynamicResource DangerBrush}" BorderThickness="1"
CornerRadius="6" Padding="14"
Background="{DynamicResource SurfaceAltBrush}">
<SelectableTextBlock Text="{Binding WalletInfoSeedText}"
FontFamily="monospace" FontSize="15"
TextWrapping="Wrap"/>
</Border>
<TextBlock Text="{Binding Loc[walletinfo.seed.warning]}"
Foreground="{DynamicResource DangerBrush}"
FontSize="12" TextWrapping="Wrap"/>
<Button Content="{Binding Loc[walletinfo.seed.hide]}"
Command="{Binding HideSeedCommand}"/>
</StackPanel>
</StackPanel>
<Button Content="{Binding Loc[addr.close]}"
HorizontalAlignment="Right"
Command="{Binding CloseWalletInfoCommand}"/>
</StackPanel>
</ScrollViewer>
</Border>
</Border>
<!-- ============ SETTINGS OVERLAY ============ -->
<!-- In-app instead of nested submenus: avoids slow OS popups on WSLg. -->
<Border Grid.Row="0" Grid.RowSpan="3"