Skip to content

build(kmp): move the balance data stack to android+jvm (desktop spike A1) - #1843

Closed
bmc08gt wants to merge 11 commits into
feat/desktop-spikefrom
feat/desktop-spike-a1
Closed

bmc08gt wants to merge 11 commits into
feat/desktop-spikefrom
feat/desktop-spike-a1

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1838. This moves the data stack behind features/balance onto an android + jvm KMP target, so a desktop build can fetch the balance with the same services, Room database and data sources the Android app uses. No UI moves here; that's A2.

What moved

flipcash.kmp.app.library, added in #1838, is the convention plugin for modules that target only Android and the JVM. Because both targets are JVMs, commonMain can use java.* directly, which keeps most conversions to a plugin swap and a git mv.

Modules this PR moves onto it: network:jwt, currency-math, encryption:keys, datetime, messaging, parcelable, logging, ui:resources, currency, models, locale:public, connectivity:public, services:opencode, services:flipcash, persistence:db, persistence:sources, tokens:core, and apps:flipcash:core (models only; its Compose code stays in androidMain until A2).

The seams that needed writing by hand:

  • Parcelize: libs:parcelable exposes a common @Parcelize/Parcelable alias. Classes with hand-written parcel code implement ManualParcelable, which on Android is a sub-interface of android.os.Parcelable.
  • Logging: PlatformLog expect/actual, a LogFileStore interface for the file tree, and BuildFlags, set from the app's BuildConfig in TraceInitializer. Library modules on the KMP android target don't get a BuildConfig.
  • Resources: ResourceHelper is common; the Android-typed members moved to AndroidResourceHelper, which only accesskey uses.
  • ed25519: callers move from the Java JNI class to a common com.getcode.ed25519kmp.Ed25519 with the same members (about 150 imports). Key derivation that went through Base64.DEFAULT now derives from the byte seed and produces the same key.
  • Analytics: UserManager takes an AnalyticsIdentifier instead of MixpanelAPI; FlipcashModule binds it to Mixpanel.
  • Room: migrations run on SQLiteConnection, the database uses @ConstructedBy, and withTransaction goes through an inTransaction expect/actual. The JVM opens the database with BundledSQLiteDriver.

Build shape changes on Android

  • Hilt's Gradle plugin refuses the AGP KMP android target. These modules drop the plugin and run Hilt's processors through kspAndroid; only the app and notifications modules use @AndroidEntryPoint, so libraries didn't need the plugin. Modules with a Hilt @Module need withJava() on the android target, or the app's graph can't see their bindings.
  • AGP testFixtures aren't available on the KMP android target, so ui:resources and services:flipcash each get a :test-fixtures module (as coroutines did in feat(kmp): add a jvm() target to the shared KMP libraries #1838), and their consumers point at it.
  • NOTIFY_ERRORS in the app's BuildConfig is now set per build type and copied into BuildFlags; nothing read the old field.

Headless balance test

services:opencode has a jvmTest that derives the owner from a recovery phrase and calls getTokenAccounts over a plain grpc-okhttp channel. It reads the phrase from FLIPCASH_TEST_MNEMONIC or flipcash.test.mnemonic in local.properties, and skips when neither is set, so CI stays green without a credential.

It hasn't run against the backend yet: no phrase is configured on the machine that built this.

Not in this PR

shared:tokens (the token coordinator), shared:transaction-history, and moving shared:funding out of androidMain are deferred to A2. shared:tokens depends on amount-entry, onramp:*, shareable and haze, so converting it means converting UI modules first.

Converts network:jwt, currency-math, encryption:keys, datetime and
messaging onto flipcash.kmp.app.library and gives encryption:utils a
jvmShared source set for its java.*/protobuf helpers.

Adds :libs:parcelable, a common Parcelable/@parcelize stand-in that
typealiases android.os.Parcelable on Android, so parcelable models can
live in commonMain. currency-math's test fixtures become
:libs:currency-math:test-fixtures, and the desktop build reads the curve
tables from the classpath.
…lable

A common @parcelize class implemented the JVM Parcelable's abstract members on Android only,
so it failed to compile for jvm. Parcelable is now a member-free marker; PublicKey, which writes
its own parcel format, implements ManualParcelable.
Timber, SystemClock and android.database.SQLException move behind PlatformLog
actuals; FileTree stays in androidMain and plugs in through LogFileStore.
The library had debug and release values for NOTIFY_ERRORS, which a
single-variant KMP target can't carry, so the app now sets BuildFlags from its
own BuildConfig at startup.
ResourceHelper keeps the members that return plain types in commonMain.
The four that return Drawable, Typeface, DisplayMetrics and File move
to AndroidResourceHelper, which only the access-key screens use; the
app binds both. FakeResourceHelper moves from testFixtures to
:ui:resources:test-fixtures, since the KMP library target has no
testFixtures.
…droid+jvm

Currency icons sit behind an expect object (null on desktop), and
SocialProfile's file extension lookup gets a JVM copy of the AOSP
MimeTypeMap rules.
services:opencode now builds for jvm with Hilt wiring left in androidMain.
Its keys came from the Java JNI class com.getcode.ed25519.Ed25519, which
has no desktop equivalent, so:

- ed25519 gains a jvmShared source set with a Kotlin Ed25519 object
  (same members as the Java class) over Ed25519Kmp. KeyPair keeps the
  Base64 string fields and its Parcelable form, so stored keys are
  unchanged. Callers move from com.getcode.ed25519 to
  com.getcode.ed25519kmp.
- mnemonic's DerivedKey, MnemonicPhrase and MnemonicCache, and utils'
  Ed25519 extensions, move from androidMain to jvmShared.
- logging gains a plain Log (Timber on Android) for the two opencode
  files that logged without trace()'s gating.
- toInstantFromMillis moves to datetime's commonMain.
- UserManager reads BuildFlags.isDebug instead of opencode's
  BuildConfig, which a KMP library no longer generates.

The KMP Android target now compiles Java (withJava()): Hilt's KSP
processors emit hilt_aggregated_deps as Java, and without it the app
graph lost every opencode @module binding.

Adds HeadlessBalanceTest (jvmTest), which fetches token balances from
the OCP backend over grpc-okhttp with no UI or DI. It reads the phrase
from FLIPCASH_TEST_MNEMONIC or flipcash.test.mnemonic in
local.properties and skips when neither is set.
services:flipcash now applies flipcash.kmp.app.library with Hilt run
through kspAndroid. Android-only pieces sit behind seams:

- UserManager takes an AnalyticsIdentifier instead of MixpanelAPI;
  FlipcashModule binds it to Mixpanel.identify.
- ForegroundGate is a common interface; the process-lifecycle
  implementation stays in androidMain.
- ChatId's hand-written parcel code moves to ManualParcelable.
- KeyPair derivation goes through the byte-seed path shared with
  services:opencode, so derived keys are unchanged.

Test fakes move to :services:flipcash:test-fixtures. shared:analytics
reads BuildFlags.isDebug instead of this module's BuildConfig.
FlipcashDatabase, its DAOs, entities and migrations now live in commonMain.
Migrations run on SQLiteConnection, MessageDao's raw query takes a
RoomRawQuery, and init() takes a builder factory: Android passes
Room.databaseBuilder(context), desktop opens a file through
BundledSQLiteDriver. isOpen() stays Android-only, since Room exposes
RoomDatabase.isOpen there alone.
The module keeps everything it had in androidMain, with the dependencies
flipcash.android.feature used to add. Only the blocklist and activity
feed models, which persistence:sources maps to and from, move to
commonMain.
Data sources call a common inTransaction instead of room-ktx's
withTransaction, which Room KMP only offers on Android. Android keeps
withTransaction; the JVM runs the block on Room's writer connection.
Token icons encode with java.util.Base64, which matches Android's NO_WRAP
output. The draft data source and its mappers use Uri, so they stay in
androidMain.
@bmc08gt bmc08gt self-assigned this Oct 23, 2026
@github-actions github-actions Bot added type: build Build system, Gradle, dependencies area: payments Payments, transfers, intents, billing area: crypto Solana, keys, encryption, signing area: auth Login, session, access keys, identity area: ui Compose UI, theme, components, resources area: network gRPC, connectivity, API, exchange rates area: notifications Push notifications, in-app messaging area: build-system Gradle, convention plugins, build-logic area: tokens Token accounts, balances, token info area: intents Intent construction, submission, server-side state area: session area: onboarding and removed type: build Build system, Gradle, dependencies labels Oct 23, 2026
@bmc08gt

bmc08gt commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Landed in #1894 as one squash against code/cash.

@bmc08gt bmc08gt closed this Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth Login, session, access keys, identity area: build-system Gradle, convention plugins, build-logic area: crypto Solana, keys, encryption, signing area: intents Intent construction, submission, server-side state area: network gRPC, connectivity, API, exchange rates area: notifications Push notifications, in-app messaging area: onboarding area: payments Payments, transfers, intents, billing area: session area: tokens Token accounts, balances, token info area: ui Compose UI, theme, components, resources

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant