From 152b186c103458e8d7833034027750a663555906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 2 Mar 2026 10:48:45 +0000 Subject: [PATCH] feat(privacy): enable privacy mode for all users and extend amount masking (#182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🚪 Why? ### Problem Privacy mode — which masks financial amounts so users can share their screen without leaking account balances — was previously gated behind an admin-only check. Regular users had no way to toggle it from the UI. Additionally, several places in the app (chart tooltips, budget pages) were rendering raw amounts via \`formatCurrency\` directly, bypassing the privacy mode check entirely. ## 🔑 What? ### Changes - Remove the \`isAdmin()\` guard from the privacy mode toggle in the user menu, making it available to all users - Mask digits with \`*\` in \`AmountDisplay\` when privacy mode is enabled (e.g. \`$1,234.56\` → \`$*,***.**\`) - Apply the same digit masking to the net worth chart tooltip totals in \`chart.tsx\` (both single-currency and multi-currency paths) - Replace raw \`formatCurrency\` calls with \`AmountDisplay\` in \`budget-list-card.tsx\` (spent, allocated, remaining) - Add a \`maskIfPrivate\` helper in \`budget-spending-chart.tsx\` CustomTooltip to mask allocated, spent, last period, and available amounts when privacy mode is active ## ✅ Verification image image --- .../components/budgets/budget-list-card.tsx | 29 +++++++------- .../budgets/budget-spending-chart.tsx | 26 ++++++------- resources/js/components/ui/amount-display.tsx | 4 +- resources/js/components/ui/chart.tsx | 18 ++++----- resources/js/components/user-menu-content.tsx | 39 ++++++++----------- 5 files changed, 53 insertions(+), 63 deletions(-) diff --git a/resources/js/components/budgets/budget-list-card.tsx b/resources/js/components/budgets/budget-list-card.tsx index c36a3e0d..7a705948 100644 --- a/resources/js/components/budgets/budget-list-card.tsx +++ b/resources/js/components/budgets/budget-list-card.tsx @@ -1,4 +1,5 @@ import { show } from '@/actions/App/Http/Controllers/BudgetController'; +import { AmountDisplay } from '@/components/ui/amount-display'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { @@ -11,7 +12,6 @@ import { import { Progress } from '@/components/ui/progress'; import { useLocale } from '@/hooks/use-locale'; import { Budget, getBudgetPeriodTypeLabel } from '@/types/budget'; -import { formatCurrency } from '@/utils/currency'; import { formatDate } from '@/utils/date'; import { __ } from '@/utils/i18n'; import { Link } from '@inertiajs/react'; @@ -102,17 +102,15 @@ export function BudgetListCard({ budget, currencyCode }: Props) { {__('Spent')} - {formatCurrency( - stats.totalSpent, - currencyCode, - locale, - )}{' '} + {' '} {__('of')}{' '} - {formatCurrency( - stats.totalAllocated, - currencyCode, - locale, - )} + - {formatCurrency( - stats.remaining, - currencyCode, - locale, - )} + diff --git a/resources/js/components/budgets/budget-spending-chart.tsx b/resources/js/components/budgets/budget-spending-chart.tsx index 90d5fc67..7f961c46 100644 --- a/resources/js/components/budgets/budget-spending-chart.tsx +++ b/resources/js/components/budgets/budget-spending-chart.tsx @@ -10,6 +10,7 @@ import { ChartTooltip, type ChartConfig, } from '@/components/ui/chart'; +import { usePrivacyMode } from '@/contexts/privacy-mode-context'; import { useLocale } from '@/hooks/use-locale'; import { BudgetPeriod } from '@/types/budget'; import { formatCurrency } from '@/utils/currency'; @@ -53,6 +54,8 @@ function CustomTooltip({ hasPreviousPeriod, locale, }: CustomTooltipProps) { + const { isPrivacyModeEnabled } = usePrivacyMode(); + if (!active || !payload || !payload.length) { return null; } @@ -64,6 +67,11 @@ function CustomTooltip({ const percentage = allocated > 0 ? Math.round((available / allocated) * 100) : 0; + const maskIfPrivate = (value: number) => { + const formatted = formatCurrency(value, currencyCode, locale); + return isPrivacyModeEnabled ? formatted.replace(/\d/g, '*') : formatted; + }; + return (

@@ -77,16 +85,14 @@ function CustomTooltip({ {__('Allocated:')} - {formatCurrency(allocated, currencyCode, locale)} + {maskIfPrivate(allocated)}

{__('Spent:')} - - {formatCurrency(spent, currencyCode, locale)} - + {maskIfPrivate(spent)}
{hasPreviousPeriod && data.prevSpent !== undefined && (
@@ -94,11 +100,7 @@ function CustomTooltip({ {__('Last period:')} - {formatCurrency( - data.prevSpent, - currencyCode, - locale, - )} + {maskIfPrivate(data.prevSpent)}
)} @@ -110,11 +112,7 @@ function CustomTooltip({ {percentage}% / - {formatCurrency( - available, - currencyCode, - locale, - )} + {maskIfPrivate(available)} diff --git a/resources/js/components/ui/amount-display.tsx b/resources/js/components/ui/amount-display.tsx index 9d760d95..1c74acc0 100644 --- a/resources/js/components/ui/amount-display.tsx +++ b/resources/js/components/ui/amount-display.tsx @@ -1,4 +1,5 @@ import { useEncryptionKey } from '@/contexts/encryption-key-context'; +import { usePrivacyMode } from '@/contexts/privacy-mode-context'; import { useLocale } from '@/hooks/use-locale'; import { cn } from '@/lib/utils'; import { formatCurrency } from '@/utils/currency'; @@ -57,6 +58,7 @@ export function AmountDisplay({ highlightPositive = false, }: AmountDisplayProps) { const { isKeySet } = useEncryptionKey(); + const { isPrivacyModeEnabled } = usePrivacyMode(); const locale = useLocale(); const [amount, setAmount] = useState(amountInCents / 100); const isPositive = amountInCents > 0 @@ -111,7 +113,7 @@ export function AmountDisplay({ )} > {showSign && amount >= 0 && '+'} - {formatted} + {isPrivacyModeEnabled ? formatted.replace(/\d/g, '*') : formatted} ); } diff --git a/resources/js/components/ui/chart.tsx b/resources/js/components/ui/chart.tsx index a8e9e6c3..13fcc99c 100644 --- a/resources/js/components/ui/chart.tsx +++ b/resources/js/components/ui/chart.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import * as RechartsPrimitive from 'recharts'; +import { usePrivacyMode } from '@/contexts/privacy-mode-context'; import { useLocale } from '@/hooks/use-locale'; import { cn } from '@/lib/utils'; import { formatCurrency } from '@/utils/currency'; @@ -164,6 +165,7 @@ const ChartTooltipContent = React.forwardRef< ) => { const { config } = useChart(); const locale = useLocale(); + const { isPrivacyModeEnabled } = usePrivacyMode(); const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) { @@ -346,11 +348,9 @@ const ChartTooltipContent = React.forwardRef< Total {currency} - {formatCurrencyWithCode( - total, - currency, - locale, - )} + {isPrivacyModeEnabled + ? formatCurrencyWithCode(total, currency, locale).replace(/\d/g, '*') + : formatCurrencyWithCode(total, currency, locale)} )) @@ -361,11 +361,9 @@ const ChartTooltipContent = React.forwardRef< {currencyTotals && currencyTotals[0] - ? formatCurrencyWithCode( - currencyTotals[0][1], - currencyTotals[0][0], - locale, - ) + ? isPrivacyModeEnabled + ? formatCurrencyWithCode(currencyTotals[0][1], currencyTotals[0][0], locale).replace(/\d/g, '*') + : formatCurrencyWithCode(currencyTotals[0][1], currencyTotals[0][0], locale) : payload .reduce( ( diff --git a/resources/js/components/user-menu-content.tsx b/resources/js/components/user-menu-content.tsx index 5302c20d..9747e2e3 100644 --- a/resources/js/components/user-menu-content.tsx +++ b/resources/js/components/user-menu-content.tsx @@ -6,7 +6,6 @@ import { } from '@/components/ui/dropdown-menu'; import { UserInfo } from '@/components/user-info'; import { usePrivacyMode } from '@/contexts/privacy-mode-context'; -import { isAdmin } from '@/hooks/use-admin'; import { useMobileNavigation } from '@/hooks/use-mobile-navigation'; import { clearKey } from '@/lib/key-storage'; import { logout } from '@/routes'; @@ -46,27 +45,23 @@ export function UserMenuContent({ user }: UserMenuContentProps) { - {isAdmin() && ( - <> - - { - togglePrivacyMode(); - cleanup(); - }} - > - {isPrivacyModeEnabled ? ( - - ) : ( - - )} - {isPrivacyModeEnabled - ? __('Disable privacy mode') - : __('Enable privacy mode')} - - - - )} + + { + togglePrivacyMode(); + cleanup(); + }} + > + {isPrivacyModeEnabled ? ( + + ) : ( + + )} + {isPrivacyModeEnabled + ? __('Disable privacy mode') + : __('Enable privacy mode')} + +