From 8faa4b8f7318734e2e98abca02d6420a74b67d2c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 20:07:33 +0000 Subject: [PATCH 1/7] docs: stand up architecture documentation for the Android app Add a docs/architecture/ suite modeled on the iOS app's architecture docs (code-ios-app e53fe01), tailored to the Android codebase: a README index plus nine numbered topic docs (modules & boundaries, state & DI, navigation, networking, persistence, payments & operations, design system, cross-cutting concerns, separation of concerns) and a representative feature catalog. Each doc opens with an intent statement and a Mermaid diagram. Documents the code as it exists today, correcting three stale CLAUDE.md notes: navigation is Navigation3 + a custom CodeNavigator (no Voyager); the Room DB is not SQLCipher-wrapped but uses a per-user database name derived from account entropy; async is effectively pure Coroutines + Flow (no RxJava). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JRVfsXp4HDrDMy7Pbmw9fD --- .../architecture/01-modules-and-boundaries.md | 141 +++++++++++++++ .../02-state-and-dependency-injection.md | 165 ++++++++++++++++++ docs/architecture/03-navigation.md | 153 ++++++++++++++++ docs/architecture/04-networking.md | 137 +++++++++++++++ docs/architecture/05-persistence.md | 108 ++++++++++++ .../06-payments-and-operations.md | 137 +++++++++++++++ docs/architecture/07-design-system.md | 67 +++++++ .../architecture/08-cross-cutting-concerns.md | 109 ++++++++++++ .../architecture/09-separation-of-concerns.md | 76 ++++++++ docs/architecture/README.md | 77 ++++++++ docs/architecture/features/README.md | 70 ++++++++ 11 files changed, 1240 insertions(+) create mode 100644 docs/architecture/01-modules-and-boundaries.md create mode 100644 docs/architecture/02-state-and-dependency-injection.md create mode 100644 docs/architecture/03-navigation.md create mode 100644 docs/architecture/04-networking.md create mode 100644 docs/architecture/05-persistence.md create mode 100644 docs/architecture/06-payments-and-operations.md create mode 100644 docs/architecture/07-design-system.md create mode 100644 docs/architecture/08-cross-cutting-concerns.md create mode 100644 docs/architecture/09-separation-of-concerns.md create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/features/README.md diff --git a/docs/architecture/01-modules-and-boundaries.md b/docs/architecture/01-modules-and-boundaries.md new file mode 100644 index 000000000..ae27f1ca0 --- /dev/null +++ b/docs/architecture/01-modules-and-boundaries.md @@ -0,0 +1,141 @@ +# 01 — Modules & boundaries + +Flipcash Android is a multi-module Gradle project of roughly **132 modules**. The +module graph is the architecture: layering, ownership, and what may depend on what +are all enforced by where a module lives and which **convention plugin** it applies. +This document is the map. + +## Module inventory + +| Group | Path prefix | Count | Role | +|-------|-------------|-------|------| +| App | `:apps:flipcash:app` | 1 | Entry point — `FlipcashApp`, `MainActivity`, the navigation host, DI wiring. Assembles everything. | +| Core | `:apps:flipcash:core` | 1 | App-wide infrastructure: `AppRoute` graph, `Local*` composition locals, shared models. Auto-injected into every feature. | +| Features | `:apps:flipcash:features:*` | 26 | Self-contained screens (login, cash, balance, tokens, scanner, withdrawal, …). Each owns its state, ViewModels, and UI. | +| Shared | `:apps:flipcash:shared:*` | 34 | Cross-feature coordinators, controllers, and services (authentication, session, router, payments, persistence, …). | +| Services | `:services:*` | 4 | gRPC wrappers: `flipcash`, `flipcash-compose`, `opencode`, `opencode-compose`. | +| Definitions | `:definitions:*` | 4 | Protobuf sources (`*/protos`) and generated models (`*/models`) for Flipcash and OCP. | +| UI | `:ui:*` | 9 | Compose layer: `theme`, `components`, `core`, `resources`, `navigation`, `scanner`, `biometrics`, `emojis`, `testing`. | +| Libs | `:libs:*` | 21 | Leaf utilities: crypto/encryption, network, logging, currency, coroutines, permissions, locale, … | +| Vendor | `:vendor:*` | 3 | Third-party SDKs wrapped as modules: Kik scanner, OpenCV, TipKit. | + +Every module is declared in [`settings.gradle.kts`](../../settings.gradle.kts). + +## Dependency graph + +```mermaid +graph TD + App[":apps:flipcash:app"] + Feat[":apps:flipcash:features:*"] + Shared[":apps:flipcash:shared:*"] + Core[":apps:flipcash:core"] + Svc[":services:*"] + Defs[":definitions:*:models"] + UI[":ui:*"] + Libs[":libs:*"] + Vendor[":vendor:*"] + + App --> Feat + App --> Shared + App --> Core + Feat --> Shared + Feat --> Core + Feat --> UI + Feat --> Libs + Shared --> Svc + Shared --> Core + Shared --> UI + Shared --> Libs + Svc --> Defs + Svc --> Libs + Core --> Svc + Core --> UI + UI --> Libs + Defs --> Libs + Libs --> Vendor +``` + +*Arrows point to dependencies; the graph is acyclic.* + +## Convention plugins + +All modules apply a convention plugin from +[`build-logic/convention`](../../build-logic/convention/src/main/kotlin/) rather +than repeating Gradle config. There are four: + +| Plugin | Source | Applies | Notable injected deps | +|--------|--------|---------|-----------------------| +| `flipcash.android.library` | `AndroidLibraryConventionPlugin.kt` | `com.android.library`, serialization, Java/Kotlin 21 toolchain, unit-test defaults | `timber`, `kotlinx-coroutines-core`, test utils | +| `flipcash.android.library.compose` | `AndroidLibraryComposeConventionPlugin.kt` | the base library plugin + Compose compiler | Compose BOM, `compose-ui`, `compose-foundation` | +| `flipcash.android.feature` | `AndroidFeatureConventionPlugin.kt` | the compose plugin + KSP + Hilt + Parcelize | see below | +| `flipcash.android.ed25519.shadow` | `AndroidEd25519ShadowConventionPlugin.kt` | shadow-jar handling for the Ed25519 native lib | used by `:services:opencode` | + +### What the feature plugin gives you for free + +`AndroidFeatureConventionPlugin` is the workhorse — every feature and most shared +modules use it. From +[`AndroidFeatureConventionPlugin.kt`](../../build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt): + +```kotlin +"implementation"(libs.findBundle("hilt").get()) +"ksp"(libs.findBundle("hilt-compiler").get()) +"implementation"(libs.findBundle("compose").get()) +"implementation"(libs.findLibrary("rinku-compose").get()) + +"implementation"(project(":libs:logging")) +"implementation"(project(":ui:core")) +"implementation"(project(":ui:components")) +"implementation"(project(":ui:navigation")) +"implementation"(project(":ui:resources")) +"implementation"(project(":ui:theme")) + +if (path != ":apps:flipcash:core") { + "implementation"(project(":apps:flipcash:core")) +} +``` + +So a feature module never declares Hilt, Compose, the UI layer, logging, or +`:apps:flipcash:core` by hand — it inherits them. Its `build.gradle.kts` only lists +the *additional* shared modules, libs, or sibling features it needs. + +## The `public` / `impl` / `bindings` pattern + +Several libs split into three modules so consumers depend on an interface, not an +implementation: + +- `:libs::public` — pure API: interfaces and data classes, **no Hilt**. +- `:libs::impl` — the implementation, `api(public)`, Hilt bindings. +- `:libs::bindings` — composition layer: `implementation(impl)` + + `api(public)`, re-exporting the API and wiring Hilt. + +Consumers depend on `:bindings` and receive the API plus injection. Examples: +`:libs:locale:*`, `:libs:permissions:*`, `:libs:network:connectivity:*`, +`:libs:vibrator:*`. + +## Boundary rules (enforced by convention) + +1. **`ui/*` and `libs/*` never depend on app modules.** They may depend on other + `ui/*` / `libs/*` and on external libraries only. `ui/components` knows about + `libs/currency` and `ui/theme`, never about a feature. +2. **`libs/*` is leaf-level.** It may depend on other libs, `vendor/*`, and + `definitions/*:models` (data only), but never on services or app modules. +3. **`services/*` wrap protobuf, nothing app-specific.** They depend on + `definitions/*:models`, libs, and the gRPC runtime; they re-export public + interfaces with `api(...)`. They never depend on features or shared modules. +4. **`definitions/*:models` is generated; don't hand-edit it.** Regenerate from the + `.proto` sources in `definitions/*/protos`. +5. **Features may depend on shared modules, libs, ui, core, and (occasionally) + other features** — e.g. `:features:login` pulls in `:features:purchase` for the + onboarding hand-off. Keep cross-feature edges rare and acyclic. +6. **Shared modules depend on services, libs, ui, core, and each other** — never on + features. This is where business logic that several features share lives. +7. **`:apps:flipcash:app` is the only universal collector.** It depends on every + feature and the critical shared modules and assembles the graph. + +## Why this matters + +The plugin-injected baseline keeps modules uniform and small, the directory a +module lives in tells you its layer, and the acyclic graph means a change in a leaf +lib can't secretly reach back into a feature. New code almost never needs a new +convention plugin — pick the right directory and the right existing plugin, and the +boundaries enforce themselves. diff --git a/docs/architecture/02-state-and-dependency-injection.md b/docs/architecture/02-state-and-dependency-injection.md new file mode 100644 index 000000000..7d0fd7b4e --- /dev/null +++ b/docs/architecture/02-state-and-dependency-injection.md @@ -0,0 +1,165 @@ +# 02 — State & dependency injection + +Two patterns dominate how Flipcash wires dependencies and manages UI state: +**Hilt** provides singletons and ViewModels at the object-graph level, and a +**CompositionLocal** layer re-exposes the long-lived controllers to the Compose +tree so screens read them as ambient values instead of injecting each one. State +itself flows through a small MVI base class, `BaseViewModel`. + +```mermaid +graph TD + Hilt["Hilt SingletonComponent
controllers, services, channels"] + Activity["MainActivity (@AndroidEntryPoint)
@Inject lateinit var ..."] + CL["CompositionLocalProvider
LocalRouter, LocalExchange, LocalSessionController, ..."] + Screen["Composable screen
LocalRouter.current"] + VM["hiltViewModel()"] + State["stateFlow / eventFlow"] + + Hilt --> Activity --> CL --> Screen + Screen --> VM --> State --> Screen +``` + +## Hilt setup + +The application class +[`FlipcashApp`](../../apps/flipcash/app/src/main/kotlin/com/flipcash/app/FlipcashApp.kt) +is annotated `@HiltAndroidApp`. Bindings are organized into `@Module` objects, each +`@InstallIn(SingletonComponent::class)`, living next to the thing they provide +(e.g. `services/flipcash/.../inject/FlipcashModule.kt`, +`apps/flipcash/shared/router/.../inject/RouterModule.kt`, +`apps/flipcash/shared/session/.../inject/`). Most app-level dependencies are +`@Singleton`; ViewModels use `@HiltViewModel` (activity-retained scope). Custom +qualifiers disambiguate same-typed bindings — for example +`@FlipcashManagedChannel` vs `@FlipcashManagedStreamingChannel` for the two gRPC +channels (see [04 — Networking](04-networking.md)). + +## The CompositionLocal injection pattern + +Rather than `@Inject`-ing a dozen controllers into every screen, +[`MainActivity`](../../apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt) +injects them once and republishes them through a `CompositionLocalProvider`: + +```kotlin +@AndroidEntryPoint +class MainActivity : FragmentActivity() { + @Inject lateinit var router: Router + @Inject lateinit var userManager: UserManager + @Inject lateinit var sessionController: SessionController + @Inject lateinit var exchange: Exchange + // ... ~20 injected controllers/services + + override fun onCreate(savedInstanceState: Bundle?) { + // ... + setContent { + CompositionLocalProvider( + LocalRouter provides router, + LocalUserManager provides userManager, + LocalSessionController provides sessionController, + LocalExchange provides exchange, + // ... LocalShareController, LocalFeatureFlags, LocalToastController, etc. + ) { + ProvidePermissionChecker(permissionChecker) { + Rinku { App(tipsEngine = tipsEngine) } + } + } + } + } +} +``` + +Each `Local*` is a `staticCompositionLocalOf` declared next to the type it carries, +so the dependency and its ambient handle live together. For example +[`Router.kt`](../../apps/flipcash/shared/router/src/main/kotlin/com/flipcash/app/router/Router.kt): + +```kotlin +val LocalRouter = staticCompositionLocalOf { null } +``` + +Representative composition locals and where they're defined: + +| Local | Type | Defined in | +|-------|------|-----------| +| `LocalRouter` | `Router` | `shared/router/.../Router.kt` | +| `LocalSessionController` | `SessionController` | `shared/session/.../SessionController.kt` | +| `LocalUserManager` | `UserManager` | `apps/flipcash/core/.../Locals.kt` | +| `LocalExchange` | `Exchange` | `services/opencode-compose/.../LocalExchange` | +| `LocalFeatureFlags` | `FeatureFlagController` | `shared/featureflags/.../FeatureFlagController.kt` | +| `LocalToastController` | `ToastController` | `apps/flipcash/core/.../toast/ToastController.kt` | +| `LocalNetworkObserver` | `NetworkConnectivityListener` | `libs/network/connectivity/...` | + +A screen then reads `val session = LocalSessionController.current` instead of +taking it as a constructor parameter. Hilt still owns the lifecycle and singleton +identity — CompositionLocal is purely the delivery mechanism into Compose. + +> **When to use which.** Inject **per-screen** dependencies through the ViewModel +> constructor (`@HiltViewModel`). Reach for a **`Local*`** only for the long-lived, +> app-wide controllers that `MainActivity` already provides. + +## State: `BaseViewModel` + +The MVI base class lives at +[`BaseViewModel.kt`](../../ui/navigation/src/main/kotlin/com/getcode/view/BaseViewModel.kt): + +```kotlin +abstract class BaseViewModel( + initialState: ViewState, + private val updateStateForEvent: (Event) -> (ViewState.() -> ViewState), + private val defaultDispatcher: CoroutineContext = Dispatchers.Default, +) : ViewModel() { + + private val _eventFlow = MutableSharedFlow() + val eventFlow: SharedFlow = _eventFlow.asSharedFlow() + + private val _stateFlow = MutableStateFlow(initialState) + val stateFlow: StateFlow = _stateFlow.asStateFlow() + + fun dispatchEvent(event: Event) { + setState(updateStateForEvent(event)) // synchronous reducer + viewModelScope.launch(defaultDispatcher) { + _eventFlow.emit(event) // async side-effect channel + } + } +} +``` + +The contract for every concrete ViewModel: + +- **State** — an immutable `data class` describing the screen. +- **Event** — a `sealed interface` of user actions and system signals. +- **Reducer** — `updateStateForEvent`, a pure `(Event) -> (State.() -> State)` + function (conventionally a `companion object val`) that maps each event to a state + transform. It runs synchronously inside `dispatchEvent`. +- **Side effects** — collected off `eventFlow` in the `init` block, using Flow + operators (`filterIsInstance<…>()`, `onEach`, `flatMapLatest`, `combine`, + `launchIn(viewModelScope)`) to react to events and external `StateFlow`s. + +A representative implementation is +[`CashScreenViewModel`](../../apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt), +which combines token, balance, and exchange-rate flows into its `State` and emits a +`PresentBill` event that the screen consumes to show a cash bill. + +`BaseViewModel.kt` also ships `LoadingSuccessState` (`loading` / `success` / `error` +with an `Idle` default) for the common async-status pattern. + +## Consuming state in Compose + +```kotlin +@Composable +fun CashScreen(/* args */) { + val session = LocalSessionController.current!! // ambient controller + val viewModel = hiltViewModel() // Hilt-built VM + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + LaunchedEffect(viewModel) { + viewModel.eventFlow + .filterIsInstance() + .onEach { session.showBill(it.bill) } + .launchIn(this) + } + // render from `state`, send Event via viewModel.dispatchEvent(...) +} +``` + +`stateFlow` is collected lifecycle-aware for rendering; `eventFlow` is collected in +a `LaunchedEffect` for one-shot side effects (navigation, showing a bill, toasts). +This keeps rendering a pure function of `state` while side effects stay explicit. diff --git a/docs/architecture/03-navigation.md b/docs/architecture/03-navigation.md new file mode 100644 index 000000000..3d88d20d0 --- /dev/null +++ b/docs/architecture/03-navigation.md @@ -0,0 +1,153 @@ +# 03 — Navigation + +> **Note on stacks:** Flipcash navigates with **Jetpack Navigation 3** +> (`androidx.navigation3`) wrapped by a custom `CodeNavigator`. There is **no +> Voyager** dependency in the project today — older references to Voyager are stale. + +Navigation has three moving parts: a **typed route graph** (`AppRoute`), a +**navigator** that drives a Navigation3 back stack (`CodeNavigator` + +`AppNavHost`), and a **`Router`** that turns external deeplinks into navigation +actions, gated by auth state. + +```mermaid +graph TD + DL["DeepLink (URL / QR)"] + Router["Router.dispatch()"] + Auth{"AuthState.Ready?"} + Action["DeeplinkAction (Navigate / Login / OpenCashLink / None)"] + Nav["CodeNavigator (push / pop / replaceAll / hide)"] + BackStack["NavBackStack"] + Entry["appEntryProvider: AppRoute -> Composable"] + Scene["SceneStrategy (SinglePane / ModalBottomSheet)"] + Screen["Feature screen"] + + DL --> Router --> Auth + Auth -->|yes| Action + Auth -->|no| Action + Action --> Nav --> BackStack --> Entry --> Scene --> Screen +``` + +## The route graph: `AppRoute` + +Routes are a serializable, parcelable sealed hierarchy in +[`AppRoute.kt`](../../apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt). +Every route is a Navigation3 `NavKey`: + +```kotlin +@Serializable +@Parcelize +sealed interface AppRoute : NavKey, Parcelable { + + @Serializable @Parcelize + data object Loading : AppRoute + + @Serializable @Parcelize + data class OnboardingFlow( + val phase: Phase = Phase.Account, + val seed: String? = null, + val resumeAt: ResumePoint = ResumePoint.Login, + // ... + ) : AppRoute, FlowRoute { + override val initialStack: List get() = /* steps for this phase */ + } + + sealed interface Token : AppRoute { /* Info, Swap, ... */ } + sealed interface Transfers : AppRoute { /* Deposit, Withdrawal */ } + sealed interface Messaging : AppRoute { /* Chat */ } + // Onboarding, Main, Sheets, Menu, ... +} +``` + +Two marker interfaces from `com.getcode.navigation.flow` model multi-screen flows: + +- **`FlowRoute`** — a route that expands into an `initialStack` of inner steps + (e.g. `OnboardingFlow` → `OnboardingStep.Start`, `…AccessKey`, `…Purchase`). +- **`FlowRouteWithResult`** — a flow that returns a typed result to its caller + (e.g. `Token.Swap` → `SwapResult`, `Transfers.Deposit` → `DepositResult`). + +Because routes are `@Serializable` + `@Parcelize`, the back stack survives process +death and deeplinks can be expressed as route lists. + +## The navigator and host + +The root composable +[`App.kt`](../../apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt) +creates the back stack and navigator and hosts the graph: + +```kotlin +val backStack = remember { NavBackStack(AppRoute.Loading) } +val codeNavigator = rememberCodeNavigator(backStack = backStack, /* ... */) + +CompositionLocalProvider(LocalCodeNavigator provides codeNavigator) { + AppNavHost( + navigator = codeNavigator, + sceneStrategies = listOf( + ModalBottomSheetSceneStrategy(/* ... */), + SinglePaneSceneStrategy(), + ), + entryProvider = appEntryProvider(/* ... */), + ) +} +``` + +- **`CodeNavigator`** (`com.getcode.navigation.core`, in `:ui:navigation`) is the + public navigation API. Screens obtain it via `LocalCodeNavigator.current` and call + `push(route)`, `push(routes)`, `pop()`, `replaceAll(routes)`, and `hide()` (to + dismiss a bottom sheet). +- **Scene strategies** decide presentation: `SinglePaneSceneStrategy` for + full-screen content, `ModalBottomSheetSceneStrategy` for sheets. +- **`appEntryProvider`** maps each `AppRoute` to its composable. + +## Registering screens + +Each feature exports its screen composable; the app wires routes to screens in one +place, +[`AppScreenContent.kt`](../../apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt), +via an `entryProvider { … }` builder: + +```kotlin +fun appEntryProvider(/* ... */): (NavKey) -> NavEntry = entryProvider { + annotatedEntry { MainRoot(/* ... */) } + annotatedEntry { key -> OnboardingFlowScreen(route = key, /* ... */) } + annotatedEntry { key -> CashScreen(key.mint, key.fromTokenInfo) } + annotatedEntry { key -> TokenInfoScreen(mint = key.mint) } + annotatedEntry { key -> SwapFlowScreen(route = key, /* ... */) } + annotatedEntry { key -> WithdrawalFlowScreen(route = key, /* ... */) } + annotatedEntry { key -> ChatFlowScreen(route = key, /* ... */) } + // ... +} +``` + +Features stay self-contained (own screens, ViewModels, Hilt modules); all routes +converge here. Adding a screen means: define the `AppRoute`, export the composable +from the feature, and register one `annotatedEntry`. + +## Deeplinks: the `Router` + +The [`Router`](../../apps/flipcash/shared/router/src/main/kotlin/com/flipcash/app/router/Router.kt) +interface has two jobs: + +```kotlin +interface Router { + /** Parse + classify + resolve routes in one call. Called once per deeplink. */ + fun dispatch(deepLink: DeepLink): DeeplinkAction + /** Classify a URL (for QR scanning) without resolving routes. */ + fun classify(deepLink: DeepLink): DeeplinkType? +} +``` + +`AppRouter` (the implementation, in `router/internal/`) classifies the URL into a +`DeeplinkType` (`Login`, `CashLink`, `TokenInfo`, `Chat`, …) and then **gates on +auth**: unauthenticated users are redirected into onboarding; authenticated users +get the real `DeeplinkAction` (`Navigate(routes)`, `Login(entropy)`, +`OpenCashLink(entropy)`, or `None`). Deeplink intake itself is handled by **Rinku** +(`dev.theolm.rinku`), wired in `MainActivity`; `App.kt` observes the incoming link +and feeds it to `router.dispatch(...)`, then pushes the resulting routes onto the +navigator. + +## Why this matters + +Typed `NavKey` routes give compile-time-checked navigation and free state +restoration; the single `appEntryProvider` keeps feature modules decoupled from one +another; and routing all external links through `Router` means **auth gating lives +in exactly one place** instead of being re-checked on every screen. diff --git a/docs/architecture/04-networking.md b/docs/architecture/04-networking.md new file mode 100644 index 000000000..514b5b00c --- /dev/null +++ b/docs/architecture/04-networking.md @@ -0,0 +1,137 @@ +# 04 — Networking + +Flipcash talks to two **gRPC** backends — the Flipcash service (accounts, +profiles, chat, activity) and the Open Code Protocol / OCP service (transactions, +intents, exchange rates) — plus a little **REST** for the Coinbase on-ramp. The +gRPC code is organized as a strict four-layer stack so that protobuf details never +leak up into features. + +```mermaid +graph TD + Feature["Feature / shared coordinator"] + Controller["Controller — public, stateful API"] + Repository["Repository — orchestration, caching"] + Service["Service — Result, error mapping"] + Api["Api — gRPC stub, request build, validation, signing"] + Channel["ManagedChannel (unary / streaming)"] + Backend["gRPC backend"] + + Feature --> Controller --> Repository --> Service --> Api --> Channel --> Backend +``` + +## The four layers + +For both `:services:flipcash` and `:services:opencode`: + +| Layer | Location | Responsibility | +|-------|----------|----------------| +| **Api** | `…/internal/network/api/` | Holds the generated gRPC stub, builds requests, validates them (`protovalidate`), signs/authenticates, returns raw protobuf. E.g. `AccountApi`, `TransactionApi`. | +| **Service** | `…/internal/network/services/` | Wraps an Api, converts responses to `Result`, maps RPC/status errors to typed domain errors (e.g. `RegisterError.InvalidSignature`). E.g. `AccountService`. | +| **Repository** | `…/internal/repositories/` (or `…/repositories/`) | Coordinates one or more services, applies caching, exposes suspend functions / flows. E.g. `InternalAccountRepository`, `TransactionRepository`. | +| **Controller** | `…/controllers/` | The public, often stateful API features consume. Holds `StateFlow`s, pulls key material from `UserManager`. E.g. `AccountController`, `TransactionController`. | + +The `*-compose` modules (`:services:flipcash-compose`, `:services:opencode-compose`) +add Compose-facing bindings — for example `LocalExchange`, the composition local +that surfaces exchange rates to the UI. + +## Managed channels + +Each service module provides **two** gRPC channels via Hilt, distinguished by +qualifier — see +[`FlipcashModule.kt`](../../services/flipcash/src/main/kotlin/com/flipcash/services/inject/FlipcashModule.kt): + +```kotlin +@Provides @FlipcashManagedChannel +fun provideManagedChannel(/* context, config */): ManagedChannel = + AndroidChannelBuilder + .usingBuilder(OkHttpChannelBuilder.forAddress(config.baseUrl, config.port)) + .keepAliveWithoutCalls(false) + .intercept(LoggingClientInterceptor()) + .build() + +@Provides @FlipcashManagedStreamingChannel +fun provideManagedStreamingChannel(/* ... */): ManagedChannel = + AndroidChannelBuilder + .usingBuilder(OkHttpChannelBuilder.forAddress(config.baseUrl, config.port)) + .keepAliveTime(config.keepAlive.inWholeMilliseconds, TimeUnit.MILLISECONDS) + .keepAliveTimeout(config.keepAliveTimeout.inWholeMilliseconds, TimeUnit.MILLISECONDS) + .keepAliveWithoutCalls(true) // held open for bidirectional streams + .intercept(LoggingClientInterceptor()) + .build() +``` + +- The **unary** channel is for request/response RPCs; the **streaming** channel + keeps a keep-alive open for long-lived bidirectional streams. +- `@OpenCodeManagedChannel` / `@OpenCodeManagedStreamingChannel` mirror this in the + OCP module against its own base URL. +- `LoggingClientInterceptor` traces every call (see + [08 — Cross-cutting concerns](08-cross-cutting-concerns.md)). + +## Signing & authentication + +Requests are signed with the owner's **Ed25519** key. Builder extensions in +`services/flipcash/.../internal/network/extensions/` add the signature/auth fields +before send: + +```kotlin +// SignMessage.kt — sign the serialized message +fun GeneratedMessageLite.Builder.sign(owner: Ed25519.KeyPair): Common.Signature { + val bytes = buildPartial().toByteArray() + return Ed25519.sign(bytes, owner).asSignature() +} + +// AuthenticateMessage.kt — attach pubkey + signature as Common.Auth +fun GeneratedMessageLite.Builder.authenticate(owner: Ed25519.KeyPair): Common.Auth { /* ... */ } +``` + +Companion `LocalToProtobuf` / `ProtobufToLocal` extensions translate between domain +models (keys, token amounts, timestamps) and their protobuf representations at the +Api boundary. + +## Streaming: `SubmitIntent` + +Payments use a **bidirectional stream**. `TransactionApi.submitIntent` takes a +`Flow` and returns a `Flow`; the +client/server handshake is: + +1. Client sends `SubmitActions` (the intent + actions). +2. Server validates and returns `ServerParameters`. +3. Client builds the Solana transactions locally and signs them. +4. Client sends `SubmitSignatures`. +5. Server verifies and returns `Success`. + +The full handshake and the intent types are covered in +[06 — Payments & operations](06-payments-and-operations.md). + +## REST, JWT & the Coinbase on-ramp + +The Coinbase on-ramp uses Retrofit. `CoinbaseApi` +(`libs/network/coinbase/onramp/...`) declares `@POST`/`@GET` endpoints that take an +`@Header("Authorization")` JWT and dynamic `@Url`s. JWTs are minted per endpoint by +`JwtProvider` (`libs/network/jwt/...`): + +```kotlin +interface JwtProvider { + suspend fun provideJwtForEndpoint(apiKey: String, endpoint: JwtSecuredEndpoint): Result +} +``` + +Tokens are short-lived and passed per request. + +## Exchange rates & connectivity + +- **Exchange** (`libs/network/exchange/.../Exchange.kt`, implemented by + `OpenCodeExchange`) exposes current and observable rates as Flows + (`observeLocalRate()`, `observeRates()`, `fetchRatesIfNeeded()`) and is surfaced + to Compose as `LocalExchange`. All transfers verify a cryptographically-signed + exchange rate. +- **Connectivity** (`libs/network/connectivity/...`, `Api24NetworkObserver`) + watches `ConnectivityManager` via a `callbackFlow`, `debounce(2000)`s WiFi↔mobile + transitions, and `shareIn`s a single connectivity flow. It's published app-wide as + `LocalNetworkObserver`. + +## Why this matters + +The four-layer split means a proto or transport change is absorbed at the Api/ +Service boundary and never ripples into a feature. Features depend only on +**controllers**, which speak in domain types and `Result`, not in gRPC stubs. diff --git a/docs/architecture/05-persistence.md b/docs/architecture/05-persistence.md new file mode 100644 index 000000000..5d352adf6 --- /dev/null +++ b/docs/architecture/05-persistence.md @@ -0,0 +1,108 @@ +# 05 — Persistence + +Local state lives in a **Room** database, with **DataStore** for lightweight +key/value preferences and a **Paging 3 `RemoteMediator`** layer to back paged lists +(activity feed, chat) from the network. The persistence layer is split into three +shared sub-modules so storage details don't leak into features. + +> **Note on encryption:** the Flipcash Room database is **not** wrapped with +> SQLCipher today (there is no `SupportFactory` / `net.zetetic` in +> `apps/flipcash/shared/persistence`). Instead, **each account gets its own +> database file**, named from the account entropy. Older notes describing a +> SQLCipher-encrypted Room DB are stale. + +```mermaid +graph TD + Feature["Feature / shared coordinator"] + Provider[":shared:persistence:provider — access facade"] + Sources[":shared:persistence:sources — RemoteMediators, paged sources"] + Db[":shared:persistence:db — FlipcashDatabase, DAOs, entities"] + Room["Room (SQLite)"] + DataStore["DataStore (preferences)"] + Backend["gRPC backend"] + + Feature --> Provider --> Sources --> Db --> Room + Provider --> DataStore + Sources --> Backend +``` + +## Sub-modules + +| Module | Role | +|--------|------| +| `:apps:flipcash:shared:persistence:db` | The Room `FlipcashDatabase`, entities, DAOs, type converters, migrations. | +| `:apps:flipcash:shared:persistence:sources` | Paging `RemoteMediator`s and data sources that bridge network ↔ database. | +| `:apps:flipcash:shared:persistence:provider` | The injected facade features use to reach DAOs/DataStore without depending on Room directly. | + +## The database + +[`FlipcashDatabase`](../../apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/FlipcashDatabase.kt) +is a standard Room `@Database` (currently **version 20**) with a long chain of +`@AutoMigration`s and `@TypeConverters` for token and chat payloads: + +```kotlin +@Database( + entities = [ + MessageEntity::class, TokenEntity::class, SocialLinkEntity::class, + TokenValuationEntity::class, CurrencyCreatorDraftEntity::class, + ContactSyncStateEntity::class, ContactMappingEntity::class, + ChatMetadataEntity::class, ChatMessageEntity::class, ChatMemberEntity::class, + ], + autoMigrations = [ /* 1->2 ... 19->20 */ ], + version = 20, +) +@TypeConverters(TokenTypeConverters::class, ChatTypeConverters::class) +abstract class FlipcashDatabase : RoomDatabase() { + abstract fun messageDao(): MessageDao + abstract fun tokenDao(): TokenDao + abstract fun contactDao(): ContactDao + abstract fun chatMetadataDao(): ChatMetadataDao + abstract fun chatMessageDao(): ChatMessageDao + abstract fun chatMemberDao(): ChatMemberDao + abstract fun currencyCreatorDraftDao(): CurrencyCreatorDraftDao + // ... +} +``` + +### Per-user database naming + +The database is opened with a name derived from the signed-in account's entropy, so +each user gets an isolated file and switching accounts can't cross-contaminate +cached state: + +```kotlin +fun init(context: Context, entropyB64: String) { + val dbUniqueName = Base58.encode(entropyB64.toByteArray().subByteArray(0, 6)) + dbName = "$dbNamePrefix-$dbUniqueName.db" // e.g. fcash_database-.db + instance = Room.databaseBuilder(context, FlipcashDatabase::class.java, dbName) + .fallbackToDestructiveMigration() + .build() +} +``` + +Room schema JSON is checked in under +`apps/flipcash/shared/persistence/db/schemas/` to keep migrations honest. + +## DAOs & paged sources + +DAOs (`…/persistence/dao/`) expose suspend functions and `Flow`s for reads; +mutations are suspend. Paged lists are populated by `RemoteMediator`s in the +`sources` module — e.g. a feed mediator and a chat-message mediator — which fetch +pages from the backend, write them through the DAO, and let Room/Paging serve the UI +from the local copy. This gives offline reads and a single source of truth. + +## DataStore + +Preferences and small caches use Jetpack **DataStore** rather than the database. +A representative example is +[`OpenGraphCacheProvider`](../../libs/opengraph/src/main/kotlin/com/getcode/libs/opengraph/OpenGraphCacheProvider.kt), +which caches link-preview metadata via `PreferenceDataStoreFactory.create(...)` with +a corruption handler and JSON-serialized values. User flags and similar small state +follow the same pattern. + +## Why this matters + +Routing all storage access through the `provider` facade keeps Room out of feature +modules; per-user database files make account isolation structural rather than +something each query has to remember; and the `RemoteMediator` layer keeps the +network-to-cache sync in one place so screens just observe the database. diff --git a/docs/architecture/06-payments-and-operations.md b/docs/architecture/06-payments-and-operations.md new file mode 100644 index 000000000..30e84854c --- /dev/null +++ b/docs/architecture/06-payments-and-operations.md @@ -0,0 +1,137 @@ +# 06 — Payments & operations + +Flipcash is **self-custodial**: the device holds the keys, derives Solana accounts, +and signs every transaction locally. This document covers key management, the +account model, the auth-state machine, and how a payment is actually submitted. + +```mermaid +graph TD + Mnemonic["MnemonicPhrase (BIP39 entropy)"] + Derived["DerivedKey (Ed25519.KeyPair per path)"] + Cluster["AccountCluster (authority + timelock + deposits)"] + User["UserManager (AuthState, current account)"] + TxController["TransactionController / TransactionOperations"] + Stream["SubmitIntent bidirectional stream"] + Backend["OCP backend + Solana"] + + Mnemonic --> Derived --> Cluster --> User + User --> TxController --> Stream --> Backend +``` + +## Keys & cryptography + +The crypto primitives live under `libs/encryption/*`: + +| Module | Provides | +|--------|----------| +| `libs/encryption/ed25519` | `Ed25519` — native (JNI) key generation, signing, verification; `Ed25519.KeyPair` (Parcelable). | +| `libs/encryption/mnemonic` | `MnemonicPhrase` (BIP39, 12/24 words), `DerivedKey`, `DerivePath` (Solana derivation paths). | +| `libs/encryption/keys` | Solana primitives: `PublicKey`, `Mint`, account-address derivation helpers. | +| `libs/encryption/base58`, `sha256/512`, `hmac`, `utils` | Encoding and hashing helpers. | + +A user's identity starts from entropy: + +```kotlin +val phrase = MnemonicPhrase.generate() // or fromEntropyB64(...) +val keyPair = phrase.getSolanaKeyPair(DerivePath.primary) // Ed25519.KeyPair +``` + +`MnemonicPhrase` caches seed derivation and exposes the entropy in Base58/Base64 — +the same Base58 entropy prefix that names the per-user database (see +[05 — Persistence](05-persistence.md)). + +## The account model: `AccountCluster` + +[`AccountCluster`](../../services/opencode/src/main/kotlin/com/getcode/opencode/model/accounts/AccountCluster.kt) +bundles the derived accounts that make up a wallet: + +```kotlin +class AccountCluster( + val authority: DerivedKey, // master key controlling the account + val timelock: TimelockDerivedAccounts, // custody / vault accounts +) { + val rendezvous: Ed25519.KeyPair get() = authority.keyPair // signing key for requests + val vaultPublicKey: PublicKey get() = timelock.vault.publicKey + val usdfDepositAddress: PublicKey get() = depositAddressFor(Token.usdf) + + fun depositAddressFor(token: Token): PublicKey { /* derive VM + deposit account */ } +} +``` + +- **authority** — the master keypair that controls the account. +- **timelock** — vault accounts in the timelock virtual machine that hold custody. +- **rendezvous** — the key used to sign requests. +- **deposit addresses** — derived per token, used for on-ramp funding. + +## Auth state: `UserManager` + +[`UserManager`](../../services/flipcash/src/main/kotlin/com/flipcash/services/user/UserManager.kt) +owns the mnemonic lifecycle, derives the `AccountCluster`, and exposes the current +account and a state machine as `StateFlow`s: + +```kotlin +sealed interface AuthState { + data object Unknown : AuthState + data class Onboarding(val resumePoint: ResumePoint = ResumePoint.PostAccessKey) : AuthState + data object Authenticating : AuthState, LoggedIn + data object Ready : AuthState, LoggedIn // can call authenticated APIs + data object LoggedOut : AuthState + + val canAccessAuthenticatedApis: Boolean get() = this is Ready +} +``` + +`AuthState` is what the [`Router`](03-navigation.md) gates deeplinks on, and what the +`SessionController` (`:shared:session`) uses to route between onboarding, login, and +the main app. + +## Submitting a payment + +Payments are expressed as **intents** and submitted over the OCP `SubmitIntent` +bidirectional stream. `TransactionController` (in `:services:opencode`, implementing +`TransactionOperations`) is the public entry point; it also tracks send limits: + +```kotlin +@Singleton +class TransactionController @Inject constructor( + private val repository: TransactionRepository, + private val swapRepository: SwapRepository, + private val accountController: AccountController, +) : TransactionOperations { + private val _limits = MutableStateFlow(Limits.Empty) + override val limits: StateFlow = _limits.asStateFlow() + + override suspend fun updateLimits(owner: AccountCluster, force: Boolean) { /* ... */ } +} +``` + +Intent kinds include `IntentTransfer`, `IntentRemoteSend` / `IntentRemoteReceive` +(cash links), `IntentWithdraw`, `IntentStatefulSwap`, and `IntentDistribution`. The +client builds and signs the Solana transactions locally during the handshake +described in [04 — Networking](04-networking.md): send actions → receive server +parameters → sign → send signatures → success. + +## Funding: `PurchaseMethodController` + +[`PurchaseMethodController`](../../apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/app/payments/PurchaseMethodController.kt) +coordinates how a user adds funds and exposes the selection as state: + +```kotlin +interface PurchaseMethodController { + val state: StateFlow + val selections: Flow + fun present(metadata: PurchaseMethodMetadata = PurchaseMethodMetadata()) + suspend fun presentDepositOptions(popToRoot: Boolean = false): AppRoute? +} +``` + +Methods include in-app purchase, the **Coinbase on-ramp** (REST + JWT, see +[04](04-networking.md)), and manual deposit to the cluster's deposit address. + +## Why this matters + +Keys never leave the device; everything from the database name to the request +signature is derived from one mnemonic. Modeling money movement as signed +**intents** over a streamed handshake — rather than fire-and-forget RPCs — is what +lets the backend verify exchange rates and limits while the client retains custody +of signing. diff --git a/docs/architecture/07-design-system.md b/docs/architecture/07-design-system.md new file mode 100644 index 000000000..6d5153dc6 --- /dev/null +++ b/docs/architecture/07-design-system.md @@ -0,0 +1,67 @@ +# 07 — Design system + +The `ui/*` modules are the shared visual layer: theme tokens, reusable Compose +components, string/drawable resources, the camera scanner, and biometric prompts. +They depend only on `libs/*` and each other — **never on app/feature modules** — so +they can be reused everywhere without creating cycles (see +[01 — Modules & boundaries](01-modules-and-boundaries.md)). + +```mermaid +graph TD + Feature["Feature screen (Compose)"] + Components[":ui:components — buttons, bars, pills, sheets"] + Core[":ui:core — primitives, modifiers, RestrictionType"] + Theme[":ui:theme — colors, type, MODE_NIGHT_YES"] + Resources[":ui:resources — strings, drawables"] + Scanner[":ui:scanner — camera / Kik code capture"] + Biometrics[":ui:biometrics — biometric prompt state"] + Libs["libs/* (currency, models, datetime, ...)"] + + Feature --> Components --> Core --> Theme + Feature --> Resources + Feature --> Scanner + Feature --> Biometrics + Components --> Libs +``` + +## The modules + +| Module | Role | +|--------|------| +| `:ui:theme` | Color, typography, shapes, spacing tokens. The app is **dark-mode only** (`MODE_NIGHT_YES`), so the theme targets a single palette. | +| `:ui:components` | Reusable composables — app bars, buttons, pills, bottom sheets, list rows. Depends on `libs/currency`, `libs/models`, `libs/network/exchange`, and `ui/theme`. | +| `:ui:core` | Lower-level Compose primitives, modifiers, and shared types (e.g. `RestrictionType`). Re-exported by components via `api(...)`. | +| `:ui:resources` | Strings and drawables, behind `ResourceHelper` so non-Compose code can resolve them too. | +| `:ui:navigation` | The navigation runtime used by the app: `CodeNavigator`, `BaseViewModel`, flow-route types. (See [02](02-state-and-dependency-injection.md) and [03](03-navigation.md).) | +| `:ui:scanner` | Camera preview and Kik-code capture surface used by the scanner feature. | +| `:ui:biometrics` | `rememberBiometricsState` and `LocalBiometricsState` for gating sensitive screens. | +| `:ui:emojis` | Emoji rendering/lookup helpers. | +| `:ui:testing` | Compose test utilities and `LocalUiTesting` for UI-test affordances. | + +## How features consume the UI layer + +Feature modules don't declare the UI layer by hand — the +`flipcash.android.feature` convention plugin injects `:ui:core`, `:ui:components`, +`:ui:navigation`, `:ui:resources`, and `:ui:theme` automatically (see +[01](01-modules-and-boundaries.md)). So a screen can use the shared components and +theme tokens immediately, and only adds an explicit dependency for the less common +pieces (`:ui:scanner`, `:ui:biometrics`, `:ui:emojis`). + +## Compose conventions + +- **State in, events out.** Components are stateless where possible: they take state + plus callback lambdas and let the caller's `BaseViewModel` own the state (see + [02](02-state-and-dependency-injection.md)). +- **Theme tokens, not literals.** Colors, type, and spacing come from `:ui:theme` + rather than hard-coded values, so the single dark palette stays consistent. +- **Resources via `ResourceHelper`.** Strings/drawables resolve through `:ui:resources` + / `LocalResources` so logic modules (which have no Compose) can format text too. +- **Ambient controllers via `Local*`.** Cross-cutting UI controllers (toasts, + scrim, biometrics, navigator) are read from composition locals rather than passed + down by hand. + +## Why this matters + +Keeping the design system in `ui/*` with a no-app-dependencies rule means visual +consistency is shared, not copy-pasted, and the convention-plugin baseline means +every feature starts from the same component and theme set with zero boilerplate. diff --git a/docs/architecture/08-cross-cutting-concerns.md b/docs/architecture/08-cross-cutting-concerns.md new file mode 100644 index 000000000..1a271f8e0 --- /dev/null +++ b/docs/architecture/08-cross-cutting-concerns.md @@ -0,0 +1,109 @@ +# 08 — Cross-cutting concerns + +Concerns that touch every layer: logging/tracing, error reporting, analytics, +biometrics, and the async model. These are centralized so features get them +"for free" without each one reinventing instrumentation. + +> **Note on async:** the app is effectively **pure Kotlin Coroutines + Flow**. +> There is no meaningful RxJava usage in app source today; older notes describing +> "RxJava 3 coexisting" are stale. + +```mermaid +graph TD + Code["Any module"] + Trace["trace(tag, message, type) -> TraceManager"] + Plugins["TraceLogPlugin chain (PII masking, RPC body filter)"] + File["FileTree (rotated log file)"] + Sinks["BreadcrumbSink(s)"] + Bugsnag["Bugsnag (error reporting)"] + Mixpanel["Mixpanel (analytics)"] + + Code --> Trace --> Plugins --> File + Trace --> Sinks --> Bugsnag + Code --> Mixpanel +``` + +## Logging & tracing + +`libs/logging` centralizes tracing behind `TraceManager` and a single `trace(...)` +call: + +```kotlin +trace(tag = "Transactions", message = "updating limits", type = TraceType.Process) +trace(tag = "gRPC", message = "opencode => READY", type = TraceType.StateChange) +``` + +- **`TraceType`** routes/labels each line: `Silent`, `Error`, `Log`, `Navigation`, + `Process`, `Network`, `StateChange`, `User`. +- **`TraceLogPlugin`** transforms or drops lines before they're written — used for + **PII masking** and **RPC body filtering** so secrets never hit the log file. +- **`FileTree`** writes a time-rotated local log (surfaced in the device-logs + feature for support). +- **`BreadcrumbSink`** forwards structured breadcrumbs to external services so a + crash report carries recent context. +- `LoggingClientInterceptor` (used by both gRPC channels, see + [04](04-networking.md)) feeds network traces through this pipeline. + +## Error reporting + +`ErrorReporter` (`libs/logging`) is the abstraction over **Bugsnag**: + +```kotlin +interface ErrorReporter { + fun report(error: Throwable, cause: Throwable, isNotifiable: Boolean) +} +``` + +Errors implementing `NotifiableError` are flagged `isNotifiable` and routed for +elevated alerting; reports are enriched with the current `userId` (from +`TraceManager`) and recent breadcrumbs. + +## Analytics + +`AnalyticsService` (`libs/analytics`) defines the event surface; the Flipcash +implementation (`apps/flipcash/shared/analytics`) wraps the **Mixpanel** SDK +(`MixpanelAnalyticsDelegate`): + +```kotlin +interface AnalyticsService { + fun onAppStart() + fun action(action: AppAction, source: AppActionSource? = null) + // ... +} +``` + +It's exposed to Compose as `LocalAnalytics` and used by `UserManager` to track +auth-state transitions and user properties. + +## Biometrics + +`ui/biometrics` provides `rememberBiometricsState(...)` and `LocalBiometricsState` +for gating sensitive screens. It checks device support via `BiometricManager`, +prompts when required, is **lifecycle-aware** (clears the pass when the app +backgrounds), and applies a short cooldown so it doesn't re-prompt on every resume. + +## Async model + +- **`DispatcherProvider`** (`libs/coroutines`) abstracts `Default` / `Main` / `IO` + dispatchers and is injected (rather than referencing `Dispatchers.*` directly) so + code is testable. +- **Controllers** hold a `CoroutineScope(Dispatchers.IO + SupervisorJob())` and + expose state as `StateFlow`. +- **Networking** uses `suspend` + `Result` for unary calls and `Flow` for + streams. +- **Compose** drives side effects with `LaunchedEffect` / `rememberCoroutineScope` + and collects state with `collectAsStateWithLifecycle()`. + +## Build configuration + +`compileSdk 36`, `minSdk 29`, **Java/Kotlin 21**, all set by the convention plugins +(see [01](01-modules-and-boundaries.md)). API keys (Bugsnag, Fingerprint, Mixpanel, +Google Cloud project number) are read from `local.properties` and surfaced through +`BuildConfig`; `google-services.json` lives under `apps/flipcash/app/src/`. + +## Why this matters + +One trace pipeline (with PII masking applied centrally), one error-reporting +abstraction, one analytics surface, and an injected dispatcher provider mean +instrumentation is consistent and safe by default — a feature opts in by calling +`trace(...)` or reading a `Local*`, not by wiring up its own logging. diff --git a/docs/architecture/09-separation-of-concerns.md b/docs/architecture/09-separation-of-concerns.md new file mode 100644 index 000000000..a739e4643 --- /dev/null +++ b/docs/architecture/09-separation-of-concerns.md @@ -0,0 +1,76 @@ +# 09 — Separation of concerns + +This document ties the others together. Flipcash's architecture is, at its core, +**one rule applied consistently**: dependencies point downward through clear +layers, and each layer has a single job. The previous documents describe the +pieces; this one states the principles. + +```mermaid +graph TD + UI["UI — Compose screens + components (state in, events out)"] + VM["Presentation — BaseViewModel (MVI reducer + effects)"] + Domain["Domain — controllers, coordinators, models"] + Data["Data — repositories, services, persistence"] + Transport["Transport — gRPC / REST / Solana"] + + UI --> VM --> Domain --> Data --> Transport +``` + +## The principles + +### 1. Dependencies are acyclic and point downward +`ui/*` and `libs/*` never depend on app code; `services/*` never depend on features; +features depend on shared modules, not the other way around. The module's directory +and its convention plugin enforce this — see +[01 — Modules & boundaries](01-modules-and-boundaries.md). + +### 2. UI is a function of state +Screens render from an immutable `State` and emit `Event`s; they hold no business +logic. The `BaseViewModel` reducer is the only place state changes, +and side effects are explicit on `eventFlow`. See +[02 — State & dependency injection](02-state-and-dependency-injection.md). + +### 3. MVI for screens, controllers for shared logic +Per-screen concerns live in a feature's ViewModel. Logic shared across features +lives in a **controller/coordinator** in a `shared/*` module, exposed as a +`StateFlow`-bearing interface and delivered to Compose as a `Local*`. Features never +reach into each other's internals; they go through shared modules. + +### 4. Transport details stop at the data layer +The gRPC stack's four layers (Api → Service → Repository → Controller) mean protobuf +types, channels, and signing never appear in a feature. Features consume +**controllers** that speak domain types and `Result`. See +[04 — Networking](04-networking.md). + +### 5. One source of truth for cached state +The database is the source of truth for lists; `RemoteMediator`s sync the network +into it and the UI observes the database. Per-user database files make account +isolation structural. See [05 — Persistence](05-persistence.md). + +### 6. Cross-cutting concerns are centralized +Logging, error reporting, analytics, and biometrics are single shared +abstractions, opted into via `trace(...)` or a `Local*`, never reimplemented per +feature. See [08 — Cross-cutting concerns](08-cross-cutting-concerns.md). + +### 7. Generated and signed artifacts are not hand-edited +`definitions/*:models` is generated from `.proto`; don't edit it — regenerate. +Signing and key derivation live in `libs/encryption/*` and `services/*`, not in +feature code. See [06 — Payments & operations](06-payments-and-operations.md). + +## Where does this code go? + +| If you're adding… | It belongs in… | +|-------------------|----------------| +| A new screen | a `:apps:flipcash:features:*` module (screen + ViewModel + Hilt module) | +| Logic two+ features share | a `:apps:flipcash:shared:*` controller/coordinator | +| A new backend call | the appropriate `:services:*` layer (Api → Service → Repository → Controller) | +| A reusable component or token | `:ui:components` / `:ui:theme` | +| A domain-agnostic utility | a `:libs:*` module | +| A new persisted entity | `:apps:flipcash:shared:persistence:db` (+ migration) | + +## Why this matters + +Consistency is the feature. Because every module follows the same layering, a new +engineer can predict where code lives, a change in a leaf library can't secretly +reach a screen, and each concern — rendering, state, domain logic, data, transport — +can be reasoned about and tested in isolation. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 000000000..58dd8d793 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,77 @@ +# Flipcash Android — Architecture + +A guide to how the Android app is structured. Each document covers one +architectural concern; start here, then jump to the topic you need. + +> **These docs describe structure and intent, not every line of code.** If the +> documentation and the code disagree, **the code is authoritative** — fix the +> docs. When you change a subsystem, update its document in the same commit. +> +> Where this suite contradicts older notes in `CLAUDE.md` (navigation, persistence +> encryption, RxJava), it does so deliberately: these documents reflect the code as +> it exists today. See the per-topic docs for details. + +## What Flipcash is + +Flipcash is a **self-custodial mobile wallet** for instant, global, private +payments. Users hold **USDF** (a USD stablecoin) and **launchpad tokens** (custom +on-chain currencies). The signature interaction is a **device-to-device cash +bill**: one phone renders an animated circular Kik Code on screen, another phone +scans it, and a peer-to-peer handshake settles the payment. The app also supports +contact/username sends, on-ramp funding (Coinbase, in-app purchase), token swaps, +and on-chain withdrawals. + +Under the hood it talks to **two gRPC backends** — the Flipcash service +(accounts, profiles, chat, activity) and the Open Code Protocol / OCP service +(transactions, intents, exchange rates) — and signs operations on **Solana** with +**Ed25519** keys derived from a BIP39 mnemonic. State is cached locally in **Room**, +the UI is **Jetpack Compose** (dark-mode only), and dependencies are wired with +**Hilt** + **CompositionLocal**. + +## The layer cake + +```mermaid +graph TD + App["apps/flipcash/app — entry point, navigation host, DI wiring"] + Features["apps/flipcash/features/* — 26 self-contained screens + ViewModels"] + Shared["apps/flipcash/shared/* — 34 coordinators / controllers / services"] + Core["apps/flipcash/core — app-wide routes, locals, infra"] + Services["services/* — gRPC wrappers (API → Service → Repository → Controller)"] + Defs["definitions/* — protobuf sources + generated models"] + UI["ui/* — Compose components, theme, navigation, scanner"] + Libs["libs/* — crypto, network, logging, currency (leaf utilities)"] + Vendor["vendor/* — Kik scanner, OpenCV, TipKit"] + + App --> Features --> Shared --> Services --> Defs --> Libs + App --> Core + Features --> Core + Features --> UI --> Libs + Shared --> Core + Services --> Libs + Libs --> Vendor +``` + +*Arrows point to dependencies; the graph is acyclic. `ui/*` and `libs/*` never +depend on app modules.* See [01 — Modules & boundaries](01-modules-and-boundaries.md). + +## Documentation index + +| # | Topic | Scope | +|---|-------|-------| +| 01 | [Modules & boundaries](01-modules-and-boundaries.md) | ~132 modules, convention plugins, dependency graph, `public`/`impl`/`bindings` pattern, enforced boundaries | +| 02 | [State & dependency injection](02-state-and-dependency-injection.md) | Hilt setup, the CompositionLocal injection pattern, `BaseViewModel` MVI | +| 03 | [Navigation](03-navigation.md) | Navigation3, the `AppRoute` sealed graph, `CodeNavigator`, `Router` deeplink dispatch with auth gating | +| 04 | [Networking](04-networking.md) | gRPC layering, managed channels, Ed25519 signing, `SubmitIntent` streaming, REST/JWT/Coinbase, exchange rates, connectivity | +| 05 | [Persistence](05-persistence.md) | Room, per-user database naming, DAOs, Paging `RemoteMediator`s, DataStore | +| 06 | [Payments & operations](06-payments-and-operations.md) | Key management, `AccountCluster`, `AuthState`, intent submission handshake, purchase methods | +| 07 | [Design system](07-design-system.md) | The `ui/*` layer, theme, components, scanner, biometrics, Compose conventions | +| 08 | [Cross-cutting concerns](08-cross-cutting-concerns.md) | Logging/tracing, error reporting, analytics, biometrics, coroutines, build config | +| 09 | [Separation of concerns](09-separation-of-concerns.md) | Layering principles, MVI/MVVM split, where logic lives, the acyclic discipline | +| — | [Feature catalog](features/README.md) | Representative features by category, with screens, ViewModels, and patterns | + +## Suggested reading paths + +- **New to the codebase** → README → 01 → 02 → 09, then the feature catalog. +- **Building a feature** → feature catalog → 03 → 07 → 02. +- **Working on payments** → 06 → 04 → 05. +- **Backend / proto changes** → 04 → 05 → 06. diff --git a/docs/architecture/features/README.md b/docs/architecture/features/README.md new file mode 100644 index 000000000..3cfa726ba --- /dev/null +++ b/docs/architecture/features/README.md @@ -0,0 +1,70 @@ +# Feature catalog + +The app has **26 feature modules** under +[`apps/flipcash/features/`](../../../apps/flipcash/features/). Each is +self-contained — it owns its screens, ViewModels, and Hilt wiring, inherits the UI +layer and `:apps:flipcash:core` from the `flipcash.android.feature` convention +plugin ([01](../01-modules-and-boundaries.md)), and is reachable through a route in +`AppRoute` registered in `appEntryProvider` ([03](../03-navigation.md)). + +This catalog highlights **representative features per category**, not every module. +Paths are relative to each module's `src/main/kotlin/`. + +## Patterns + +Two recurring shapes show up in the **Pattern** column: + +- **VM-driven** — a `BaseViewModel` ([02](../02-state-and-dependency-injection.md)) + coordinates multi-step state and async work; the screen renders `state` and + dispatches `Event`s. +- **Flow** — a `FlowRoute` / `FlowRouteWithResult` whose screen hosts an inner + back stack of steps and (often) returns a typed result to its caller. + +## A. Onboarding & auth + +| Feature | Purpose | Screen / ViewModel | Pattern & integration | +|---------|---------|--------------------|-----------------------| +| **login** | Account creation, seed login, access-key, and permission steps | `login/OnboardingFlowScreen.kt` · `…/internal/SeedInputViewModel`, `LoginAccessKeyViewModel` | **Flow** (`AppRoute.OnboardingFlow`); drives `UserManager`/`AuthState`, hands off to **purchase** | +| **purchase** | First-purchase / account funding during onboarding | `purchase/PurchaseAccountScreen.kt` · `…/internal/PurchaseAccountViewModel` | **VM-driven**; uses `PurchaseMethodController` ([06](../06-payments-and-operations.md)) | +| **backupkey** | Show / confirm the recovery access key | `backupkey/BackupKeyScreen.kt` · `…/internal/BackupKeyScreenViewModel` | **VM-driven**; reads mnemonic from `UserManager` | + +## B. Money movement + +| Feature | Purpose | Screen / ViewModel | Pattern & integration | +|---------|---------|--------------------|-----------------------| +| **cash** | Amount entry for a device-to-device cash bill | `cash/CashScreen.kt` · `…/internal/CashScreenViewModel` | **VM-driven**; combines token + balance + rate flows, emits `PresentBill`, calls `SessionController.showBill` | +| **scanner** | Camera capture of a peer's cash bill / QR | `scanner/ScannerScreen.kt` | Screen-local; uses `:ui:scanner`, routes scans through `Router.classify` | +| **direct-send** | Send to a contact / phone number | `directsend/SendFlowScreen.kt` · `…/internal/SendFlowViewModel` | **Flow**; contact list + phone gate steps | +| **withdrawal** | Withdraw funds on-chain | `withdrawal/WithdrawalFlowScreen.kt` · `withdrawal/WithdrawalViewModel` | **Flow** (`AppRoute.Transfers.Withdrawal`); entry → destination → confirmation | +| **deposit** | Add funds via on-ramp / deposit address | `deposit/DepositFlowScreen.kt` · `…/internal/DepositViewModel` | **Flow** (`FlowRouteWithResult`); USDC deposit info, Coinbase on-ramp | +| **transactions** | Activity / transaction history | `transactions/TransactionHistoryScreen.kt` · `…/internal/TransactionHistoryViewModel` | **VM-driven**; Paging-backed list from persistence ([05](../05-persistence.md)) | + +## C. Currencies & tokens + +| Feature | Purpose | Screen / ViewModel | Pattern & integration | +|---------|---------|--------------------|-----------------------| +| **balance** | Wallet balance across tokens | `balance/BalanceScreen.kt` · `…/internal/BalanceViewModel` | **VM-driven**; observes token balances + exchange rates | +| **tokens** | Token info, selection, and swaps | `tokens/TokenInfoScreen.kt`, `tokens/SwapFlowScreen.kt`, `tokens/TokenSelectScreen.kt` | **Flow** for swap (`FlowRouteWithResult`); screen-local for info/selection | +| **currency-creator** | Create a launchpad currency | `currencycreator/CurrencyCreatorFlowScreen.kt` · `…/internal/CurrencyCreatorViewModel` | **Flow**; name → icon → info → processing steps | + +## D. Profile, settings & misc + +| Feature | Purpose | Screen / ViewModel | Pattern & integration | +|---------|---------|--------------------|-----------------------| +| **myaccount** | Account screen + user profile | `myaccount/MyAccountScreen.kt`, `myaccount/UserProfileScreen.kt` · `…/internal/MyAccountScreenViewModel` | **VM-driven**; reactive feature-flag / staff binding, event-driven nav | +| **messenger** | In-app chat & chat-amount entry | `messenger/ChatFlowScreen.kt` · `…/internal/ChatViewModel` | **Flow** (`AppRoute.Messaging.Chat`); Paging-backed messages | +| **appsettings** | App preferences | `apps/flipcash/features/appsettings/…` | **VM-driven**; backed by `AppSettingsCoordinator` (`LocalAppSettings`) | +| **invite / shareapp** | Invite & share flows | `apps/flipcash/features/invite`, `…/shareapp` | Screen-local; use `InviteController` / `ShareSheetController` locals | +| **device-logs / lab / advanced** | Debug & internal tooling | `apps/flipcash/features/device-logs`, `…/lab`, `…/advanced` | Internal; surface trace logs and feature-flag overrides | + +## Full module list + +The 26 feature directories: `advanced`, `appsettings`, `appupdates`, `backupkey`, +`balance`, `bill-customization`, `cash`, `contact-verification`, `currency-creator`, +`deposit`, `device-logs`, `direct-send`, `discovery`, `invite`, `lab`, `login`, +`menu`, `messenger`, `myaccount`, `purchase`, `scanner`, `shareapp`, `tokens`, +`transactions`, `userflags`, `withdrawal`. + +> Keep this catalog representative, not exhaustive — when you add a feature, add a +> row to the relevant category if it introduces a new user-facing capability or a +> notable pattern, and update the full list above. From 110aabbdb4607cf990e1c0a681e514a022d20314 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 22:53:14 +0000 Subject: [PATCH 2/7] docs: define Coordinator and contrast it with Controller/Manager/Service The architecture suite used "coordinator" loosely. Add a canonical "Roles" section in 02 (state & DI) defining Coordinator, Controller, Manager, and Service, with the TokenController (stateless network gateway) vs TokenCoordinator (session/lifecycle-aware owner of cached domain state) example drawn from their own KDoc. Point the loose mentions in 01 and 09 at this definition and split the "where does this code go?" guidance into Coordinator (cached/synced domain state) vs Controller (stateless shared logic). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JRVfsXp4HDrDMy7Pbmw9fD --- .../architecture/01-modules-and-boundaries.md | 2 +- .../02-state-and-dependency-injection.md | 36 +++++++++++++++++++ .../architecture/09-separation-of-concerns.md | 16 ++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/architecture/01-modules-and-boundaries.md b/docs/architecture/01-modules-and-boundaries.md index ae27f1ca0..2f15dc47e 100644 --- a/docs/architecture/01-modules-and-boundaries.md +++ b/docs/architecture/01-modules-and-boundaries.md @@ -12,7 +12,7 @@ This document is the map. | App | `:apps:flipcash:app` | 1 | Entry point — `FlipcashApp`, `MainActivity`, the navigation host, DI wiring. Assembles everything. | | Core | `:apps:flipcash:core` | 1 | App-wide infrastructure: `AppRoute` graph, `Local*` composition locals, shared models. Auto-injected into every feature. | | Features | `:apps:flipcash:features:*` | 26 | Self-contained screens (login, cash, balance, tokens, scanner, withdrawal, …). Each owns its state, ViewModels, and UI. | -| Shared | `:apps:flipcash:shared:*` | 34 | Cross-feature coordinators, controllers, and services (authentication, session, router, payments, persistence, …). | +| Shared | `:apps:flipcash:shared:*` | 34 | Cross-feature coordinators, controllers, and services (authentication, session, router, payments, persistence, …). Coordinators own a domain's cached, session-aware state; see [02 — Roles](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services). | | Services | `:services:*` | 4 | gRPC wrappers: `flipcash`, `flipcash-compose`, `opencode`, `opencode-compose`. | | Definitions | `:definitions:*` | 4 | Protobuf sources (`*/protos`) and generated models (`*/models`) for Flipcash and OCP. | | UI | `:ui:*` | 9 | Compose layer: `theme`, `components`, `core`, `resources`, `navigation`, `scanner`, `biometrics`, `emojis`, `testing`. | diff --git a/docs/architecture/02-state-and-dependency-injection.md b/docs/architecture/02-state-and-dependency-injection.md index 7d0fd7b4e..252895309 100644 --- a/docs/architecture/02-state-and-dependency-injection.md +++ b/docs/architecture/02-state-and-dependency-injection.md @@ -95,6 +95,42 @@ identity — CompositionLocal is purely the delivery mechanism into Compose. > constructor (`@HiltViewModel`). Reach for a **`Local*`** only for the long-lived, > app-wide controllers that `MainActivity` already provides. +## Roles: coordinators, controllers, managers, services + +The injected dependencies above carry recurring suffixes — `Coordinator`, +`Controller`, `Manager`, `Service` — and they are **not interchangeable**. Each +names a specific role: + +| Role | Responsibility | Lives in | Exposes | +|------|----------------|----------|---------| +| **Coordinator** | The **single source of truth for a domain** (contacts, tokens, chat, settings, activity feed). Wraps one or more stateless Controllers and adds **caching (memory + Room), persistence, and sync/consistency**. **Session- and lifecycle-aware.** | `apps/flipcash/shared/*` | `StateFlow` domain state; usually `: SessionListener, DefaultLifecycleObserver` | +| **Controller** | A domain/feature API. Service-layer controllers are often **stateless network gateways** (no caching/state); app-layer controllers expose light UI-facing state/actions. | `services/*/controllers/*`, `apps/flipcash/shared/*` | `suspend` actions + `Result`, or light `StateFlow` | +| **Manager** | Owns a **state machine or resource lifecycle** (auth flow, credential storage), coordinating side effects across coordinators/controllers. | `services/*`, `apps/flipcash/shared/*` | `StateFlow` of a lifecycle/auth state (e.g. `UserManager` → `AuthState`) | +| **Service** | The **internal gRPC/REST adapter** — translates network responses to domain models. Never exposed to the UI. | `services/*/internal/network/services/*` | `Result` of domain models | + +### Coordinator vs Controller, by example + +The cleanest illustration is tokens. `TokenController` +(`services/opencode/.../controllers/TokenController.kt`) describes itself in its +own KDoc as a **stateless network gateway**: + +> *"This controller provides direct access to token-related network APIs without +> any caching, persistence, or state management… usable both within the Flipcash +> app (wrapped by `TokenCoordinator`) and as part of a standalone public SDK. All +> state management (caching, persistence, lifecycle, balance tracking) is the +> responsibility of the consumer."* + +That consumer is the **Coordinator**. `TokenCoordinator` +(`apps/flipcash/shared/tokens/.../TokenCoordinator.kt`) wraps the controller, +implements `SessionListener, DefaultLifecycleObserver`, holds `StateFlow` state, and +serves reads from a **Memory → Room → network** cache — rehydrating on login and +reacting to foreground/background. `ContactCoordinator` +(`apps/flipcash/shared/contacts/.../ContactCoordinator.kt`) follows the same shape, +syncing device contacts ↔ persistence ↔ server. So: **a Controller is the stateless +domain API; a Coordinator is the stateful, session-aware owner of that domain's +cached state.** When in doubt, [09 — Separation of concerns](09-separation-of-concerns.md) +has a "where does this code go?" table. + ## State: `BaseViewModel` The MVI base class lives at diff --git a/docs/architecture/09-separation-of-concerns.md b/docs/architecture/09-separation-of-concerns.md index a739e4643..1dcfe89d4 100644 --- a/docs/architecture/09-separation-of-concerns.md +++ b/docs/architecture/09-separation-of-concerns.md @@ -30,11 +30,16 @@ logic. The `BaseViewModel` reducer is the only place state changes and side effects are explicit on `eventFlow`. See [02 — State & dependency injection](02-state-and-dependency-injection.md). -### 3. MVI for screens, controllers for shared logic +### 3. MVI for screens, coordinators/controllers for shared logic Per-screen concerns live in a feature's ViewModel. Logic shared across features -lives in a **controller/coordinator** in a `shared/*` module, exposed as a -`StateFlow`-bearing interface and delivered to Compose as a `Local*`. Features never -reach into each other's internals; they go through shared modules. +lives in a `shared/*` module, exposed as a `StateFlow`-bearing interface and +delivered to Compose as a `Local*`. Use a **Coordinator** when that shared logic +**owns a domain's cached, synced state** (it wraps a stateless controller and is +session/lifecycle-aware); use a **Controller** for a stateless domain API or light +UI-facing state. The full vocabulary — Coordinator vs Controller vs Manager vs +Service — is defined in +[02 — Roles](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services). +Features never reach into each other's internals; they go through shared modules. ### 4. Transport details stop at the data layer The gRPC stack's four layers (Api → Service → Repository → Controller) mean protobuf @@ -62,7 +67,8 @@ feature code. See [06 — Payments & operations](06-payments-and-operations.md). | If you're adding… | It belongs in… | |-------------------|----------------| | A new screen | a `:apps:flipcash:features:*` module (screen + ViewModel + Hilt module) | -| Logic two+ features share | a `:apps:flipcash:shared:*` controller/coordinator | +| Cached, synced state for a domain (contacts, tokens, chat) | a `:apps:flipcash:shared:*` **Coordinator** ([roles](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services)) | +| Stateless shared logic two+ features call | a `:apps:flipcash:shared:*` **Controller** | | A new backend call | the appropriate `:services:*` layer (Api → Service → Repository → Controller) | | A reusable component or token | `:ui:components` / `:ui:theme` | | A domain-agnostic utility | a `:libs:*` module | From 7775e891672f6819570ac26c67b5266242a0fe24 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 23:00:22 +0000 Subject: [PATCH 3/7] docs: add build/run, adding-a-feature, and testing guides; fix CLAUDE.md Add three how-to guides under docs/architecture/ that cover working in the codebase rather than its structure: - 10-build-and-run: prerequisites, the API keys actually consumed (MIXPANEL/BUGSNAG/COINBASE_ONRAMP/GOOGLE_CLOUD_PROJECT_NUMBER) and how tryReadProperty resolves them, Gradle commands, variants, CI. - 11-adding-a-feature: end-to-end walkthrough (module -> ViewModel -> screen -> AppRoute -> appEntryProvider -> shared deps), tying together docs 01/02/03. - 12-testing: what to test where, :libs:test-utils (TestDispatchers, MainCoroutineRule), Robolectric, fakes, and Turbine; links to the existing Compose UI testing guide. Index and reading paths updated to surface the guides. Fix stale claims in CLAUDE.md to match the code: required local.properties keys (FINGERPRINT_API_KEY is not used; COINBASE_ONRAMP_API_KEY is), navigation is Navigation 3 + a custom CodeNavigator (not Voyager), async is Coroutines + Flow (not RxJava), and Room uses a per-user database name from entropy (not SQLCipher). Point CLAUDE.md at docs/architecture/. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JRVfsXp4HDrDMy7Pbmw9fD --- CLAUDE.md | 14 +- docs/architecture/10-build-and-run.md | 101 ++++++++++++++ docs/architecture/11-adding-a-feature.md | 165 +++++++++++++++++++++++ docs/architecture/12-testing.md | 130 ++++++++++++++++++ docs/architecture/README.md | 13 +- 5 files changed, 415 insertions(+), 8 deletions(-) create mode 100644 docs/architecture/10-build-and-run.md create mode 100644 docs/architecture/11-adding-a-feature.md create mode 100644 docs/architecture/12-testing.md diff --git a/CLAUDE.md b/CLAUDE.md index 2c35bc121..35f2bb97b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ Code/Flipcash is a mobile wallet app for instant, global, private payments using bundle exec fastlane android flipcash_tests ``` -**Requirements**: Java 21 (Corretto), `google-services.json` in `apps/flipcash/app/src/`, API keys in `local.properties` (BUGSNAG_API_KEY, FINGERPRINT_API_KEY, GOOGLE_CLOUD_PROJECT_NUMBER, MIXPANEL_API_KEY). +**Requirements**: Java 21 (Corretto), `google-services.json` in `apps/flipcash/app/src/`, API keys in `local.properties` (BUGSNAG_API_KEY, MIXPANEL_API_KEY, COINBASE_ONRAMP_API_KEY, GOOGLE_CLOUD_PROJECT_NUMBER). Keys resolve via `tryReadProperty` (local.properties → env var → empty), so a missing key won't fail the build but disables the dependent feature. See `docs/architecture/10-build-and-run.md`. ## Module Structure @@ -72,13 +72,15 @@ The feature plugin automatically includes `:libs:logging`, `:ui:core`, `:ui:comp ## Architecture -- **Pattern**: MVI/MVVM hybrid with Compose-driven UI and reactive state +- **Pattern**: MVI/MVVM hybrid with Compose-driven UI and reactive state (`BaseViewModel`) - **DI**: Hilt — all feature modules get Hilt via the convention plugin -- **Navigation**: Jetpack Navigation + Voyager for compose-based screens; custom `Router` controller +- **Navigation**: Jetpack **Navigation 3** (`androidx.navigation3`) wrapped by a custom `CodeNavigator`; a custom `Router` resolves deeplinks. (No Voyager.) - **Networking**: gRPC with Protobuf for backend services; Retrofit/OkHttp for REST -- **Async**: Kotlin Coroutines + RxJava 3 (both coexist) -- **Persistence**: Room (encrypted with SQLCipher), DataStore for preferences -- **Crypto**: Libsodium, Ed25519, Solana/Kin SDK for on-chain operations +- **Async**: Kotlin Coroutines + Flow (no RxJava); dispatchers injected via `DispatcherProvider` +- **Persistence**: Room with a per-user database name derived from account entropy (not SQLCipher-encrypted); DataStore for preferences +- **Crypto**: Ed25519, mnemonic/key derivation, Solana SDK for on-chain operations + +> Full architecture documentation lives in `docs/architecture/` (modules, state & DI, navigation, networking, persistence, payments, testing, and a "build & run" / "adding a feature" guide). ## Key Patterns diff --git a/docs/architecture/10-build-and-run.md b/docs/architecture/10-build-and-run.md new file mode 100644 index 000000000..fd603bd09 --- /dev/null +++ b/docs/architecture/10-build-and-run.md @@ -0,0 +1,101 @@ +# 10 — Build & run + +How to get a debug build of Flipcash compiling and running locally, and what the +build actually needs from you. This is the day-one setup doc. + +## Prerequisites + +| Requirement | Notes | +|-------------|-------| +| **JDK 21** | Amazon Corretto 21 is what CI uses. The convention plugins pin the Java/Kotlin toolchain to 21 ([01](01-modules-and-boundaries.md)). | +| **Android SDK** | `compileSdk 36`, `minSdk 29`, `targetSdk` per the version catalog. | +| **`google-services.json`** | Required at `apps/flipcash/app/src/google-services.json` (Firebase). The build fails without it. | +| **`local.properties`** | API keys (below) plus the standard `sdk.dir`. | + +## API keys in `local.properties` + +Keys are read by `tryReadProperty(...)` +([`buildSrc/.../LocalPropertyFetcher.kt`](../../buildSrc/src/main/java/LocalPropertyFetcher.kt)), +which resolves **`local.properties` → environment variable → empty string**. A +missing key therefore **does not fail the build** — it compiles with an empty value +and the dependent feature simply won't work. The keys that are actually consumed: + +| Key | Read in | Powers | +|-----|---------|--------| +| `MIXPANEL_API_KEY` | `apps/flipcash/app/build.gradle.kts` → `BuildConfig`, used in `app/.../inject/ApiModule.kt` | Analytics ([08](08-cross-cutting-concerns.md)) | +| `BUGSNAG_API_KEY` | `apps/flipcash/app/build.gradle.kts` → manifest placeholder | Crash/error reporting ([08](08-cross-cutting-concerns.md)) | +| `COINBASE_ONRAMP_API_KEY` | `apps/flipcash/shared/onramp/coinbase/build.gradle.kts` → `BuildConfig` | Coinbase on-ramp ([04](04-networking.md)) | +| `GOOGLE_CLOUD_PROJECT_NUMBER` | `services/{flipcash,opencode}{,-compose}/build.gradle.kts` → `BuildConfig` | gRPC / backend integration | + +A minimal `local.properties`: + +```properties +sdk.dir=/path/to/Android/sdk +MIXPANEL_API_KEY=... +BUGSNAG_API_KEY=... +COINBASE_ONRAMP_API_KEY=... +GOOGLE_CLOUD_PROJECT_NUMBER=... +``` + +> There is **no** `FINGERPRINT_API_KEY`. (`Build.FINGERPRINT` appears in +> `GooglePayReadiness` for emulator detection — unrelated to any key.) In CI these +> values come from secrets injected into `local.properties` by `.github/workflows/ci.yml`. + +## Common Gradle commands + +```bash +# Debug APK +./gradlew :apps:flipcash:app:assembleDebug + +# Release bundle (AAB) +./gradlew :apps:flipcash:app:bundleRelease + +# All unit tests +./gradlew test + +# Unit tests for one module (fast inner loop) +./gradlew :apps:flipcash:features:cash:test +./gradlew :apps:flipcash:shared:router:test + +# Instrumented tests (needs a device/emulator) +./gradlew connectedAndroidTest + +# What CI runs (unit tests via Fastlane) +bundle exec fastlane android flipcash_tests +``` + +See [12 — Testing](12-testing.md) for the testing approach. + +## Build variants & namespaces + +| | Application ID | +|---|----------------| +| Release | `com.flipcash.app.android` | +| Debug | `com.flipcash.app.android.dev` (suffix lets debug + release coexist on one device) | + +The app forces **dark mode** (`MODE_NIGHT_YES`, see [07](07-design-system.md)). +`versionCode` comes from `Packaging.Flipcash.versionCode` or `gitVersionCode()`. + +## Module structure at a glance + +The project is ~132 modules; the directory a module lives in defines its layer and +the convention plugin it applies. If you're about to add code, read +[01 — Modules & boundaries](01-modules-and-boundaries.md) and +[11 — Adding a feature](11-adding-a-feature.md) first. + +## CI + +[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs on PRs: it sets up +JDK 21 + Ruby, decodes `google-services.json`, writes the API-key secrets into +`local.properties`, and runs `bundle exec fastlane android flipcash_tests` +(Fastfile lane `flipcash_tests`). Other workflows handle version bumps, dev prep, +and the upload pipeline. + +## Troubleshooting + +- **`google-services.json` missing** → place it at `apps/flipcash/app/src/`. +- **A feature behaves as if unconfigured** (no analytics, on-ramp fails) → the + corresponding key is absent from `local.properties` (the build won't warn loudly, + because the fallback is an empty string). +- **Wrong JDK** → ensure `JAVA_HOME` points at a 21 JDK; the toolchain is pinned but + Gradle itself must launch on a compatible JVM. diff --git a/docs/architecture/11-adding-a-feature.md b/docs/architecture/11-adding-a-feature.md new file mode 100644 index 000000000..af3dd6284 --- /dev/null +++ b/docs/architecture/11-adding-a-feature.md @@ -0,0 +1,165 @@ +# 11 — Adding a feature + +A practical, end-to-end walkthrough for the most common task: adding a new screen +as a feature module. It ties together the module system ([01](01-modules-and-boundaries.md)), +state & DI ([02](02-state-and-dependency-injection.md)), and navigation +([03](03-navigation.md)). Follow the steps in order; each links to the reference +doc for the "why." + +```mermaid +graph TD + A["1. Scaffold module
apps/flipcash/features/"] + B["2. Register in settings.gradle.kts"] + C["3. Build ViewModel
BaseViewModel"] + D["4. Build the Composable screen"] + E["5. Add an AppRoute"] + F["6. Register in appEntryProvider"] + G["7. (optional) expose a Local* / shared dep"] + A --> B --> C --> D --> E --> F --> G +``` + +## 1. Scaffold the module + +Create `apps/flipcash/features//` with a `build.gradle.kts` applying the +feature convention plugin: + +```kotlin +plugins { + alias(libs.plugins.flipcash.android.feature) +} + +dependencies { + // Only the EXTRA deps you need — Hilt, Compose, the ui/* layer, + // :libs:logging, and :apps:flipcash:core are injected by the plugin. + implementation(project(":apps:flipcash:shared:session")) + // implementation(project(":apps:flipcash:shared:tokens")) // etc. +} +``` + +The `flipcash.android.feature` plugin already provides Hilt, Compose, KSP, +Parcelize, `:ui:core/components/navigation/resources/theme`, `:libs:logging`, and +`:apps:flipcash:core` — don't redeclare them ([01](01-modules-and-boundaries.md)). +Use the package convention `com.flipcash.app.` (see `CLAUDE.md` namespaces). + +> There is a **`module-scaffolder`** agent that generates this skeleton +> (build file, package structure, entry-point files, `settings.gradle.kts` +> inclusion). Prefer it for the boilerplate, then follow the steps below to wire +> behavior. + +## 2. Register the module + +Add the module path to [`settings.gradle.kts`](../../settings.gradle.kts) so Gradle +includes it, then add a dependency on it from `:apps:flipcash:app`'s +`build.gradle.kts` (the app is the universal collector — [01](01-modules-and-boundaries.md)). + +## 3. Build the ViewModel + +Extend `BaseViewModel` +([`ui/navigation/.../BaseViewModel.kt`](../../ui/navigation/src/main/kotlin/com/getcode/view/BaseViewModel.kt)), +following the MVI contract from [02](02-state-and-dependency-injection.md): + +```kotlin +@HiltViewModel +internal class WidgetViewModel @Inject constructor( + private val tokenCoordinator: TokenCoordinator, // inject domain coordinators/controllers + dispatchers: DispatcherProvider, // inject dispatchers (testable) +) : BaseViewModel( + initialState = State(), + updateStateForEvent = updateStateForEvent, + defaultDispatcher = dispatchers.Default, +) { + internal data class State(val loading: Boolean = false /* ... */) + + sealed interface Event { + data object OnAppear : Event + data class OnLoaded(val value: String) : Event + } + + init { + // side effects: collect off eventFlow / external StateFlows + eventFlow.filterIsInstance() + .onEach { /* ... */ } + .launchIn(viewModelScope) + } + + internal companion object { + val updateStateForEvent: (Event) -> (State.() -> State) = { event -> + when (event) { + is Event.OnLoaded -> { state -> state.copy(loading = false) } + else -> { state -> state } + } + } + } +} +``` + +- Inject **Coordinators** for cached domain state, **Controllers** for stateless + domain APIs (see [Roles](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services)). +- Inject `DispatcherProvider` rather than touching `Dispatchers.*` — this keeps the + ViewModel testable ([12](12-testing.md)). +- If you need a new Hilt binding, add a `@Module @InstallIn(SingletonComponent::class)` + in the module's `inject/` package. + +## 4. Build the screen + +```kotlin +@Composable +fun WidgetScreen(/* route args */) { + val navigator = LocalCodeNavigator.current + val session = LocalSessionController.current!! // ambient controllers via Local* + val viewModel = hiltViewModel() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + LaunchedEffect(viewModel) { viewModel.dispatchEvent(WidgetViewModel.Event.OnAppear) } + + // render from `state`; emit via viewModel.dispatchEvent(...) +} +``` + +Read long-lived controllers from `Local*`, build per-screen state from the +ViewModel, and use components/theme from the `ui/*` layer ([07](07-design-system.md)). + +## 5. Add a route + +Add a case to the `AppRoute` sealed hierarchy in +[`apps/flipcash/core/.../AppRoute.kt`](../../apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt). +For a single screen, a `@Serializable @Parcelize data class`/`data object` is +enough. For a multi-step flow, implement `FlowRoute` (with an `initialStack`) or +`FlowRouteWithResult` if it returns a value ([03](03-navigation.md)): + +```kotlin +@Serializable +data class Widget(val id: String) : AppRoute +``` + +## 6. Register route → screen + +Add one `annotatedEntry` in +[`AppScreenContent.kt`](../../apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt): + +```kotlin +annotatedEntry { key -> WidgetScreen(id = key.id) } +``` + +Navigate to it from elsewhere with `navigator.push(AppRoute.Widget(id))`. + +## 7. (Optional) expose a shared dependency + +If the feature owns state or a controller that **other** features need, don't let +them depend on your feature module — put the shared piece in a +`:apps:flipcash:shared:*` module (a **Coordinator** if it owns cached/synced domain +state, a **Controller** otherwise — [Roles](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services)), +provide it via Hilt, and if it's app-wide, surface it to Compose by adding a +`Local*` and providing it in `MainActivity`'s `CompositionLocalProvider` +([02](02-state-and-dependency-injection.md)). + +## Checklist + +- [ ] Module under `apps/flipcash/features/` with the feature plugin +- [ ] Listed in `settings.gradle.kts` and depended on by `:apps:flipcash:app` +- [ ] `BaseViewModel` with an `Event` sealed interface + reducer +- [ ] Screen reads `Local*` controllers and `collectAsStateWithLifecycle()` +- [ ] `AppRoute` case added +- [ ] `annotatedEntry` registered in `appEntryProvider` +- [ ] Cross-feature logic lives in a `shared/*` module, not in the feature +- [ ] Tests for the ViewModel / any new coordinator ([12](12-testing.md)) diff --git a/docs/architecture/12-testing.md b/docs/architecture/12-testing.md new file mode 100644 index 000000000..a2b6b9019 --- /dev/null +++ b/docs/architecture/12-testing.md @@ -0,0 +1,130 @@ +# 12 — Testing + +How testing is set up across the project and what to test where. The project favors +**fast JVM unit tests** (Robolectric where Android APIs are needed) over instrumented +tests — there are ~155 unit tests and only a handful of instrumented ones. For the +Compose-UI-specific patterns, see the companion +[Compose UI Testing Guide](../compose-ui-testing.md). + +```mermaid +graph TD + Unit["Unit tests (src/test, JVM + Robolectric)
ViewModels, coordinators, mappers, DAOs"] + Fakes["Fakes of injected interfaces
(controllers, coordinators, providers)"] + Utils[":libs:test-utils
TestDispatchers, MainCoroutineRule"] + Turbine["Turbine — Flow/StateFlow assertions"] + Compose["Compose UI tests
(see compose-ui-testing.md)"] + + Unit --> Fakes + Unit --> Utils + Unit --> Turbine + Compose --> Fakes +``` + +## What goes where + +| Test what | How | Lives in | +|-----------|-----|----------| +| **ViewModel** logic (reducer + event side effects) | Plain JUnit; inject fakes + `TestDispatchers`; assert on `stateFlow` with Turbine | the feature module's `src/test` | +| **Coordinator** sync/cache logic | Fake the wrapped Controller + persistence; drive session/lifecycle callbacks | the shared module's `src/test` | +| **DAO / Room** queries & migrations | Robolectric + in-memory Room, `runTest` | `shared/persistence/*/src/test` | +| **Mappers / pure logic** (currency, crypto, encoding) | Plain JUnit, no Android | the relevant `:libs:*` module | +| **Compose UI** | Compose test rule + fakes | see [Compose UI Testing Guide](../compose-ui-testing.md) | + +## What the convention plugin gives every module + +`flipcash.android.library` (and therefore every module — +[01](01-modules-and-boundaries.md)) configures unit tests automatically +([`AndroidLibraryConventionPlugin.kt`](../../build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt)): + +- `testOptions { unitTests.isReturnDefaultValues = true; unitTests.isIncludeAndroidResources = true }` +- `kotlin-test-junit` on the test classpath +- A `robolectric.properties` pinning the Robolectric SDK to **36** +- `:libs:test-utils` available to tests (except for itself) + +So a module can write Robolectric-backed unit tests with zero extra setup. + +## Shared test utilities (`:libs:test-utils`) + +Two helpers make coroutine code deterministic: + +**`TestDispatchers`** — a `DispatcherProvider` whose `IO`/`Main`/`Default` all map to +a single `StandardTestDispatcher`, so injected dispatchers are controllable in tests: + +```kotlin +class TestDispatchers(scheduler: TestCoroutineScheduler) : DispatcherProvider { + val dispatcher = StandardTestDispatcher(scheduler) + override val IO = dispatcher + override val Main = dispatcher + override val Default = dispatcher +} +``` + +**`MainCoroutineRule`** — a JUnit `TestWatcher` that `Dispatchers.setMain(...)`/ +`resetMain()` around each test so `viewModelScope` uses the test dispatcher. + +Because production code injects `DispatcherProvider` rather than referencing +`Dispatchers.*` ([08](08-cross-cutting-concerns.md)), passing `TestDispatchers` +makes a ViewModel or coordinator fully deterministic. + +## The interface + fake pattern + +The codebase is built on interfaces (controllers, coordinators, providers exposed +behind interfaces and delivered via Hilt / `Local*`). Tests substitute **hand-written +fakes** for those interfaces rather than using a mocking framework. This is the +dominant pattern — the [Compose UI Testing Guide](../compose-ui-testing.md) +documents it in detail and it applies equally to ViewModel/coordinator unit tests. + +## Asserting on flows + +State is exposed as `StateFlow`/`Flow`, so tests assert on emissions with +**Turbine** (`app.cash.turbine.test`) inside `runTest`: + +```kotlin +@Test +fun `loads on appear`() = runTest { + val vm = WidgetViewModel(fakeCoordinator, TestDispatchers(testScheduler)) + vm.stateFlow.test { + assertEquals(false, awaitItem().loading) // initial + vm.dispatchEvent(Widget.Event.OnAppear) + // advance/await subsequent states... + } +} +``` + +## Example: a Room DAO test + +DAO tests run on Robolectric against an in-memory database and use `runTest` +(e.g. `ContactDaoTest`, +`apps/flipcash/shared/persistence/db/src/test/.../dao/ContactDaoTest.kt`): + +```kotlin +@RunWith(RobolectricTestRunner::class) +class ContactDaoTest { + @Before fun setup() { /* Room.inMemoryDatabaseBuilder(...) */ } + + @Test fun `upsertSyncState and getSyncState roundtrip`() = runTest { /* ... */ } +} +``` + +## Running tests + +```bash +./gradlew test # all modules +./gradlew :apps:flipcash:features:cash:test # one module (fast loop) +./gradlew :apps:flipcash:shared:router:test +bundle exec fastlane android flipcash_tests # what CI runs +``` + +See [10 — Build & run](10-build-and-run.md) for the wider command set. + +## Guidance + +- **Inject, don't reach.** A ViewModel/coordinator that takes its dependencies + (incl. `DispatcherProvider`) as constructor params is trivially testable; one that + reaches for singletons or `Dispatchers.*` is not. +- **Test the reducer and the effects.** Assert state transitions for events, and + assert the side effects emitted on `eventFlow`. +- **Prefer unit + Robolectric over instrumented.** Reserve `connectedAndroidTest` + for things that genuinely need a device. +- **Fakes over mocks.** Write a small fake implementing the interface; it's clearer + and survives refactors better than mock setups. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 58dd8d793..94229990e 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -69,9 +69,18 @@ depend on app modules.* See [01 — Modules & boundaries](01-modules-and-boundar | 09 | [Separation of concerns](09-separation-of-concerns.md) | Layering principles, MVI/MVVM split, where logic lives, the acyclic discipline | | — | [Feature catalog](features/README.md) | Representative features by category, with screens, ViewModels, and patterns | +### Guides (how to work in the codebase) + +| # | Topic | Scope | +|---|-------|-------| +| 10 | [Build & run](10-build-and-run.md) | Prerequisites, the real `local.properties` keys, Gradle commands, variants, CI | +| 11 | [Adding a feature](11-adding-a-feature.md) | End-to-end: scaffold module → ViewModel → screen → route → register → share | +| 12 | [Testing](12-testing.md) | What to test where, `:libs:test-utils`, Robolectric, fakes, Turbine (+ Compose UI guide) | + ## Suggested reading paths -- **New to the codebase** → README → 01 → 02 → 09, then the feature catalog. -- **Building a feature** → feature catalog → 03 → 07 → 02. +- **New to the codebase** → 10 (build & run) → README → 01 → 02 → 09, then the feature catalog. +- **Building a feature** → 11 (adding a feature) → feature catalog → 03 → 07 → 02. +- **Writing tests** → 12 (testing) → [Compose UI Testing Guide](../compose-ui-testing.md). - **Working on payments** → 06 → 04 → 05. - **Backend / proto changes** → 04 → 05 → 06. From 0356acf6ae5a0f8e7e863661e11ee81d7b5aac65 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 23:06:00 +0000 Subject: [PATCH 4/7] docs: add protobuf, error-handling, CI/release, agents/skills docs + glossary Fill the remaining documentation gaps for new developers and agents: - 13-protobuf-and-codegen: definitions/* layout, the protobuf/protoc codegen, the "never hand-edit generated models" rule, and the /fetch-protos + proto-change-tracer workflow. - 14-error-handling: Result at the service boundary, per-operation sealed error hierarchies over CodeServerError, NotifiableError (bug vs expected), and retryable/retryableOrThrow. - 15-ci-and-release: the CI check, Fastlane lanes, release workflows, and the build-lookup/r8-mapping/release-notes skills. - 16-agents-and-skills: a task -> tool map for the repo's Claude Code agents and skills. - glossary: domain & architecture vocabulary (USDF, intent, timelock, rendezvous, coordinator, etc.). Index and reading paths updated to surface all of the above. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JRVfsXp4HDrDMy7Pbmw9fD --- docs/architecture/13-protobuf-and-codegen.md | 92 ++++++++++++++ docs/architecture/14-error-handling.md | 121 +++++++++++++++++++ docs/architecture/15-ci-and-release.md | 72 +++++++++++ docs/architecture/16-agents-and-skills.md | 71 +++++++++++ docs/architecture/README.md | 13 +- docs/architecture/glossary.md | 62 ++++++++++ 6 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 docs/architecture/13-protobuf-and-codegen.md create mode 100644 docs/architecture/14-error-handling.md create mode 100644 docs/architecture/15-ci-and-release.md create mode 100644 docs/architecture/16-agents-and-skills.md create mode 100644 docs/architecture/glossary.md diff --git a/docs/architecture/13-protobuf-and-codegen.md b/docs/architecture/13-protobuf-and-codegen.md new file mode 100644 index 000000000..31bbc2493 --- /dev/null +++ b/docs/architecture/13-protobuf-and-codegen.md @@ -0,0 +1,92 @@ +# 13 — Protobuf & code generation + +The backend contract is **Protocol Buffers**. Generated gRPC/proto code is the +foundation the whole service layer sits on ([04 — Networking](04-networking.md)), +so this doc explains where it comes from, how it's generated, and how to update it +safely. + +```mermaid +graph TD + Upstream["Upstream proto repos (Flipcash, OCP)"] + Protos[":definitions:*:protos — .proto sources (java-library)"] + Models[":definitions:*:models — generated Java/Kotlin + grpc stubs"] + Wrap[":services:* — hand-written Api/Service/Repository/Controller"] + Feat["features / shared"] + + Upstream -->|/fetch-protos| Protos --> Models --> Wrap --> Feat +``` + +## Layout + +| Module | Plugin | Contents | +|--------|--------|----------| +| `:definitions:flipcash:protos`, `:definitions:opencode:protos` | `java-library` | The raw `.proto` source files under `src/main/proto/`. The `java-library` plugin packages them so the `models` module can compile them. | +| `:definitions:flipcash:models`, `:definitions:opencode:models` | `flipcash.android.library` + `protobuf` + `protobuf.validate` | The **generated** Java/Kotlin message classes and gRPC stubs. | + +The `models` build consumes the matching `protos` module via the `protobuf(...)` +configuration and runs `protoc`: + +```kotlin +// definitions//models/build.gradle.kts +plugins { + alias(libs.plugins.flipcash.android.library) + alias(libs.plugins.protobuf) + alias(libs.plugins.protobuf.validate) +} +dependencies { + protobuf(project(":definitions::protos")) // source of .proto files + implementation(libs.grpc.protobuf.lite) + implementation(libs.protobuf.kotlin.lite) +} +protobuf { + protoc { artifact = "com.google.protobuf:protoc:$protobufVersion$archSuffix" } + plugins { /* protoc-gen-grpc-java */ } +} +``` + +Generated output uses the **lite** runtime (`protobuf-kotlin-lite`, +`grpc-protobuf-lite`) suited to Android, and `protobuf.validate` wires up +`protovalidate` so requests can be validated at the Api boundary +(`...orThrow()` — see [04](04-networking.md)). + +## The golden rule + +> **Never hand-edit anything under `:definitions:*:models`.** It is generated from +> the `.proto` sources and will be overwritten. To change a model, change the +> `.proto` (upstream) and regenerate. + +The hand-written code lives one layer up, in `:services:*` — the +Api/Service/Repository/Controller wrappers and the `LocalToProtobuf` / +`ProtobufToLocal` extensions that translate between protobuf and domain types. + +## Updating protos + +Use the **`/fetch-protos`** skill rather than copying files by hand. It fetches the +latest `.proto`s from the upstream repos, verifies they compile, summarizes the API +changes, and scaffolds missing service-layer stubs: + +``` +/fetch-protos # both targets at HEAD +/fetch-protos flipcash # flipcash only +/fetch-protos opencode # opencode at a specific commit +``` + +After fetching, run the **`proto-change-tracer`** agent to trace the impact through +`generated models → Api → Service → Repository → Controller → features` and get the +list of files that need updating. + +## Typical workflow + +1. `/fetch-protos [commit]` — pull and regenerate. +2. Build `:definitions::models` to confirm codegen succeeds. +3. Run `proto-change-tracer` to find affected wrappers. +4. Update the hand-written `:services:*` layer (new RPCs → new Api/Service methods; + changed messages → mapper updates). +5. Add/adjust tests ([12 — Testing](12-testing.md)) and build. + +## Why this matters + +Keeping generated code isolated in `:definitions:*:models` and all +human-maintained code in `:services:*` means a proto bump is a mechanical +regenerate-plus-rewire, and the boundary ([01](01-modules-and-boundaries.md)) keeps +protobuf types from leaking past the service layer. diff --git a/docs/architecture/14-error-handling.md b/docs/architecture/14-error-handling.md new file mode 100644 index 000000000..45724458c --- /dev/null +++ b/docs/architecture/14-error-handling.md @@ -0,0 +1,121 @@ +# 14 — Error handling + +How failures are represented and surfaced across the app. The conventions are: +suspend functions return **`Result`** with **typed domain errors**, errors that +represent genuine bugs are tagged **`NotifiableError`** so they reach Bugsnag/Slack, +and transient network calls are wrapped in **`retryable`**. + +```mermaid +graph TD + Api["Api — raw gRPC call"] + Service["Service — foldWithSuppression"] + Result["Result with typed *Error"] + Caller["Controller / Coordinator / ViewModel"] + UI["UI — user-facing message"] + Reporter["ErrorReporter -> Bugsnag (if NotifiableError)"] + + Api --> Service --> Result --> Caller + Caller --> UI + Caller --> Reporter +``` + +## Typed errors over raw exceptions + +Backend calls map RPC status to a **sealed error hierarchy** per operation, rather +than throwing generic exceptions. The base type is `CodeServerError` +([`libs/logging/.../CodeServerError.kt`](../../libs/logging/src/main/kotlin/com/getcode/utils/CodeServerError.kt)); +each RPC defines its own sealed subclass with one case per failure mode. Example +from [`services/flipcash/.../models/Errors.kt`](../../services/flipcash/src/main/kotlin/com/flipcash/services/models/Errors.kt): + +```kotlin +sealed class RegisterError(message: String?, cause: Throwable? = null) : CodeServerError(message, cause) { + class InvalidSignature : RegisterError("Invalid signature"), NotifiableError + class Denied : RegisterError("Denied") + class Unrecognized : RegisterError("Unrecognized"), NotifiableError + data class Other(override val cause: Throwable? = null) : + RegisterError(message = cause?.message, cause = cause), NotifiableError +} +``` + +OCP defines the same shape for its operations (e.g. `SubmitIntentError`, +`CodeAccountCheckError` in +`services/opencode/.../model/core/errors/Errors.kt`). The recurring cases are an +expected/benign set (`Denied`, `InvalidTimestamp`, …) plus a catch-all +`Other(cause)`. + +## `Result` at the service boundary + +The **Service** layer converts a raw Api response into a `Result`, mapping the +status code to the right typed error via `foldWithSuppression` +(`services/*/internal/network/extensions/`): + +```kotlin +suspend fun register(owner: KeyPair): Result { + return api.register(owner).foldWithSuppression( + onResult = { response -> + when (response.result) { + OK -> Result.success(response.userId.toDomain()) + INVALID_SIGNATURE -> Result.failure(RegisterError.InvalidSignature()) + DENIED -> Result.failure(RegisterError.Denied()) + else -> Result.failure(RegisterError.Other()) + } + }, + onError = { cause -> Result.failure(cause.toValidationOrElse { RegisterError.Other(cause = it) }) }, + ) +} +``` + +Callers (controllers, coordinators, ViewModels) then use `onSuccess` / `onFailure` +or `fold` — they never see gRPC types ([04](04-networking.md)). + +## Notifiable vs expected errors + +`NotifiableError` +([`libs/logging/.../NotifiableError.kt`](../../libs/logging/src/main/kotlin/com/getcode/utils/NotifiableError.kt)) +is a marker that distinguishes **bugs** from **expected outcomes**: + +```kotlin +/** + * Marker interface for errors representing unexpected failures (not user-caused). + * Errors implementing this are tagged in Bugsnag with metadata that triggers Slack notifications. + */ +interface NotifiableError : ConditionallyNotifiable { + override val isNotifiable: Boolean get() = true +} +``` + +- **Tag it `NotifiableError`** when the case "should never happen" (an invalid + signature, an unrecognized server response) — these flow through `ErrorReporter` + to Bugsnag and alert the team ([08 — Cross-cutting concerns](08-cross-cutting-concerns.md)). +- **Don't** tag expected, user-driven outcomes (a `Denied`, a validation failure) — + surfacing them as noise defeats the purpose. + +## Retrying transient failures + +For flaky network calls, wrap the call in `retryable` / `retryableOrThrow` +([`libs/network/connectivity/public/.../Retry.kt`](../../libs/network/connectivity/public/src/main/kotlin/com/getcode/utils/network/Retry.kt)) +rather than hand-rolling a loop: + +```kotlin +suspend inline fun retryable( + maxRetries: Int = 3, + delayDuration: Duration = 2.seconds, + backoffFactor: Double = 1.0, // 1.0 = fixed delay; >1 = exponential + retryIf: (Throwable) -> Boolean = { true }, + // onRetry / onError default to tracing +): T? +``` + +`retryable` returns `null` when retries are exhausted; `retryableOrThrow` rethrows +the last exception. Use `retryIf` to retry only on transient conditions. Retries are +traced automatically ([08](08-cross-cutting-concerns.md)). + +## Guidance + +- **Return `Result` with a typed error**, not a thrown exception, from anything + that can fail at the service boundary. +- **One sealed error hierarchy per operation**, with an `Other(cause)` catch-all. +- **Mark genuine-bug cases `NotifiableError`**; leave expected outcomes unmarked. +- **Wrap transient calls in `retryable`** with a sensible `retryIf`. +- **Map to user-facing copy at the UI layer** — the ViewModel decides what (if + anything) the user sees for each error case. diff --git a/docs/architecture/15-ci-and-release.md b/docs/architecture/15-ci-and-release.md new file mode 100644 index 000000000..788ce53ef --- /dev/null +++ b/docs/architecture/15-ci-and-release.md @@ -0,0 +1,72 @@ +# 15 — CI & release + +How the project builds, tests, versions, and ships. Day-to-day you mostly care +about the **CI** check on PRs; the rest is reference for the release pipeline and +the helper skills. + +```mermaid +graph TD + PR["Pull request"] --> CI["ci.yml — JDK 21 + Ruby, secrets -> local.properties"] + CI --> Tests["fastlane flipcash_tests (unit tests)"] + Prep["prep-dev.yml (monthly cron)"] --> Bump["bump-patch.yml (manual)"] + Bump --> Deploy["build-fcash2-upload-android.yml -> Play Store"] + Build["fastlane build_flipcash"] --> Deploy +``` + +## CI (every PR) + +[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs on `pull_request`: + +1. Checks out and sets up **JDK 21 (Corretto)** and Ruby. +2. Restores the Gradle build cache. +3. Decodes `google-services.json` and writes the API-key secrets into + `local.properties` (`BUGSNAG_API_KEY`, `GOOGLE_CLOUD_PROJECT_NUMBER`, + `MIXPANEL_API_KEY`, `COINBASE_ONRAMP_API_KEY` — see [10 — Build & run](10-build-and-run.md)). +4. Runs `bundle exec fastlane android flipcash_tests`. + +So CI == the `flipcash_tests` Fastlane lane == unit tests. Keep PRs green by running +`./gradlew test` locally first ([12 — Testing](12-testing.md)). + +## Fastlane lanes + +[`fastlane/Fastfile`](../../fastlane/Fastfile): + +| Lane | Purpose | +|------|---------| +| `flipcash_tests` | Generates the emoji list, then runs `flipcashTestDebug` (the CI lane). | +| `build_flipcash` | Builds the release bundle (`apps:flipcash:app:bundle`) and uploads the Bugsnag/ProGuard (R8) mapping. | +| `upload_flipcash` | Uploads the build to Google Play. | +| `deploy_flipcash` | Build + deploy to Play in one step. | +| `download_from_playstore_flipcash` | Pulls store metadata. | + +## Release workflows + +| Workflow | Trigger | What it does | +|----------|---------|--------------| +| [`prep-dev.yml`](../../.github/workflows/prep-dev.yml) | Cron, 1st of each month (+ manual) | Increments Flipcash versioning for the new month. | +| [`bump-patch.yml`](../../.github/workflows/bump-patch.yml) | Manual (`workflow_dispatch`, choose a track) | Updates the release manifest and bumps the patch version. | +| [`build-fcash2-upload-android.yml`](../../.github/workflows/build-fcash2-upload-android.yml) | Manual | Builds and deploys Flipcash to the Play Store. | +| [`labeler.yml`](../../.github/workflows/labeler.yml) | PR opened/synchronized | Applies area labels to PRs. | + +`versionCode` comes from `Packaging.Flipcash.versionCode` or `gitVersionCode()` +([10](10-build-and-run.md)). + +## Helper skills + +Three skills support the release/debug loop (invoke with `/`): + +| Skill | Use | +|-------|-----| +| `/build-lookup ` | Find the git commit and GitHub Actions run for a given Flipcash `versionCode`. | +| `/r8-mapping ` | Download the R8/ProGuard mapping for a release build to deobfuscate a stack trace. | +| `/release-notes ` | Generate polished GitHub release notes from git refs. | + +For where these fit among the other automation, see +[16 — Agents & skills](16-agents-and-skills.md). + +## Why this matters + +CI is intentionally narrow (unit tests via one Fastlane lane), so a fast local +`./gradlew test` is a faithful preview. Versioning is automated (monthly prep + +patch bumps) and releases go through Fastlane, so manual version edits are rarely +needed — reach for the skills above when investigating a shipped build. diff --git a/docs/architecture/16-agents-and-skills.md b/docs/architecture/16-agents-and-skills.md new file mode 100644 index 000000000..2394a4a37 --- /dev/null +++ b/docs/architecture/16-agents-and-skills.md @@ -0,0 +1,71 @@ +# 16 — Agents & skills + +This repo ships **Claude Code** automation tuned to its architecture: subagents +(under `.claude/agents/`) for multi-step investigations, and slash-command skills +(under `.claude/skills/`) for repeatable workflows. This page maps a task to the +right tool so you (or an agent) don't reinvent work the repo already automates. + +## Task → tool + +| When you want to… | Use | Type | +|-------------------|-----|------| +| Triage a Bugsnag issue / crash with evidence and a fix direction | `/triage` | skill | +| Investigate a crash/stack trace and trace it to a root cause | `bug-triage` | agent | +| Create a new feature / shared / lib module skeleton | `module-scaffolder` | agent | +| Add a screen end-to-end (uses the scaffolder) | follow [11 — Adding a feature](11-adding-a-feature.md) | guide | +| Fetch latest protobufs, verify, summarize, scaffold stubs | `/fetch-protos` | skill | +| Trace the impact of a proto change through the codebase | `proto-change-tracer` | agent | +| Assess the blast radius of a dependency bump | `dependency-impact` | agent | +| Review a Dependabot PR for breaking changes | `/dep-review` | skill | +| Review a PR (or local changes) for quality & arch consistency | `pr-reviewer` | agent | +| Find untested code and scaffold tests | `test-gap-finder` | agent | +| Map a `versionCode` to its commit / CI run | `/build-lookup` | skill | +| Deobfuscate a release stack trace | `/r8-mapping` | skill | +| Generate GitHub release notes | `/release-notes` | skill | + +## Agents (`.claude/agents/`) + +Agents are launched via the `Agent` tool for open-ended, multi-step work: + +- **bug-triage** — traces a Bugsnag link / stack trace / error through the codebase + to a root cause and suggested fix. +- **module-scaffolder** — generates a full module skeleton: `build.gradle.kts`, + package structure, entry points, navigation registration, `settings.gradle.kts` + inclusion. +- **proto-change-tracer** — after `/fetch-protos`, traces + `generated models → Api → Service → Repository → Controller → features` + ([13](13-protobuf-and-codegen.md)). +- **dependency-impact** — for a dependency bump, finds dependent modules, breaking + API changes, and targeted tests to run. +- **pr-reviewer** — reviews a PR or local diff against the project's patterns + (CompositionLocal injection, MVI/MVVM, convention plugins, proto boundaries). +- **test-gap-finder** — finds coverage gaps and scaffolds tests in the project's + style ([12](12-testing.md)). + +## Skills (`.claude/skills/`) + +Skills are slash commands for repeatable workflows: + +- **/triage** — triage a Bugsnag production issue (top open or a specific + URL/ID) end-to-end. +- **/fetch-protos** `[flipcash|opencode] [commit]` — pull + regenerate protos + ([13](13-protobuf-and-codegen.md)). +- **/dep-review** `` — review a Dependabot PR for breaking changes and required + code updates. +- **/build-lookup** `` — git commit + Actions run for a build + ([15](15-ci-and-release.md)). +- **/r8-mapping** `` — download the R8 mapping to deobfuscate a trace. +- **/release-notes** ` ` — generate polished release notes. + +## Project context for agents + +The repo root **`CLAUDE.md`** is the canonical orientation file (build commands, +module layout, key patterns, namespaces, git conventions) and points here. New +agents should read it first, then this `docs/architecture/` suite for depth. + +## Why this matters + +The architecture has sharp conventions (layered modules, proto boundaries, +coordinator/controller roles, MVI), and these agents/skills already encode them. +Reaching for the right one keeps work consistent with the codebase instead of +re-deriving the patterns each time. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 94229990e..803b6db56 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -76,11 +76,20 @@ depend on app modules.* See [01 — Modules & boundaries](01-modules-and-boundar | 10 | [Build & run](10-build-and-run.md) | Prerequisites, the real `local.properties` keys, Gradle commands, variants, CI | | 11 | [Adding a feature](11-adding-a-feature.md) | End-to-end: scaffold module → ViewModel → screen → route → register → share | | 12 | [Testing](12-testing.md) | What to test where, `:libs:test-utils`, Robolectric, fakes, Turbine (+ Compose UI guide) | +| 13 | [Protobuf & codegen](13-protobuf-and-codegen.md) | proto sources → generated models → services; updating protos with `/fetch-protos` | +| 14 | [Error handling](14-error-handling.md) | `Result`, typed sealed errors, `NotifiableError`, `retryable` | +| 15 | [CI & release](15-ci-and-release.md) | The CI check, Fastlane lanes, release workflows, helper skills | +| 16 | [Agents & skills](16-agents-and-skills.md) | The repo's Claude Code agents/skills and which task each one fits | +| — | [Glossary](glossary.md) | Domain & architecture terms (USDF, intent, timelock, coordinator, …) | ## Suggested reading paths - **New to the codebase** → 10 (build & run) → README → 01 → 02 → 09, then the feature catalog. - **Building a feature** → 11 (adding a feature) → feature catalog → 03 → 07 → 02. - **Writing tests** → 12 (testing) → [Compose UI Testing Guide](../compose-ui-testing.md). -- **Working on payments** → 06 → 04 → 05. -- **Backend / proto changes** → 04 → 05 → 06. +- **Working on payments** → 06 → 04 → 05 → 14 (error handling). +- **Backend / proto changes** → 13 (protobuf & codegen) → 04 → 05 → 14. +- **New to Claude Code automation here** → 16 (agents & skills) → CLAUDE.md. + +Unfamiliar with a term? The [Glossary](glossary.md) defines the domain and +architecture vocabulary (USDF, intent, timelock, coordinator, …). diff --git a/docs/architecture/glossary.md b/docs/architecture/glossary.md new file mode 100644 index 000000000..7d9fc0f78 --- /dev/null +++ b/docs/architecture/glossary.md @@ -0,0 +1,62 @@ +# Glossary + +Domain and architecture terms used throughout this codebase and these docs. +Definitions are kept short; the **See** column points to the doc (or code) with the +full story. + +## Product & money + +| Term | Meaning | See | +|------|---------|-----| +| **USDF** | The USD-pegged stablecoin users primarily hold and transact in. | [README](README.md) | +| **Launchpad token / currency** | A custom on-chain currency (governed by an on-chain bonding curve) that users can create and trade, in addition to USDF. | [06](06-payments-and-operations.md) | +| **Cash bill** | The animated, circular on-screen code that represents a payment for **device-to-device** transfer — one phone shows it, another scans it. | [README](README.md), [features](features/README.md) | +| **Kik Code** | The scannable code format rendered as a cash bill (captured via the camera scanner). | [07](07-design-system.md) | +| **On-ramp** | Funding the wallet with fiat (Coinbase) or in-app purchase. | [04](04-networking.md), [06](06-payments-and-operations.md) | +| **Swap** | Exchanging one token for another (e.g. via a launchpad currency's curve). | [03](03-navigation.md), [06](06-payments-and-operations.md) | +| **Withdrawal** | Moving funds on-chain to an external Solana address. | [features](features/README.md) | + +## Identity, keys & accounts + +| Term | Meaning | See | +|------|---------|-----| +| **Self-custodial** | Keys live on the device; the user — not a server — controls funds. | [06](06-payments-and-operations.md) | +| **Mnemonic** | The BIP39 word phrase from which all of a user's keys are derived. | [06](06-payments-and-operations.md) | +| **Entropy** | The raw seed bytes behind the mnemonic; its Base58 prefix also names the user's local database. | [05](05-persistence.md), [06](06-payments-and-operations.md) | +| **Access key** | The user-facing recovery credential (backed by the mnemonic) that the user saves during onboarding. | [features](features/README.md) | +| **Ed25519** | The signature scheme used to sign requests and transactions. | [04](04-networking.md), [06](06-payments-and-operations.md) | +| **AccountCluster** | The bundle of derived accounts that makes up a wallet: authority + timelock + per-token deposit addresses. | [06](06-payments-and-operations.md) | +| **Authority** | The master keypair that controls an account. | [06](06-payments-and-operations.md) | +| **Timelock** | The custody mechanism — vault accounts in a timelock virtual machine that hold funds. | [06](06-payments-and-operations.md) | +| **Vault** | A timelock account that actually holds a token balance. | [06](06-payments-and-operations.md) | +| **Rendezvous** | The keypair used to sign requests during the payment handshake (the cluster's authority key). | [06](06-payments-and-operations.md) | +| **Deposit address** | A per-token address derived from the cluster, used for on-ramp funding. | [06](06-payments-and-operations.md) | +| **AuthState** | The auth-state machine (`Unknown → Onboarding → Authenticating → Ready → LoggedOut`) owned by `UserManager`. | [06](06-payments-and-operations.md) | + +## Backend & transactions + +| Term | Meaning | See | +|------|---------|-----| +| **OCP / Open Code Protocol** | The gRPC backend for transactions, intents, swaps, and exchange rates (module `:services:opencode`). | [04](04-networking.md) | +| **Flipcash service** | The gRPC backend for accounts, profiles, chat, and activity (module `:services:flipcash`). | [04](04-networking.md) | +| **Intent** | A signed unit of money movement (transfer, remote send/receive, withdraw, swap, distribution) submitted over the `SubmitIntent` bidirectional stream. | [04](04-networking.md), [06](06-payments-and-operations.md) | +| **Mint** | A Solana token mint address (`PublicKey` subtype); identifies a token such as USDF or a launchpad currency. | [06](06-payments-and-operations.md) | +| **Protobuf / proto** | The Protocol Buffers contract; generated code lives in `:definitions:*:models` and is never hand-edited. | [13](13-protobuf-and-codegen.md) | +| **NotifiableError** | Marker for errors that represent bugs (not user-caused) and should alert via Bugsnag/Slack. | [14](14-error-handling.md) | + +## Architecture roles + +| Term | Meaning | See | +|------|---------|-----| +| **Coordinator** | App-layer, session/lifecycle-aware owner of a domain's cached, synced state; wraps stateless controllers. | [02](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services) | +| **Controller** | A domain/feature API — often a stateless network gateway, or light UI-facing state. | [02](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services) | +| **Manager** | Owner of a state machine / resource lifecycle (e.g. `UserManager`, `AuthManager`). | [02](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services) | +| **Service** | Internal gRPC/REST adapter returning `Result`; never exposed to the UI. | [02](02-state-and-dependency-injection.md#roles-coordinators-controllers-managers-services), [04](04-networking.md) | +| **CompositionLocal / `Local*`** | The ambient handle (e.g. `LocalRouter`) through which Compose reads app-wide controllers. | [02](02-state-and-dependency-injection.md) | +| **AppRoute** | A typed, serializable navigation destination (a Navigation3 `NavKey`). | [03](03-navigation.md) | +| **CodeNavigator** | The custom navigator wrapping the Navigation3 back stack. | [03](03-navigation.md) | +| **Convention plugin** | A `build-logic` Gradle plugin (`flipcash.android.*`) that standardizes a module's setup and dependencies. | [01](01-modules-and-boundaries.md) | + +> Solana/Kin lineage: the codebase descends from the Code/Kin wallet; some packages +> use the legacy `com.getcode` namespace. Current accounts and tokens are +> Solana-based. From 7f654c5ee0bc6b725716cd4e9eb0acb90cd64d31 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 00:16:23 +0000 Subject: [PATCH 5/7] docs: correct the USDF / launchpad-currency model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit USDF is the base reserve currency ("core mint"); launchpad currencies are the user-facing tradable tokens backed by USDF reserves (like memecoins backed by USDC). The docs previously framed them as parallel holdings, understating the backing relationship. - 06: add a "Currency model" section — USDF (MintMetadata with launchpadMetadata = null) vs launchpad currencies (non-null LaunchpadMetadata: liquidityPool, coreMintVault USDF backing, price/marketCap in USDF, sellFeeBps), on-chain bonding-curve price discovery, buy/sell against USDF via SwapPurpose, and the two distinct senses of "reserves" (pool backing vs the user's USDF balance). Adds a reserve/pool/token Mermaid diagram. - glossary: rewrite USDF, launchpad token, and swap; add core mint, bonding curve, liquidity pool/coreMintVault, reserves, MintMetadata/Token. - README: reframe the overview around USDF-as-reserve / launchpad-as-circulating. - features: align cash, tokens (buy/sell vs USDF), and currency-creator rows. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JRVfsXp4HDrDMy7Pbmw9fD --- .../06-payments-and-operations.md | 79 ++++++++++++++++++- docs/architecture/README.md | 16 ++-- docs/architecture/features/README.md | 6 +- docs/architecture/glossary.md | 15 ++-- 4 files changed, 100 insertions(+), 16 deletions(-) diff --git a/docs/architecture/06-payments-and-operations.md b/docs/architecture/06-payments-and-operations.md index 30e84854c..7e66b1514 100644 --- a/docs/architecture/06-payments-and-operations.md +++ b/docs/architecture/06-payments-and-operations.md @@ -1,8 +1,9 @@ # 06 — Payments & operations Flipcash is **self-custodial**: the device holds the keys, derives Solana accounts, -and signs every transaction locally. This document covers key management, the -account model, the auth-state machine, and how a payment is actually submitted. +and signs every transaction locally. This document covers the currency model, key +management, the account model, the auth-state machine, and how a payment is actually +submitted. ```mermaid graph TD @@ -18,6 +19,80 @@ graph TD User --> TxController --> Stream --> Backend ``` +## Currency model: USDF & launchpad currencies + +Flipcash has **two kinds of currency**, and the relationship between them is the +heart of the product: + +- **USDF** is the **base / reserve currency** — a USD-pegged stablecoin. In code it + is the *"core mint"*: `Mint.usdf` + ([`libs/encryption/keys/.../Mint.kt`](../../libs/encryption/keys/src/main/kotlin/com/getcode/solana/keys/Mint.kt)), + modeled as a `MintMetadata` (alias `Token`) with **`launchpadMetadata = null`**. +- **Launchpad currencies** are the **user-facing, tradable tokens** — the unit people + actually create, buy, sell, and share. Each is a custom on-chain currency **backed + by USDF reserves**. + +> **Mental model:** launchpad currencies are to USDF what memecoins are to USDC on +> Solana. USDF is the reserve everything is priced in and backed by; launchpad +> currencies are what circulates socially. + +A launchpad currency is a `MintMetadata` whose `launchpadMetadata` is **non-null** +([`services/opencode/.../model/financial/MintMetadata.kt`](../../services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/MintMetadata.kt)). +`LaunchpadMetadata` describes the backing and pricing: + +| Field | Meaning | +|-------|---------| +| `liquidityPool` | The on-chain bonding-curve pool. | +| `mintVault` | Where the launchpad token itself is locked against the pool. | +| `coreMintVault` | **Where USDF is locked against the pool — the on-chain backing/reserves.** | +| `currentCirculatingSupplyQuarks` | Circulating supply; drives price via the curve. | +| `price`, `marketCap` | Both denominated in **USDF** (`Fiat`). | +| `sellFeeBps` | Sell fee in basis points (currently 1%). | + +```mermaid +graph LR + User["User USDF balance
(spendable reserve)"] + Pool["Liquidity pool (bonding curve)"] + Core["coreMintVault
(USDF backing)"] + MintV["mintVault
(launchpad token)"] + Token["Launchpad currency
price = f(supply) in USDF"] + + User -->|buy: USDF in| Pool + Pool -->|tokens out| Token + Token -->|sell: tokens in, −1% fee| Pool + Pool -->|USDF out| User + Pool --- Core + Pool --- MintV +``` + +**Price discovery is deterministic and on-chain.** `price = f(currentSupply)` is +computed by a discrete bonding curve +([`libs/currency-math/.../curves/DiscreteBondingCurve.kt`](../../libs/currency-math/src/main/kotlin/com/flipcash/libs/currency/math/internal/curves/DiscreteBondingCurve.kt)); +supply updates stream in via `LaunchpadReserveStateSnapshot` and `TokenCoordinator`, +which recomputes balances and appreciation as the price moves. (This is separate +from the **fiat** display rates in [04 — Networking](04-networking.md), which convert +USDF to the user's local currency for display.) + +**Buying and selling always goes through USDF.** `SwapPurpose` +([`apps/flipcash/core/.../tokens/TokenSwapPurpose.kt`](../../apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tokens/TokenSwapPurpose.kt)) +is either `Buy(mint, fundingSource)` or `Sell(mint)`, and the swap's counter-currency +is `Mint.usdf`: a buy spends USDF to mint tokens (adding USDF to the pool's +`coreMintVault`), a sell burns tokens for USDF (returning USDF, minus the 1% fee). +Creating a currency (`CurrencyCreatorViewModel`) seeds an **initial USDF buy** +(default ~$5, from a user flag) and hands the creator a cash bill of the new token. + +**Sending/sharing carries any token.** A cash bill (`Bill.Cash`) holds whichever +`Token` is selected — a launchpad currency *or* USDF. Launchpad currencies render as +a custom bill (`renderAsBill = token.address != Mint.usdf`); USDF renders as plain +cash. + +> **Two senses of "reserves" — don't conflate them.** (1) The on-chain USDF locked in +> a launchpad token's `coreMintVault` is the token's **backing**. (2) +> `ReservesBalanceProvider.observeReservesBalance()` → +> `balanceForToken(Mint.usdf)` is the **user's own USDF balance**, i.e. the spendable +> reserve they buy launchpad currencies with. Both are USDF, but one is pool-side +> backing and the other is the user's wallet balance. + ## Keys & cryptography The crypto primitives live under `libs/encryption/*`: diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 803b6db56..9e0697656 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -14,12 +14,16 @@ architectural concern; start here, then jump to the topic you need. ## What Flipcash is Flipcash is a **self-custodial mobile wallet** for instant, global, private -payments. Users hold **USDF** (a USD stablecoin) and **launchpad tokens** (custom -on-chain currencies). The signature interaction is a **device-to-device cash -bill**: one phone renders an animated circular Kik Code on screen, another phone -scans it, and a peer-to-peer handshake settles the payment. The app also supports -contact/username sends, on-ramp funding (Coinbase, in-app purchase), token swaps, -and on-chain withdrawals. +payments. Its base reserve currency is **USDF** (a USD stablecoin); on top of it, +users create, buy, sell, and share **launchpad currencies** — custom on-chain tokens +**backed by USDF reserves** (think memecoins backed by USDC on Solana). Launchpad +currencies are the unit that circulates socially; USDF is what they're priced in and +redeemable for. The signature interaction is a **device-to-device cash bill** (which +can carry a launchpad currency or USDF): one phone renders an animated circular Kik +Code on screen, another phone scans it, and a peer-to-peer handshake settles the +payment. The app also supports contact/username sends, on-ramp funding (Coinbase, +in-app purchase), buying/selling launchpad currencies against USDF, and on-chain +withdrawals. Under the hood it talks to **two gRPC backends** — the Flipcash service (accounts, profiles, chat, activity) and the Open Code Protocol / OCP service diff --git a/docs/architecture/features/README.md b/docs/architecture/features/README.md index 3cfa726ba..dc426afc8 100644 --- a/docs/architecture/features/README.md +++ b/docs/architecture/features/README.md @@ -32,7 +32,7 @@ Two recurring shapes show up in the **Pattern** column: | Feature | Purpose | Screen / ViewModel | Pattern & integration | |---------|---------|--------------------|-----------------------| -| **cash** | Amount entry for a device-to-device cash bill | `cash/CashScreen.kt` · `…/internal/CashScreenViewModel` | **VM-driven**; combines token + balance + rate flows, emits `PresentBill`, calls `SessionController.showBill` | +| **cash** | Amount entry for a device-to-device cash bill (sends the selected token — a launchpad currency or USDF) | `cash/CashScreen.kt` · `…/internal/CashScreenViewModel` | **VM-driven**; combines token + balance + rate flows, emits `PresentBill`, calls `SessionController.showBill` | | **scanner** | Camera capture of a peer's cash bill / QR | `scanner/ScannerScreen.kt` | Screen-local; uses `:ui:scanner`, routes scans through `Router.classify` | | **direct-send** | Send to a contact / phone number | `directsend/SendFlowScreen.kt` · `…/internal/SendFlowViewModel` | **Flow**; contact list + phone gate steps | | **withdrawal** | Withdraw funds on-chain | `withdrawal/WithdrawalFlowScreen.kt` · `withdrawal/WithdrawalViewModel` | **Flow** (`AppRoute.Transfers.Withdrawal`); entry → destination → confirmation | @@ -44,8 +44,8 @@ Two recurring shapes show up in the **Pattern** column: | Feature | Purpose | Screen / ViewModel | Pattern & integration | |---------|---------|--------------------|-----------------------| | **balance** | Wallet balance across tokens | `balance/BalanceScreen.kt` · `…/internal/BalanceViewModel` | **VM-driven**; observes token balances + exchange rates | -| **tokens** | Token info, selection, and swaps | `tokens/TokenInfoScreen.kt`, `tokens/SwapFlowScreen.kt`, `tokens/TokenSelectScreen.kt` | **Flow** for swap (`FlowRouteWithResult`); screen-local for info/selection | -| **currency-creator** | Create a launchpad currency | `currencycreator/CurrencyCreatorFlowScreen.kt` · `…/internal/CurrencyCreatorViewModel` | **Flow**; name → icon → info → processing steps | +| **tokens** | Token info (price/market cap in USDF), selection, and buy/sell swaps against USDF | `tokens/TokenInfoScreen.kt`, `tokens/SwapFlowScreen.kt`, `tokens/TokenSelectScreen.kt` | **Flow** for swap (`FlowRouteWithResult`, `SwapPurpose.Buy`/`Sell`); screen-local for info/selection | +| **currency-creator** | Create a launchpad currency (USDF-backed), seeded by an initial USDF buy | `currencycreator/CurrencyCreatorFlowScreen.kt` · `…/internal/CurrencyCreatorViewModel` | **Flow**; name → icon → info → processing steps | ## D. Profile, settings & misc diff --git a/docs/architecture/glossary.md b/docs/architecture/glossary.md index 7d9fc0f78..ef078ecb3 100644 --- a/docs/architecture/glossary.md +++ b/docs/architecture/glossary.md @@ -8,12 +8,16 @@ full story. | Term | Meaning | See | |------|---------|-----| -| **USDF** | The USD-pegged stablecoin users primarily hold and transact in. | [README](README.md) | -| **Launchpad token / currency** | A custom on-chain currency (governed by an on-chain bonding curve) that users can create and trade, in addition to USDF. | [06](06-payments-and-operations.md) | -| **Cash bill** | The animated, circular on-screen code that represents a payment for **device-to-device** transfer — one phone shows it, another scans it. | [README](README.md), [features](features/README.md) | +| **USDF** | The USD-pegged **base / reserve** stablecoin (the "core mint"). Everything is priced in and backed by USDF; it is itself sendable. In code: a `Token` with `launchpadMetadata = null`. | [06](06-payments-and-operations.md) | +| **Launchpad token / currency** | The user-facing, tradable currency people **create, buy, sell, and share** — a custom on-chain token **backed by USDF reserves** and priced by an on-chain bonding curve. Like a memecoin backed by USDC. | [06](06-payments-and-operations.md) | +| **Core mint** | The codebase's name for USDF — the reserve currency that launchpad currencies are denominated in and backed by. | [06](06-payments-and-operations.md) | +| **Bonding curve** | The on-chain formula that sets a launchpad currency's price from its circulating supply (`price = f(supply)`, in USDF). | [06](06-payments-and-operations.md) | +| **Liquidity pool / `coreMintVault`** | A launchpad currency's bonding-curve pool; its `coreMintVault` holds the **USDF reserves** backing the token (vs `mintVault`, which holds the token). | [06](06-payments-and-operations.md) | +| **Reserves** | Two senses: (1) on-chain USDF backing a token in its `coreMintVault`; (2) the **user's own USDF balance** (`observeReservesBalance()`) spent to buy launchpad currencies. | [06](06-payments-and-operations.md) | +| **Cash bill** | The animated, circular on-screen code that represents a payment for **device-to-device** transfer — one phone shows it, another scans it. Carries any token (a launchpad currency or USDF). | [README](README.md), [features](features/README.md) | | **Kik Code** | The scannable code format rendered as a cash bill (captured via the camera scanner). | [07](07-design-system.md) | | **On-ramp** | Funding the wallet with fiat (Coinbase) or in-app purchase. | [04](04-networking.md), [06](06-payments-and-operations.md) | -| **Swap** | Exchanging one token for another (e.g. via a launchpad currency's curve). | [03](03-navigation.md), [06](06-payments-and-operations.md) | +| **Swap** | Buying or selling a launchpad currency **against USDF** (`SwapPurpose.Buy`/`Sell`); sells charge a ~1% fee. | [03](03-navigation.md), [06](06-payments-and-operations.md) | | **Withdrawal** | Moving funds on-chain to an external Solana address. | [features](features/README.md) | ## Identity, keys & accounts @@ -40,7 +44,8 @@ full story. | **OCP / Open Code Protocol** | The gRPC backend for transactions, intents, swaps, and exchange rates (module `:services:opencode`). | [04](04-networking.md) | | **Flipcash service** | The gRPC backend for accounts, profiles, chat, and activity (module `:services:flipcash`). | [04](04-networking.md) | | **Intent** | A signed unit of money movement (transfer, remote send/receive, withdraw, swap, distribution) submitted over the `SubmitIntent` bidirectional stream. | [04](04-networking.md), [06](06-payments-and-operations.md) | -| **Mint** | A Solana token mint address (`PublicKey` subtype); identifies a token such as USDF or a launchpad currency. | [06](06-payments-and-operations.md) | +| **Mint** | A Solana token mint address (`PublicKey` subtype); identifies a token such as USDF (`Mint.usdf`) or a launchpad currency. | [06](06-payments-and-operations.md) | +| **MintMetadata / Token** | The model for any currency (`MintMetadata`, aliased `Token`). A non-null `launchpadMetadata` makes it a launchpad currency; `null` means it's USDF (the core mint). | [06](06-payments-and-operations.md) | | **Protobuf / proto** | The Protocol Buffers contract; generated code lives in `:definitions:*:models` and is never hand-edited. | [13](13-protobuf-and-codegen.md) | | **NotifiableError** | Marker for errors that represent bugs (not user-caused) and should alert via Bugsnag/Slack. | [14](14-error-handling.md) | From 76b421f08968cead611b3e1bb5b9f72ff4a0adde Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 14:22:14 +0000 Subject: [PATCH 6/7] docs: fix Cash bill vs Kik Code definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These were conflated in the glossary. They are distinct: - Kik Code is the scannable, animated circular code itself — the device-to-device transport. It is surface-agnostic: currently displayed on cash bills, soon on digital gold bars, and could later back other flows such as merchant payments. - Cash bill is a digital representation of "cash" (like a US dollar), modeled as Bill.Cash; it carries a token (launchpad currency or USDF) and displays a Kik Code for the recipient to scan — one of the surfaces the code rides on. Rewrites the two glossary rows and aligns the README overview line and the features scanner row to match. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JRVfsXp4HDrDMy7Pbmw9fD --- docs/architecture/README.md | 6 +++--- docs/architecture/features/README.md | 2 +- docs/architecture/glossary.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9e0697656..1ff5a0a25 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -19,9 +19,9 @@ users create, buy, sell, and share **launchpad currencies** — custom on-chain **backed by USDF reserves** (think memecoins backed by USDC on Solana). Launchpad currencies are the unit that circulates socially; USDF is what they're priced in and redeemable for. The signature interaction is a **device-to-device cash bill** (which -can carry a launchpad currency or USDF): one phone renders an animated circular Kik -Code on screen, another phone scans it, and a peer-to-peer handshake settles the -payment. The app also supports contact/username sends, on-ramp funding (Coinbase, +can carry a launchpad currency or USDF): one phone renders a cash bill displaying an +animated circular **Kik Code** on screen, another phone scans the code, and a +peer-to-peer handshake settles the payment. The app also supports contact/username sends, on-ramp funding (Coinbase, in-app purchase), buying/selling launchpad currencies against USDF, and on-chain withdrawals. diff --git a/docs/architecture/features/README.md b/docs/architecture/features/README.md index dc426afc8..488423cbf 100644 --- a/docs/architecture/features/README.md +++ b/docs/architecture/features/README.md @@ -33,7 +33,7 @@ Two recurring shapes show up in the **Pattern** column: | Feature | Purpose | Screen / ViewModel | Pattern & integration | |---------|---------|--------------------|-----------------------| | **cash** | Amount entry for a device-to-device cash bill (sends the selected token — a launchpad currency or USDF) | `cash/CashScreen.kt` · `…/internal/CashScreenViewModel` | **VM-driven**; combines token + balance + rate flows, emits `PresentBill`, calls `SessionController.showBill` | -| **scanner** | Camera capture of a peer's cash bill / QR | `scanner/ScannerScreen.kt` | Screen-local; uses `:ui:scanner`, routes scans through `Router.classify` | +| **scanner** | Camera capture of a peer's Kik Code (e.g. on a cash bill) | `scanner/ScannerScreen.kt` | Screen-local; uses `:ui:scanner`, routes scans through `Router.classify` | | **direct-send** | Send to a contact / phone number | `directsend/SendFlowScreen.kt` · `…/internal/SendFlowViewModel` | **Flow**; contact list + phone gate steps | | **withdrawal** | Withdraw funds on-chain | `withdrawal/WithdrawalFlowScreen.kt` · `withdrawal/WithdrawalViewModel` | **Flow** (`AppRoute.Transfers.Withdrawal`); entry → destination → confirmation | | **deposit** | Add funds via on-ramp / deposit address | `deposit/DepositFlowScreen.kt` · `…/internal/DepositViewModel` | **Flow** (`FlowRouteWithResult`); USDC deposit info, Coinbase on-ramp | diff --git a/docs/architecture/glossary.md b/docs/architecture/glossary.md index ef078ecb3..8187e13fa 100644 --- a/docs/architecture/glossary.md +++ b/docs/architecture/glossary.md @@ -14,8 +14,8 @@ full story. | **Bonding curve** | The on-chain formula that sets a launchpad currency's price from its circulating supply (`price = f(supply)`, in USDF). | [06](06-payments-and-operations.md) | | **Liquidity pool / `coreMintVault`** | A launchpad currency's bonding-curve pool; its `coreMintVault` holds the **USDF reserves** backing the token (vs `mintVault`, which holds the token). | [06](06-payments-and-operations.md) | | **Reserves** | Two senses: (1) on-chain USDF backing a token in its `coreMintVault`; (2) the **user's own USDF balance** (`observeReservesBalance()`) spent to buy launchpad currencies. | [06](06-payments-and-operations.md) | -| **Cash bill** | The animated, circular on-screen code that represents a payment for **device-to-device** transfer — one phone shows it, another scans it. Carries any token (a launchpad currency or USDF). | [README](README.md), [features](features/README.md) | -| **Kik Code** | The scannable code format rendered as a cash bill (captured via the camera scanner). | [07](07-design-system.md) | +| **Cash bill** | A digital representation of "cash" — a note/denomination, like a US dollar — used for device-to-device transfers. Modeled as `Bill.Cash`; carries a token (a launchpad currency or USDF) and **displays a Kik Code** for the recipient to scan. One of the surfaces a Kik Code rides on. | [06](06-payments-and-operations.md), [features](features/README.md) | +| **Kik Code** | The scannable, animated **circular code** itself — the device-to-device transfer transport. It is **surface-agnostic**: today it's displayed on cash bills, soon on digital gold bars, and could later back other flows such as merchant payments. Captured via the camera scanner (`:ui:scanner`). | [07](07-design-system.md) | | **On-ramp** | Funding the wallet with fiat (Coinbase) or in-app purchase. | [04](04-networking.md), [06](06-payments-and-operations.md) | | **Swap** | Buying or selling a launchpad currency **against USDF** (`SwapPurpose.Buy`/`Sell`); sells charge a ~1% fee. | [03](03-navigation.md), [06](06-payments-and-operations.md) | | **Withdrawal** | Moving funds on-chain to an external Solana address. | [features](features/README.md) | From 6e5416a33bd7a54cdf6a0af2a642f05b496698ea Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 14:24:14 +0000 Subject: [PATCH 7/7] docs: drop "outdated CLAUDE.md" callouts now that CLAUDE.md is fixed CLAUDE.md was corrected earlier on this branch (navigation, persistence encryption, async, API keys), so the notes claiming this suite contradicts "older notes / stale references" no longer apply. Reframe them as plain factual statements: - README: remove the "contradicts older notes in CLAUDE.md" paragraph; keep the "code is authoritative" guidance. - 03/05/08: state the facts (Navigation 3 not Voyager; Room not SQLCipher; Coroutines + Flow, no RxJava) without the "are stale" clauses. Remaining CLAUDE.md references are plain pointers and left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JRVfsXp4HDrDMy7Pbmw9fD --- docs/architecture/03-navigation.md | 2 +- docs/architecture/05-persistence.md | 5 ++--- docs/architecture/08-cross-cutting-concerns.md | 3 +-- docs/architecture/README.md | 4 ---- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/architecture/03-navigation.md b/docs/architecture/03-navigation.md index 3d88d20d0..38f5f69fc 100644 --- a/docs/architecture/03-navigation.md +++ b/docs/architecture/03-navigation.md @@ -2,7 +2,7 @@ > **Note on stacks:** Flipcash navigates with **Jetpack Navigation 3** > (`androidx.navigation3`) wrapped by a custom `CodeNavigator`. There is **no -> Voyager** dependency in the project today — older references to Voyager are stale. +> Voyager** dependency in the project. Navigation has three moving parts: a **typed route graph** (`AppRoute`), a **navigator** that drives a Navigation3 back stack (`CodeNavigator` + diff --git a/docs/architecture/05-persistence.md b/docs/architecture/05-persistence.md index 5d352adf6..3c39cc9ba 100644 --- a/docs/architecture/05-persistence.md +++ b/docs/architecture/05-persistence.md @@ -6,10 +6,9 @@ key/value preferences and a **Paging 3 `RemoteMediator`** layer to back paged li shared sub-modules so storage details don't leak into features. > **Note on encryption:** the Flipcash Room database is **not** wrapped with -> SQLCipher today (there is no `SupportFactory` / `net.zetetic` in +> SQLCipher (there is no `SupportFactory` / `net.zetetic` in > `apps/flipcash/shared/persistence`). Instead, **each account gets its own -> database file**, named from the account entropy. Older notes describing a -> SQLCipher-encrypted Room DB are stale. +> database file**, named from the account entropy. ```mermaid graph TD diff --git a/docs/architecture/08-cross-cutting-concerns.md b/docs/architecture/08-cross-cutting-concerns.md index 1a271f8e0..802a61be3 100644 --- a/docs/architecture/08-cross-cutting-concerns.md +++ b/docs/architecture/08-cross-cutting-concerns.md @@ -5,8 +5,7 @@ biometrics, and the async model. These are centralized so features get them "for free" without each one reinventing instrumentation. > **Note on async:** the app is effectively **pure Kotlin Coroutines + Flow**. -> There is no meaningful RxJava usage in app source today; older notes describing -> "RxJava 3 coexisting" are stale. +> There is no meaningful RxJava usage in app source. ```mermaid graph TD diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1ff5a0a25..e068f0544 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -6,10 +6,6 @@ architectural concern; start here, then jump to the topic you need. > **These docs describe structure and intent, not every line of code.** If the > documentation and the code disagree, **the code is authoritative** — fix the > docs. When you change a subsystem, update its document in the same commit. -> -> Where this suite contradicts older notes in `CLAUDE.md` (navigation, persistence -> encryption, RxJava), it does so deliberately: these documents reflect the code as -> it exists today. See the per-topic docs for details. ## What Flipcash is