Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
25 changes: 23 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,13 @@ secrets/*
.env.local
!.env.local.example

# Fork identity — committed for real (org.mifospay): non-secret app/org/store metadata,
# no API keys or credentials. See gradle/fork.properties.template for the neutral schema.
# Fork identity — filled-in values. Committed schema: gradle/fork.properties.template
gradle/fork.properties

# Resolver-generated secret path map (build_secrets.rb#export-paths). Env-dependent
# (live-wins-else-sample), regenerated by CI / `/secrets pull` — never committed. The
# Gradle build reads it instead of shelling out to the resolver at configuration time.
gradle/secrets-paths.properties
PROJECT_SETUP_INFO.txt

# iOS Secrets - NEVER COMMIT THESE!
Expand All @@ -111,6 +116,16 @@ fastlane/screenshots/
# The HTML files ARE committed (they're the source); only the rendered PNGs are transient.
deployment/*/metadata/**/*.png
deployment/*/metadata/**/screenshots/**/*.png

# Store-listing text + declarations + rendered media — DERIVED from app-profile/ by syncForkConfig
# (customization-surface owner:generated). app-profile/ is the SINGLE source of truth; the Fastfile
# `before_all` regenerates these from the SoT before every deploy, so they are NOT committed here.
# `**` covers all metadata roots (android/metadata, ios/appstore/metadata, desktop/mac-app-store/metadata,
# windows/metadata, linux/metadata). READMEs (*.md) stay tracked as docs.
deployment/**/metadata/**/*.txt
deployment/**/metadata/**/*.json
deployment/**/metadata/**/*.csv
deployment/**/metadata/**/*.png
# Ruby stuff we don't care about
.bundle/
vendor/
Expand Down Expand Up @@ -143,4 +158,10 @@ deployment/fastlane/**/report.xml
# *.keystore / *.key rules above would ignore them. (Real secrets live in gitignored secrets/live/.)
!secrets/sample/android/keystores/upload_keystore.keystore
!secrets/sample/desktop/linux/gpg_signing.key

# OG social images — DERIVED from app-profile/platforms/web/media/og-images (owner:generated)
deployment/**/og-images/**

# Per-machine store-listing drift cache — records the last-pushed listing hash per platform so
# deploy lanes skip re-uploading an unchanged listing. Derived/transient, never committed.
deployment/fastlane/.listing_sync_state.json
17 changes: 5 additions & 12 deletions .template-version
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
# .template-version — the openMF/kmp-project-template state this working tree corresponds to.
#
# In the TEMPLATE repo (openMF/kmp-project-template) this is the release baseline the tree
# corresponds to. In a FORK it is the template commit the fork last synced FROM — updated
# automatically by scripts/white-label/sync-dirs.sh on every /kmp-project-template-sync.
#
# DO NOT hand-edit. It lets a fork tell whether it is behind the template (compare template_sha
# against the template's latest release) and anchors the next sync's 3-way merge base.
#
# Format: bash-parseable `key=value` (same convention as gradle/fork.properties).
# .template-version — the openMF/kmp-project-template commit this tree last synced from.
# Managed by scripts/white-label/sync-dirs.sh — DO NOT hand-edit.
# Format: bash-parseable key=value (same convention as gradle/fork.properties).
template_repo=openMF/kmp-project-template
template_ref=dev
template_sha=7f38d2be7cc0d5a0bd194139406ede8628558c48
template_sha=6fe88bade82f36c5b8ab0b66569087ea6e340ef2
template_tag=
synced_at=2026-08-15T00:00:00Z
synced_at=2026-08-19T18:21:27Z
10 changes: 7 additions & 3 deletions build-logic/convention/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ dependencies {
compileOnly(libs.ktlint.gradlePlugin)
compileOnly(libs.spotless.gradle)
implementation(libs.truth)
// SnakeYAML — lets SyncForkConfigPlugin parse app-profile/app.yaml + platforms/**/*.yaml
// (the fork-owned white-label SoT). Not in the version catalog; literal coordinate.
implementation("org.yaml:snakeyaml:2.2")
compileOnly(libs.androidx.room.gradle.plugin)
compileOnly(libs.firebase.crashlytics.gradlePlugin)
compileOnly(libs.firebase.performance.gradlePlugin)
Expand All @@ -38,9 +41,10 @@ dependencies {
// META-INF/gradle-plugins descriptor is on the build-logic runtime classpath,
// letting WorkerComposeConventionPlugin apply it via pluginManager.apply(id).
implementation(libs.worker.app.plugin)
// SnakeYAML — lets SyncForkConfigPlugin parse app-profile/app.yaml + platforms/**/*.yaml
// (the fork-owned white-label SoT). Not in the version catalog; literal coordinate.
implementation("org.yaml:snakeyaml:2.2")
// roborazzi gradle plugin — `implementation` so its plugin descriptor is on the build-logic
// runtime classpath, letting CMPFeatureConventionPlugin apply it via pluginManager.apply(id)
// to give every feature module the device-free verifyRoborazziDesktop render tier.
implementation(libs.roborazzi.gradlePlugin)
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class CMPFeatureConventionPlugin : Plugin<Project> {
apply("org.jetbrains.compose")
apply("org.convention.detekt.plugin")
apply("org.convention.spotless.plugin")
// Device-free CMP render tier (SCREENSHOT_TEST.md CMP-PRIMARY): renders commonMain
// @Preview from desktopTest (JVM, no emulator/Robolectric) → verifyRoborazziDesktop.
apply("io.github.takahirom.roborazzi")
}

// Compose Multiplatform UI-test infra (RULE-KMP-COMPOSE-UITEST-001):
Expand All @@ -40,13 +43,20 @@ class CMPFeatureConventionPlugin : Plugin<Project> {
"org.jetbrains.compose.ui:ui-test-junit4:$composeVersion",
)

// Device-free render tier deps — desktopTest ONLY (JVM-capable). roborazzi-compose-desktop
// has no JS/Wasm variant, so it MUST NOT go in the js/wasm-shared commonTest. The plain
// `roborazzi` AAR is never added here (no JVM variant → breaks desktopTest classpath).
add("desktopTestImplementation", libs.findLibrary("roborazzi-composeDesktop").get())
add("desktopTestImplementation", libs.findLibrary("composablePreviewScanner-common").get())
add("desktopTestImplementation", libs.findLibrary("composablePreviewScanner-jvm").get())

add("commonMainImplementation", project(":core:ui"))
add("commonMainImplementation", project(":core-base:ui"))
add("commonMainImplementation", project(":core:designsystem"))
add("commonMainImplementation", project(":core-base:designsystem"))
add("commonMainImplementation", project(":core:data"))
add("commonMainImplementation", project(":core-base:designsystem"))
add("commonMainImplementation", project(":core:analytics"))
add("commonMainImplementation", project(":core:firebase"))

add("commonMainImplementation", libs.findLibrary("koin.compose").get())
add("commonMainImplementation", libs.findLibrary("koin.compose.viewmodel").get())
Expand Down
16 changes: 12 additions & 4 deletions build-logic/convention/src/main/kotlin/SyncForkConfigPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,21 @@ abstract class SyncForkConfigTask : DefaultTask() {
val demoPassword = get("demo.password")
val logTag = get("log.tag")

// ── 2b. Write app.id BACK into gradle/libs.versions.toml#appId ─────────
// The whole build reads the bundle id via libs.versions.appId; fork.properties#app.id is the
// authored SoT. Sync the catalog from it so a fork edits app.id in ONE place. No-op when they
// already agree (e.g. the upstream template). product-health's appid-consistency check guards drift.
// ── 2b. Write app.id + app display name BACK into gradle/libs.versions.toml ─────────
// The whole build reads the bundle id via libs.versions.appId AND the app name via
// libs.versions.appDisplayName (Android resValue app_name / iOS CFBundleDisplayName). Both are
// resolved app-profile-first (identity.app_id / identity.app_name — priority ENV > app-profile >
// fork.properties > TOML), so sync BOTH back into the catalog: a fork edits identity in ONE place
// (app-profile/app.yaml) and the catalog follows. No-op when they already agree (e.g. the upstream
// template). Without the appDisplayName write the catalog drifts — a fork renamed in app.yaml would
// still INSTALL under the old name while the store listing showed the new one. appid-consistency
// guards the appId drift.
if (appId.isNotBlank()) {
patchTomlVersion(File(root, "gradle/libs.versions.toml"), "appId", appId)
}
if (appDisplayName.isNotBlank()) {
patchTomlVersion(File(root, "gradle/libs.versions.toml"), "appDisplayName", appDisplayName)
}

// Apple
val appleTeamId = get("apple.team.id", "APPLE_TEAM_ID", "iosTeamId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal fun Project.configureDetekt(extension: DetektExtension) = extension.app
tasks.named<Detekt>("detekt") {
mustRunAfter(":cmp-android:dependencyGuard")
jvmTarget = "17"
baseline.set(rootDir.resolve("config/detekt/baseline.xml"))
source(files(rootDir))
include("**/*.kt")
exclude("**/*.kts")
Expand Down
65 changes: 17 additions & 48 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,42 +15,40 @@ buildscript {
classpath(libs.google.oss.licenses.plugin) {
exclude(group = "com.google.protobuf")
}
// Pin R8 to a version that understands Kotlin 2.3 metadata. The R8 bundled
// with AGP 8.12.3 reads up to Kotlin metadata 2.1 only, so every release-mode
// build with Kotlin 2.3.20 emits "R8: An error occurred when parsing kotlin
// metadata" warnings for almost every class. Override it with R8 9.1.x stable.
// Compatibility matrix: https://developer.android.com/studio/build/kotlin-d8-r8-versions
classpath("com.android.tools:r8:9.1.31")
// No manual R8 override: AGP 9.3.x bundles a matched R8 (9.3+/9.4 line) that
// reads this repo's Kotlin 2.4 class metadata natively. The former
// `com.android.tools:r8:9.1.31` pin dated to the AGP 8.12.3 / Kotlin 2.3.x era
// (old bundled R8 topped out at Kotlin metadata 2.1) and is obsolete — pinning an
// older R8 under a newer AGP only risks a version-skew failure. Let AGP own R8.
}
}

plugins {
alias(libs.plugins.kotlinCocoapods) apply false
// SKIE — declared on the root classpath (apply false) so cmp-shared can apply it
// for the Swift-enhanced XCFramework export (E6). Replaces the removed Kotlin
// CocoaPods plugin (`libs.plugins.kotlinCocoapods`).
alias(libs.plugins.skie) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.dependencyGuard) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.gms) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.secrets) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.module.graph) apply true
alias(libs.plugins.detekt) apply false
alias(libs.plugins.spotless) apply false
alias(libs.plugins.version.catalog.linter) apply true
alias(libs.plugins.moduleGraph) apply true
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.gms) apply false
alias(libs.plugins.roborazzi) apply false
// Multiplatform plugins
alias(libs.plugins.jetbrainsCompose) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.wire) apply false
// Template renamed the ktorfit plugin alias to `ktrofit`; the merged catalog
// also exposes `ktorfit` so the fork's original accessor works too.
alias(libs.plugins.ktrofit) apply false

alias(libs.plugins.room) apply false
Expand Down Expand Up @@ -78,6 +76,7 @@ plugins {
id("org.convention.fork.sync-config")
}


object DynamicVersion {
fun setDynamicVersion(file: File, version: String) {
val cleanedVersion = version.split('+')[0]
Expand All @@ -101,8 +100,8 @@ tasks.register("printModulePaths") {
}
}

// Force consistent versions across all subprojects to fix KLIB resolver duplicate warnings.
// The conflict is between org.jetbrains.androidx.* (CMP) and androidx.* (Google) transitive deps.
// Force consistent versions across all subprojects to fix KLIB resolver duplicate warnings
// The conflict is between org.jetbrains.androidx.* (CMP) and androidx.* (Google) transitive deps
subprojects {
configurations.all {
resolutionStrategy.eachDependency {
Expand Down Expand Up @@ -143,33 +142,3 @@ moduleGraphAssert {
configurations += setOf("wasmJsMainImplementation", "wasmJsMainApi")
}

// ── Detekt baseline (fork) ────────────────────────────────────────────────────────────────────
// The offline-first template migration adopted the template's stricter detekt config
// (detekt-formatting + twitter-detekt-compose), surfacing ~544 pre-existing violations across the
// fork's existing code (MaxLineLength, ComposableParamOrder, ViewModelForwarding,
// CyclomaticComplexMethod, ModifierReused, LongMethod, …). A committed baseline accepts the CURRENT
// set so CI is green while NEW violations still fail — the standard detekt "adopt-on-existing-
// codebase" mechanism. Wired HERE (not in the template-mirrored build-logic/.../Detekt.kt) so that
// convention file stays canonical. Regenerate after fixing violations: `./gradlew detektBaseline`.
allprojects {
tasks.matching { it.name == "detekt" }.configureEach {
(this as? io.gitlab.arturbosch.detekt.Detekt)?.baseline?.set(
rootProject.file("config/detekt/baseline.xml"),
)
}
tasks.matching { it.name == "detektBaseline" }.configureEach {
(this as? io.gitlab.arturbosch.detekt.DetektCreateBaselineTask)?.apply {
setSource(rootProject.files(rootProject.rootDir))
baseline.set(rootProject.file("config/detekt/baseline.xml"))
include("**/*.kt")
exclude("**/*.kts")
exclude("**/resources/**")
exclude("**/build/**")
exclude("**/generated/**")
exclude("**/build-logic/**")
exclude("**/spotless/**")
exclude("core-base/designsystem/**")
exclude("feature/home/**")
}
}
}
42 changes: 42 additions & 0 deletions cmp-android/CONSUMPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Consuming `cmp-android` in a fork

`cmp-android` is the Android app-shell — `AndroidApp` (the `Application`) and `MainActivity`. Both
are thin: they call into `cmp-shared`'s `SharedApp`, which owns the actual UI/DI. A fork rarely
edits these files directly.

## What's here

- **`AndroidApp`** (`app/AndroidApp.kt`) — calls `initKoin { androidContext(this); androidLogger() }`
on `onCreate`, restores the user's saved language (`UserDataRepository.userData.appLanguage`) to
`AppCompatDelegate` before any `Activity` is created, and implements
`SingletonImageLoader.Factory` for a disk-cached Coil `ImageLoader`.
- **`MainActivity`** (`app/MainActivity.kt`) — installs the splash screen, wires
`AppUpdateManagerImpl`, `ShareUtils.setActivityProvider`, `FileKit.init`, and
`AnalyticsHelper`/`AppLifecycleTracker`, then calls `SharedApp` with the platform callbacks
(`updateScreenCapture`, `handleRecreate`, `handleThemeMode` → `AppCompatDelegate`,
`handleAppLocale` → `AppCompatDelegate` + `Locale.setDefault`, `onSplashScreenRemoved`).
- **`BuildConfigUtils`**, **`ComponentActivityExtensions`**, **`ConfigurationExtension`**,
**`AppThemeExtensions`** — small platform helpers these two classes lean on.

## What a fork touches

- **Identity / signing** — `applicationId`, versioning, and the release `signingConfigs` all read
from `app-profile/` via the `appId`/`appDisplayName` version catalog entries and
`resolveSecretPath("upload_keystore")` (see `build.gradle.kts`). Set identity in
`app-profile/app.yaml`, run `./gradlew syncForkConfig`, and manage the keystore via
`/secrets pull` — never hand-edit `build.gradle.kts` signing config.
- **Screen-capture policy** — `MainActivity.updateScreenCapture` defaults `FLAG_SECURE` on for
release builds; adjust the `BuildConfig.DEBUG` bypass if your fork's threat model differs.
- **App behavior/UI** — add features via the `cmp-navigation` registries (`FeatureRegistry`,
`BackboneRegistry`, `TabRegistry`, `AppInitializers`), not by editing `AndroidApp`/`MainActivity`.

## What to leave to sync

`AndroidApp`/`MainActivity`'s Koin bootstrap, splash-screen wiring, and `SharedApp` call are
template infrastructure — a template sync full-copies this module's shell files while your
`app-profile/` identity and registry registrations survive untouched.

See [`README.md`](README.md) for the module graph, and `cmp-navigation`'s
[`CONSUMPTION.md`](../cmp-navigation/CONSUMPTION.md) for how to add features.

Symbols: AndroidApp, MainActivity
6 changes: 3 additions & 3 deletions cmp-android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ graph LR
:core:data["data"]
:core:ui["ui"]
end
:mifospay-android --> :mifospay-shared
:mifospay-android --> :core:data
:mifospay-android --> :core:ui
:cmp-android --> :cmp-shared
:cmp-android --> :core:data
:cmp-android --> :core:ui
```
Loading
Loading