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
This commit is contained in:
Claude
2026-01-18 22:00:08 +00:00
parent e6e663c3d8
commit 57fbe5cdd3
5 changed files with 82 additions and 15 deletions

View File

@@ -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 */}
<div className="py-4 flex flex-col items-center justify-center">
<div className="text-4xl font-bold font-mono">
{formatSats(balance)}
</div>
<button
onClick={toggleWalletBalancesBlur}
className="text-4xl font-bold font-mono hover:opacity-70 transition-opacity cursor-pointer flex items-center gap-3"
title="Click to toggle privacy blur"
>
<span className={state.walletBalancesBlurred ? "blur-sm" : ""}>
{formatSats(balance)}
</span>
{state.walletBalancesBlurred ? (
<EyeOff className="size-6 text-muted-foreground" />
) : (
<Eye className="size-6 text-muted-foreground" />
)}
</button>
</div>
{/* Send / Receive Buttons */}
@@ -1144,7 +1161,9 @@ export default function WalletViewer() {
<TransactionLabel transaction={tx} />
</div>
<div className="flex-shrink-0 ml-4">
<p className="text-sm font-semibold font-mono">
<p
className={`text-sm font-semibold font-mono ${state.walletBalancesBlurred ? "blur-sm" : ""}`}
>
{formatSats(tx.amount)}
</p>
</div>
@@ -1232,7 +1251,9 @@ export default function WalletViewer() {
? "Received"
: "Sent"}
</p>
<p className="text-2xl font-bold font-mono">
<p
className={`text-2xl font-bold font-mono ${state.walletBalancesBlurred ? "blur-sm" : ""}`}
>
{formatSats(selectedTransaction.amount)} sats
</p>
</div>
@@ -1266,7 +1287,9 @@ export default function WalletViewer() {
<Label className="text-xs text-muted-foreground">
Fees Paid
</Label>
<p className="text-sm font-mono">
<p
className={`text-sm font-mono ${state.walletBalancesBlurred ? "blur-sm" : ""}`}
>
{formatSats(selectedTransaction.fees_paid)} sats
</p>
</div>
@@ -1423,7 +1446,9 @@ export default function WalletViewer() {
{invoiceDetails?.amount && !sendAmount && (
<div className="flex justify-between">
<span className="text-muted-foreground">Amount:</span>
<span className="font-semibold font-mono">
<span
className={`font-semibold font-mono ${state.walletBalancesBlurred ? "blur-sm" : ""}`}
>
{Math.floor(invoiceDetails.amount).toLocaleString()}{" "}
sats
</span>
@@ -1432,7 +1457,9 @@ export default function WalletViewer() {
{sendAmount && (
<div className="flex justify-between">
<span className="text-muted-foreground">Amount:</span>
<span className="font-semibold font-mono">
<span
className={`font-semibold font-mono ${state.walletBalancesBlurred ? "blur-sm" : ""}`}
>
{parseInt(sendAmount).toLocaleString()} sats
</span>
</div>

View File

@@ -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:
</span>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold">
{formatBalance(balance ?? nwcConnection.balance)}
</span>
<button
onClick={toggleWalletBalancesBlur}
className="text-lg font-semibold hover:opacity-70 transition-opacity cursor-pointer flex items-center gap-1.5"
title="Click to toggle privacy blur"
>
<span
className={state.walletBalancesBlurred ? "blur-sm" : ""}
>
{formatBalance(balance ?? nwcConnection.balance)}
</span>
{state.walletBalancesBlurred ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5 text-muted-foreground" />
)}
</button>
<Button
size="sm"
variant="ghost"
@@ -325,7 +348,9 @@ export default function UserMenu() {
<Wallet className="size-4 text-muted-foreground" />
{balance !== undefined ||
nwcConnection.balance !== undefined ? (
<span className="text-sm">
<span
className={`text-sm ${state.walletBalancesBlurred ? "blur-sm" : ""}`}
>
{formatBalance(balance ?? nwcConnection.balance)}
</span>
) : null}

View File

@@ -593,3 +593,12 @@ export const disconnectNWC = (state: GrimoireState): GrimoireState => {
nwcConnection: undefined,
};
};
export const toggleWalletBalancesBlur = (
state: GrimoireState,
): GrimoireState => {
return {
...state,
walletBalancesBlurred: !state.walletBalancesBlurred,
};
};

View File

@@ -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,
};
};

View File

@@ -136,4 +136,5 @@ export interface GrimoireState {
isPublished?: boolean; // Whether it has been published to Nostr
};
nwcConnection?: NWCConnection;
walletBalancesBlurred?: boolean; // Privacy: blur balances and transaction amounts
}