Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .gitignore
Binary file not shown.
10 changes: 5 additions & 5 deletions src/components/Expense/ConvertibleBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface ConvertibleBalanceProps {
forceShowButton?: boolean;
withText?: boolean;
entityId?: number;
entityType?: 'group';
}

export const ConvertibleBalance: React.FC<ConvertibleBalanceProps> = ({
Expand All @@ -35,6 +36,7 @@ export const ConvertibleBalance: React.FC<ConvertibleBalanceProps> = ({
forceShowButton = false,
withText = false,
entityId,
entityType,
}) => {
const { t } = useTranslationWithUtils();
const [open, setOpen] = useState(false);
Expand All @@ -43,7 +45,7 @@ export const ConvertibleBalance: React.FC<ConvertibleBalanceProps> = ({

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
Expand All @@ -60,14 +62,14 @@ export const ConvertibleBalance: React.FC<ConvertibleBalanceProps> = ({
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 {
Expand Down Expand Up @@ -160,8 +162,6 @@ export const ConvertibleBalance: React.FC<ConvertibleBalanceProps> = ({
return total;
}, [shouldShowAll, balances, ratesQuery, selectedCurrency, t, setSelectedCurrency]);

console.log(selectedCurrency, groupDefaultCurrency);

if (0 === balances.length) {
return <AmountDisplay className={className} amount={0n} currency="USD" />;
}
Expand Down
11 changes: 9 additions & 2 deletions src/components/Expense/CumulatedBalances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -31,6 +32,7 @@ export const CumulatedBalances: React.FC<{
<CumulatedBalanceDisplay
prefix={`${t('ui.total_balance')}: `}
entityId={entityId}
entityType={entityType}
cumulatedBalances={balances}
currencies={allNonZeroCurrencies}
/>
Expand All @@ -39,6 +41,7 @@ export const CumulatedBalances: React.FC<{
<CumulatedBalanceDisplay
prefix={`${t('actors.you')} ${t('ui.expense.you.lent')}`}
entityId={entityId}
entityType={entityType}
cumulatedBalances={youLent}
className="text-positive"
forceShowButton={allNonZeroCurrencies.length > 1}
Expand All @@ -47,6 +50,7 @@ export const CumulatedBalances: React.FC<{
<CumulatedBalanceDisplay
prefix={`${t('actors.you')} ${t('ui.expense.you.owe')}`}
entityId={entityId}
entityType={entityType}
className="text-negative"
cumulatedBalances={youOwe}
forceShowButton={allNonZeroCurrencies.length > 1}
Expand All @@ -61,13 +65,15 @@ export const CumulatedBalances: React.FC<{
const CumulatedBalanceDisplay: React.FC<{
prefix?: string;
entityId: number;
entityType?: 'group';
className?: string;
cumulatedBalances?: { currency: string; amount: bigint }[];
forceShowButton?: boolean;
currencies: string[];
}> = ({
prefix = '',
entityId,
entityType,
className = '',
cumulatedBalances,
forceShowButton = false,
Expand All @@ -83,6 +89,7 @@ const CumulatedBalanceDisplay: React.FC<{
<ConvertibleBalance
balances={cumulatedBalances}
entityId={entityId}
entityType={entityType}
forceShowButton={forceShowButton}
showMultiOption
overrideCurrencies={currencies}
Expand Down
2 changes: 1 addition & 1 deletion src/components/group/GroupMyBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const GroupMyBalance: React.FC<GroupMyBalanceProps> = ({
return (
<div className="flex gap-2">
<div className="flex flex-col gap-2">
<CumulatedBalances entityId={groupId} balances={cumulatedBalances} />
<CumulatedBalances entityId={groupId} entityType="group" balances={cumulatedBalances} />

{Object.entries(friendBalances)
.slice(0, 2)
Expand Down
6 changes: 3 additions & 3 deletions src/store/currencyPreferenceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -50,9 +50,9 @@ export const useCurrencyPreferenceStore = create<CurrencyPreferenceState>()(
[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) =>
Expand Down
67 changes: 67 additions & 0 deletions src/tests/currencyPreferenceStore.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});