From 9f6e524ea964b8a4589ec836ab11f14c875c409c Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 18 Jan 2026 23:22:00 +0100 Subject: [PATCH] feat: add tap-to-blur privacy feature for wallet balances (#143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add tap-to-blur privacy feature for wallet balances Implement privacy toggle for wallet balances and transaction amounts. Tapping any balance display toggles a global blur effect across all wallet UIs. Persisted to localStorage for consistent privacy. - Add walletBalancesBlurred state to GrimoireState - Add toggleWalletBalancesBlur pure function in core/logic - Make big balance in WalletViewer clickable with eye icon indicator - Apply blur to all transaction amounts in list and detail views - Add blur to send/receive dialog amounts - Make balance in user menu wallet info clickable with eye icon - Apply blur to balance in dropdown menu item UX matches common financial app pattern: tap balance → blur on/off * refactor: replace blur with fixed-width placeholders for privacy Prevent balance size information leakage by using fixed-width placeholder characters instead of blur effect. A blurred "1000000" would still reveal it's a large balance vs "100" even when blurred. Changes: - Replace blur-sm class with conditional placeholder text - Use "••••••" for main balances - Use "••••" for transaction amounts in lists - Use "•••••• sats" for detailed amounts with unit - Use "•••• sats" for smaller amounts like fees Security improvement: No information about balance size is leaked when privacy mode is enabled. All hidden amounts appear identical. * refactor: improve privacy UX with stars and clearer send flow Three UX improvements to the wallet privacy feature: 1. Don't hide amounts in send confirmation dialog - Users need to verify invoice amounts before sending - Privacy mode now only affects viewing, not sending 2. Replace bullet placeholders (••••) with stars (✦✦✦✦) - More visually distinct and recognizable as privacy indicator - Unicode BLACK FOUR POINTED STAR (U+2726) - Better matches common "redacted" aesthetic 3. Reduce eye icon sizes for subtler presence - Main balance: size-6 → size-5 - Wallet info dialog: size-3.5 → size-3 - Smaller icons feel less intrusive Result: Clearer privacy state, safer payment flow, better aesthetics. --------- Co-authored-by: Claude --- src/components/WalletViewer.tsx | 37 ++++++++++++++++++++++++------ src/components/nostr/user-menu.tsx | 37 +++++++++++++++++++++++++----- src/core/logic.ts | 9 ++++++++ src/core/state.ts | 5 ++++ src/types/app.ts | 1 + 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/components/WalletViewer.tsx b/src/components/WalletViewer.tsx index 7034f14..23e7e8e 100644 --- a/src/components/WalletViewer.tsx +++ b/src/components/WalletViewer.tsx @@ -20,6 +20,8 @@ import { LogOut, ChevronDown, ChevronRight, + Eye, + EyeOff, } from "lucide-react"; import { Virtuoso } from "react-virtuoso"; import { useWallet } from "@/hooks/useWallet"; @@ -341,7 +343,11 @@ function TransactionLabel({ transaction }: { transaction: Transaction }) { } export default function WalletViewer() { - const { state, disconnectNWC: disconnectNWCFromState } = useGrimoire(); + const { + state, + disconnectNWC: disconnectNWCFromState, + toggleWalletBalancesBlur, + } = useGrimoire(); const { wallet, balance, @@ -1053,9 +1059,20 @@ export default function WalletViewer() { {/* Big Centered Balance */}
-
- {formatSats(balance)} -
+
{/* Send / Receive Buttons */} @@ -1145,7 +1162,9 @@ export default function WalletViewer() {

- {formatSats(tx.amount)} + {state.walletBalancesBlurred + ? "✦✦✦✦" + : formatSats(tx.amount)}

@@ -1233,7 +1252,9 @@ export default function WalletViewer() { : "Sent"}

- {formatSats(selectedTransaction.amount)} sats + {state.walletBalancesBlurred + ? "✦✦✦✦✦✦ sats" + : `${formatSats(selectedTransaction.amount)} sats`}

@@ -1267,7 +1288,9 @@ export default function WalletViewer() { Fees Paid

- {formatSats(selectedTransaction.fees_paid)} sats + {state.walletBalancesBlurred + ? "✦✦✦✦ sats" + : `${formatSats(selectedTransaction.fees_paid)} sats`}

)} diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx index 3b04c00..7274dd6 100644 --- a/src/components/nostr/user-menu.tsx +++ b/src/components/nostr/user-menu.tsx @@ -1,4 +1,13 @@ -import { User, HardDrive, Palette, Wallet, X, RefreshCw } from "lucide-react"; +import { + User, + HardDrive, + Palette, + Wallet, + X, + RefreshCw, + Eye, + EyeOff, +} from "lucide-react"; import accounts from "@/services/accounts"; import { useProfile } from "@/hooks/useProfile"; import { use$ } from "applesauce-react/hooks"; @@ -66,7 +75,8 @@ function UserLabel({ pubkey }: { pubkey: string }) { export default function UserMenu() { const account = use$(accounts.active$); - const { state, addWindow, disconnectNWC } = useGrimoire(); + const { state, addWindow, disconnectNWC, toggleWalletBalancesBlur } = + useGrimoire(); const relays = state.activeAccount?.relays; const blossomServers = state.activeAccount?.blossomServers; const nwcConnection = state.nwcConnection; @@ -182,9 +192,22 @@ export default function UserMenu() { Balance:
- - {formatBalance(balance ?? nwcConnection.balance)} - +
diff --git a/src/core/logic.ts b/src/core/logic.ts index 328d004..07527c1 100644 --- a/src/core/logic.ts +++ b/src/core/logic.ts @@ -593,3 +593,12 @@ export const disconnectNWC = (state: GrimoireState): GrimoireState => { nwcConnection: undefined, }; }; + +export const toggleWalletBalancesBlur = ( + state: GrimoireState, +): GrimoireState => { + return { + ...state, + walletBalancesBlurred: !state.walletBalancesBlurred, + }; +}; diff --git a/src/core/state.ts b/src/core/state.ts index 6c84040..98f8618 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -370,6 +370,10 @@ export const useGrimoire = () => { setState((prev) => Logic.disconnectNWC(prev)); }, [setState]); + const toggleWalletBalancesBlur = useCallback(() => { + setState((prev) => Logic.toggleWalletBalancesBlur(prev)); + }, [setState]); + return { state, isTemporary, @@ -400,5 +404,6 @@ export const useGrimoire = () => { updateNWCBalance, updateNWCInfo, disconnectNWC, + toggleWalletBalancesBlur, }; }; diff --git a/src/types/app.ts b/src/types/app.ts index 1f8b561..b7d1e88 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -137,4 +137,5 @@ export interface GrimoireState { isPublished?: boolean; // Whether it has been published to Nostr }; nwcConnection?: NWCConnection; + walletBalancesBlurred?: boolean; // Privacy: blur balances and transaction amounts }