diff --git a/src/components/AddExpense/AddExpensePage.tsx b/src/components/AddExpense/AddExpensePage.tsx index 90a206e4..e61c988e 100644 --- a/src/components/AddExpense/AddExpensePage.tsx +++ b/src/components/AddExpense/AddExpensePage.tsx @@ -172,16 +172,14 @@ export const AddOrEditExpensePage: React.FC<{ navPromise = async () => router.back(); } + navPromise().catch(console.error); update((session: any) => ({ ...session, user: { ...(session?.user ?? {}), currency, }, - })) - .then(() => navPromise()) - .then(() => resetState()) - .catch(console.error); + })).catch(console.error); } } }, @@ -206,7 +204,6 @@ export const AddOrEditExpensePage: React.FC<{ expenseDate, expenseId, router, - resetState, addExpenseMutation, group, paidBy, diff --git a/src/components/Expense/ConvertibleBalance.tsx b/src/components/Expense/ConvertibleBalance.tsx index c9f6e951..6446affb 100644 --- a/src/components/Expense/ConvertibleBalance.tsx +++ b/src/components/Expense/ConvertibleBalance.tsx @@ -160,8 +160,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/ui/currency-input.tsx b/src/components/ui/currency-input.tsx index c5c94178..4e0ac4fc 100644 --- a/src/components/ui/currency-input.tsx +++ b/src/components/ui/currency-input.tsx @@ -21,7 +21,7 @@ const CurrencyInput: React.FC< className={cn('text-lg placeholder:text-sm', className)} inputMode="decimal" value={strValue} - onFocus={() => onValueChange({ strValue: parseToCleanString(strValue) })} + onFocus={() => onValueChange({ strValue: parseToCleanString(strValue, allowNegative) })} onBlur={() => { const formattedValue = format(strValue, { signed: allowNegative, hideSymbol }); return onValueChange({ strValue: formattedValue }); diff --git a/src/pages/add.tsx b/src/pages/add.tsx index ec37bd4f..d26d049e 100644 --- a/src/pages/add.tsx +++ b/src/pages/add.tsx @@ -44,7 +44,15 @@ const AddPage: NextPageWithUser<{ const initializedFriendIdRef = useRef(null); const initializedExpenseIdRef = useRef(null); - useEffect(() => () => resetState(), [resetState]); + useEffect( + () => () => { + resetState(); + initializedExpenseIdRef.current = null; + initializedGroupIdRef.current = null; + initializedFriendIdRef.current = null; + }, + [resetState], + ); // TODO: Set this globally from env var with app router later const { setMaxUploadFileSizeMB } = useAppStore((s) => s.actions); @@ -195,7 +203,7 @@ const AddPage: NextPageWithUser<{ setAmountStr( getCurrencyHelpersCached(expenseQuery.data.currency).toUIString( expenseQuery.data.amount, - false, + true, true, ), ); diff --git a/src/tests/addStore.test.ts b/src/tests/addStore.test.ts index 84a86b36..376a6eca 100644 --- a/src/tests/addStore.test.ts +++ b/src/tests/addStore.test.ts @@ -6,7 +6,9 @@ import { calculateParticipantSplit, calculateSplitShareBasedOnAmount, initSplitShares, + useAddExpenseStore, } from '~/store/addStore'; +import { getCurrencyHelpers } from '~/utils/numbers'; // Mock dependencies jest.mock('~/utils/array', () => ({ @@ -1079,3 +1081,51 @@ describe('Function Reversibility Tests', () => { }); }); }); + +// Regression test for #658: editing a negative expense must preserve the sign +describe('useAddExpenseStore sign preservation on edit (#658)', () => { + const { toUIString } = getCurrencyHelpers({ locale: 'en-US', currency: 'USD' }); + const { actions } = useAddExpenseStore.getState(); + + beforeEach(() => { + actions.resetState(); + }); + + it('loads a negative expense with the minus sign visible in amountStr', () => { + actions.setAmount(-20000n); + actions.setAmountStr(toUIString(-20000n, true, true)); + + const state = useAddExpenseStore.getState(); + expect(state.amountStr).toBe('-200'); + expect(state.isNegative).toBe(true); + expect(state.amount).toBe(20000n); + }); + + it('preserves the sign when the user edits digits without removing the minus', () => { + actions.setAmount(-20000n); + actions.setAmountStr(toUIString(-20000n, true, true)); + + // Simulate the user changing -200 to -200.01 via the input + actions.setAmountStr('-200.01'); + actions.setAmount(-20001n); + + const state = useAddExpenseStore.getState(); + expect(state.amountStr).toBe('-200.01'); + expect(state.isNegative).toBe(true); + expect(state.amount).toBe(20001n); + }); + + it('flips the sign when the user deletes the minus and edits the value', () => { + actions.setAmount(-20000n); + actions.setAmountStr(toUIString(-20000n, true, true)); + + // Simulate the user deleting the minus and changing the value to 200.01 + actions.setAmountStr('200.01'); + actions.setAmount(20001n); + + const state = useAddExpenseStore.getState(); + expect(state.amountStr).toBe('200.01'); + expect(state.isNegative).toBe(false); + expect(state.amount).toBe(20001n); + }); +}); diff --git a/src/tests/number.test.ts b/src/tests/number.test.ts index 3d191032..0cec4f20 100644 --- a/src/tests/number.test.ts +++ b/src/tests/number.test.ts @@ -46,6 +46,15 @@ describe('getCurrencyHelpers', () => { ])('should format %p as %p with signed flag', (value, expected) => { expect(toUIString(value, true)).toBe(expected); }); + + it.each([ + [12345n, '123.45'], + [-12345n, '-123.45'], + [-50n, '-0.5'], + [-0n, '0'], + ])('should format %p as %p with signed flag and hideSymbol', (value, expected) => { + expect(toUIString(value, true, true)).toBe(expected); + }); }); describe('JPY (no decimals)', () => { const currency = 'JPY'; @@ -173,6 +182,38 @@ describe('getCurrencyHelpers', () => { expect(sanitizeInput(input, true)).toBe(expected); }); }); + + describe('parseToCleanString', () => { + const { parseToCleanString } = getCurrencyHelpers({ + locale: 'en-US', + currency: 'USD', + }); + + it.each([ + ['-200', '-200'], + ['-200.01', '-200.01'], + ['--200', '-200'], + ['-$200.00', '-200.00'], + ])('should keep the minus sign for %p with signed flag', (input, expected) => { + expect(parseToCleanString(input, true)).toBe(expected); + }); + + it.each([ + [-20000n, '-200.00'], + [-12345n, '-123.45'], + [-50n, '-0.50'], + [-0n, '0.00'], + ])('should keep the minus sign for bigint %p with signed flag', (value, expected) => { + expect(parseToCleanString(value, true)).toBe(expected); + }); + + it.each([ + ['-200', '200'], + ['-123.45', '123.45'], + ])('should drop the minus sign for %p without signed flag', (input, expected) => { + expect(parseToCleanString(input)).toBe(expected); + }); + }); }); describe('currencyConversion', () => { diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts index ea7ef7de..3bed072a 100644 --- a/src/utils/numbers.ts +++ b/src/utils/numbers.ts @@ -130,8 +130,8 @@ export const getCurrencyHelpers = ({ return cleaned; }; - const normalizeToMaxLength = (inputString: string) => { - const sanitized = sanitizeInput(inputString); + const normalizeToMaxLength = (inputString: string, signed = false) => { + const sanitized = sanitizeInput(inputString, signed); const trimmedExceedingDecimals = trimExceedingDecimals(sanitized); return trimmedExceedingDecimals.endsWith(decimalSeparator) ? trimmedExceedingDecimals.slice(0, -1) @@ -173,7 +173,7 @@ export const getCurrencyHelpers = ({ } if (typeof value === 'string') { - return normalizeToMaxLength(value); + return normalizeToMaxLength(value, signed); } return ''; @@ -233,7 +233,6 @@ export const getCurrencyHelpers = ({ return { parseToCleanString, toUIString, - toUIStringSigned: (value: unknown) => toUIString(value, true), format, formatter, sanitizeInput,