Repository navigation
feat(desktop): Compose Desktop client for the wallet and linking - #1894
Merged
Merged
Conversation
All 14 KMP library modules repeated the same android compileSdk/minSdk block and the same five Apple targets, and only text-format applied the convention plugin. The plugin now declares those targets, and every module applies it and keeps only its namespace and test opt-ins. shared-core attaches its SharedCore framework to each native target via targets.withType<KotlinNativeTarget>() instead of a local target list. discrete-curve hard-coded compileSdk = 37; it now picks up the catalog's 37 with minor API level 2 like the other modules.
First step of the desktop client: the KMP libraries need a JVM target before Compose Desktop modules can depend on them. flipcash.kmp.library now declares jvm() with jvmTarget from the android-java catalog version. Two expect declarations needed JVM actuals: - sha512 derivePlatform: the JDK ships PBKDF2WithHmacSHA512, so the actual is the Android JCA one with the same ASCII-only guard. - Ed25519Kmp: pure Java on net.i2p.crypto:eddsa, which already backs the test-utils shadow. The 64-byte private key is eddsa's clamped hash `h`, the same orlp layout the JNI and cinterop actuals return, so keys move between platforms unchanged. The RFC 8032 vectors in ed25519.json pass on jvmTest, so no native library is loaded on desktop. sign.c includes sha3.h, but that header declares the sha512_* functions and sha3.c is libtom's SHA-512 (it carries the 0x428a2f98d728ae22 round constant). It's a misnamed file, not SHA-3, which is why standard SHA-512 on the JVM reproduces the native signatures.
Hilt's Gradle plugin rejects the AGP KMP android target, so KMP modules run the Hilt KSP processors through kspAndroid without the plugin. Only the app and notifications use @androidentrypoint, which is what the plugin's bytecode rewrite exists for. libs:coroutines moves onto the plugin, and its test fixtures become a :libs:coroutines:test-fixtures module, since the KMP android target has no testFixtures. shared:funding moves its sources to androidMain as the first Hilt-bearing module on the KMP plugin.
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.
…ose resources :apps:desktop is a kotlin.jvm + compose.desktop app with an empty Metro graph. ui:resources now also generates a Compose Multiplatform Res class from its Android res/ through a Sync task, so common code can read the same strings and drawables without a second copy in the repo.
Fonts load through an expect seam: Android keeps R.font, desktop reads the same files from the classpath. Dynamic color and the View helpers stay in androidMain.
Only the files the balance screen reaches move to commonMain; the rest stay in androidMain unchanged. Drawable lookups by Android resource id go through painterForResId, with a desktop ResourceHelper over Compose resources. Blur support and hex colour parsing get small platform seams.
CodeNavigator, its options, flow scope and the nav-result types move to commonMain; navigation3 1.1.4 publishes a desktop variant, so the back stack type is the same artifact on both platforms. Scenes, flow hosts and AppNavHost stay in androidMain. RepeatOnLifecycle reads LocalLifecycleOwner from lifecycle-runtime-compose, the class the old androidx.compose.ui.platform one already aliases on Android.
The FlipcashAnalytics interface, the token symbol resolver and the CompositionLocal move to common code. Conversions (toUri and app:core's DeeplinkType) and the Mixpanel delegate stay Android. RecordingAnalytics moves to a :test-fixtures module, since the KMP Android target has no testFixtures; its 10 consumers point at that module. Also moves ui:navigation's BaseViewModel to common code; it uses only lifecycle and coroutines.
Flag labels sit behind a FlagStrings expect object (Android keeps the R.string ids; desktop has no flag editor). UserFlagsFiles replaces the Context parameter so the DataStore file comes from the platform.
AppRoute, its step and result types, DeviceContact and TipCardOwner move to app:core's commonMain with the shared Parcelable aliases. The three files that use Uri or R (CurrencyCreatorDraft, TokenDisplayName, DeeplinkType) stay in androidMain. FlowRoute moves to ui:navigation's commonMain. ui:resources adds stringRes/pluralStringRes for composables that hold an Int resource ID, and lets other modules register their own Compose resources with ComposeResourceIds to get an Int ID on desktop.
… composables Lifts the nine composables the balance screen and activity feed render (TileButton, TokenCard, TokenCardStack, TokenImageWithName, ViewerAmount, ActivityAmount, CurrencyAppreciationLabel, FlagWithFiat, NewUserTutorial) to commonMain. Everything else stays in androidMain unchanged. Seams: screenHeightPx (Configuration on Android, window container on desktop) and ImageRequestPlaceholders for Coil placeholders. Flags load through painterForResId. app:core exposes its res/ as Compose resources and gains CoreStrings/CoreDrawables id objects for the lifted files. opencode-compose moves to KMP with LocalExchange in commonMain.
…vatar BlurHash decodes to pixels in common code; each platform wraps them (Android still builds the same ARGB_8888 Bitmap, and keeps BlurHash.decode as an extension). ContactAvatar uses Coil's LocalPlatformContext and passes the URL string to Coil, which maps it as it did the Uri. app:core's media resolver and TimeProvider move to common; the Coil-backed ImageCachePresence stays Android.
Lift the activity feed coordinator and updater, both mappers and the recent-activity composables to commonMain. String ids go through a TxStrings expect object (R.string on Android, the Compose resource registry on desktop); the 24-hour clock check is an expect seam. NetworkUpdater moves to app:core's commonMain.
PurchaseMethod, PurchaseMethodController and PurchaseMethodState move to commonMain so the balance view model can take the controller on desktop. The controller implementation and its UI stay in androidMain. The module moves to flipcash.kmp.app.library, since its app-module dependencies have no Apple targets.
…let coordinators TokenCoordinator, WalletRevealCoordinator, TokenSelectionResolver and SelectTokenViewModel move to commonMain; the rest of the module stays in androidMain. Two app:core seams replace the Android types TokenCoordinator used: - PreferencesFiles hands out the DataStore file; Android binds it to Context.preferencesDataStoreFile through Hilt. - AppProcessLifecycle is ProcessLifecycleOwner on Android and a lifecycle held in RESUMED on desktop, since lifecycle-process has no desktop artifact. SelectTokenViewModel becomes an open class without Hilt annotations. HiltSelectTokenViewModel in androidMain carries @hiltviewmodel, and the ten hiltViewModel<SelectTokenViewModel>() call sites across eight features now request it.
WalletViewModel becomes an open class in commonMain with an @Inject constructor; the @hiltviewmodel subclass HiltWalletViewModel stays in androidMain, and the Android entry point asks Hilt for it. The wallet content and header composables move to commonMain and read their strings and drawables through CoreStrings/CoreDrawables. The view model only called ChatCoordinator.hasEverTipped(), so it now takes a one-method TipHistorySource from app:core. ChatCoordinator extends it and ChatModule binds it, so Android injects the same object and balance no longer depends on shared:chat (10k lines). ReportDrawnWhen becomes an expect seam (no-op on jvm). card-expand moves to android+jvm with everything in commonMain.
Desktop builds WalletViewModel from a Metro graph that reuses the Dagger modules Android's Hilt graph installs. OpenCodeModule and FlipcashModule split into shared providers (commonMain), the Android channel/protocol config (androidMain) and an okhttp channel module in apps/desktop. Internal @provides functions carry @JvmName so Dagger's factory names stay unmangled for Metro's runtime interop. DesktopModule binds the ten seams Android provides from Hilt modules; funding, analytics and connectivity are stubs. shared:theme moves to common so the window paints the Flipcash colour scheme.
DevSession reads FLIPCASH_DEV_MNEMONIC, which Gradle sets for the desktop JavaExec tasks only when local.properties has flipcash.test.mnemonic. It goes in the environment rather than a -D flag so the phrase stays out of the process command line, and it is never logged. Four gaps kept the wallet on its loading state after sign-in: - Nothing constructed EventRepository, which turns Events.OnLoggedIn into SessionListener.onUserLoggedIn. Android builds it eagerly in FlipcashApp; the graph now exposes it and devSignIn touches it first. - TokenCoordinator joins the listener set in TokenModule, which is Android-only. DesktopModule now adds it. - Through Dagger's generated factories, Metro 1.4.5 drops provider-level @singleton, so each injection site got its own ChannelEventBus and events never reached the repository. services:opencode now applies Metro with Dagger interop and generates its own factories instead of running dagger-compiler on jvm. - The bonding curve tables were never loaded, so every balance threw "DiscreteBondingCurve not initialized" inside a retry that swallowed it. devSignIn calls Curves.initialize() first, as Android's Application does. OpenCodeExchange's state fields move above init: init hands streamRates to another thread, which could read them before they were assigned.
Token logos are https URLs. Coil 3 ships no network fetcher, and the desktop app had no coil-network artifact on its classpath, so every logo failed with "Unable to create a fetcher" and the card stack showed blank avatars. coil-network-okhttp registers its fetcher through ServiceLoader, so adding the dependency is the fix. installImageLoader mirrors FlipcashApp.newImageLoader (crossfade, memory cache, 50 MB disk cache), minus the chat photo keyer and fetcher.
…ke-land * origin/code/cash: feat(payments): persist payment requests for offline (#1886) (#1892) feat(chat): add pin message contract scaffolding (#1884) # Conflicts: # apps/flipcash/shared/persistence/db/src/androidHostTest/kotlin/com/flipcash/app/persistence/dao/GroupKeyEnvelopeDaoTest.kt # apps/flipcash/shared/persistence/db/src/androidHostTest/kotlin/com/flipcash/app/persistence/dao/PaymentRequestDaoTest.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/dao/GroupKeyEnvelopeDao.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/dao/PaymentRequestDao.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/entities/GroupKeyEnvelopeEntity.kt # apps/flipcash/shared/persistence/db/src/commonMain/kotlin/com/flipcash/app/persistence/entities/PaymentRequestEntity.kt # apps/flipcash/shared/persistence/sources/src/androidHostTest/kotlin/com/flipcash/app/persistence/sources/ChatRemovalDataTest.kt # apps/flipcash/shared/persistence/sources/src/commonMain/kotlin/com/flipcash/app/persistence/sources/GroupKeyEnvelopeDataSource.kt # apps/flipcash/shared/persistence/sources/src/commonMain/kotlin/com/flipcash/app/persistence/sources/PaymentRequestDataSource.kt # services/flipcash/src/androidHostTest/kotlin/com/flipcash/services/internal/network/extensions/PinMappingTest.kt # services/flipcash/src/androidHostTest/kotlin/com/flipcash/services/models/paymentrequest/PaymentRequestStorageTest.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/chat/GroupKeyEnvelopeStore.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/models/chat/Pin.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/models/chat/PinSummaryStorage.kt # services/flipcash/src/commonMain/kotlin/com/flipcash/services/models/paymentrequest/PaymentRequestStorage.kt # services/flipcash/test-fixtures/src/commonMain/kotlin/com/flipcash/services/chat/InMemoryGroupKeyEnvelopeStore.kt
… code/cash persistence code
This was referenced Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands the desktop stack (#1838, #1843, #1844, #1849, #1850) as one squash, merged up to current
code/cash. Those PRs will be closed in favour of this one.What it adds:
jvm()target on the shared KMP libraries, and the balance data stack, services, persistence, tokens, transaction history, core-ui and the UI modules moved toandroid+jvmsource sets.apps:desktop: the wallet screen on Compose Desktop, with the account seed in the OS keyring.Merging
code/cashin:code/cashstrings were added there.FlagWithFiat,NewUserTutorialandPriceWithFlagstay deleted, as oncode/cashafter feat(onboarding): new-user onboarding tweaks #1862.androidMain.Test fixes:
flipcashTestDebugnow runs theandroidHostTesttasks of the moved modules. Before this, CI skipped those tests.FakeResourceHelperresolves Compose string resources fromR.stringstubs. Without that, 26 transaction-history mapper tests failed.ic_delete.xmluses#FFFFFFFFbecause the Compose resources parser rejects@android:color/white.