Skip to content

feat: local friends, full data export/restore & Splitwise Pro import#695

Open
xHecktor wants to merge 53 commits into
oss-apps:mainfrom
xHecktor:feat/local-friends-import-export
Open

feat: local friends, full data export/restore & Splitwise Pro import#695
xHecktor wants to merge 53 commits into
oss-apps:mainfrom
xHecktor:feat/local-friends-import-export

Conversation

@xHecktor

Copy link
Copy Markdown

Description

Summary

This PR bundles three related additions that make SplitPro usable without
forcing every participant to have an account, and that make moving data
in/out of SplitPro robust:

  1. Local friends — split with people who have no account/email.
  2. Full data export & restore — complete backup, not just balances.
  3. Splitwise Pro import — import a full Splitwise history with live
    progress.

Related to #683 (add local/non-registered users)
Related to #307 (Splitwise import 2.0)
Related to #302 (export group data)

1. Local friends

  • Add a friend by name only — no email required
    (AddMembers, UserInput, SelectUserOrGroup).
  • Rename a friend — RenameFriend component + updateFriendName
    mutation, from the friend balance screen.
  • Merge a local friend into a real account once they sign up —
    MergeLocalFriend component + mergeLocalFriend mutation; moves all
    expense participations to the target user and removes the placeholder.

2. Full data export & restore

  • downloadData now returns a complete export (users, groups, expenses,
    participants) via getFullExportData.
  • New import-splitpro page with two modes:
    • Merge — add imported data to the current account, with per-group
      selection.
    • Restore — wipe and replace (destructive, clearly warned).

3. Splitwise Pro import

  • Imports the full Splitwise Pro JSON export, including group structure
    and per-expense participants reconstructed from Splitwise repayments.
  • Wrong-file detection: uploading a Splitwise file on the SplitPro page
    (and vice-versa) shows a clear error toast instead of failing silently.
  • Splitwise import page now explains the two export paths (balance-only
    link vs. full Splitwise Pro backup).

Shared infrastructure

  • Live progress via SSE — streaming endpoints
    (import-splitpro-stream, import-splitwise-stream) on a small
    sseHelper (keep-alive pings, compression bypass), surfaced through the
    useImportStream hook + ImportLogWindow.
  • Account matching on login — imported placeholder users are
    auto-matched to the real account by email (server/auth.ts).
  • i18n — all new strings translated across all 23 locales.

Checklist

  • Add / rename / merge a local friend (participations transfer correctly)
  • Export data → re-import (merge with group selection) → restore (wipe)
  • Import a Splitwise Pro JSON export; groups & participants reconstructed
  • Wrong-format upload shows the error toast
  • Live progress log streams during a large import

xHecktor and others added 30 commits June 22, 2026 20:34
* test: check write access

* feat: add local friends (name-only) and rename support

- Allow adding friends by name only (without email) in all three flows:
  expense dialog, group member picker, and friend list
- Add RenameFriend component (AlertDialog) on the friend detail page,
  visible only for local users without an email address
- Add updateFriendName tRPC mutation; authorization uses shared
  ExpenseParticipant records so renaming works even after settle-up
  or when the friend was just added but no expense exists yet
- Server-side guard prevents renaming registered users (linked OAuth
  account or verified email)

* ci: add Docker Hub build and push workflow

* refactor: move rename into settings drawer, remove separate pencil button
* feat: merge local friend into registered account

* feat: merge local friend into registered account
…merge in all 21 locales (#3)

Co-authored-by: Claude <noreply@anthropic.com>
- Export now includes individual expenses with all participants
- New importSplitProData mutation maps users by email, creates local
  placeholders for unknown users, and skips duplicate expenses by ID
- Import page at /import-splitpro with file preview before importing
- Auto-match at login: when a new user signs in, any local placeholder
  with the same email is merged into the new account automatically
- All 23 locales updated with new translation keys


Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX

Co-authored-by: Claude <noreply@anthropic.com>
…match

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX
When an expense already exists but a participant was deleted (e.g. local
user removed), re-add the missing ExpenseParticipant rows so balances
are correctly restored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX
- Detect Splitwise Pro backup format (has repayments in expenses)
- Reconstruct expense participants from repayments
- Map users by email, create local placeholders for unknown users
- Create groups from backup if they don't exist yet
- Deterministic UUID from Splitwise expense ID prevents duplicates on re-import
- Legacy balance-only import mode still works unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX
claude added 23 commits June 23, 2026 13:32
Large backups (1334 expenses) exceed the tRPC JSON body limit. Instead
upload the file directly to a dedicated API route that parses it server-side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX
Users referenced in expenses but not in friends list are now created as
local placeholders using their name from the group member data.
Re-importing is still safe — existing users are matched by name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX
For regular expenses, 'to' in repayments = who paid the bill.
For payment expenses, 'from' = who sent the settlement money.
Using wrong direction caused incorrect balances for settled friends.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEcTdEHzqDa6ubbvaRz1tX
VND, JPY, KRW and other zero-decimal currencies must not be multiplied
by 100 when converting amounts to internal storage, as they have no
subunits. Also repairs oxlint-corrupted code patterns from previous
commit attempts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Splitwise repayments use the same from/to semantics for both regular
expenses and payments: from=debtor, to=creditor. The previous change
incorrectly swapped these for payment expenses, causing ~10x balance
errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Imported users have an email address but no linked account and no
emailVerified flag. The previous checks treated any user with an email
as a registered user, blocking rename/merge in both UI and backend.

- Frontend: show rename/merge options when accounts.length == 0 and
  emailVerified is null, regardless of email presence
- Backend getFriend: include accounts so the frontend can check
- Backend mergeLocalFriend: drop the email fallback from the local-user
  guard; only accounts.length and emailVerified matter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Adds a choice between two import modes on the import page:
- Merge (default): new data is added, existing expenses are kept
- Restore: all existing expenses and groups of the current user are
  deleted first, then the backup is imported cleanly

The restore function removes all expenses the user is involved in
(as payer or participant), cleans up group memberships, and then
calls the existing importSplitProData logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Replace hardcoded German strings with i18n keys for the SplitPro
import mode selection (merge vs restore). Adds translations for
all 23 locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
When two exported user IDs map to the same DB user ID, createMany
would fail with a unique constraint error on (expenseId, userId).
Using skipDuplicates silently ignores these conflicts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
When two exported user IDs map to the same DB user ID (e.g. after
a user merge), the participants array could contain duplicate userIds,
causing a unique constraint violation on (expenseId, userId).

Deduplicate by userId using a Map before the expense create call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
When importing with restore mode, group memberships (groupUsers) were
deleted but not re-created for groups that already existed (matched by
publicId). Now restores missing member entries on existing groups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Both import flows now stream real-time progress via Server-Sent Events:
- New SSE API routes: /api/import-splitpro-stream, /api/import-splitwise-stream
- ImportLogWindow component shows timestamped log entries with color coding
- useImportStream hook manages SSE stream reading and log state
- Service functions accept optional onLog callback for progress/error reporting
- Logs show user mapping, group creation, per-100 expense progress, and errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Next.js compress middleware buffers the entire response, preventing SSE
events from reaching the client in real-time. Disable compress globally
(reverse proxy handles compression in production), add explicit flush()
after each write, and set anti-buffering headers (X-Accel-Buffering,
Content-Encoding, no-transform).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Extract shared SSE setup into sseHelper. Send an SSE comment (: ping)
every 5 seconds to keep connections alive through any reverse proxy
(nginx, Apache, Traefik, Synology Web Station etc.) and force buffer
flushes even on proxies that ignore X-Accel-Buffering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Remove automatic redirect to /balances after import finishes so
users can review the log before navigating away manually.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
Use raw ServerResponse.writeHead() to write SSE headers directly,
bypassing the Next.js compression middleware. This keeps gzip
compression for all normal pages while allowing SSE to stream
unbuffered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
- Log why each expense was skipped (already exists / unknown payer)
- Include expense ID in skip messages for debugging
- Warn when multiple exported user IDs map to same DB user (merged
  accounts) so the deduplication is visible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
…ut DB access

- Show amount, currency, date, and group name for skipped/failed expenses
- Show user names instead of raw IDs in dedup warnings
- Include DB user IDs in user mapping logs for traceability
- Show restored member names in group restoration logs
- Upgrade missing-user expense skips from warn to error level
- Add human-readable context to Splitwise import logs too

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
User logs now include export IDs and DB IDs, expense logs include
UUIDs (SplitPro) or Splitwise IDs with mapped UUIDs, group logs
include publicId/SW ID and DB ID. Human-readable names are kept
alongside for frontend readability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
When a SplitPro export is uploaded on the Splitwise import page (or vice
versa), show a toast explaining which import page to use instead of
silently falling through to an incompatible import mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
In merge mode, both import pages now show checkboxes for each group,
allowing users to selectively import only specific groups and their
expenses. All groups are selected by default with a select/deselect all
toggle. Restore mode continues to import everything.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
- Replace single export link with two-option layout on Splitwise import page:
  option 1 (balance-only via external link) and option 2 (full backup via Splitwise Pro)
- Increase wrong-file-format toast error duration to 10s
- Add translations for all 6 new keys in all 23 locale files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wMSFyaUDFVGr31qqm7tk7
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.

2 participants