Skip to content

fix: preserve negative sign when editing an expense#689

Merged
krokosik merged 5 commits into
mainfrom
fix-658-negative-expense-edit-sign
Jun 21, 2026
Merged

fix: preserve negative sign when editing an expense#689
krokosik merged 5 commits into
mainfrom
fix-658-negative-expense-edit-sign

Conversation

@krokosik

@krokosik krokosik commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR fixes two issues with the edit-expense flow.

1. Edit form shows empty/add flow instead of the expense (regression)

The initializedExpenseIdRef / initializedGroupIdRef / initializedFriendIdRef refs were introduced in 83268ac (v2.1.0, #608) to prevent re-initialization on PWA reconnect. They persist across React StrictMode's simulated unmount/remount in dev mode, while the existing resetState() cleanup at add.tsx:47 clears the store. On StrictMode remount, the edit effect sees ref.current === expenseId and early-returns, leaving the store empty and the form rendering the add flow ("select members", all fields empty) even though the URL has the correct expenseId and the cached expenseQuery.data is populated. A hard reload works around it because it clears the React Query cache so the effect doesn't run until data arrives, by which time StrictMode is done.

Fix: clear the three refs alongside resetState() in the unmount cleanup so refs and store are always reset together. Safe in production (refs are destroyed on real unmount anyway).

2. Negative expenses shown as positive when editing (#658)

The edit form loaded the amount string with toUIString(amount, false, true) (signed=false), which strips the minus, while setAmount(amount) correctly set isNegative=true. The store was left inconsistent: the input showed 200 but the model said "negative 200". Any digit edit re-derived isNegative from the now-positive typed BigInt, flipping the sign on save. If the user didn't touch the field, isNegative stayed true and the value was unchanged — matching the reporter's repro exactly.

The sign was lost at multiple layers, so the fix threads signed through all of them:

  • src/utils/numbers.tsnormalizeToMaxLength now accepts and forwards signed to sanitizeInput; the string branch of parseToCleanString forwards signed. The bigint branch keeps its external sign prefix (no double-minus). Removed the unused toUIStringSigned helper.
  • src/pages/add.tsx — load the edit form with toUIString(amount, true, true) so amountStr = '-200' matches isNegative = true.
  • src/components/ui/currency-input.tsx — pass allowNegative to parseToCleanString on focus so the minus is not re-stripped when the field gains focus.
  • src/components/AddExpense/AddExpensePage.tsx — keep the sign when converting currencies on a negative expense (onConvertAmount).
  • src/components/Friend/CurrencyConversion.tsx — same fix in two places.
  • src/components/Friend/Export.tsxparseToCleanString(expense.amount, true) so CSV export of negative expenses keeps the minus (same root cause, surfaced during investigation).

After the fix, editing a -200 expense shows -200 in the field. Changing digits to -200.01 preserves the sign; deleting the minus and saving flips it to +200.01 (explicit user intent).

Tests

  • src/tests/number.test.ts — added toUIString signed + hideSymbol cases, and a new parseToCleanString describe block covering signed string and bigint inputs (plus the default signed=false behavior).
  • src/tests/addStore.test.ts — regression tests driving the real Zustand store: loading a negative expense keeps the minus in amountStr + isNegative=true; editing digits without removing the minus preserves the sign; deleting the minus flips it.

Verification

  • pnpm prettier --check . — clean (only a pre-existing .opencode/package.json warning, unrelated)
  • pnpm lint — 0 errors, 284 pre-existing warnings
  • pnpm tsgo --noEmit — clean
  • pnpm test — 155/155 pass
  • pnpm build --no-lint — success
  • Manual: editing an expense now populates the form without requiring a refresh; negative amounts keep their sign across edit cycles

Closes #658

Checklist

  • I have read CONTRIBUTING.md in its entirety
  • I have performed a self-review of my own code
  • I have added unit tests to cover my changes
  • The last commit successfully passed pre-commit checks
  • Any AI code was thoroughly reviewed by me

The edit form loaded the amount with toUIString(amount, false, true),
which strips the minus, while setAmount(amount) correctly set
isNegative=true. The store was left inconsistent: the input showed
'200' but the model said negative. Any digit edit re-derived
isNegative from the now-positive typed BigInt, flipping the sign on
save. If the user didn't touch the field, isNegative stayed true and
the value was unchanged - matching the reporter's repro exactly.

Fix the sign flow through all layers:
- numbers.ts: thread 'signed' through normalizeToMaxLength and the
  string branch of parseToCleanString so the minus survives onFocus
  of CurrencyInput (allowNegative). Remove unused toUIStringSigned.
- add.tsx: load the edit form with toUIString(amount, true, true).
- currency-input.tsx: pass allowNegative to parseToCleanString on
  focus so the minus is not re-stripped.
- AddExpensePage.tsx, CurrencyConversion.tsx: keep the sign when
  converting currencies on a negative amount.
- Export.tsx: keep the minus for negative expenses in CSV export
  (same root cause, parseToCleanString(expense.amount, true)).

Closes #658
@krokosik krokosik added this to the 2.1.x milestone Jun 21, 2026
krokosik added 4 commits June 21, 2026 17:51
The initializedExpenseIdRef/groupId/friendId refs introduced in 83268ac
(v2.1.0, #608) persist across React StrictMode's simulated unmount in
dev, while the resetState() cleanup clears the store. On remount, the
edit effect sees ref === expenseId and early-returns, leaving the store
empty and the form showing the add flow even though the URL and cached
query data are correct. A hard reload fixes it because it clears the
React Query cache so the effect doesn't run until data arrives, by
which time StrictMode is done.

Clear the refs alongside resetState in the unmount cleanup so refs and
store are always reset together. Safe in production (refs are destroyed
on real unmount anyway).

Also fix: preserve negative sign when editing an expense

The edit form loaded the amount with toUIString(amount, false, true),
which strips the minus, while setAmount(amount) correctly set
isNegative=true. The store was left inconsistent: the input showed
'200' but the model said negative. Any digit edit re-derived
isNegative from the now-positive typed BigInt, flipping the sign on
save.

Fix the sign flow through all layers:
- numbers.ts: thread 'signed' through normalizeToMaxLength and the
  string branch of parseToCleanString so the minus survives onFocus
  of CurrencyInput (allowNegative). Remove unused toUIStringSigned.
- add.tsx: load the edit form with toUIString(amount, true, true).
- currency-input.tsx: pass allowNegative to parseToCleanString on
  focus so the minus is not re-stripped.
- AddExpensePage.tsx, CurrencyConversion.tsx: keep the sign when
  converting currencies on a negative amount.
- Export.tsx: keep the minus for negative expenses in CSV export
  (same root cause, parseToCleanString(expense.amount, true)).

Closes #658
The save handler awaited update(session) before navigating. Between
mutation success (loading indicator disappears) and navPromise firing,
the form was visible without a loading state, then briefly re-rendered
with cleared store state during the route transition - showing the
empty add flow for a moment.

Navigate immediately and fire update(session) in the background. The
session update is a fast client-side operation and remains valid after
AddOrEditExpensePage unmounts because SessionProvider lives in _app.
The currency value is captured in the closure before resetState clears
the store, and onCurrencyPick already persists currency to the DB via
updateProfile.mutate, so the server has the correct value regardless.
The signed=true changes to currency conversion call sites and CSV
export caused incorrect behavior. Revert to the original signed=false
usage in:
- AddExpensePage.tsx onConvertAmount
- CurrencyConversion.tsx (both call sites)
- Export.tsx CSV export

The core #658 fix (add.tsx loading with signed=true and
currency-input.tsx onFocus with allowNegative) is kept.
@krokosik krokosik merged commit 77dd6d3 into main Jun 21, 2026
3 checks passed
@krokosik krokosik deleted the fix-658-negative-expense-edit-sign branch June 21, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Negative expenses are shown as positive expenses in details view

1 participant