Open Net Worth
Open Net Worth is an open-source personal AI wealth assistant you run yourself: one ledger for your whole financial life, worked by an AI agent you control.
翻訳待ち
Split expenses with friends from your iPhone. Scan receipts on the device, see who owes whom, and settle up — free, open source, and with no account to create.
翻訳待ち
金融Swift · JavaScriptGitHub @spliit-appオンライン所有者未確認
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Loading the full README. The saved excerpt is shown below.
A SwiftUI rewrite of the Spliit mobile app, shipping as an in-place
update to the existing App Store listing.
It replaces the Expo / React Native app (v1.1.0) under the same bundle ID,
app.spliit.spliitmobile, and migrates the data that app left on disk. See
ROADMAP.md for what's planned and why, and DESIGN.md for what the
app looks like and which of those choices are load-bearing.
Requirements: Xcode 26, iOS 26 deployment target, Docker for the end-to-end suite.Read the full README ↗A SwiftUI rewrite of the Spliit mobile app, shipping as an in-place update to the existing App Store listing.
It replaces the Expo / React Native app (v1.1.0) under the same bundle ID,
app.spliit.spliitmobile, and migrates the data that app left on disk. See
ROADMAP.md for what's planned and why, and DESIGN.md for what the
app looks like and which of those choices are load-bearing.
Requirements: Xcode 26, iOS 26 deployment target, Docker for the end-to-end suite.
make setup # installs XcodeGen
make test # unit tests on the host — a couple of seconds, no simulator
make build # build the app for the simulator
You never need to open Xcode. Spliit.xcodeproj is generated from
project.yml and is not in version control — run make generate after
changing a target, a build setting or a resource. Adding a source file needs nothing: the
whole Spliit/ folder is picked up.
Run make on its own to list every task.
make e2e-up # a throwaway Spliit instance on :3009
make e2e-seed # some groups and expenses to look at
make run # build, install and launch on this worktree's simulator
make shot # screenshot it
Every worktree gets a simulator of its own, named after its directory — Spliit my-branch —
so that runs in different worktrees stay out of each other's way. make sim-clean removes it
when you are done with the worktree.
To point the app somewhere else, override the address:
make run E2E_URL=https://spliit.app/
Three layers, all runnable from the command line.
| Command | What it covers | Needs |
|---|---|---|
make test | superjson coding, response decoding, request building, the storage migration, money formatting, date bucketing | nothing |
make strings | every string in the source is in a catalogue, and translated | nothing |
make test-live | the API client against a real server, including writes | make e2e-up |
make e2e | the app itself, in a simulator, against a real server | Docker |
make e2e brings the server up if it isn't already, then runs the UI suite. It leaves the
server running — see below.
make e2e-up # once, from any worktree
make e2e WORKERS=2 # in each worktree, at the same time
Two things make that work. Each worktree drives its own simulator: two runs sharing one device would install over each other, and XCUITest names its parallel clones after the device they came from, so even the clones would collide.
And the server is shared and long-lived. One is enough — every group is addressed by the ID
the server assigned, and groups.list takes those IDs as input, so no run can see another's
data. What a run must not do is tear it down on its way out: the database lives in tmpfs, so
that would take every other run's data with it. make e2e-down stops it, deliberately, when
nothing is using it.
Nothing else is shared: build output goes to each worktree's own build/. Do watch WORKERS,
though — each run boots that many simulator clones, so the sum across runs is what has to fit
on the machine.
Fixtures are recorded, not written. The JSON under
Packages/SpliitKit/Tests/SpliitAPITests/Fixtures is captured from a real instance by
make fixtures. Hand-written fixtures only prove the decoder agrees with our own
assumptions; recorded ones prove it agrees with the server.
Accessibility identifiers go on leaves, never containers. SwiftUI's
.accessibilityIdentifier applies to every descendant of the view it modifies, and an outer
one silently replaces the identifiers set inside it — a screen-level identifier on a
NavigationStack erases the identifier of every button beneath it. All identifiers live in
Shared/AccessibilityID.swift, shared by the app and the test
bundle, and are added in the same commit as the view they belong to.
Spliit/ the app: SwiftUI views, assets, the string catalogues
Shared/ code shared with the UI test bundle (accessibility identifiers)
Packages/SpliitKit/
SpliitAPI/ tRPC client, superjson coding, models, endpoints
SpliitCore/ stores, the React Native migration, formatting
SpliitUITests/ XCUITest end-to-end suites
Scripts/ what the Makefile reaches for that isn't one line of shell
e2e/ the disposable server: compose file and seed script
SpliitAPI and SpliitCore are plain SwiftPM libraries with no third-party dependencies, so
the protocol handling, the money maths and the migration are all testable in seconds without
launching a simulator.
Spliit's API is tRPC with the superjson transformer, at {baseURL}api/trpc. There is no REST
layer, so SpliitAPI speaks it directly: queries as GET …?input=<envelope>, mutations as
POST, both unbatched, which the server accepts.
Decoding ignores superjson's meta.values annotations — our models are statically typed, so a
field the server marks as a Date is already declared Date here. That is not a shortcut:
groups.list sends createdAt with no annotation at all, so relying on the metadata would
break exactly one endpoint.
Encoding does emit annotations, because the server rebuilds real Date instances before its
own validation runs. make test-live is what proves the envelopes we send are accepted.
Money is integer minor units, and minor units are not always hundredths. amount == 1234
is 12.34 in a two-decimal currency and ¥1,234 in a group counted in yen — the group's ISO
currency code decides, which is what MoneyFormatter reads it for. One more sharp edge:
paidFor[].shares is the share value ×100 for EVENLY, BY_SHARES and BY_PERCENTAGE
whatever the currency, but a raw minor-unit amount for BY_AMOUNT.
This is the beginning of the README. Read the rest at the original. Read the full README ↗