diff --git a/.gitignore b/.gitignore index e2d67e58..8df93c6a 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/src/components/Expense/ConvertibleBalance.tsx b/src/components/Expense/ConvertibleBalance.tsx index c9f6e951..bed8a2c2 100644 --- a/src/components/Expense/ConvertibleBalance.tsx +++ b/src/components/Expense/ConvertibleBalance.tsx @@ -25,6 +25,7 @@ interface ConvertibleBalanceProps { forceShowButton?: boolean; withText?: boolean; entityId?: number; + entityType?: 'group'; } export const ConvertibleBalance: React.FC = ({ @@ -35,6 +36,7 @@ export const ConvertibleBalance: React.FC = ({ forceShowButton = false, withText = false, entityId, + entityType, }) => { const { t } = useTranslationWithUtils(); const [open, setOpen] = useState(false); @@ -43,7 +45,7 @@ export const ConvertibleBalance: React.FC = ({ const userDefaultCurrency = useCurrencyPreferenceStore((s) => s.userDefaultCurrency); const groupDefaultCurrency = useCurrencyPreferenceStore((s) => - entityId ? s.groupDefaultCurrencies[entityId] : null, + entityId && entityType === 'group' ? s.groupDefaultCurrencies[entityId] : null, ); // Available currencies from balances @@ -60,14 +62,14 @@ export const ConvertibleBalance: React.FC = ({ return res; }, [userDefaultCurrency, groupDefaultCurrency, overrideCurrencies, balances]); - const sessionPreference = useCurrencyPreferenceStore((s) => s.getPreference(entityId)); + const sessionPreference = useCurrencyPreferenceStore((s) => s.getPreference(entityId, entityType)); const setSelectedCurrency = useCurrencyPreferenceStore( (s) => (preference?: string) => s.setPreference(entityId, preference), ); const clearPreference = useCurrencyPreferenceStore((s) => s.clearPreference); const selectedCurrency = useCurrencyPreferenceStore((s) => { - const preference = s.getPreference(entityId); + const preference = s.getPreference(entityId, entityType); if (preference === SHOW_ALL_VALUE || availableCurrencies.includes(preference ?? '')) { return preference; } else { @@ -160,8 +162,6 @@ export const ConvertibleBalance: React.FC = ({ return total; }, [shouldShowAll, balances, ratesQuery, selectedCurrency, t, setSelectedCurrency]); - console.log(selectedCurrency, groupDefaultCurrency); - if (0 === balances.length) { return ; } diff --git a/src/components/Expense/CumulatedBalances.tsx b/src/components/Expense/CumulatedBalances.tsx index 67f0312c..3e355759 100644 --- a/src/components/Expense/CumulatedBalances.tsx +++ b/src/components/Expense/CumulatedBalances.tsx @@ -7,11 +7,12 @@ import { useCurrencyPreferenceStore } from '~/store/currencyPreferenceStore'; export const CumulatedBalances: React.FC<{ entityId: number; + entityType?: 'group'; balances?: { currency: string; amount: bigint }[]; -}> = ({ entityId, balances }) => { +}> = ({ entityId, entityType, balances }) => { const { t } = useTranslationWithUtils(); - const selectedCurrency = useCurrencyPreferenceStore((s) => s.getPreference(entityId)); + const selectedCurrency = useCurrencyPreferenceStore((s) => s.getPreference(entityId, entityType)); const allNonZeroCurrencies = useMemo(() => { const nonZeroBalances = balances?.filter((b) => b.amount !== 0n); @@ -31,6 +32,7 @@ export const CumulatedBalances: React.FC<{ @@ -39,6 +41,7 @@ export const CumulatedBalances: React.FC<{ 1} @@ -47,6 +50,7 @@ export const CumulatedBalances: React.FC<{ 1} @@ -61,6 +65,7 @@ export const CumulatedBalances: React.FC<{ const CumulatedBalanceDisplay: React.FC<{ prefix?: string; entityId: number; + entityType?: 'group'; className?: string; cumulatedBalances?: { currency: string; amount: bigint }[]; forceShowButton?: boolean; @@ -68,6 +73,7 @@ const CumulatedBalanceDisplay: React.FC<{ }> = ({ prefix = '', entityId, + entityType, className = '', cumulatedBalances, forceShowButton = false, @@ -83,6 +89,7 @@ const CumulatedBalanceDisplay: React.FC<{ = ({ return (
- + {Object.entries(friendBalances) .slice(0, 2) diff --git a/src/store/currencyPreferenceStore.ts b/src/store/currencyPreferenceStore.ts index 6e9ec6b7..557ea7c0 100644 --- a/src/store/currencyPreferenceStore.ts +++ b/src/store/currencyPreferenceStore.ts @@ -19,7 +19,7 @@ interface CurrencyPreferenceState { setUserDefaultCurrency: (currency: CurrencyCode | null) => void; setGroupDefaultCurrency: (groupId: number, currency: CurrencyCode | null) => void; setPreference: (key?: number | string, currency?: string) => void; - getPreference: (key?: number | string) => CurrencyPreference; + getPreference: (key?: number | string, entityType?: 'group') => CurrencyPreference; clearPreference: (key?: number | string) => void; } @@ -50,9 +50,9 @@ export const useCurrencyPreferenceStore = create()( [key]: isCurrencyCode(currency) ? currency : SHOW_ALL_VALUE, }, })), - getPreference: (key = DEFAULT_KEY) => + getPreference: (key = DEFAULT_KEY, entityType?: 'group') => get().preferences[key] || - (typeof key === 'number' && get().groupDefaultCurrencies[key]) || + (entityType === 'group' && typeof key === 'number' && get().groupDefaultCurrencies[key]) || get().userDefaultCurrency || SHOW_ALL_VALUE, clearPreference: (key = DEFAULT_KEY) => diff --git a/src/tests/currencyPreferenceStore.test.ts b/src/tests/currencyPreferenceStore.test.ts new file mode 100644 index 00000000..7213a13a --- /dev/null +++ b/src/tests/currencyPreferenceStore.test.ts @@ -0,0 +1,67 @@ +import { SHOW_ALL_VALUE, useCurrencyPreferenceStore } from '~/store/currencyPreferenceStore'; + +const resetStore = () => + useCurrencyPreferenceStore.setState({ + recentCurrencies: [], + userDefaultCurrency: null, + groupDefaultCurrencies: {}, + preferences: {}, + }); + +beforeEach(() => { + sessionStorage.clear(); + resetStore(); +}); + +describe('getPreference – entity type isolation', () => { + it('returns userDefaultCurrency for a friend entity even when a group with the same id has a different default currency', () => { + const collidingId = 5; + + // Simulate visiting the Groups page: group id=5 has USD as its default + useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(collidingId, 'USD'); + // User's own currency is EUR + useCurrencyPreferenceStore.getState().setUserDefaultCurrency('EUR'); + + // When rendering a friend balance entry (no entityType), USD must not leak in + const preference = useCurrencyPreferenceStore.getState().getPreference(collidingId); + + expect(preference).toBe('EUR'); + expect(preference).not.toBe('USD'); + }); + + it('returns the group default currency when entityType is "group"', () => { + const groupId = 5; + useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(groupId, 'USD'); + useCurrencyPreferenceStore.getState().setUserDefaultCurrency('EUR'); + + const preference = useCurrencyPreferenceStore.getState().getPreference(groupId, 'group'); + + expect(preference).toBe('USD'); + }); + + it('respects an explicit preference over all defaults for both entity types', () => { + const id = 7; + useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(id, 'USD'); + useCurrencyPreferenceStore.getState().setUserDefaultCurrency('EUR'); + useCurrencyPreferenceStore.getState().setPreference(id, 'GBP'); + + expect(useCurrencyPreferenceStore.getState().getPreference(id)).toBe('GBP'); + expect(useCurrencyPreferenceStore.getState().getPreference(id, 'group')).toBe('GBP'); + }); + + it('falls back to SHOW_ALL when no preference, no group default (for group entity), and no user default', () => { + const preference = useCurrencyPreferenceStore.getState().getPreference(99, 'group'); + expect(preference).toBe(SHOW_ALL_VALUE); + }); + + it('falls back to SHOW_ALL for a friend entity with no preference and no user default', () => { + useCurrencyPreferenceStore.getState().setGroupDefaultCurrency(3, 'USD'); + const preference = useCurrencyPreferenceStore.getState().getPreference(3); + expect(preference).toBe(SHOW_ALL_VALUE); + }); + + it('uses the global (string) key when no entityId is provided', () => { + useCurrencyPreferenceStore.getState().setUserDefaultCurrency('JPY'); + expect(useCurrencyPreferenceStore.getState().getPreference()).toBe('JPY'); + }); +});