mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 06:57:07 +02:00
feat: add tap-to-blur privacy feature for wallet balances (#143)
* 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
{state.walletBalancesBlurred ? "✦✦✦✦✦✦" : formatSats(balance)}
|
||||
</span>
|
||||
{state.walletBalancesBlurred ? (
|
||||
<EyeOff className="size-5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Send / Receive Buttons */}
|
||||
@@ -1145,7 +1162,9 @@ export default function WalletViewer() {
|
||||
</div>
|
||||
<div className="flex-shrink-0 ml-4">
|
||||
<p className="text-sm font-semibold font-mono">
|
||||
{formatSats(tx.amount)}
|
||||
{state.walletBalancesBlurred
|
||||
? "✦✦✦✦"
|
||||
: formatSats(tx.amount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1233,7 +1252,9 @@ export default function WalletViewer() {
|
||||
: "Sent"}
|
||||
</p>
|
||||
<p className="text-2xl font-bold font-mono">
|
||||
{formatSats(selectedTransaction.amount)} sats
|
||||
{state.walletBalancesBlurred
|
||||
? "✦✦✦✦✦✦ sats"
|
||||
: `${formatSats(selectedTransaction.amount)} sats`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1267,7 +1288,9 @@ export default function WalletViewer() {
|
||||
Fees Paid
|
||||
</Label>
|
||||
<p className="text-sm font-mono">
|
||||
{formatSats(selectedTransaction.fees_paid)} sats
|
||||
{state.walletBalancesBlurred
|
||||
? "✦✦✦✦ sats"
|
||||
: `${formatSats(selectedTransaction.fees_paid)} sats`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
{state.walletBalancesBlurred
|
||||
? "✦✦✦✦✦✦"
|
||||
: formatBalance(balance ?? nwcConnection.balance)}
|
||||
</span>
|
||||
{state.walletBalancesBlurred ? (
|
||||
<EyeOff className="size-3 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@@ -326,7 +349,9 @@ export default function UserMenu() {
|
||||
{balance !== undefined ||
|
||||
nwcConnection.balance !== undefined ? (
|
||||
<span className="text-sm">
|
||||
{formatBalance(balance ?? nwcConnection.balance)}
|
||||
{state.walletBalancesBlurred
|
||||
? "✦✦✦✦"
|
||||
: formatBalance(balance ?? nwcConnection.balance)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -593,3 +593,12 @@ export const disconnectNWC = (state: GrimoireState): GrimoireState => {
|
||||
nwcConnection: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleWalletBalancesBlur = (
|
||||
state: GrimoireState,
|
||||
): GrimoireState => {
|
||||
return {
|
||||
...state,
|
||||
walletBalancesBlurred: !state.walletBalancesBlurred,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user