Skip to content

chore: add missing files from kmp-project-template#1951

Draft
biplab1 wants to merge 6 commits intoopenMF:developmentfrom
biplab1:MW-328-add-missing-template-modules-files
Draft

chore: add missing files from kmp-project-template#1951
biplab1 wants to merge 6 commits intoopenMF:developmentfrom
biplab1:MW-328-add-missing-template-modules-files

Conversation

@biplab1
Copy link
Contributor

@biplab1 biplab1 commented Jan 5, 2026

Issue Fix

Jira Task: MW-328

Screenshots

Description

Summary of changes:

  • Added files from kmp-project-template that have zero or least changes compared to MifosPay

  • Apply the AndroidStyle.xml style template to your code in Android Studio.

  • Run the unit tests with ./gradlew check to make sure you didn't break anything

  • If you have multiple commits please combine them into one commit by squashing them.

Summary by CodeRabbit

  • New Features

    • Added comprehensive navigation system with splash screen, authenticated navigation, and responsive bottom/rail navigation bars.
    • Introduced desktop and web app support with platform-specific implementations.
    • Added user utility functions: open URLs, compose emails, initiate calls/SMS, copy text, and share content.
    • Implemented robust HTTP client with configurable authentication and error handling.
  • Chores

    • Enhanced build infrastructure with keystore management, code quality tools, and git hooks.
    • Reorganized network monitoring architecture.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

📝 Walkthrough

Walkthrough

This pull request introduces comprehensive keystore management infrastructure, multi-platform HTTP client configuration, a navigation system for Compose-based mobile apps, shared UI components for bottom navigation and scaffolding, and authentication state modeling. It includes new Gradle convention plugins, reorganizes network monitoring code into a repository package, and adds extensive test suites for keystore operations.

Changes

Cohort / File(s) Summary
GitHub Actions Workflow
.github/workflows/cache-cleanup.yaml
Adds reusable workflow for cache cleanup on PR close and manual dispatch, with conditional inputs based on event type and private repository status.
Gradle Convention Plugins - Core
build-logic/convention/src/main/kotlin/ *ConventionPlugin.kt
Introduces plugins for Android application flavors, Detekt, Git hooks, Ktlint, Spotless code formatting, and KMPRoom. Each plugin applies and configures corresponding Gradle/build tools.
Gradle Convention Plugins - Advanced
build-logic/convention/src/main/kotlin/ KeystoreManagementConventionPlugin.kt, AndroidApplicationComposeConventionPlugin.kt, AndroidApplicationConventionPlugin.kt, KMPKoinConventionPlugin.kt
Implements plugins for keystore management task registration, Compose configuration, and Koin DI integration with extension objects and task wiring.
Gradle Build Configuration
build-logic/convention/build.gradle.kts, cmp-web/build.gradle.kts, cmp-navigation/build.gradle.kts, cmp-shared/build.gradle.kts, settings.gradle.kts, gradle/libs.versions.toml
Updates dependencies for keystore management (Jackson, GitHub API, Commons Codec, JUnit Jupiter), adds new modules (cmp-navigation), and configures test framework.
Keystore Management - Core
build-logic/convention/src/main/kotlin/org/convention/keystore/ BaseKeystoreTask.kt, KeystoreConfig.kt, SecretsConfig.kt, KeystoreLogger.kt
Establishes base task infrastructure, configuration data classes, secrets file handling, and logging utilities for keystore operations. Provides defaults, validation, and backward compatibility.
Keystore Management - Tasks
build-logic/convention/src/main/kotlin/org/convention/keystore/ KeystoreGenerationTask.kt, ConfigurationFileUpdatesTask.kt, SecretsEnvUpdateTask.kt
Implements three main Gradle tasks: keystore generation via keytool, updating fastlane/Gradle config files, and base64-encoding keystores into secrets.env with backup/merge support.
Keystore Management - Utilities
build-logic/convention/src/main/kotlin/org/convention/keystore/ SecretsEnvParser.kt, ConfigurationValidator.kt, EnvironmentOverrideHandler.kt
Provides secrets.env parsing with heredoc support, comprehensive validation (passwords, key sizes, DN fields), and environment variable override logic with CI/CD detection.
Keystore Management - Tests
build-logic/convention/src/test/kotlin/org/convention/keystore/ *Test.kt
Comprehensive test suites for keystore generation, configuration updates, secrets parsing, and environment overrides using JUnit 5, TempDir isolation, and detailed assertions.
Convention Plugins - Build Configuration
build-logic/convention/src/main/kotlin/org/convention/ AndroidCompose.kt, AndroidInstrumentedTests.kt, AppBuildType.kt, AppFlavor.kt, Badging.kt, Detekt.kt, GradleManagedDevices.kt, HierarchyTemplate.kt, Jacoco.kt
Defines Android Compose setup, APK badging tasks, multi-variant testing, build type/flavor enums, code coverage configuration, and managed device groups for CI/CD.
Convention Plugins - Kotlin Configuration
build-logic/convention/src/main/kotlin/org/convention/ KotlinAndroid.kt, KotlinMultiplatform.kt, Spotless.kt, Jacoco.kt, PrintTestApks.kt, ProjectExtensions.kt
Configures Kotlin compiler options (JVM 17, desugaring), multiplatform targets (Android/iOS/JS/WASM/JVM), code formatting rules, and project-level extensions for version catalog and plugin access.
Network - HTTP Client (Multi-Platform)
core-base/network/src/commonMain/kotlin/template/core/base/network/ KtorHttpClient.kt, NetworkError.kt, NetworkResult.kt, factory/ResultSuspendConverterFactory.kt
Establishes expect/actual HTTP client factory with platform-specific implementations (Android/Desktop/JS/Native/WasmJs), defines NetworkResult sealed interface for type-safe error handling, and Ktorfit converter for automatic response wrapping.
Network - HTTP Client Implementations
core-base/network/src/*Main/kotlin/template/core/base/network/KtorHttpClient.*.kt
Platform-specific HTTP client factories using OkHttp (Android/Desktop), Darwin (Native), Js (JS/WasmJs engines) with configuration DSL support.
Core Model & UI - Authentication & Navigation
core/model/src/commonMain/kotlin/org/mifospay/core/model/AuthState.kt, core/ui/src/commonMain/kotlin/org/mifospay/core/ui/NavigationItem.kt
Introduces sealed AuthState class (Uninitialized, Unauthenticated, Authenticated) and NavigationItem interface defining icons, labels, routes for navigation UI.
Core UI - Navigation Components
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/ KptBottomBar.kt, KptNavigationBarItem.kt, KptNavigationRail.kt, KptNavigationRailItem.kt
Reusable Compose components for bottom navigation bar and vertical navigation rail with theme integration, selection state, and click handling.
Core UI - Scaffold & Pull-to-Refresh
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/ KptScaffold.kt, KptPullToRefreshState.kt
Provides three KptScaffold overloads with integrated top/bottom bars, FAB positioning, snackbar support, and pull-to-refresh state management via composable factory.
Share Utilities - Platform-Specific
core-base/ui/src/*/kotlin/template/core/base/ui/ShareUtils.*.kt
Adds platform implementations for openUrl, openAppInfo, callPhone, sendEmail, sendViaSMS, and copyText using Android intents, Desktop URIs, iOS UIApplication, and JS/Native stubs.
CMP Navigation System
cmp-navigation/src/commonMain/kotlin/cmp/navigation/ rootnav/, authenticatednavbar/, splash/, ui/, utils/
Establishes complete navigation architecture: RootNavScreen state machine, AuthenticatedNavbar with network offline detection, Splash screen, KptRootScaffold with rail/bar switching, and utility extensions for serialization-based routing.
CMP Navigation - View Models
cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavViewModel.kt, authenticatednavbar/AuthenticatedNavbarNavigationViewModel.kt
MVVM layer for root navigation state (Auth/Onboarding/Splash/UserLocked/UserUnlocked) and authenticated navbar (Home/Profile tabs with offline indicator).
CMP Navigation - Desktop/Non-Android
cmp-navigation/src/*Main/kotlin/cmp/navigation/rootnav/RootNavScreen.*.kt
Platform-specific implementations of ClearFocus (Android uses activity focus, non-Android is no-op).
CMP Shared App
cmp-shared/src/commonMain/kotlin/cmp/shared/ SharedApp.kt, utils/KoinExt.kt, ViewController.kt
Establishes shared app entry point composable with context providers, Koin initialization, and native iOS view controller bridge for embedding SharedApp.
Network Monitoring Relocation
Multiple files importing/using NetworkMonitor
Moves NetworkMonitor from core.data.util to core.data.repository package across all platform modules and DI configuration.
Android Manifest Updates
cmp-android/src/main/AndroidManifest.xml, cmp-android/src/prod/AndroidManifest.xml, cmp-android/prodRelease-badging.txt
Removes CAMERA and storage permissions, adds Firebase analytics/Crashlytics metadata, and updates version from 2025.10.5-beta.0.4 to 2026.1.1-beta.0.6.
Documentation & Configuration
core-base/ui/README.md
Updates ImageLoader API usage, removing context parameter requirement from rememberImageLoader().
Dependency Trees
cmp-android/dependencies/prodReleaseRuntimeClasspath.txt, cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt
Reflects kotlinx-serialization-json 1.8.1 consolidation and additions of Play services, Calf permissions, FileKit, and Compose dependencies.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant App as SharedApp
    participant RootNav as RootNavScreen
    participant UserRepo as UserDataRepository
    participant RootVM as RootNavViewModel
    participant AuthNav as Auth/Onboarding/etc
    
    User->>App: Launch App
    App->>App: initKoin()
    App->>RootNav: Create RootNavScreen
    RootNav->>RootVM: Initialize ViewModel
    RootVM->>UserRepo: Observe userData
    UserRepo-->>RootVM: Emit user state
    RootVM->>RootVM: Map to RootNavState
    alt First Time User
        RootVM->>RootNav: ShowOnboarding
        RootNav->>AuthNav: Navigate to Onboarding
    else Not Authenticated
        RootVM->>RootNav: Auth
        RootNav->>AuthNav: Navigate to Auth
    else Passcode Empty
        RootVM->>RootNav: UserLocked
        RootNav->>AuthNav: Navigate to Passcode
    else Unlocked
        RootVM->>RootNav: UserUnlocked(userId)
        RootNav->>AuthNav: Navigate to AuthenticatedGraph
    end
    AuthNav-->>User: Render Screen
Loading
sequenceDiagram
    participant Task as KeystoreGenerationTask
    participant Validator as ConfigurationValidator
    participant Keytool as keytool (CLI)
    participant Config as KeystoreConfig
    participant Secrets as SecretsEnvUpdateTask
    
    Task->>Validator: validateConfiguration()
    Validator-->>Task: isValid
    Task->>Config: Build keytool command
    Task->>Keytool: Execute keytool
    Keytool-->>Task: Keystore files created
    Task->>Secrets: Pass keystorePath
    Secrets->>Secrets: Encode keystores to Base64
    Secrets->>Secrets: Parse existing secrets.env
    Secrets->>Secrets: Merge with Base64 data
    Secrets->>Secrets: Write updated secrets.env
    Secrets-->>Task: secrets.env updated
    Task-->>Task: Print summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • HekmatullahAmin

Poem

🐰 Hop! Hop! The keystores dance,
With secrets encrypted in advance,
Navigation flows like carrots so bright,
Multi-platform apps take flight!
From Gradle to CI, all systems aligned,
A rabbit's delight to see progress designed! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'chore: add missing files from kmp-project-template' directly describes the main change in the changeset, which consists of adding numerous template and build-logic files from a KMP project template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@biplab1 biplab1 changed the title chore: Add missing template modules to prevent sync workflow diffs chore: adding missing files from kmp-project-template Jan 5, 2026
@biplab1 biplab1 changed the title chore: adding missing files from kmp-project-template chore: add missing files from kmp-project-template Jan 5, 2026
@biplab1 biplab1 force-pushed the MW-328-add-missing-template-modules-files branch from 010c032 to 4bc40f4 Compare January 6, 2026 14:44
@biplab1 biplab1 marked this pull request as ready for review January 6, 2026 15:05
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🤖 Fix all issues with AI Agents
In @build-logic/convention/src/main/kotlin/GitHooksConventionPlugin.kt:
- Around line 36-40: The Windows branch is using an incorrect attrib command to
set executable bits; instead, make the permission-setting step only run on Unix
by removing or replacing the else branch that calls commandLine("cmd", "/c",
"attrib", ...). Update the isLinuxOrMacOs() conditional so that the Exec task
invokes commandLine("chmod", "-R", "+x", ".git/hooks/") only when true and
otherwise either do nothing or perform a harmless verification (e.g., check/copy
files exist), removing the incorrect attrib call; adjust the Exec task
configuration where commandLine(...) is used to reflect this change.
- Around line 49-55: The current project.afterEvaluate block makes many build
tasks (including clean) always depend on installGitHooks; change this to only
wire installGitHooks to preBuild (remove "clean" and the broad list) and/or make
the installGitHooks task itself incremental by adding an up-to-date check
(compare existing hooks vs desired content or a marker file) so it no-ops when
nothing changed; update the matching/configureEach invocation to reference only
"preBuild" (or make the dependency opt-in via a project property) and implement
the up-to-date logic inside the installGitHooks task implementation to avoid
re-installing hooks on every build or in CI.
- Line 38: In GitHooksConventionPlugin (locate the occurrence of the stray
"}else {" token), fix the spacing around the else keyword by replacing "}else {"
with "} else {" so the else has a leading space after the closing brace and
proper spacing before the opening brace.

In
@build-logic/convention/src/main/kotlin/KeystoreManagementConventionPlugin.kt:
- Around line 40-43: The code eagerly realizes the generation task by calling
generateKeystoresTask.get() inside the registration of updateSecretsEnv; change
this to wire the dependency lazily by passing the TaskProvider (or a mapped
Provider) instead of calling .get() — e.g., remove .get() and use
generateKeystoresTask or generateKeystoresTask.flatMap { ... } (and likewise
avoid forcing keystoreExtension.secretsConfig.get() if present) when calling
SecretsEnvUpdateTask.createFromKeystoreGeneration so the updateSecretsEnv task
remains registered without realizing the generateKeystoresTask during
configuration.
- Around line 24-31: The task registration eagerly calls
keystoreExtension.secretsConfig.get() via
KeystoreGenerationTask.createWithSecretsConfig(this,
keystoreExtension.secretsConfig.get()), which forces provider evaluation during
configuration; instead remove that .get() call and either (a) wire the task
properties lazily by passing the Provider into the task (use
keystoreConfig.set(keystoreExtension.keystoreConfig) and
secretsConfig.set(keystoreExtension.secretsConfig) as you already do) and move
the createWithSecretsConfig logic into KeystoreGenerationTask's @TaskAction so
it reads secretsConfig.get() at execution time, or (b) defer execution with
generateKeystoresTask.configure { it.doFirst {
KeystoreGenerationTask.createWithSecretsConfig(it,
keystoreExtension.secretsConfig.get()) } } so the provider is resolved at
runtime; ensure all references use the task property names (keystoreConfig,
secretsConfig) and remove any direct .get() in the configuration block.

In @build-logic/convention/src/main/kotlin/org/convention/AndroidCompose.kt:
- Around line 46-61: The helper relativeToRootProject is using flatMap and
captures the provider value `it` but never uses it, so replace flatMap with map
(or otherwise stop capturing `it`) and compute the directory directly: change
Provider<*>.relativeToRootProject(dir: String) to use map {
rootProject.layout.buildDirectory.dir(projectDir.toRelativeString(rootDir)).dir(dir)
} (or document that the input provider is intentionally discarded), and remove
the unused `it` parameter to avoid the misleading capture.

In @build-logic/convention/src/main/kotlin/org/convention/Badging.kt:
- Around line 116-118: Replace the artifact reference used when wiring APK
outputs: change the call that sets apk via
variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE) to use SingleArtifact.APK
instead so apk.set(...) points to regular APK outputs; update the reference in
the apk.set(...) line where variant.artifacts.get(...) is used to use
SingleArtifact.APK in place of SingleArtifact.APK_FROM_BUNDLE.
- Around line 30-36: Finish the incomplete KDoc sentence in Badging.kt to
explain the incremental behavior of the task: update the trailing sentence so it
reads something like "This task is also incremental, meaning that if only some
inputs have changed, the task will run only for those changes rather than
reprocessing everything," ensuring the documentation for the Badging task fully
describes incremental execution semantics.

In @build-logic/convention/src/main/kotlin/org/convention/Detekt.kt:
- Around line 17-19: The hard-coded project path in
tasks.named<Detekt>("detekt") calling
mustRunAfter(":cmp-android:dependencyGuard") makes the convention plugin
non-reusable; replace the direct path with a safe conditional lookup (e.g., use
tasks.findByPath for ":cmp-android:dependencyGuard" and only call mustRunAfter
when that task exists) or make the dependency task name configurable via a
plugin extension and use that value instead of the literal; update the block
around tasks.named<Detekt>("detekt") to perform the conditional check (or read
the extension property) before invoking mustRunAfter so the plugin no longer
assumes a specific project structure.

In @build-logic/convention/src/main/kotlin/org/convention/Jacoco.kt:
- Around line 61-66: The Project.configureJacoco code calls
libs.findVersion("jacoco").get() which will throw if the version key is missing;
add a "jacoco" entry to the version catalog (gradle/libs.versions.toml) under
the [versions] section (e.g. jacoco = "0.8.8" or your desired version) so
libs.findVersion("jacoco") returns a value for configure<JacocoPluginExtension>
in configureJacoco.

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/BaseKeystoreTask.kt:
- Around line 81-96: checkKeytoolAvailable currently starts a
ProcessBuilder("keytool", "-help") then calls waitFor() without consuming
stdout/stderr or applying a timeout, which can hang; fix by consuming or
redirecting the process streams and using a timed wait: in
checkKeytoolAvailable, after starting the process (ProcessBuilder.start()),
either call process.inputStream and process.errorStream readers on background
threads or use processBuilder.redirectOutput(ProcessBuilder.Redirect.DISCARD)
and redirectError(ProcessBuilder.Redirect.DISCARD) before start(), then call
process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS) (or waitFor with a
reasonable timeout) and if it returns false destroy the process and log an
error; also catch InterruptedException and ensure streams are closed and
process.destroyForcibly() is called when needed, and include the exit code in
the log when available.

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/ConfigurationFileUpdatesTask.kt:
- Around line 156-184: Add the fastlane-config directory to .gitignore to
prevent committing plaintext credentials: update the repo .gitignore to include
"fastlane-config/" (or "fastlane-config/**") so files written by
createNewFastlaneConfig (which writes the Android Fastlane config via
configFile.writeText) are ignored; commit the .gitignore change and verify that
fastlane-config/android_config.rb is no longer tracked (remove from git index if
already committed using git rm --cached) before merging.
- Around line 167-172: The FIREBASE_CONFIG map in ConfigurationFileUpdatesTask
(symbol: FIREBASE_CONFIG) contains hardcoded Firebase app IDs that look real;
change those values to clearly fake placeholders (e.g.,
"YOUR_FIREBASE_APP_ID_HERE" and "YOUR_FIREBASE_DEMO_APP_ID_HERE") or wire them
to configurable sources (project properties / environment variables) so the task
reads keys instead of embedding real IDs; update the map entries
firebase_prod_app_id and firebase_demo_app_id accordingly and add a brief
comment explaining they must be provided via configuration when running the
task.

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/ConfigurationValidator.kt:
- Around line 214-218: validateFilePaths currently has a side effect: it calls
config.keystoreDir.mkdirs(), which creates directories; change it to a pure
read-only validation by removing the mkdirs() call and instead check existence
and writability only (e.g., use config.keystoreDir.exists() and
config.keystoreDir.isDirectory(), or
java.nio.file.Files.exists/Files.isDirectory and Files.isWritable on the path or
its nearest existing parent) and add an error if the directory doesn't exist or
cannot be written to, keeping the checks confined to the validateFilePaths
function and referencing config.keystoreDir and validateFilePaths when updating
the logic.
- Around line 42-48: The guard on config.validity contains an unreachable
branch: after checking config.validity <= 0 the subsequent else if
(config.validity < 1) can never be true for integers; update
ConfigurationValidator to replace that unreachable condition with a meaningful
short-validity threshold (for example use config.validity <= 2 to warn about 1–2
year validity), keeping the existing errors.add and warnings.add flows
(references: config.validity, errors, warnings in ConfigurationValidator).

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/EnvironmentOverrideHandler.kt:
- Around line 296-303: The constants in EnvironmentOverrideHandler
(ORIGINAL_KEYSTORE_PASSWORD, ORIGINAL_KEYSTORE_ALIAS,
ORIGINAL_KEYSTORE_ALIAS_PASSWORD, UPLOAD_KEYSTORE_PASSWORD,
UPLOAD_KEYSTORE_ALIAS, UPLOAD_KEYSTORE_ALIAS_PASSWORD) have mismatched
identifier names vs. string values (some values include "_FILE_"). Fix by making
the identifiers and their string values consistent: either rename the constants
to include "FILE" (e.g., ORIGINAL_KEYSTORE_FILE_PASSWORD,
UPLOAD_KEYSTORE_FILE_PASSWORD) or change the string values to remove "_FILE_" so
they match the current constant names; update all referenced usages to the new
names to keep callers consistent.

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/KeystoreConfig.kt:
- Around line 10-12: KeystoreConfig currently uses weak hardcoded defaults for
keystorePassword and keyPassword ("android"); remove those defaults so callers
must supply secure values (i.e., change the parameters keystorePassword and
keyPassword in KeystoreConfig to have no default), and optionally keyAlias if
you want to avoid any implicit value; additionally add a runtime check in
KeystoreConfig initialization that detects if either password equals the legacy
debug value ("android") and either logs a prominent warning or throws an error
to prevent accidental use in production (use the class/constructor
KeystoreConfig and the properties keystorePassword, keyPassword, keyAlias to
locate where to change and add the check).
- Line 51: KeystoreConfig currently flags keySize < 1024 as an error but
ConfigurationValidator.validateKeyAlgorithm() requires RSA keys to be >= 2048;
update KeystoreConfig's validation (the keySize check in KeystoreConfig) to
enforce the same minimum (use 2048) or make the check algorithm-aware (e.g., if
algorithm is "RSA" require keySize >= 2048) so both validations are consistent
with ConfigurationValidator.validateKeyAlgorithm().

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/KeystoreGenerationTask.kt:
- Around line 196-199: The log currently prints the full command (including
sensitive `-storepass`/`-keypass` values) in executeKeytoolCommand; change the
logging to mask sensitive arguments by constructing a maskedCommand from the
command List<String> before logInfo: treat both "--flag=value" and separate
flag/value forms, replace the password value with a fixed mask (e.g. "****")
whenever an element startsWith("-storepass") or startsWith("-keypass") or
immediately follows those flags, and then call logInfo with the maskedCommand
instead of the raw command to avoid exposing keystore passwords.

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/SecretsEnvParser.kt:
- Around line 96-98: The else branch that adds "Invalid multiline syntax" is
unreachable because when line.contains("<<") is true, split("<<", limit = 2)
always produces parts.size == 2; remove the dead else branch inside the
multiline handling in SecretsEnvParser (the block that checks
line.contains("<<") and assigns parts) and instead validate the second part
(parts[1]) for being empty or only whitespace — if parts[1].isBlank() then call
errors.add with a clear message; update any related tests to expect this
validation instead of relying on the unreachable branch.

In
@build-logic/convention/src/main/kotlin/org/convention/keystore/SecretsEnvUpdateTask.kt:
- Around line 396-406: The header-write logic currently checks
secretsFile.exists() after calling secretsFile.printWriter() which
creates/truncates the file, so the header condition never triggers; move the
existence check before opening the writer (i.e., evaluate !secretsFile.exists()
|| secrets.comments.isEmpty() using secretsFile.exists() prior to calling
secretsFile.printWriter().use { writer -> ... }) and only then open the writer
to write the header and body; keep the rest of the write logic (the
printWriter().use block and the header lines) unchanged.

In @build-logic/convention/src/main/kotlin/org/convention/KotlinAndroid.kt:
- Around line 28-34: Update the misleading comment above the compileOptions
block: it currently references "Up to Java 11 APIs" but the code sets
sourceCompatibility and targetCompatibility to JavaVersion.VERSION_17 and
enables isCoreLibraryDesugaringEnabled; change the comment to state that Java 17
language level and desugaring are configured (and clarify what desugaring covers
for Java 17, if needed) so it accurately reflects the settings in
compileOptions.
- Around line 48-53: Update the misleading comment above the
extensions.configure<JavaPluginExtension> block: remove the Android/desugaring
reference and state that sourceCompatibility and targetCompatibility are set to
Java 17 (JavaVersion.VERSION_17) for non-Android JVM projects; ensure the
comment succinctly reflects Java 17 configuration and its applicability (e.g.,
"Configure Java 17 (sourceCompatibility and targetCompatibility) for JVM
projects — not related to Android desugaring").

In @build-logic/convention/src/main/kotlin/org/convention/PrintTestApks.kt:
- Around line 88-91: Update the RuntimeException messages thrown after loading
APKs to include contextual details: when
builtArtifactsLoader.get().load(apkFolder.get()) returns null, throw an
exception that includes the apkFolder path (e.g.,
apkFolder.get().asFile.toString()) and the variant name if available (e.g.,
variantName or variant.toString()); similarly, when checking
builtArtifacts.elements.size != 1, include the actual size, the apkFolder path
and the variant identifier in the error message so the exceptions provide
actionable debugging info; modify the two throw RuntimeException(...) calls
accordingly near builtArtifactsLoader.get().load(...) and the size check.

In @build-logic/convention/src/main/kotlin/org/convention/Spotless.kt:
- Line 6: Update the ktlint version constant to remove the vulnerable old
release: change the const val ktlintVersion (in Spotless.kt) from "1.0.1" to
"1.8.0", then refresh dependencies/lockfiles and run the build/spotless checks
to ensure compatibility and resolve any formatter rule changes from the upgrade.

In @cmp-android/prodRelease-badging.txt:
- Line 1: The manifest badging shows versionCode='1' while
versionName='2026.1.1-beta.0.6'; update the integer versionCode to an
incremented value that reflects this new release (ensure versionCode is a
monotonically increasing integer used by the Play Store), e.g. bump versionCode
from 1 to an appropriate higher integer that corresponds to this release, and
make the same change in the canonical source of truth for app versions (e.g.,
the module's build.gradle / android.defaultConfig versionCode or wherever
versionCode is injected) so versionCode and versionName remain consistent across
builds.

In @cmp-desktop/src/jvmMain/kotlin/main.kt:
- Around line 31-34: Move the initKoin() call out of the application composable
scope so Koin is started exactly once at app startup: call initKoin() at the
start of main() before entering the application { ... } block (not inside any
composable or UI lambda), keeping the application { ... } block purely for UI;
update main() to invoke initKoin() first and then call application { /* UI */ }
to avoid reinitialization.
- Line 43: The window title is a placeholder ("DesktopApp") in main.kt; update
the title assignment (title = "DesktopApp") to a descriptive app name (e.g.,
"MifosPay" or "Mifos Mobile Wallet") so the UI shows a meaningful name; locate
the title property in the main window creation code (the title = ... line) and
replace the string accordingly.

In
@cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/iosApp.xcscheme:
- Around line 1-32: The scheme file for the iosApp (LiteDo.app, BlueprintName
"iosApp") is located under xcuserdata which is user-specific; either make the
scheme shared so it moves to xcshareddata by opening Xcode, selecting the iosApp
scheme, choosing "Manage Schemes..." and checking "Shared" (this will relocate
the scheme out of xcuserdata), or remove this xcuserdata/iosApp.xcscheme from
version control and add the appropriate xcuserdata/ path to .gitignore if the
scheme is truly user-specific.
- Line 11: Update the Xcode scheme's BuildableName entries from "LiteDo.app" to
"MifosPay.app": find the two occurrences of BuildableName = "LiteDo.app" in the
iosApp.xcscheme and replace each with BuildableName = "MifosPay.app" so the
scheme matches the product name configured in Config.xcconfig and the
org.mifospay bundle identifier.

In
@cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist:
- Around line 1-14: Update .gitignore to explicitly ignore developer xcuserdata
by adding patterns for cmp-ios/iosApp.xcodeproj/xcuserdata/ and
cmp-ios/iosApp.xcworkspace/xcuserdata/ so these per-developer Xcode files aren’t
tracked; then remove the currently tracked xcuserdata files (those under
cmp-ios/.../xcuserdata, e.g., entries for skniyajali, kapilmaurya,
hekmatullahamin) from git’s index and commit that change to stop them from
appearing in future commits; optionally, if historical removal is required,
purge those paths from the repository history using your repo’s history-rewrite
tool.

In
@cmp-ios/iosApp.xcworkspace/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist:
- Around line 1-5: The committed user-specific Xcode workspace file
xcschememanagement.plist located under xcuserdata/skniyajali.xcuserdatad should
be removed and excluded: remove the file from the repo index (e.g., git rm
--cached) and commit the deletion, then update .gitignore to ignore xcuserdata
directories for both .xcodeproj and .xcworkspace (add patterns like
*.xcodeproj/xcuserdata/ and *.xcworkspace/xcuserdata/ or equivalent), verify no
other xcuserdata files remain tracked, and commit the .gitignore change.

In
@core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt:
- Line 80: In ResultSuspendConverterFactory (locate the println calls in the
suspend conversion logic inside the class/method handling Result wrapping),
replace all println("Failure: " + result.throwable.message) uses with a proper
logger call from your chosen logging abstraction (e.g., kermit or napier);
obtain or inject a logger instance (e.g., a private val logger or constructor
parameter) and call logger.error/diagnostic with a descriptive message and
include the throwable so the stacktrace is captured (for example:
logger.error("Failure converting Result", throwable) or equivalent in your
logging API), and apply the same replacement for the other occurrences
referenced in the file.
- Line 88: In ResultSuspendConverterFactory's status handling (the when on
status), the HTTP success range currently uses 200..209 which incorrectly treats
2xx codes >=210 as errors; change the branch to use 200..299 so all RFC 7231
success codes (e.g., 201, 204, 206) are handled by the success path (i.e.,
update the condition from in 200..209 to in 200..299 and keep the existing body
that reads result.response.body(successType) and returns NetworkResult.Success
or maps serialization exceptions to NetworkError.SERIALIZATION).
- Around line 57-58: Update the KDoc on ResultSuspendConverterFactory so it
references NetworkResult (the actual type checked by the implementation) instead
of Result; change the sentence that currently says "`Result`" to
"`NetworkResult`" (and adjust surrounding wording/backticks as needed) so the
doc matches the converter's behavior and types used by suspendResponseConverter.
- Around line 40-46: The example in the KDoc uses the wrong generic types;
update the sample ApiService declaration so the suspend function getUsers()
returns NetworkResult<List<User>, NetworkError> instead of Result<List<User>,
RemoteError>, i.e., replace the type names in the documentation example to match
the codebase's NetworkResult and NetworkError types.

In
@core-base/network/src/commonMain/kotlin/template/core/base/network/KtorHttpClient.kt:
- Around line 118-126: Digest auth configuration is missing proactive credential
sending; update the digest auth block (where install(Auth) { digest {
credentials { digestCredentialsProvider() } } } ) to include a
sendWithoutRequest { request -> request.url.host in authRequiredUrl } clause
similar to the Bearer/Basic providers so digest credentials are sent proactively
to hosts in authRequiredUrl.

In
@core-base/network/src/jsMain/kotlin/template/core/base/network/KtorHttpClient.js.kt:
- Line 10: The package declaration in KtorHttpClient.js.kt uses the placeholder
package template.core.base.network; rename it to org.mifospay.core.base.network
to match the project's package structure, and apply the same change across all
files in the core-base module (ui, platform, network) so package declarations
consistently use org.mifospay.core.base.*; ensure any imports, references, and
module metadata that rely on the old package are updated accordingly (e.g.,
KtorHttpClient and other classes in core-base).
🟠 Major comments (21)
cmp-android/prodRelease-badging.txt-1-1 (1)

1-1: Address the versionCode/versionName mismatch.

The versionCode is set to '1' while versionName is '2026.1.1-beta.0.6'. This inconsistency suggests version tracking may not be properly maintained. Typically, versionCode increments with each release. If this is a production build, versionCode should be incremented accordingly (e.g., to match release numbers).

core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsEvent.kt-258-279 (1)

258-279: Documentation contradicts implementation: validation vs. sanitization.

The implementation silently sanitizes invalid inputs instead of throwing exceptions as documented:

  • Blank keys → replaced with "unknown_param" (line 272)
  • Keys > 40 chars → truncated (line 271)
  • Values > 100 chars → truncated (line 274)

However, the KDoc at lines 238, 249-253 explicitly states:

"@throws IllegalArgumentException if validation constraints are violated"

Impact:

  • Developers expecting exceptions won't receive them, potentially masking bugs
  • Blank keys silently becoming "unknown_param" obscures data quality issues
  • Silent truncation may cause unnoticed data loss in analytics

Recommendation: Choose one approach consistently:

  1. Match documentation (throw exceptions for invalid input) — better for catching bugs early, OR
  2. Match implementation (update docs to reflect sanitization) — better for resilience but risks hiding issues
Option 1: Throw exceptions to match documentation
 operator fun invoke(key: String, value: String): Param {
-    val safeKey = key
-        .takeIf { it.isNotBlank() }
-        ?.take(MAX_KEY_LENGTH)
-        ?: FALLBACK_KEY
-
-    val safeValue = value.take(MAX_VALUE_LENGTH)
-
-    return Param(safeKey, safeValue)
+    require(key.isNotBlank()) { "Parameter key must not be blank" }
+    require(key.length <= MAX_KEY_LENGTH) { 
+        "Parameter key exceeds maximum length of $MAX_KEY_LENGTH characters" 
+    }
+    require(value.length <= MAX_VALUE_LENGTH) { 
+        "Parameter value exceeds maximum length of $MAX_VALUE_LENGTH characters" 
+    }
+    
+    return Param(key, value)
 }
Option 2: Update documentation to reflect sanitization behavior

Update the KDoc to remove @throws IllegalArgumentException references (lines 238, 249-253) and add:

/**
 * Represents a validated analytics parameter with automatic constraint sanitization.
 *
 * This data class encapsulates a key-value pair for analytics events with built-in
 * sanitization that enforces analytics platform constraints. The sanitization occurs
 * during object construction to ensure data integrity.
 *
 * **Sanitization Rules:**
 * - Blank keys are replaced with "unknown_param"
 * - Keys are truncated to ≤ 40 characters (Firebase Analytics constraint)
 * - Values are truncated to ≤ 100 characters (Firebase Analytics constraint)
 * ...
 */

Also update withParam/withParams documentation at lines 70, 97, 128 to remove exception throwing claims.

cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/iosApp.xcscheme-1-32 (1)

1-32: Move scheme to shared location or remove from version control.

This scheme file is in the xcuserdata/ directory, making it user-specific. If this scheme needs to be shared across the team:

  1. In Xcode, select the scheme from the scheme dropdown
  2. Choose "Manage Schemes..."
  3. Check the "Shared" checkbox for the iosApp scheme

This will move the scheme file to xcshareddata/ instead of xcuserdata/, making it appropriate for version control. If the scheme is truly user-specific and doesn't need to be shared, it should be excluded from the repository via .gitignore.

cmp-ios/iosApp.xcworkspace/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist-1-5 (1)

1-5: Remove user-specific Xcode workspace files from version control.

This file is also located in a xcuserdata/skniyajali.xcuserdatad/ directory, making it user-specific configuration that should not be committed. Even though it contains only an empty dictionary, it will still cause unnecessary noise and potential conflicts in the repository.

Ensure that xcuserdata/ patterns are properly excluded in .gitignore for both .xcodeproj and .xcworkspace directories.

cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/iosApp.xcscheme-11-11 (1)

11-11: Update BuildableName from "LiteDo.app" to "MifosPay.app" in the Xcode scheme.

The BuildableName is set to "LiteDo.app" (lines 11 and 26), but the project's actual app name is configured as "MifosPay" in cmp-ios/Configuration/Config.xcconfig and the bundle identifier is org.mifospay. The scheme file was copied from the kmp-project-template without proper customization. Update both occurrences to "MifosPay.app" to match the configured product name.

cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist-1-14 (1)

1-14: Remove user-specific Xcode files from version control.

Multiple xcuserdata files are currently tracked in git (from developers skniyajali, kapilmaurya, and hekmatullahamin), which should be excluded. These files:

  • Are specific to each developer's machine
  • Cause merge conflicts when multiple developers commit their preferences
  • Clutter the repository with personal Xcode configuration

The root cause is that .gitignore contains patterns for iosApp/ (e.g., iosApp/iosApp.xcodeproj/*), but the actual iOS project is located in cmp-ios/. Update .gitignore to exclude cmp-ios/iosApp.xcodeproj/xcuserdata/ and cmp-ios/iosApp.xcworkspace/xcuserdata/, then remove the tracked xcuserdata files from git history.

core-base/network/src/jsMain/kotlin/template/core/base/network/KtorHttpClient.js.kt-10-10 (1)

10-10: Rename package to match project structure across all core-base modules.

The package name template.core.base.network is a placeholder that doesn't align with the project's actual package structure. The codebase consistently uses org.mifospay.* for packages (see feature modules), but the entire core-base module (ui, platform, network) uses the template.* prefix. Rename this to org.mifospay.core.base.network and apply the same fix to all other files in the core-base module.

core-base/network/src/commonMain/kotlin/template/core/base/network/KtorHttpClient.kt-141-157 (1)

141-157: Logging configuration has two issues.

  1. httpLogger parameter is ignored: Line 142 assigns logger = httpLogger, but lines 152-156 immediately overwrite it with a Kermit-based logger. The httpLogger parameter becomes dead code.

  2. Empty loggableHosts disables all logging: The filter on lines 144-148 uses loggableHosts.any { ... }. With the default empty list, this always returns false, meaning no requests are logged by default.

🔎 Proposed fix

Choose either the custom Kermit logger or the configurable httpLogger, and fix the filter logic:

     install(Logging) {
-        logger = httpLogger
         level = httpLogLevel
-        filter { request ->
-            loggableHosts.any { host ->
-                request.url.host.contains(host)
+        if (loggableHosts.isNotEmpty()) {
+            filter { request ->
+                loggableHosts.any { host ->
+                    request.url.host.contains(host)
+                }
             }
         }
         sanitizeHeader { header ->
             header in sensitiveHeaders
         }
         logger = object : Logger {
             override fun log(message: String) {
                 KermitLogger.d(tag = "KtorClient", messageString = message)
             }
         }
     }

Alternatively, if httpLogger should be used instead of the hardcoded Kermit logger, remove lines 152-156.

core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt-80-80 (1)

80-80: Replace println with proper logging framework.

Using println for logging is not production-ready. It lacks log levels, structured output, and configurability. Consider using a proper logging framework (e.g., kermit, napier, or platform-specific loggers).

🔎 Example refactor using a logging abstraction

Assuming you have a logger instance available:

  is KtorfitResult.Failure -> {
-     println("Failure: " + result.throwable.message)
+     logger.error("Network request failed: ${result.throwable.message}", result.throwable)
      NetworkResult.Error(NetworkError.UNKNOWN)
  }
  } catch (e: SerializationException) {
-     println("Serialization error: ${e.message}")
+     logger.error("Failed to deserialize response: ${e.message}", e)
      NetworkResult.Error(NetworkError.SERIALIZATION)
  }
  else -> {
-     println("Status code $status")
+     logger.warn("Unhandled HTTP status code: $status")
      NetworkResult.Error(NetworkError.UNKNOWN)
  }

Also applies to: 95-95, 107-107

core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt-88-88 (1)

88-88: Change the HTTP success range from 200..209 to 200..299.

The current range only covers status codes 200-209, leaving codes 210-299 (201 Created, 204 No Content, 206 Partial Content, etc.) to fall through to the else branch where they are incorrectly treated as UNKNOWN errors. HTTP success codes span the full 2xx range (200-299) per RFC 7231.

Code context
when (status) {
    in 200..209 -> {
        try {
            val data = result.response.body(successType) as Any
            NetworkResult.Success(data)
        } catch (e: NoTransformationFoundException) {
            NetworkResult.Error(NetworkError.SERIALIZATION)
        } catch (e: SerializationException) {
            println("Serialization error: ${e.message}")
            NetworkResult.Error(NetworkError.SERIALIZATION)
        }
    }
    // ... other branches ...
    else -> {
        println("Status code $status")
        NetworkResult.Error(NetworkError.UNKNOWN)
    }
}
build-logic/convention/src/main/kotlin/org/convention/PrintTestApks.kt-88-91 (1)

88-91: Enhance error messages for better debugging.

The error messages are generic and don't provide helpful context when things go wrong. Including the variant name and potentially the APK folder path would significantly improve debuggability.

🔎 Proposed improvement with descriptive error messages
-        val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
-            ?: throw RuntimeException("Cannot load APKs")
-        if (builtArtifacts.elements.size != 1)
-            throw RuntimeException("Expected one APK !")
+        val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
+            ?: throw RuntimeException("Cannot load APKs for variant '${variantName.get()}' from ${apkFolder.get()}")
+        if (builtArtifacts.elements.size != 1) {
+            throw RuntimeException(
+                "Expected exactly one APK for variant '${variantName.get()}', but found ${builtArtifacts.elements.size}"
+            )
+        }
         val apk = File(builtArtifacts.elements.single().outputFile).toPath()
cmp-desktop/src/jvmMain/kotlin/main.kt-31-34 (1)

31-34: Move Koin initialization outside the composable scope.

Calling initKoin() inside the application block is problematic. While application doesn't recompose frequently, Koin's startKoin should be called exactly once at application startup, outside any composable scope. This prevents potential reinitialization issues and follows DI best practices.

🔎 Proposed fix
 fun main() {
+    // Initialize Koin before entering composable scope
+    initKoin()
+
     application {
-        // Initializes the Koin dependency injection framework.
-        initKoin()
-
         // Creates a window state to manage the window's state.
         val windowState = rememberWindowState()
build-logic/convention/src/main/kotlin/GitHooksConventionPlugin.kt-49-55 (1)

49-55: Reconsider aggressive task dependency on installGitHooks.

Making all build-related tasks depend on installGitHooks will force hook installation on every build operation, including clean. This could:

  • Slow down builds unnecessarily
  • Cause issues in CI environments where .git might not be available or writable
  • Re-install hooks even when they haven't changed

Consider:

  1. Running hook installation only once (check if hooks already exist and are up-to-date)
  2. Removing clean from the dependency list
  3. Making this opt-in rather than automatic, or only depending on preBuild
🔎 Suggested improvements

Option 1: Only depend on preBuild:

         project.afterEvaluate {
             project.tasks.matching {
-                it.name in listOf("preBuild", "build", "assembleDebug", "assembleRelease", "installDebug", "installRelease", "clean")
+                it.name == "preBuild"
             }.configureEach {
                 dependsOn(project.tasks.named("installGitHooks"))
             }
         }

Option 2: Add up-to-date checks to installGitHooks to skip when hooks are already installed.

build-logic/convention/src/main/kotlin/org/convention/Detekt.kt-17-19 (1)

17-19: Hard-coded project path breaks reusability.

Line 18 hard-codes the project path :cmp-android:dependencyGuard, which assumes a specific project structure. If this convention plugin is intended to be reusable across different projects or if the project structure changes, this will break.

Consider one of these approaches:

  1. Make the dependency task name configurable via the plugin extension
  2. Use a conditional check: tasks.findByPath(":cmp-android:dependencyGuard")?.let { mustRunAfter(it) }
  3. Remove this constraint if the ordering isn't critical
🔎 Proposed fix using conditional
     tasks.named<Detekt>("detekt") {
-        mustRunAfter(":cmp-android:dependencyGuard")
+        tasks.findByPath(":cmp-android:dependencyGuard")?.let { mustRunAfter(it) }
         jvmTarget = "17"
build-logic/convention/src/main/kotlin/KtlintConventionPlugin.kt-7-18 (1)

7-18: Plugin ID is correct, but configuration implementation is missing compared to similar convention plugins.

The plugin ID "org.jlleitschuh.gradle.ktlint" correctly matches the version catalog declaration (version 12.1.1). However, unlike MifosSpotlessConventionPlugin and MifosDetektConventionPlugin which both call configuration functions after applying their plugins, KtlintConventionPlugin applies the plugin without any additional configuration. No configureKtlint function or Ktlint.kt configuration file exists in the codebase, breaking the established pattern. Either add configuration support consistent with the other convention plugins, or document why ktlint intentionally uses defaults.

build-logic/convention/src/main/kotlin/org/convention/GradleManagedDevices.kt-27-57 (1)

27-57: Add API levels 35 and 36 to device configurations.

The current API levels 30–31 are still valid (API 30 is required for ATDs, and API 31 is supported). However, as of January 2026, the Google Play target API requirement is now 35+, and API 36 (Android 16) is the latest platform. Add test configurations for these levels to ensure compatibility with Play requirements and catch forward-compatibility issues.

Optionally, document why Pixel 6 (API 31) is excluded from CI if this is intentional for performance reasons.

build-logic/convention/src/main/kotlin/org/convention/keystore/KeystoreConfig.kt-10-12 (1)

10-12: Default passwords are weak and may pose a security risk if used inadvertently in production.

The default values "android" for keystorePassword and keyPassword are common debug defaults. Consider either:

  1. Requiring explicit password configuration (no defaults)
  2. Adding prominent documentation that these are debug-only defaults
  3. Logging a warning when defaults are used
build-logic/convention/src/main/kotlin/KeystoreManagementConventionPlugin.kt-40-43 (1)

40-43: Calling .get() on a task provider during configuration realizes the task eagerly.

generateKeystoresTask.get() on line 42 forces the task to be created immediately, defeating the purpose of tasks.register(). Use flatMap or wire dependencies lazily.

🔎 Suggested approach
 val updateSecretsEnvTask = tasks.register("updateSecretsEnv", SecretsEnvUpdateTask::class.java) {
-    SecretsEnvUpdateTask.createFromKeystoreGeneration(this, generateKeystoresTask.get(), keystoreExtension.secretsConfig.get())
+    dependsOn(generateKeystoresTask)
+    // Wire outputs lazily or defer configuration to doFirst/TaskAction
 }

Committable suggestion skipped: line range outside the PR's diff.

build-logic/convention/src/main/kotlin/KeystoreManagementConventionPlugin.kt-24-31 (1)

24-31: Avoid calling .get() on providers during task configuration phase.

Calling keystoreExtension.secretsConfig.get() at configuration time (line 30) forces eager evaluation and breaks Gradle's lazy configuration model. This can cause ordering issues if the extension hasn't been configured yet by the build script.

🔎 Suggested approach

Wire task properties lazily using provider APIs:

 val generateKeystoresTask = tasks.register("generateKeystores", KeystoreGenerationTask::class.java) {
     keystoreConfig.set(keystoreExtension.keystoreConfig)
     secretsConfig.set(keystoreExtension.secretsConfig)
-
-    // Load configuration from secrets.env if it exists
-    KeystoreGenerationTask.createWithSecretsConfig(this, keystoreExtension.secretsConfig.get())
 }

Move the createWithSecretsConfig logic into the task's @TaskAction method or use doFirst to defer execution to runtime.

Committable suggestion skipped: line range outside the PR's diff.

build-logic/convention/src/main/kotlin/org/convention/keystore/ConfigurationValidator.kt-214-218 (1)

214-218: Validation method has side effects: it creates directories.

validateFilePaths calls config.keystoreDir.mkdirs() on line 216, which modifies the filesystem. Validation should be a pure, read-only operation to avoid unexpected behavior when called multiple times or in dry-run scenarios.

🔎 Suggested fix

Check only if the directory exists or is creatable without actually creating it:

 private fun validateFilePaths(config: KeystoreConfig, errors: MutableList<String>, warnings: MutableList<String>) {
     // Check keystore directory
-    if (!config.keystoreDir.exists() && !config.keystoreDir.mkdirs()) {
-        errors.add("Cannot create keystore directory: ${config.keystoreDir.absolutePath}")
+    if (!config.keystoreDir.exists()) {
+        val parent = config.keystoreDir.parentFile
+        if (parent != null && !parent.canWrite()) {
+            errors.add("Cannot create keystore directory (no write permission): ${config.keystoreDir.absolutePath}")
+        } else {
+            // Directory will be created during task execution
+        }
     }
build-logic/convention/src/main/kotlin/org/convention/keystore/SecretsEnvUpdateTask.kt-396-406 (1)

396-406: Logic issue: File existence check occurs after file creation.

printWriter().use { writer -> ... } creates/truncates the file before the existence check on line 399. The condition !secretsFile.exists() will always be false at this point, so the header is never written for new files.

🔎 Proposed fix - check before opening writer
 private fun writeSecretsFile(
     secretsFile: File,
     secrets: ParsedSecretsData,
 ) {
     try {
+        val isNewFile = !secretsFile.exists() || secrets.comments.isEmpty()
         secretsFile.printWriter().use { writer ->
             // Write header comment if it's a new file or no comments exist
-            if (!secretsFile.exists() || secrets.comments.isEmpty()) {
+            if (isNewFile) {
                 writer.println("# GitHub Secrets Environment File")
🟡 Minor comments (17)
core-base/network/src/commonMain/kotlin/template/core/base/network/KtorHttpClient.kt-118-126 (1)

118-126: Digest auth missing sendWithoutRequest configuration.

Bearer and Basic auth providers use sendWithoutRequest { request -> request.url.host in authRequiredUrl } to proactively send credentials to specified hosts. Digest auth lacks this, causing inconsistent behavior across auth types.

🔎 Proposed fix
         digestCredentialsProvider != null -> {
             install(Auth) {
                 digest {
                     credentials {
                         digestCredentialsProvider()
                     }
+                    sendWithoutRequest { request ->
+                        request.url.host in authRequiredUrl
+                    }
                 }
             }
         }
build-logic/convention/src/main/kotlin/org/convention/KotlinAndroid.kt-28-34 (1)

28-34: Update comment to reflect Java 17 configuration.

The comment mentions "Up to Java 11 APIs" but the code configures Java 17. Update the comment to accurately reflect the Java version and desugaring capabilities.

🔎 Suggested comment update
         compileOptions {
-            // Up to Java 11 APIs are available through desugaring
-            // https://developer.android.com/studio/write/java11-minimal-support-table
+            // Java 17 target with core library desugaring for backward compatibility
+            // https://developer.android.com/studio/write/java11-minimal-support-table
             sourceCompatibility = JavaVersion.VERSION_17
             targetCompatibility = JavaVersion.VERSION_17
             isCoreLibraryDesugaringEnabled = true
build-logic/convention/src/main/kotlin/org/convention/KotlinAndroid.kt-48-53 (1)

48-53: Update comment to reflect Java 17 configuration.

The comment references Java 11 and Android desugaring, but this configures Java 17 for non-Android JVM projects where desugaring isn't applicable. Update the comment for clarity.

🔎 Suggested comment update
     extensions.configure<JavaPluginExtension> {
-        // Up to Java 11 APIs are available through desugaring
-        // https://developer.android.com/studio/write/java11-minimal-support-table
+        // Configure Java 17 for JVM projects
         sourceCompatibility = JavaVersion.VERSION_17
         targetCompatibility = JavaVersion.VERSION_17
core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt-57-58 (1)

57-58: Fix the documentation to reference the correct type.

The documentation mentions "Result" but should reference "NetworkResult" for consistency with the actual implementation.

🔎 Proposed fix
  * @return A [Converter.SuspendResponseConverter] if the return type is
- *    `Result`, or `null` otherwise.
+ *    `NetworkResult`, or `null` otherwise.
core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt-40-46 (1)

40-46: Fix the example code to use correct type names.

The example shows Result<List<User>, RemoteError> but should use NetworkResult<List<User>, NetworkError> to match the actual types defined in this codebase.

🔎 Proposed fix
  * Example usage:
  * ```kotlin
  * interface ApiService {
  *     @GET("users")
- *     suspend fun getUsers(): Result<List<User>, RemoteError>
+ *     suspend fun getUsers(): NetworkResult<List<User>, NetworkError>
  * }
  * ```
cmp-desktop/src/jvmMain/kotlin/main.kt-43-43 (1)

43-43: Use a more descriptive window title.

The hardcoded title "DesktopApp" appears to be a placeholder from the template. Consider using a more descriptive name that reflects the actual application, such as "MifosPay" or "Mifos Mobile Wallet".

🔎 Proposed fix
-            title = "DesktopApp",
+            title = "MifosPay",
build-logic/convention/src/main/kotlin/GitHooksConventionPlugin.kt-38-38 (1)

38-38: Fix spacing after else keyword.

Missing space after the closing brace.

🔎 Proposed fix
-            }else {
+            } else {
build-logic/convention/src/main/kotlin/org/convention/keystore/KeystoreConfig.kt-51-51 (1)

51-51: Key size validation is inconsistent with ConfigurationValidator.

Here, keySize < 1024 is an error, but ConfigurationValidator.validateKeyAlgorithm() enforces keySize < 2048 for RSA. This inconsistency could allow 1024-bit RSA keys, which are considered weak.

🔎 Suggested fix
-if (keySize < 1024) errors.add("Key size must be at least 1024 bits")
+if (keySize < 2048) errors.add("Key size must be at least 2048 bits for secure key generation")
build-logic/convention/src/main/kotlin/org/convention/keystore/ConfigurationValidator.kt-42-48 (1)

42-48: Unreachable code: validity < 1 condition can never be true.

After checking validity <= 0 on line 42, any value that reaches line 44 must be >= 1. The condition validity < 1 is therefore unreachable for integer types.

🔎 Suggested fix
 // Validate validity period
 if (config.validity <= 0) {
     errors.add("Validity period must be positive")
-} else if (config.validity < 1) {
-    warnings.add("Validity period is very short (${config.validity} years)")
 } else if (config.validity > 50) {
     warnings.add("Validity period is very long (${config.validity} years)")
 }

If you want to warn about short validity (e.g., 1-2 years), change the condition:

} else if (config.validity <= 2) {
    warnings.add("Validity period is very short (${config.validity} years)")
}
build-logic/convention/src/main/kotlin/org/convention/keystore/BaseKeystoreTask.kt-81-96 (1)

81-96: Process output streams are not consumed, which may cause hangs on some systems.

When a process writes to stdout/stderr and the buffers fill up, waitFor() can block indefinitely. Additionally, there's no timeout, so a stuck keytool process would hang the build.

🔎 Suggested fix
 protected fun checkKeytoolAvailable(): Boolean {
     return try {
-        val process = ProcessBuilder("keytool", "-help").start()
-        val exitCode = process.waitFor()
+        val process = ProcessBuilder("keytool", "-help")
+            .redirectErrorStream(true)
+            .start()
+        // Consume output to prevent buffer blocking
+        process.inputStream.bufferedReader().readText()
+        val exitCode = process.waitFor(30, java.util.concurrent.TimeUnit.SECONDS)
+            .let { completed -> if (completed) process.exitValue() else -1 }
         if (exitCode == 0) {
             logInfo("keytool is available")
             true
         } else {
             logError("keytool command failed")
             false
         }
     } catch (e: Exception) {
         logError("keytool not found. Please ensure JDK is installed and keytool is in PATH")
         false
     }
 }
build-logic/convention/src/main/kotlin/org/convention/AndroidCompose.kt-46-61 (1)

46-61: The relativeToRootProject helper ignores its input provider's value.

The it parameter from flatMap on line 48 is captured but never used. The function computes the directory path based on projectDir regardless of what the provider contains. If the intent is simply to transform the provider to a directory, consider documenting that the input provider's value is intentionally discarded or refactor to use map directly instead of flatMap.

build-logic/convention/src/main/kotlin/org/convention/keystore/EnvironmentOverrideHandler.kt-218-229 (1)

218-229: Overly broad regex patterns will match unrelated environment variables.

Patterns like .*UPLOAD.*, .*ORIGINAL.*, .*RELEASE.*, .*DEBUG.* will match many unrelated variables (e.g., FILE_UPLOAD_LIMIT, ORIGINAL_IMAGE_PATH, RELEASE_DATE, DEBUG_MODE). Consider more specific patterns:

🔎 Suggested more specific patterns
val keystorePatterns = listOf(
    ".*KEYSTORE.*",
    ".*SIGNING.*",
    "STORE_PASSWORD", "STORE_FILE",
    "KEY_ALIAS", "KEY_PASSWORD",
    "UPLOAD_KEYSTORE.*", "ORIGINAL_KEYSTORE.*",
    "RELEASE_KEYSTORE.*", "DEBUG_KEYSTORE.*"
)
build-logic/convention/src/main/kotlin/org/convention/keystore/ConfigurationFileUpdatesTask.kt-167-172 (1)

167-172: Hardcoded Firebase app IDs look like real values.

The Firebase app IDs on lines 168-169 appear to be real-looking identifiers rather than obvious placeholders. Consider using clearly fake placeholder values (e.g., "YOUR_FIREBASE_APP_ID_HERE") or making these configurable to avoid confusion.

build-logic/convention/src/main/kotlin/org/convention/keystore/SecretsEnvParser.kt-114-116 (1)

114-116: Unreachable code: same issue as multiline block handling.

When split("=", limit = 2) is called on a string that contains("="), it will always return exactly 2 parts. This else branch is dead code.

build-logic/convention/src/main/kotlin/org/convention/keystore/EnvironmentOverrideHandler.kt-296-303 (1)

296-303: Naming inconsistency: Constant names don't match their values.

ORIGINAL_KEYSTORE_PASSWORD contains "ORIGINAL_KEYSTORE_FILE_PASSWORD" and UPLOAD_KEYSTORE_PASSWORD contains "UPLOAD_KEYSTORE_FILE_PASSWORD". This is confusing and error-prone. Either align the constant names with the values or add clarifying comments.

🔎 Proposed fix - align names
             // Original keystore specific
-            const val ORIGINAL_KEYSTORE_PASSWORD = "ORIGINAL_KEYSTORE_FILE_PASSWORD"
+            const val ORIGINAL_KEYSTORE_FILE_PASSWORD = "ORIGINAL_KEYSTORE_FILE_PASSWORD"
             const val ORIGINAL_KEYSTORE_ALIAS = "ORIGINAL_KEYSTORE_ALIAS"
             const val ORIGINAL_KEYSTORE_ALIAS_PASSWORD = "ORIGINAL_KEYSTORE_ALIAS_PASSWORD"

             // Upload keystore specific
-            const val UPLOAD_KEYSTORE_PASSWORD = "UPLOAD_KEYSTORE_FILE_PASSWORD"
+            const val UPLOAD_KEYSTORE_FILE_PASSWORD = "UPLOAD_KEYSTORE_FILE_PASSWORD"
             const val UPLOAD_KEYSTORE_ALIAS = "UPLOAD_KEYSTORE_ALIAS"
             const val UPLOAD_KEYSTORE_ALIAS_PASSWORD = "UPLOAD_KEYSTORE_ALIAS_PASSWORD"

Committable suggestion skipped: line range outside the PR's diff.

build-logic/convention/src/main/kotlin/org/convention/keystore/SecretsEnvParser.kt-96-98 (1)

96-98: Unreachable code: parts.size will always be 2 when line.contains("<<") is true.

When split("<<", limit = 2) is called on a string that contains("<<"), it will always return exactly 2 parts. The else branch on line 96-98 is dead code.

🔎 Proposed fix
-                        if (parts.size == 2) {
-                            multilineKey = parts[0].trim()
-                            multilineEnd = parts[1].trim()
-
-                            if (multilineKey.isBlank()) {
-                                errors.add("Line $lineNumber: Empty key in multiline declaration")
-                            } else if (multilineEnd.isBlank()) {
-                                errors.add("Line $lineNumber: Empty delimiter in multiline declaration")
-                            } else {
-                                inMultilineBlock = true
-                                multilineValue.clear()
-                            }
-                        } else {
-                            errors.add("Line $lineNumber: Invalid multiline syntax")
-                        }
+                        multilineKey = parts[0].trim()
+                        multilineEnd = parts[1].trim()
+
+                        if (multilineKey.isBlank()) {
+                            errors.add("Line $lineNumber: Empty key in multiline declaration")
+                        } else if (multilineEnd.isBlank()) {
+                            errors.add("Line $lineNumber: Empty delimiter in multiline declaration")
+                        } else {
+                            inMultilineBlock = true
+                            multilineValue.clear()
+                        }

Committable suggestion skipped: line range outside the PR's diff.

build-logic/convention/src/main/kotlin/org/convention/Badging.kt-30-36 (1)

30-36: Complete the incomplete documentation sentence.

The KDoc comment has an incomplete sentence on lines 34-35 that trails off without completing the explanation of incremental behavior.

🔎 Proposed fix
 /**
  * Generates the badging information of the APK.
  * This task is cacheable, meaning that if the inputs and outputs have not changed,
  * the task will be considered up-to-date and will not run.
- * This task is also incremental, meaning that if the inputs have not changed,
- *
+ * This task is also incremental, meaning that if the inputs have not changed,
+ * the task will skip execution and reuse the previous output.
  */

Comment on lines +36 to +40
if (isLinuxOrMacOs()) {
commandLine("chmod", "-R", "+x", ".git/hooks/")
}else {
commandLine("cmd", "/c", "attrib", "-R", "+X", ".git/hooks/*.*")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Windows command incorrect: attrib doesn't set executable permissions.

The Windows command uses attrib -R +X, but attrib is for file attributes (read-only, hidden, system, archive) and doesn't control executable permissions. Windows doesn't use Unix-style executable bits. Git for Windows handles hook executability automatically when hooks are placed in .git/hooks/ without a file extension.

🔎 Recommended fix

For Windows, either skip the permission-setting step or simply verify hooks were copied:

             if (isLinuxOrMacOs()) {
                 commandLine("chmod", "-R", "+x", ".git/hooks/")
-            } else {
-                commandLine("cmd", "/c", "attrib", "-R", "+X", ".git/hooks/*.*")
             }

Alternatively, you can make the Exec task conditional:

             if (isLinuxOrMacOs()) {
                 commandLine("chmod", "-R", "+x", ".git/hooks/")
+            } else {
+                // Git for Windows handles hook executability automatically
+                commandLine("cmd", "/c", "echo", "Git hooks copied (Windows)")
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isLinuxOrMacOs()) {
commandLine("chmod", "-R", "+x", ".git/hooks/")
}else {
commandLine("cmd", "/c", "attrib", "-R", "+X", ".git/hooks/*.*")
}
if (isLinuxOrMacOs()) {
commandLine("chmod", "-R", "+x", ".git/hooks/")
}
🤖 Prompt for AI Agents
In @build-logic/convention/src/main/kotlin/GitHooksConventionPlugin.kt around
lines 36 - 40, The Windows branch is using an incorrect attrib command to set
executable bits; instead, make the permission-setting step only run on Unix by
removing or replacing the else branch that calls commandLine("cmd", "/c",
"attrib", ...). Update the isLinuxOrMacOs() conditional so that the Exec task
invokes commandLine("chmod", "-R", "+x", ".git/hooks/") only when true and
otherwise either do nothing or perform a harmless verification (e.g., check/copy
files exist), removing the incorrect attrib call; adjust the Exec task
configuration where commandLine(...) is used to reflect this change.

Comment on lines +116 to +118
apk.set(
variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for bundle-related build configurations
echo "=== Searching for bundle configurations ==="
rg -i "bundle" --type gradle -C 2

echo ""
echo "=== Searching for regular APK build configurations ==="
rg -i "assemble|apk" --type gradle -C 2 | head -100

echo ""
echo "=== Checking build.gradle files in repository ==="
find . -name "build.gradle*" -type f | head -20

Repository: openMF/mobile-wallet

Length of output: 1206


🏁 Script executed:

#!/bin/bash
# Search gradle files directly without type restriction
echo "=== Searching for bundle configurations in gradle files ==="
rg -i "bundle" ./build.gradle.kts ./cmp-android/build.gradle.kts ./build-logic/convention/build.gradle.kts 2>/dev/null | head -50

echo ""
echo "=== Searching for APK/assemble configurations ==="
rg -i "apk|assemble" ./build.gradle.kts ./cmp-android/build.gradle.kts 2>/dev/null | head -50

echo ""
echo "=== Examining the Badging.kt file for context ==="
cat -n ./build-logic/convention/src/main/kotlin/org/convention/Badging.kt | head -130

echo ""
echo "=== Check if there are other artifact types referenced in the codebase ==="
rg "SingleArtifact\." ./build-logic/convention/src/main/kotlin/org/convention/ 2>/dev/null

Repository: openMF/mobile-wallet

Length of output: 6133


🏁 Script executed:

#!/bin/bash
# Check the main build.gradle.kts to understand the project structure
echo "=== Examining main build.gradle.kts ==="
cat ./build.gradle.kts | head -100

echo ""
echo "=== Checking cmp-android build configuration ==="
cat ./cmp-android/build.gradle.kts | head -150

echo ""
echo "=== Check for bundle release configuration in cmp-android ==="
rg -i "release|bundle" ./cmp-android/build.gradle.kts | head -20

Repository: openMF/mobile-wallet

Length of output: 7944


🏁 Script executed:

#!/bin/bash
# Check if there's any bundle configuration elsewhere
echo "=== Search for 'bundle' keyword across entire codebase ==="
rg "bundle" --type kotlin -C 2 | head -50

echo ""
echo "=== Check the entire Badging.kt file to understand context ==="
wc -l ./build-logic/convention/src/main/kotlin/org/convention/Badging.kt

echo ""
echo "=== View the rest of Badging.kt (lines 130-200) ==="
sed -n '130,200p' ./build-logic/convention/src/main/kotlin/org/convention/Badging.kt

echo ""
echo "=== Check if variant.artifacts supports APK_FROM_BUNDLE or if APK should be used ==="
rg "APK_FROM_BUNDLE|SingleArtifact\.APK" --type kotlin -B 3 -A 3

Repository: openMF/mobile-wallet

Length of output: 8813


🏁 Script executed:

#!/bin/bash
# Check how the Badging tasks are integrated into the build
echo "=== Search for where configureBadgingTasks is called ==="
rg "configureBadgingTasks" --type kotlin -B 5 -A 5

echo ""
echo "=== Check if badging tasks are in the default build flow ==="
rg "configureBadgingTasks|GenerateBadgingTask|CheckBadgingTask" ./build-logic/convention/build.gradle.kts -C 3

echo ""
echo "=== Check the convention plugins to see if badging is always applied ==="
fd ".*\.kt$" ./build-logic/convention/src/main/kotlin/ | xargs rg "configureBadgingTasks" -l

Repository: openMF/mobile-wallet

Length of output: 5060


🏁 Script executed:

#!/bin/bash
# Check if there are any CI/CD workflows or gradle tasks that actually build bundles
echo "=== Search for bundleRelease or bundle tasks in gradle ==="
rg "bundleRelease|bundle" ./cmp-android/build.gradle.kts -i

echo ""
echo "=== Check if there's a separate module or flavor for bundle builds ==="
rg "bundle" ./cmp-android/build.gradle.kts -C 5

echo ""
echo "=== Check the AndroidApplicationConventionPlugin more carefully ==="
cat ./build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt | head -80

echo ""
echo "=== Check if both Badging.kt files are identical (convention vs mifospay) ==="
diff ./build-logic/convention/src/main/kotlin/org/convention/Badging.kt ./build-logic/convention/src/main/kotlin/org/mifospay/Badging.kt | head -50

Repository: openMF/mobile-wallet

Length of output: 2718


🏁 Script executed:

#!/bin/bash
# Let me check if the project actually produces APK_FROM_BUNDLE artifacts
echo "=== Check if the app's build actually creates APK_FROM_BUNDLE artifacts ==="
rg "APK_FROM_BUNDLE" --type kotlin -B 3 -A 3

echo ""
echo "=== Verify what artifact types are available for main variants (not test) ==="
# Search for Android Gradle Plugin documentation usage
rg "SingleArtifact\." --type kotlin | grep -v androidTest | head -20

echo ""
echo "=== Check if there are any conditional checks or try-catch around APK_FROM_BUNDLE ==="
sed -n '100,156p' ./build-logic/convention/src/main/kotlin/org/convention/Badging.kt

echo ""
echo "=== Check git history to see if this is newly added or changed ==="
git log --oneline -n 20 -- ./build-logic/convention/src/main/kotlin/org/convention/Badging.kt 2>/dev/null | head -10

Repository: openMF/mobile-wallet

Length of output: 4393


Use SingleArtifact.APK instead of SingleArtifact.APK_FROM_BUNDLE for regular APK builds.

The project builds regular APKs (not Android App Bundles), as evidenced by the absence of any bundleRelease or bundle-related configuration. The current code uses SingleArtifact.APK_FROM_BUNDLE, which only provides APKs extracted from bundles and will not wire correctly for standard APK builds. Change to SingleArtifact.APK at line 117.

🤖 Prompt for AI Agents
In @build-logic/convention/src/main/kotlin/org/convention/Badging.kt around
lines 116 - 118, Replace the artifact reference used when wiring APK outputs:
change the call that sets apk via
variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE) to use SingleArtifact.APK
instead so apk.set(...) points to regular APK outputs; update the reference in
the apk.set(...) line where variant.artifacts.get(...) is used to use
SingleArtifact.APK in place of SingleArtifact.APK_FROM_BUNDLE.

Comment on lines +61 to +66
internal fun Project.configureJacoco(
androidComponentsExtension: AndroidComponentsExtension<*, *, *>,
) {
configure<JacocoPluginExtension> {
toolVersion = libs.findVersion("jacoco").get().toString()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify that the jacoco version is defined in the version catalog
# Expected: A 'jacoco' version entry should exist

fd -t f 'libs.versions.toml' --exec cat {} | grep -i 'jacoco'

Repository: openMF/mobile-wallet

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check if libs.versions.toml exists
fd -t f 'libs.versions.toml'

# List gradle/build configuration files
echo "=== Gradle files ==="
fd -t f '\.(gradle|toml)$' | head -20

# Search for any jacoco references in the codebase
echo "=== JaCoCo references ==="
rg -i 'jacoco' --type gradle --type toml --type kotlin | head -30

Repository: openMF/mobile-wallet

Length of output: 2188


🏁 Script executed:

cat gradle/libs.versions.toml

Repository: openMF/mobile-wallet

Length of output: 28053


Add JaCoCo version to the version catalog.

The code references libs.findVersion("jacoco") but no jacoco version entry exists in gradle/libs.versions.toml. This will cause a NoSuchElementException at runtime when .get() is called on an empty Optional. Define the jacoco version in the version catalog under the [versions] section.

🤖 Prompt for AI Agents
In @build-logic/convention/src/main/kotlin/org/convention/Jacoco.kt around lines
61 - 66, The Project.configureJacoco code calls libs.findVersion("jacoco").get()
which will throw if the version key is missing; add a "jacoco" entry to the
version catalog (gradle/libs.versions.toml) under the [versions] section (e.g.
jacoco = "0.8.8" or your desired version) so libs.findVersion("jacoco") returns
a value for configure<JacocoPluginExtension> in configureJacoco.

Comment on lines +156 to +184
private fun createNewFastlaneConfig(configFile: File, config: KeystoreConfig, keystoreName: String) {
val content = """
module FastlaneConfig
module AndroidConfig
STORE_CONFIG = {
default_store_file: "$keystoreName",
default_store_password: "${config.keystorePassword}",
default_key_alias: "${config.keyAlias}",
default_key_password: "${config.keyPassword}"
}

FIREBASE_CONFIG = {
firebase_prod_app_id: "1:728433984912738:android:3902eb32kjaska3363b0938f1a1dbb",
firebase_demo_app_id: "1:72843493212738:android:8392hjks3298ak9032skja",
firebase_service_creds_file: "secrets/firebaseAppDistributionServiceCredentialsFile.json",
firebase_groups: "mifos-mobile-apps"
}

BUILD_PATHS = {
prod_apk_path: "cmp-android/build/outputs/apk/prod/release/cmp-android-prod-release.apk",
demo_apk_path: "cmp-android/build/outputs/apk/demo/release/cmp-android-demo-release.apk",
prod_aab_path: "cmp-android/build/outputs/bundle/prodRelease/cmp-android-prod-release.aab"
}
end
end
""".trimIndent()

configFile.writeText(content)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check if .gitignore exists and what it contains regarding fastlane
cat .gitignore | grep -i fastlane || echo "No fastlane entries found"

Repository: openMF/mobile-wallet

Length of output: 83


🏁 Script executed:

# Check if there are any references to the actual fastlane config directory
rg -i "fastlane" --type kotlin -A 2 -B 2

Repository: openMF/mobile-wallet

Length of output: 15074


🏁 Script executed:

# Also check what the actual output path is in the code
rg "configFile" build-logic/convention/src/main/kotlin/org/convention/keystore/ConfigurationFileUpdatesTask.kt -B 5 -A 5

Repository: openMF/mobile-wallet

Length of output: 2708


🏁 Script executed:

# Check entire .gitignore for any patterns that might match fastlane-config
cat .gitignore

Repository: openMF/mobile-wallet

Length of output: 1302


Add fastlane-config/ directory to .gitignore to prevent credential exposure.

The fastlane config file writes plaintext keystore passwords to fastlane-config/android_config.rb, which is not currently gitignored and risks accidental version control commits. Add fastlane-config/ to .gitignore immediately.

🤖 Prompt for AI Agents
In
@build-logic/convention/src/main/kotlin/org/convention/keystore/ConfigurationFileUpdatesTask.kt
around lines 156 - 184, Add the fastlane-config directory to .gitignore to
prevent committing plaintext credentials: update the repo .gitignore to include
"fastlane-config/" (or "fastlane-config/**") so files written by
createNewFastlaneConfig (which writes the Android Fastlane config via
configFile.writeText) are ignored; commit the .gitignore change and verify that
fastlane-config/android_config.rb is no longer tracked (remove from git index if
already committed using git rm --cached) before merging.

Comment on lines +177 to +191
private fun buildKeytoolCommand(config: KeystoreConfig, keystorePath: File): List<String> {
return listOf(
"keytool",
"-genkey",
"-v",
"-keystore", keystorePath.absolutePath,
"-alias", config.keyAlias,
"-keyalg", config.keyAlgorithm,
"-keysize", config.keySize.toString(),
"-validity", (config.validity * 365).toString(), // Convert years to days
"-storepass", config.keystorePassword,
"-keypass", config.keyPassword,
"-dname", config.distinguishedName
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

keytool -storepass env file password input methods

💡 Result:

Options for supplying a keystore password to keytool

  • Direct on command line (not recommended — visible in process list/shell history):
    keytool ... -storepass MyPassword

  • From an environment variable (recommended over plain CLI):
    keytool ... -storepass:env MY_VAR
    (keytool reads the env var named MY_VAR). [1]

  • From a file:
    keytool ... -storepass:file /path/to/passfile
    (keytool reads the file contents as the password). [1]

  • Interactive prompt:
    Omit -storepass and keytool will prompt you to type the password (no echo). [1]

Notes / best practices

  • Avoid plain CLI passwords (exposed in history / ps). Use -storepass:env or -storepass:file, or the interactive prompt. [1][2]
  • Ensure file permissions or environment handling keep secrets protected (e.g., restrict file mode, avoid exporting vars in shared shells). [2]

Sources: Oracle keytool docs and community examples. [1][2]


🏁 Script executed:

# First, let's examine the file context to understand how passwords are handled
cd build-logic/convention/src/main/kotlin/org/convention/keystore && wc -l KeystoreGenerationTask.kt

Repository: openMF/mobile-wallet

Length of output: 93


🏁 Script executed:

# Examine the file to understand the password handling context
cat -n build-logic/convention/src/main/kotlin/org/convention/keystore/KeystoreGenerationTask.kt | head -200 | tail -100

Repository: openMF/mobile-wallet

Length of output: 4706


🏁 Script executed:

# Also check how this function is called and where passwords come from
rg -B5 -A5 "buildKeytoolCommand" build-logic/convention/src/main/kotlin/org/convention/keystore/

Repository: openMF/mobile-wallet

Length of output: 2696


🏁 Script executed:

# Check the executeKeytoolCommand function to see how the command is executed
cat -n build-logic/convention/src/main/kotlin/org/convention/keystore/KeystoreGenerationTask.kt | sed -n '196,230p'

Repository: openMF/mobile-wallet

Length of output: 1659


🏁 Script executed:

# Check KeystoreConfig to understand where passwords come from
rg -B3 -A10 "class KeystoreConfig" build-logic/convention/src/main/kotlin/org/convention/keystore/

Repository: openMF/mobile-wallet

Length of output: 1718


Security: Passwords exposed in command-line arguments and build logs.

Passwords passed directly as command-line arguments (-storepass, -keypass) are exposed in process listings (ps aux) and build logs. Line 198 logs the complete command including passwords via logInfo(). Use keytool's -storepass:env or -storepass:file options instead, and mask sensitive values when logging commands.

Comment on lines +196 to +199
private fun executeKeytoolCommand(command: List<String>): Boolean {
return try {
logInfo("Command: ${command.joinToString(" ") { if (it.contains(" ")) "\"$it\"" else it }}")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Logging passwords in plain text.

Line 198 logs the entire command including -storepass and -keypass values. This will expose keystore passwords in build logs.

🔎 Proposed fix - mask sensitive arguments
 private fun executeKeytoolCommand(command: List<String>): Boolean {
     return try {
-        logInfo("Command: ${command.joinToString(" ") { if (it.contains(" ")) "\"$it\"" else it }}")
+        // Mask password arguments in logs
+        val maskedCommand = command.mapIndexed { index, arg ->
+            val prevArg = command.getOrNull(index - 1)
+            if (prevArg in listOf("-storepass", "-keypass")) "****" else arg
+        }
+        logInfo("Command: ${maskedCommand.joinToString(" ") { if (it.contains(" ")) "\"$it\"" else it }}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun executeKeytoolCommand(command: List<String>): Boolean {
return try {
logInfo("Command: ${command.joinToString(" ") { if (it.contains(" ")) "\"$it\"" else it }}")
private fun executeKeytoolCommand(command: List<String>): Boolean {
return try {
// Mask password arguments in logs
val maskedCommand = command.mapIndexed { index, arg ->
val prevArg = command.getOrNull(index - 1)
if (prevArg in listOf("-storepass", "-keypass")) "****" else arg
}
logInfo("Command: ${maskedCommand.joinToString(" ") { if (it.contains(" ")) "\"$it\"" else it }}")
🤖 Prompt for AI Agents
In
@build-logic/convention/src/main/kotlin/org/convention/keystore/KeystoreGenerationTask.kt
around lines 196 - 199, The log currently prints the full command (including
sensitive `-storepass`/`-keypass` values) in executeKeytoolCommand; change the
logging to mask sensitive arguments by constructing a maskedCommand from the
command List<String> before logInfo: treat both "--flag=value" and separate
flag/value forms, replace the password value with a fixed mask (e.g. "****")
whenever an element startsWith("-storepass") or startsWith("-keypass") or
immediately follows those flags, and then call logInfo with the maskedCommand
instead of the raw command to avoid exposing keystore passwords.

import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.Project

const val ktlintVersion = "1.0.1"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

ktlint latest version 2025 stable release

💡 Result:

The latest stable ktlint release is 1.8.0 (released Nov 14, 2025). [1][2]

Sources:
[1] GitHub Releases — pinterest/ktlint (1.8.0).
[2] Maven / mvnrepository listing for ktlint artifacts (shows 1.8.0 as the newest version).


🌐 Web query:

ktlint 1.0.1 security vulnerabilities CVE

💡 Result:

Short answer: ktlint 1.0.1 has no CVE assigned to the ktlint project itself, but scanners/metadata flag a dependency vulnerability (CVE-2023-6378 — a Logback deserialization/DOS issue). The older ktlint CVE (CVE-2019-1010260) affected ktlint versions before 0.30.0 and therefore does NOT apply to 1.0.1. [1][2][3]

Sources:

  • mvnrepository entry for ktlint-cli 1.0.1 (lists CVE-2023-6378 from dependencies). [1]
  • NVD entry for CVE-2019-1010260 (ktlint custom-ruleset MITM → RCE; fixed in 0.30.0+). [2]
  • NVD / vendor pages for CVE-2023-6378 (Logback receiver deserialization DoS). [3]

Upgrade ktlint from 1.0.1 to 1.8.0.

ktlint 1.0.1 is significantly outdated and has a transitive dependency vulnerability (CVE-2023-6378 in Logback, a deserialization/DoS issue). The latest stable release is 1.8.0.

🤖 Prompt for AI Agents
In @build-logic/convention/src/main/kotlin/org/convention/Spotless.kt at line 6,
Update the ktlint version constant to remove the vulnerable old release: change
the const val ktlintVersion (in Spotless.kt) from "1.0.1" to "1.8.0", then
refresh dependencies/lockfiles and run the build/spotless checks to ensure
compatibility and resolve any formatter rule changes from the upgrade.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
@build-logic/convention/src/test/kotlin/org/convention/keystore/SecretsEnvUpdateTaskTest.kt:
- Around line 3-4: Replace the internal TestNG imports with JUnit 5 assertions
by removing imports of org.gradle.internal.impldep.org.testng.Assert and
importing org.junit.jupiter.api.Assertions; update all assertion calls in
SecretsEnvUpdateTaskTest (e.g., usages of assertEquals and assertTrue) to use
Assertions.assertEquals/assertTrue and, for assertEquals, swap the argument
order from (actual, expected, message) to (expected, actual, message) to match
JUnit 5's signature; ensure static import or fully-qualified References to
Assertions are consistent across the test file.
🧹 Nitpick comments (1)
build-logic/convention/src/test/kotlin/org/convention/keystore/ConfigurationFileUpdatesTaskTest.kt (1)

269-363: Test double doesn't test the actual task and misses critical error-recovery logic.

The TestConfigurationFileUpdatesTask reimplements helper methods instead of testing the real ConfigurationFileUpdatesTask. More critically, the test's testUpdateGradleConfig() is missing the backup-and-restore mechanism present in the actual implementation (lines 214–250 of the real task), meaning backup file failures go undetected.

Recommended approaches:

  1. Test the actual task via Gradle's ProjectBuilder instead of a duplicate helper class
  2. Extract the configuration update logic into a utility class that both the real task and tests can use
  3. Ensure test coverage includes the backup/recovery error path
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7a009a and 41a46c2.

📒 Files selected for processing (7)
  • build-logic/convention/build.gradle.kts
  • build-logic/convention/src/main/kotlin/KMPRoomConventionPlugin.kt
  • build-logic/convention/src/test/kotlin/org/convention/keystore/ConfigurationFileUpdatesTaskTest.kt
  • build-logic/convention/src/test/kotlin/org/convention/keystore/KeystoreGenerationTaskTest.kt
  • build-logic/convention/src/test/kotlin/org/convention/keystore/SecretsEnvParserTest.kt
  • build-logic/convention/src/test/kotlin/org/convention/keystore/SecretsEnvUpdateTaskTest.kt
  • gradle/libs.versions.toml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: PR Checks / Static Analysis Check
🔇 Additional comments (17)
build-logic/convention/src/test/kotlin/org/convention/keystore/SecretsEnvUpdateTaskTest.kt (2)

34-51: LGTM!

The test setup is well-structured, using ProjectBuilder for Gradle integration testing and @TempDir for isolated file system operations. The configuration of task properties is clear and follows good testing practices.


376-437: LGTM!

The helper methods are well-designed for test isolation. The base64 validation helpers (isValidBase64, isValidBase64Line, extractBase64Content) provide good utilities for verifying the encoding output, and the file creation helpers set up realistic test scenarios.

build-logic/convention/src/test/kotlin/org/convention/keystore/ConfigurationFileUpdatesTaskTest.kt (2)

25-44: LGTM!

The test setup creates a comprehensive KeystoreConfig with all relevant fields populated. The use of @TempDir for file isolation and proper cleanup via the annotation is appropriate.


184-202: LGTM!

Good validation coverage testing multiple error conditions in a single configuration. The test verifies that all expected validation messages are present for invalid inputs (empty password/alias, small key size, negative validity, invalid country code).

build-logic/convention/src/test/kotlin/org/convention/keystore/KeystoreGenerationTaskTest.kt (3)

22-29: LGTM!

The setup correctly uses ProjectBuilder to create a Gradle project context and registers the actual KeystoreGenerationTask for testing. This approach ensures tests exercise the real implementation rather than a test double.


91-157: LGTM!

Excellent test coverage for loading configuration from secrets. The test validates all expected fields for both ORIGINAL and UPLOAD keystore configurations, including passwords, aliases, DN components, cryptographic parameters, and the overwrite flag.


243-275: LGTM!

Good validation testing that covers both valid and invalid configurations. The test properly checks for specific error messages using any { it.contains(...) } which is flexible enough to handle message variations while still verifying the error types.

build-logic/convention/src/test/kotlin/org/convention/keystore/SecretsEnvParserTest.kt (4)

24-28: LGTM!

Simple and clean test setup. Using a default SecretsConfig() is appropriate since individual tests can override configuration as needed.


151-170: LGTM!

Good error handling coverage testing both empty delimiter (BAD_KEY<<) and unterminated block scenarios. The assertions properly verify that specific error messages are generated for each failure mode.


360-382: LGTM!

This is an important edge case test. The parser correctly treats everything between the outer OUTER delimiters as literal content, including what looks like nested heredoc syntax. This matches expected shell heredoc behavior where the content is opaque until the terminating delimiter.


339-358: LGTM!

Comprehensive coverage of malformed input scenarios. The test verifies that the parser:

  • Successfully parses valid lines
  • Handles empty values (key_without_value=)
  • Reports errors for truly malformed lines (missing key, no equals sign)
  • Continues parsing after encountering errors (partial parse with error reporting)
build-logic/convention/src/main/kotlin/KMPRoomConventionPlugin.kt (1)

1-17: LGTM! Clean plugin setup.

The plugin structure and KSP configuration for Room are correct.

gradle/libs.versions.toml (2)

206-209: LGTM! Library declarations follow standard conventions.

The library declarations are properly structured and correctly reference the version catalog entries.

Also applies to: 242-250


42-46: Consider upgrading Jackson and OkHttp to current releases; JUnit version is safe but behind latest.

These versions exist and are valid. However:

  • JUnit 5.13.4 is behind the latest (6.0.2, released Jan 6, 2026), but is safe: it includes the fix for CVE-2025-53103 that affected 5.12.0–5.13.1.
  • Jackson 2.19.2 (latest 2.x: 2.20.1) addressed CVEs in the 2.19 line; consider upgrading if compatible.
  • OkHttp 4.12.0 (latest: 5.3.0) lacks fixes for upstream vulnerabilities (okio CVE-2023-3635, brotli decompression-bomb DoS); upgrade recommended if compatible.

Likely an incorrect or invalid review comment.

build-logic/convention/build.gradle.kts (3)

32-49: LGTM! Dependencies are correctly configured.

  • Room gradle plugin correctly added as compileOnly
  • Keystore management dependencies properly declared as implementation
  • JUnit 5 test dependencies appropriately configured

57-63: LGTM! Test configuration is correct.

JUnit Platform is properly configured with useful test logging.


131-143: Plugin implementation classes are properly defined.

Both plugin registrations reference existing implementation classes in build-logic/convention/src/main/kotlin/:

  • KMPRoomConventionPlugin.kt
  • KeystoreManagementConventionPlugin.kt

The registrations are correctly structured.

Comment on lines +26 to +41
dependencies {
// Adding ksp dependencies for multiple platforms
"implementation"(libs.findLibrary("androidx.room.ktx").get())
listOf(
"kspDesktop",
"kspAndroid",
"kspIosArm64",
"kspIosX64",
"kspIosSimulatorArm64",
// Add any other platform you may support
).forEach { platform ->
add(platform, libs.findLibrary("androidx.room.compiler").get())
// Kotlin Extensions and Coroutines support for Room
// add(platform, libs.findLibrary("androidx.room.ktx").get())
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Android-specific dependency in KMP context will break non-Android builds.

Line 28 adds androidx.room.ktx as a general "implementation" dependency, but this is Android-specific and will cause failures on other KMP targets (Desktop, iOS).

In KMP projects, platform-specific dependencies must be added conditionally. The Room KTX extension should only be added for Android.

🔧 Proposed fix for platform-specific dependencies
 dependencies {
     // Adding ksp dependencies for multiple platforms
-    "implementation"(libs.findLibrary("androidx.room.ktx").get())
     listOf(
         "kspDesktop",
         "kspAndroid",
         "kspIosArm64",
         "kspIosX64",
         "kspIosSimulatorArm64",
         // Add any other platform you may support
     ).forEach { platform ->
         add(platform, libs.findLibrary("androidx.room.compiler").get())
-//                    Kotlin Extensions and Coroutines support for Room
-//                    add(platform, libs.findLibrary("androidx.room.ktx").get())
+        // Add Room KTX only for Android (it's Android-specific)
+        if (platform == "kspAndroid") {
+            add("androidMainImplementation", libs.findLibrary("androidx.room.ktx").get())
+        }
     }
 }

Alternatively, configure it using Kotlin Multiplatform source sets:

// After the dependencies block, configure source sets
val commonMain = extensions.getByType<KotlinMultiplatformExtension>()
    .sourceSets.getByName("commonMain")
val androidMain = extensions.getByType<KotlinMultiplatformExtension>()
    .sourceSets.getByName("androidMain")

androidMain.dependencies {
    implementation(libs.findLibrary("androidx.room.ktx").get())
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dependencies {
// Adding ksp dependencies for multiple platforms
"implementation"(libs.findLibrary("androidx.room.ktx").get())
listOf(
"kspDesktop",
"kspAndroid",
"kspIosArm64",
"kspIosX64",
"kspIosSimulatorArm64",
// Add any other platform you may support
).forEach { platform ->
add(platform, libs.findLibrary("androidx.room.compiler").get())
// Kotlin Extensions and Coroutines support for Room
// add(platform, libs.findLibrary("androidx.room.ktx").get())
}
}
dependencies {
// Adding ksp dependencies for multiple platforms
listOf(
"kspDesktop",
"kspAndroid",
"kspIosArm64",
"kspIosX64",
"kspIosSimulatorArm64",
// Add any other platform you may support
).forEach { platform ->
add(platform, libs.findLibrary("androidx.room.compiler").get())
}
}
// Add Room KTX only for Android using proper KMP sourceSet configuration
extensions.getByType<KotlinMultiplatformExtension>().apply {
sourceSets.getByName("androidMain").dependencies {
implementation(libs.findLibrary("androidx.room.ktx").get())
}
}

Comment on lines +3 to +4
import org.gradle.internal.impldep.org.testng.Assert.assertEquals
import org.gradle.internal.impldep.org.testng.Assert.assertTrue
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid using internal Gradle APIs for assertions.

The imports org.gradle.internal.impldep.org.testng.Assert reference internal Gradle implementation classes that are not part of the public API and may change or be removed without notice. Use org.junit.jupiter.api.Assertions instead, which is the standard for JUnit 5 and is already used in other test files in this PR.

Proposed fix
-import org.gradle.internal.impldep.org.testng.Assert.assertEquals
-import org.gradle.internal.impldep.org.testng.Assert.assertTrue
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue

Note: After switching to JUnit 5 assertions, the argument order for assertEquals changes from (actual, expected, message) to (expected, actual, message). You'll need to swap the first two arguments in calls like:

-assertEquals(originalKeystoreFile.readBytes().toList(), originalDecoded.toList(), 
-    "Decoded original keystore should match original file")
+assertEquals(originalDecoded.toList(), originalKeystoreFile.readBytes().toList(), 
+    "Decoded original keystore should match original file")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import org.gradle.internal.impldep.org.testng.Assert.assertEquals
import org.gradle.internal.impldep.org.testng.Assert.assertTrue
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
🤖 Prompt for AI Agents
In
@build-logic/convention/src/test/kotlin/org/convention/keystore/SecretsEnvUpdateTaskTest.kt
around lines 3 - 4, Replace the internal TestNG imports with JUnit 5 assertions
by removing imports of org.gradle.internal.impldep.org.testng.Assert and
importing org.junit.jupiter.api.Assertions; update all assertion calls in
SecretsEnvUpdateTaskTest (e.g., usages of assertEquals and assertTrue) to use
Assertions.assertEquals/assertTrue and, for assertEquals, swap the argument
order from (actual, expected, message) to (expected, actual, message) to match
JUnit 5's signature; ensure static import or fully-qualified References to
Assertions are consistent across the test file.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
core-base/ui/README.md (2)

388-405: Update code examples to provide required context parameter to rememberImageLoader() and rememberImageRequest().

Both the CircularProfileImage and BackgroundImage examples are broken. The functions rememberImageLoader(context: PlatformContext) and rememberImageRequest(context: PlatformContext, url: String) require a context parameter, but the examples either don't pass it or reference an undefined context variable.

🐛 Fixes required

For CircularProfileImage (lines 388-405):

 @Composable
 fun CircularProfileImage(url: String, size: Dp = 48.dp) {
+    val context = LocalPlatformContext.current
-    val imageLoader = rememberImageLoader()
+    val imageLoader = rememberImageLoader(context)
 
     AsyncImage(
         model = rememberImageRequest(context, url),

For BackgroundImage (lines 411-432):

 @Composable
 fun BackgroundImage(url: String, overlay: Color = Color.Black.copy(alpha = 0.3f)) {
+    val context = LocalPlatformContext.current
-    val imageLoader = rememberImageLoader()
+    val imageLoader = rememberImageLoader(context)
 
     Box {
         AsyncImage(
             model = rememberImageRequest(context, url),

411-431: Both rememberImageLoader() and rememberImageRequest() require context parameter, but none is provided.

Line 414 calls rememberImageLoader() without the required PlatformContext parameter, and line 417 references an undefined context variable in rememberImageRequest(). Both function signatures require a context parameter that must be retrieved from LocalPlatformContext.current:

🐛 Proposed fix
 @Composable
 fun BackgroundImage(url: String, overlay: Color = Color.Black.copy(alpha = 0.3f)) {
+    val context = LocalPlatformContext.current
-    val imageLoader = rememberImageLoader()
+    val imageLoader = rememberImageLoader(context)
 
     Box {
         AsyncImage(
             model = rememberImageRequest(context, url),
cmp-android/src/main/AndroidManifest.xml (1)

14-16: Re-add the android.permission.CAMERA permission to the manifest.

The codebase uses CameraX (androidx.camera library) extensively for QR code scanning in feature/qr, not intent-based camera access as assumed. CameraX requires the android.permission.CAMERA permission to function. Without it, the camera will fail to initialize and the QR scanning feature will not work. The optional hardware feature declaration does not eliminate the need for the runtime permission.

🤖 Fix all issues with AI agents
In @build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt:
- Line 42: KMPLibraryConventionPlugin currently adds the
kotlinx.serialization.json dependency but does not apply the Kotlin
serialization compiler plugin, causing @Serializable uses to fail; update
KMPLibraryConventionPlugin to also apply the
"org.jetbrains.kotlin.plugin.serialization" plugin when configuring the KMP
convention (or alternatively remove the add("commonMainImplementation",
libs.findLibrary("kotlinx.serialization.json").get()) line and require modules
to opt into both the dependency and plugin themselves); locate
KMPLibraryConventionPlugin and either call the Kotlin serialization plugin
application in its setup or remove the serialization dependency entry to keep
plugin/dependency inclusion consistent.

In @cmp-android/src/prod/AndroidManifest.xml:
- Around line 16-19: The prod manifest enables
google_analytics_adid_collection_enabled via the meta-data tag
(android:name="google_analytics_adid_collection_enabled") but the main manifest
intentionally strips the AD_ID permission; to resolve, change the prod manifest
meta-data android:value to "false" to match the main manifest intent (or
alternatively remove the AD_ID removal in the main manifest and add proper
disclosure if you genuinely need ad ID collection); update the meta-data entry
in the prod manifest (google_analytics_adid_collection_enabled) or adjust the
AD_ID permission removal to keep manifest behavior consistent and avoid runtime
failures.

In
@core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt:
- Around line 84-96: The SMS URL in sendViaSMS is using "&" between the
recipient and query string; change it to use "?" so the body query parameter is
valid: update the smsUrl construction in sendViaSMS (where smsUrl is built) to
use "sms:$number?body=$encodedMessage" when number is present and
"sms:?body=$encodedMessage" when empty, keeping the existing encode function and
the subsequent
NSURL.URLWithString/UIApplication.sharedApplication.canOpenURL/openURL logic
intact.
🧹 Nitpick comments (6)
core-base/ui/src/jsCommonMain/kotlin/template/core/base/ui/ShareUtils.kt (1)

46-62: Document or implement the empty JS platform functions.

All six utility functions are empty stubs with no implementation. If these operations are not supported on the JS/web platform, consider:

  • Adding KDoc comments explaining why they're no-ops
  • Throwing NotImplementedError to make the limitation explicit to callers
  • Implementing web-based alternatives where possible (e.g., openUrl could use window.open())

Silent no-ops may confuse developers who expect these functions to work.

core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt (1)

69-82: Extract the duplicate encode function to reduce code duplication.

The local encode function is defined identically in both sendEmail (line 70) and sendViaSMS (line 85). Extract it as a private top-level function or extension to avoid duplication.

♻️ Proposed refactor

Add this private function before the ShareUtils object:

private fun String.encodeForUrl(): String = 
    replace(" ", "%20").replace("\n", "%0A")

Then use it in both functions:

 actual fun sendEmail(to: String, subject: String?, body: String?) {
-    fun encode(s: String): String = s.replace(" ", "%20").replace("\n", "%0A")
-    val encodedSubject = subject?.let { encode(it) } ?: ""
-    val encodedBody = body?.let { encode(it) } ?: ""
+    val encodedSubject = subject?.encodeForUrl() ?: ""
+    val encodedBody = body?.encodeForUrl() ?: ""
     // ... rest of implementation
 }
core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt (1)

47-110: Consider using structured logging instead of println.

Error messages are printed to console using println (lines 53, 67, 82, 98, 108). For production code, consider using a proper logging framework (e.g., SLF4J, kotlin-logging) to:

  • Enable log level control
  • Support structured logging and filtering
  • Facilitate debugging in production environments
core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt (2)

108-116: Remove redundant let block.

Line 110 uses url.let { url.toUri() }, but the lambda doesn't use the it parameter and directly references url. This can be simplified to just url.toUri().

♻️ Proposed refactor
     actual fun openUrl(url: String) {
         val context = ShareUtils.activityProvider.invoke().application.baseContext
-        val uri = url.let { url.toUri() }
+        val uri = url.toUri()
         val intent = Intent(Intent.ACTION_VIEW).apply {
             data = uri
             addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
         }
         context.startActivity(intent)
     }

108-173: Consider adding error handling for Intent operations.

None of the new functions handle ActivityNotFoundException that could be thrown if no app is available to handle the intent. While this might be acceptable for the current use case, consider wrapping startActivity calls in try-catch blocks to prevent crashes.

Example:

try {
    context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
    Log.w("ShareUtils", "No activity found to handle intent: ${e.message}")
}
cmp-android/src/prod/AndroidManifest.xml (1)

21-25: Clarify the double-negative configuration naming.

The comment says "Enable Firebase analytics for prod builds" but the metadata name is firebase_analytics_collection_deactivated set to false. While the logic is correct (false = not deactivated = enabled), the double-negative is confusing.

Consider adding a clarifying comment such as:

<!-- Enable Firebase analytics for `prod` builds (deactivated=false means enabled) -->
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41a46c2 and 7ddb9a2.

📒 Files selected for processing (15)
  • build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
  • build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
  • build-logic/convention/src/main/kotlin/KMPKoinConventionPlugin.kt
  • build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt
  • cmp-android/src/main/AndroidManifest.xml
  • cmp-android/src/prod/AndroidManifest.xml
  • cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/hekmatullahamin.xcuserdatad/UserInterfaceState.xcuserstate
  • cmp-web/build.gradle.kts
  • core-base/ui/README.md
  • core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt
  • core-base/ui/src/commonMain/kotlin/template/core/base/ui/ShareUtils.kt
  • core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt
  • core-base/ui/src/jsCommonMain/kotlin/template/core/base/ui/ShareUtils.kt
  • core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt
✅ Files skipped from review due to trivial changes (3)
  • build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
  • build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
  • build-logic/convention/src/main/kotlin/KMPKoinConventionPlugin.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: PR Checks / Static Analysis Check
🔇 Additional comments (5)
cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt (1)

1-3082: This is a generated dependency tree artifact—verify actual dependency declarations in build.gradle files.

This file documents the resolved Gradle dependency graph for the prodReleaseRuntimeClasspath configuration. The marked changes show kotlinx-serialization-json:1.8.1 being consistently added across numerous modules (core:common, core:datastore, core:model, core-base:network, and all feature modules).

Key observations:

  • No dependency conflicts or resolution issues are evident; version convergence is clean.
  • The serialization library addition aligns with the PR objective to include template modules.

Recommendations:

  1. Verify that the actual dependency declarations (e.g., build.gradle.kts or gradle.toml) in each module have been updated to explicitly declare kotlinx-serialization-json:1.8.1. This generated tree is a consequence, not the source.
  2. Check whether this file should be committed to version control. Dependency trees can often be regenerated on-demand via ./gradlew dependencies, so verify whether keeping this in the repo adds value or introduces merge-conflict noise.
  3. Run ./gradlew check (as mentioned in the PR checklist) to ensure the new dependencies don't introduce compilation or test failures.
cmp-web/build.gradle.kts (1)

53-54: No issues with the Koin and Ktor client dependencies added at lines 53–54.

libs.koin.core is actively used via initKoin() in both jsMain and wasmJsMain entry points. libs.ktor.client.js correctly provides the JavaScript engine; ktor-client-core is already available through the transitive dependency chain via core-base/network (which exposes it as an api dependency), and the same Js engine is properly configured for both JS and WASM targets in core-base/network/src/jsMain/ and core-base/network/src/wasmJsMain/.

core-base/ui/src/commonMain/kotlin/template/core/base/ui/ShareUtils.kt (1)

43-87: LGTM! Well-documented API surface.

The expect declarations are clear, well-documented, and provide a consistent cross-platform API for utility operations. The KDoc comments effectively explain the purpose and parameters of each function.

cmp-android/src/main/AndroidManifest.xml (1)

69-76: Firebase configuration pattern looks good.

The default-disabled configuration for Firebase analytics and Crashlytics in the main manifest, with the prod variant overriding to enable them, follows a good privacy-conscious pattern. This ensures analytics/crash reporting is only active in production builds.

build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt (1)

12-14: LGTM!

Clear and concise documentation that accurately describes the plugin's purpose.

}

dependencies {
add("commonMainImplementation", libs.findLibrary("kotlinx.serialization.json").get())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for serialization plugin application in convention plugins
rg -n "plugin.serialization|kotlin.*serialization" build-logic/convention/src/ -A 2 -B 2

Repository: openMF/mobile-wallet

Length of output: 2069


🏁 Script executed:

#!/bin/bash
# Read the full KMPLibraryConventionPlugin.kt file to see context around line 42
wc -l build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt

Repository: openMF/mobile-wallet

Length of output: 135


🏁 Script executed:

#!/bin/bash
# List all convention plugin files to check for serialization plugin elsewhere
fd -e ".kt" build-logic/convention/src/main/kotlin/ | head -20

Repository: openMF/mobile-wallet

Length of output: 525


🏁 Script executed:

#!/bin/bash
cat -n build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt

Repository: openMF/mobile-wallet

Length of output: 2229


🏁 Script executed:

#!/bin/bash
# Also read the CMPFeatureConventionPlugin.kt to see the context
cat -n build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt

Repository: openMF/mobile-wallet

Length of output: 3905


🏁 Script executed:

#!/bin/bash
# Search for modules/build files that apply org.convention.kmp.library
rg -n "org.convention.kmp.library" --type kotlin --type gradle-kotlin

Repository: openMF/mobile-wallet

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Also search for plugins.id pattern in build files
fd -e ".gradle.kts" -e ".gradle" | head -30

Repository: openMF/mobile-wallet

Length of output: 1012


🏁 Script executed:

#!/bin/bash
# Search for org.convention.kmp.library in build files
rg -n "org.convention.kmp.library" .

Repository: openMF/mobile-wallet

Length of output: 401


🏁 Script executed:

#!/bin/bash
# Also search for which plugins are commonly applied
rg -n "id\(\"org.convention" . -A 1 -B 1 | head -50

Repository: openMF/mobile-wallet

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for modules that use org.convention.kmp.library directly
rg -n "kmp-library-convention|org\.convention\.kmp\.library" --type gradle-kts . 2>/dev/null || rg -n "kmp-library-convention|org\.convention\.kmp\.library" .

Repository: openMF/mobile-wallet

Length of output: 401


🏁 Script executed:

#!/bin/bash
# Check a few module build files to see what convention plugins they apply
head -20 core/common/build.gradle.kts

Repository: openMF/mobile-wallet

Length of output: 662


🏁 Script executed:

#!/bin/bash
# Check more module build files
head -20 core/data/build.gradle.kts

Repository: openMF/mobile-wallet

Length of output: 681


🏁 Script executed:

#!/bin/bash
# Check more module build files to see the pattern of plugin usage
for file in core/designsystem/build.gradle.kts core/domain/build.gradle.kts core/model/build.gradle.kts core/network/build.gradle.kts; do
  echo "=== $file ===" && head -20 "$file" && echo ""
done

Repository: openMF/mobile-wallet

Length of output: 2506


🏁 Script executed:

#!/bin/bash
# Count how many modules apply kotlin.serialization
rg -l "kotlin.serialization|kotlin\.serialization" . --glob "*/build.gradle.kts" | wc -l

Repository: openMF/mobile-wallet

Length of output: 65


🏁 Script executed:

#!/bin/bash
# List all modules that apply kmp-library-convention
rg -l "kmp.library.convention|kmp-library-convention" . --glob "*/build.gradle.kts"

Repository: openMF/mobile-wallet

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Get a complete list of modules by checking all build.gradle.kts files
find . -name "build.gradle.kts" -not -path "*/build-logic/*" -not -path "*/build/*" -not -path "*/.gradle/*" | sort

Repository: openMF/mobile-wallet

Length of output: 1668


🏁 Script executed:

#!/bin/bash
# Check which modules apply serialization plugin
for file in $(find . -name "build.gradle.kts" -not -path "*/build-logic/*" -not -path "*/build/*" -not -path "*/.gradle/*" | sort); do
  if grep -q "kotlin.serialization\|kotlinx-serialization" "$file" 2>/dev/null; then
    echo "$file: YES"
  else
    echo "$file: NO"
  fi
done

Repository: openMF/mobile-wallet

Length of output: 1866


🏁 Script executed:

#!/bin/bash
# Check the full plugin section of core/designsystem/build.gradle.kts since it was truncated
head -40 core/designsystem/build.gradle.kts

Repository: openMF/mobile-wallet

Length of output: 1294


🏁 Script executed:

#!/bin/bash
# Check core/domain which applies kmp-library-convention without serialization
cat -n core/domain/build.gradle.kts | head -30

Repository: openMF/mobile-wallet

Length of output: 1117


Add the Kotlin serialization plugin to KMPLibraryConventionPlugin.

The kotlinx-serialization-json dependency is added at line 42, but the org.jetbrains.kotlin.plugin.serialization plugin is not applied in this convention plugin. This creates a mismatch: modules that use KMPLibraryConventionPlugin (like core/domain, core/designsystem, core/ui) receive the dependency but lack the required compiler plugin. If any of these modules attempt to use @Serializable annotations, compilation will fail.

Either apply the serialization plugin here alongside the dependency, or remove the dependency and require it to be added explicitly in modules that need it (along with the plugin).

🤖 Prompt for AI Agents
In @build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt at line
42, KMPLibraryConventionPlugin currently adds the kotlinx.serialization.json
dependency but does not apply the Kotlin serialization compiler plugin, causing
@Serializable uses to fail; update KMPLibraryConventionPlugin to also apply the
"org.jetbrains.kotlin.plugin.serialization" plugin when configuring the KMP
convention (or alternatively remove the add("commonMainImplementation",
libs.findLibrary("kotlinx.serialization.json").get()) line and require modules
to opt into both the dependency and plugin themselves); locate
KMPLibraryConventionPlugin and either call the Kotlin serialization plugin
application in its setup or remove the serialization dependency entry to keep
plugin/dependency inclusion consistent.

Comment on lines +16 to +19
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="true"
tools:replace="android:value" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd AndroidManifest.xml | head -20

Repository: openMF/mobile-wallet

Length of output: 1081


🏁 Script executed:

cat -n cmp-android/src/main/AndroidManifest.xml

Repository: openMF/mobile-wallet

Length of output: 3474


🏁 Script executed:

cat -n cmp-android/src/prod/AndroidManifest.xml

Repository: openMF/mobile-wallet

Length of output: 1389


Critical conflict: Prod manifest enables AD_ID collection but permission is removed.

The main manifest explicitly removes the AD_ID permission (lines 25-27) with a clear comment stating that Firebase adds it automatically but the app doesn't use it to avoid Google Play rejection. It also disables google_analytics_adid_collection_enabled (line 66).

However, the prod variant overrides this and enables google_analytics_adid_collection_enabled="true" (lines 16-19), which requires the AD_ID permission that was removed. This will cause runtime failures when the analytics library attempts to collect ad IDs in production builds.

Recommended action: Either set android:value="false" in the prod manifest to align with the main manifest and its documented intent, or remove the AD_ID permission removal from the main manifest and add proper disclosure in the privacy policy if ad ID collection is actually needed.

🤖 Prompt for AI Agents
In @cmp-android/src/prod/AndroidManifest.xml around lines 16 - 19, The prod
manifest enables google_analytics_adid_collection_enabled via the meta-data tag
(android:name="google_analytics_adid_collection_enabled") but the main manifest
intentionally strips the AD_ID permission; to resolve, change the prod manifest
meta-data android:value to "false" to match the main manifest intent (or
alternatively remove the AD_ID removal in the main manifest and add proper
disclosure if you genuinely need ad ID collection); update the meta-data entry
in the prod manifest (google_analytics_adid_collection_enabled) or adjust the
AD_ID permission removal to keep manifest behavior consistent and avoid runtime
failures.

Comment on lines +84 to +96
actual fun sendViaSMS(number: String, message: String) {
fun encode(s: String): String = s.replace(" ", "%20").replace("\n", "%0A")
val encodedMessage = encode(message)
val smsUrl = if (number.isNotEmpty()) {
"sms:$number&body=$encodedMessage"
} else {
"sms:&body=$encodedMessage"
}
val url = NSURL.URLWithString(smsUrl)
if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
UIApplication.sharedApplication.openURL(url)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix incorrect SMS URL format.

Line 88 uses & as the separator between the phone number and the body parameter, but SMS URLs require ? for the query string separator (standard URL format).

🐛 Proposed fix
     actual fun sendViaSMS(number: String, message: String) {
         fun encode(s: String): String = s.replace(" ", "%20").replace("\n", "%0A")
         val encodedMessage = encode(message)
         val smsUrl = if (number.isNotEmpty()) {
-            "sms:$number&body=$encodedMessage"
+            "sms:$number?body=$encodedMessage"
         } else {
-            "sms:&body=$encodedMessage"
+            "sms:?body=$encodedMessage"
         }
         val url = NSURL.URLWithString(smsUrl)
         if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
             UIApplication.sharedApplication.openURL(url)
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
actual fun sendViaSMS(number: String, message: String) {
fun encode(s: String): String = s.replace(" ", "%20").replace("\n", "%0A")
val encodedMessage = encode(message)
val smsUrl = if (number.isNotEmpty()) {
"sms:$number&body=$encodedMessage"
} else {
"sms:&body=$encodedMessage"
}
val url = NSURL.URLWithString(smsUrl)
if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
UIApplication.sharedApplication.openURL(url)
}
}
actual fun sendViaSMS(number: String, message: String) {
fun encode(s: String): String = s.replace(" ", "%20").replace("\n", "%0A")
val encodedMessage = encode(message)
val smsUrl = if (number.isNotEmpty()) {
"sms:$number?body=$encodedMessage"
} else {
"sms:?body=$encodedMessage"
}
val url = NSURL.URLWithString(smsUrl)
if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
UIApplication.sharedApplication.openURL(url)
}
}
🤖 Prompt for AI Agents
In
@core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt
around lines 84 - 96, The SMS URL in sendViaSMS is using "&" between the
recipient and query string; change it to use "?" so the body query parameter is
valid: update the smsUrl construction in sendViaSMS (where smsUrl is built) to
use "sms:$number?body=$encodedMessage" when number is present and
"sms:?body=$encodedMessage" when empty, keeping the existing encode function and
the subsequent
NSURL.URLWithString/UIApplication.sharedApplication.canOpenURL/openURL logic
intact.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
settings.gradle.kts (2)

50-51: Pre-existing duplicate module include.

Line 51 duplicates the :core:network include from line 50. While this doesn't break the build, it should be cleaned up to avoid confusion.

Suggested fix
 include(":core:network")
-include(":core:network")

76-77: Pre-existing duplicate module include.

Line 77 duplicates the :feature:invoices include from line 76.

Suggested fix
 include(":feature:invoices")
-include(":feature:invoices")
cmp-android/dependencies/prodReleaseRuntimeClasspath.txt (1)

337-346: Align FileKit artifact versions — all modules must use the same version number.

FileKit publishes all modules (core, dialogs, coil, compose) with matching version numbers and they are designed to be used together. The current dependencies mix versions:

  • filekit-compose* at 0.8.8
  • filekit-coil*, filekit-core*, filekit-dialogs* at 0.10.0-beta04

Ensure all FileKit artifacts in the dependency tree use the same version. Additionally, avoid beta versions (0.10.0-beta04) in production runtime classpaths; use a stable release version for all FileKit modules.

🤖 Fix all issues with AI agents
In `@cmp-android/dependencies/prodReleaseRuntimeClasspath.txt`:
- Around line 312-315: Replace the RC artifacts with the stable 3.2.0 release:
update the entries for io.coil-kt.coil3:coil-compose-android and
io.coil-kt.coil3:coil-compose from 3.2.0-rc02 to 3.2.0 so all Coil artifacts
consistently use the stable 3.2.0 version in the runtime classpath.

In `@cmp-navigation/src/commonMain/composeResources/values/strings.xml`:
- Line 12: Replace the placeholder string value for the resource named
"app_name" in strings.xml (the <string name="app_name"> entry) with the real
application name used by this project (e.g., "MifosPay" or "Mobile Wallet"),
ensuring the displayed app title/branding throughout the UI is correct.

In
`@cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigation.kt`:
- Around line 1-35: Implement the missing composable
AuthenticatedNavbarNavigationScreen in this package: add a new Kotlin file
(e.g., AuthenticatedNavbarNavigationScreen.kt) and define a `@Composable` function
named AuthenticatedNavbarNavigationScreen that accepts the same parameter used
here (navigateToSettingsScreen: () -> Unit) and composes the authenticated
navbar UI; ensure it is accessible from AuthenticatedNavbarNavigation.kt
(internal or public as needed) and matches the call signature used in
authenticatedNavbarGraph and
composableWithStayTransitions<AuthenticatedNavbarRoute>.

In
`@cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavViewModel.kt`:
- Around line 15-21: Fix the Spotless import-order violation in
RootNavViewModel.kt by reordering the imports to match the project's
Kotlin/Spotless rules (group standard library, third-party, then internal
packages) or simply run the formatter: execute ./gradlew
:cmp-navigation:spotlessApply to automatically correct the import ordering in
the imports block (the lines importing kotlinx.coroutines.flow.*,
org.mifospay.core.*, template.core.base.ui.BaseViewModel).
- Around line 83-91: The nested sealed class hierarchy is broken because
Internal does not extend RootNavAction while its member UserStateUpdateReceive
directly extends RootNavAction; fix this by making Internal inherit from
RootNavAction (change the declaration to sealed class Internal :
RootNavAction()) so UserStateUpdateReceive can remain as-is, or alternatively
make UserStateUpdateReceive extend Internal (data class
UserStateUpdateReceive(...) : Internal()); pick one consistent approach so the
sealed hierarchy is correct.
- Around line 29-37: Remove the hardcoded
AuthState.Authenticated("sample-token") in the init mapping of
userDataRepository.userData; instead obtain the real auth state from the
appropriate source (e.g., an AuthRepository or from userDataRepository if it
exposes authentication info) and pass that into UserStateUpdateReceive, or if
authState is unused by handleUserStateUpdateReceive/handleAction then remove the
authState field from UserStateUpdateReceive entirely; update the mapping in init
(the lambda that builds UserStateUpdateReceive), references to
UserStateUpdateReceive, and any call sites of handleAction to use the real auth
state or the trimmed payload.

In `@cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt`:
- Around line 21-26: The file imports the wrong package for KoinModules causing
a compile error; update the import to reference the actual object location and
keep the initKoin implementation as-is: replace the incorrect import of
cmp.navigation.di.KoinModules with org.mifospay.shared.di.KoinModules so that
the initKoin function can call KoinModules.allModules successfully.

In `@core/model/src/commonMain/kotlin/org/mifospay/core/model/AuthState.kt`:
- Around line 27-30: Authenticated is a data class whose auto-generated
toString() will include the sensitive accessToken; override toString() on
Authenticated to return a masked representation (e.g., show only last 4 chars or
a fixed placeholder) instead of the raw token to prevent accidental logging
exposure, or convert Authenticated to a regular class if you need to avoid
data-class auto-generated toString/copy behavior; update any call sites if you
change the class type.

In
`@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationBarItem.kt`:
- Around line 60-61: KptNavigationBarItem currently hardcodes the
NavigationBarItem parameter selected = false, preventing selection styling;
update the call in KptNavigationBarItem to pass selected = isSelected (same as
KptNavigationRailItem) so the component reflects the selected state, ensuring
alwaysShowLabel remains tied to isSelected as well.

In
`@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptPullToRefreshState.kt`:
- Around line 29-48: The KDoc for rememberKptPullToRefreshState is missing
documentation for the isEnabled parameter; update the function KDoc to add a
line describing isEnabled (e.g., whether pull-to-refresh is enabled, default
false) so all parameters (isEnabled, isRefreshing, onRefresh) are documented;
locate the function rememberKptPullToRefreshState and add the isEnabled
description in its KDoc block to match the data class docs.
- Around line 15-27: The KDoc for the KptPullToRefreshState data class is
missing documentation for the isEnabled parameter; update the class KDoc to add
a brief description for isEnabled (e.g., whether pull-to-refresh is
enabled/allowed) alongside the existing entries for isRefreshing and onRefresh
so all constructor parameters are documented.

In `@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptScaffold.kt`:
- Line 221: Variant C is passing contentWindowInsets into KptScaffold/Scaffold
and then also applying windowInsetsPadding to the same content, causing double
insets; update KptScaffold so that when using contentWindowInsets it does not
reapply windowInsetsPadding (make Scaffold receive WindowInsets(0.dp) like
Variants A/B or remove the extra windowInsetsPadding call), adjust references
around contentWindowInsets and the windowInsetsPadding invocation in
KptScaffold/Scaffold to ensure only one application of insets (keep unique
symbols: KptScaffold, contentWindowInsets, windowInsetsPadding, Scaffold).
- Around line 213-217: The Variant C implementation of KptScaffold wraps the
floatingActionButton with Modifier.navigationBarsPadding(), but Variants A and B
do not, causing inconsistent FAB positioning; update the other overloads of
KptScaffold (the variants that accept floatingActionButton) to apply the same
Modifier.navigationBarsPadding() to their floatingActionButton Box (or
alternatively remove it from Variant C if consistent removal is desired) so all
floatingActionButton usages use the same padding behavior; look for the
floatingActionButton lambdas in KptScaffold and ensure they consistently wrap
the content with Box(modifier = Modifier.navigationBarsPadding()) or remove that
wrapper from all to match the chosen consistent behavior.
🧹 Nitpick comments (15)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptScaffold.kt (4)

51-54: Remove or document commented-out code.

The commented-out .union(WindowInsets.displayCutout) appears in all three variants. If this is intentional, add a comment explaining why it's disabled. Otherwise, remove the dead code.

Also applies to: 125-128, 202-205


81-110: Consider extracting duplicated pull-to-refresh content logic.

The content block implementation (lines 81-110, 156-185, 222-249) is duplicated across all three variants. Extract this into a private helper composable to improve maintainability.

♻️ Suggested extraction
`@OptIn`(ExperimentalMaterial3Api::class)
`@Composable`
private fun KptScaffoldContent(
    paddingValues: PaddingValues,
    contentWindowInsets: WindowInsets,
    pullToRefreshState: KptPullToRefreshState,
    content: `@Composable` () -> Unit,
) {
    val internalPullToRefreshState = rememberPullToRefreshState()
    Box(
        modifier = Modifier
            .padding(paddingValues = paddingValues)
            .consumeWindowInsets(paddingValues = paddingValues)
            .imePadding()
            .navigationBarsPadding(),
    ) {
        Box(
            modifier = Modifier
                .windowInsetsPadding(insets = contentWindowInsets)
                .pullToRefresh(
                    state = internalPullToRefreshState,
                    isRefreshing = pullToRefreshState.isRefreshing,
                    onRefresh = pullToRefreshState.onRefresh,
                    enabled = pullToRefreshState.isEnabled,
                ),
        ) {
            content()

            PullToRefreshDefaults.Indicator(
                modifier = Modifier.align(Alignment.TopCenter),
                isRefreshing = pullToRefreshState.isRefreshing,
                state = internalPullToRefreshState,
            )
        }
    }
}

Also applies to: 156-185


55-55: API inconsistency in snackbar handling.

Variants A/B accept snackbarHost: @composable () -> Unit while Variant C accepts snackbarHostState: SnackbarHostState. Consider aligning the APIs for a more consistent developer experience, or document the intentional difference.

Also applies to: 129-129, 197-197


254-258: Consider adding containerColor to FloatingActionButtonContent.

The data class exposes contentColor but not containerColor, limiting FAB background customization. Consider adding it for fuller customization support.

♻️ Suggested enhancement
 data class FloatingActionButtonContent(
     val onClick: (() -> Unit),
     val contentColor: Color,
+    val containerColor: Color = Color.Unspecified,
     val content: (`@Composable` () -> Unit),
 )

Then update the FloatingActionButton usages:

 FloatingActionButton(
     onClick = content.onClick,
     contentColor = content.contentColor,
+    containerColor = content.containerColor.takeIf { it != Color.Unspecified }
+        ?: FloatingActionButtonDefaults.containerColor,
     content = content.content,
 )
cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigationViewModel.kt (1)

42-47: Empty handler for UserStateUpdateReceive action.

The UserStateUpdateReceive action handler has an empty body, which appears to be a placeholder. Consider either implementing the intended behavior or adding a TODO comment to document the pending work.

📝 Suggested: Add TODO comment for clarity
     private fun handleInternalAction(action: AuthenticatedNavBarAction.Internal) {
         when (action) {
             is AuthenticatedNavBarAction.Internal.UserStateUpdateReceive -> {
+                // TODO: Implement user state update handling
             }
         }
     }

Would you like me to help implement user state handling logic or open an issue to track this?

cmp-navigation/src/commonMain/kotlin/cmp/navigation/splash/SplashScreen.kt (1)

19-28: Hardcoded color and modifier placement concerns.

Two issues to consider:

  1. Color.White is hardcoded, which may cause issues with dark theme support. Consider using MaterialTheme.colorScheme.surface or MaterialTheme.colorScheme.background instead.

  2. The modifier parameter is applied to the inner Box rather than the outermost Surface. Conventionally, the modifier should be applied to the root composable for proper sizing/positioning by parent layouts.

♻️ Suggested fix
 `@Composable`
 fun SplashScreen(
     modifier: Modifier = Modifier,
 ) {
     Surface(
-        color = Color.White,
+        modifier = modifier,
+        color = MaterialTheme.colorScheme.surface,
     ) {
-        Box(modifier = modifier.fillMaxSize())
+        Box(modifier = Modifier.fillMaxSize())
     }
 }

You'll need to add the import:

import androidx.compose.material3.MaterialTheme
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRail.kt (1)

40-44: Hardcoded Color.White does not support dark theme.

Using a hardcoded color prevents proper dark mode support. Consider using a theme-aware color from KptTheme.colorScheme (e.g., surface or background) for consistency with the design system used elsewhere in this file.

♻️ Suggested fix
     Surface(
-        color = Color.White,
+        color = KptTheme.colorScheme.surface,
         contentColor = Color.Unspecified,
         modifier = modifier,
     ) {
cmp-navigation/src/commonMain/kotlin/cmp/navigation/ui/KptRootScaffold.kt (1)

111-125: Remove or address commented-out code.

The commented-out block for consuming display cutout insets appears to be work-in-progress. Either remove it or add a TODO comment explaining why it's disabled and when it should be re-enabled.

♻️ Suggested cleanup
                 Box(
-                    modifier = Modifier.run {
-//                        if (isNavigationRailVisible) {
-//                            consumeWindowInsets(
-//                                insets = WindowInsets.displayCutout.only(WindowInsetsSides.Start),
-//                            )
-//                        } else
-                        if (isNavigationBarVisible) {
+                    modifier = Modifier.run {
+                        // TODO: Handle display cutout insets for navigation rail layout
+                        if (isNavigationBarVisible) {
                             consumeWindowInsets(
                                 insets = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom),
                             )
                         } else {
                             this
                         }
                     },
                 )
cmp-navigation/src/androidMain/kotlin/cmp/navigation/rootnav/RootNavScreen.android.kt (1)

15-18: Side effect executed directly during composition.

The clearFocus() call runs directly during composition rather than as a controlled side effect. This can cause the effect to execute multiple times on recomposition and may run before the UI is fully settled.

Consider wrapping in SideEffect to ensure it runs after successful composition:

♻️ Suggested refactor
 `@Composable`
 actual fun ClearFocus() {
-    LocalActivity.current?.currentFocus?.clearFocus()
+    val activity = LocalActivity.current
+    SideEffect {
+        activity?.currentFocus?.clearFocus()
+    }
 }

This requires adding the import:

import androidx.compose.runtime.SideEffect
core/data/src/desktopMain/kotlin/org/mifospay/core/data/JsPlatformDependentDataModule.kt (1)

19-19: Consider renaming the class to DesktopPlatformDependentDataModule.

This class resides in the desktopMain source set but is named JsPlatformDependentDataModule. This naming inconsistency could cause confusion when navigating the codebase.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavScreen.kt (1)

96-98: ClearFocus() called outside of a side-effect scope.

ClearFocus() is a @Composable function called directly in the composition body. This will execute on every recomposition where the early return (lines 88-93) doesn't trigger. If state changes frequently, focus will be cleared repeatedly.

Consider wrapping this in a LaunchedEffect keyed on state or targetRoute to ensure it only runs when navigation actually occurs, not on every recomposition.

Suggested approach
-    // In some scenarios on an emulator the Activity can leak when recreated
-    // if we don't first clear focus anytime we change the root destination.
-    ClearFocus()
-
     // When state changes, navigate to different root navigation state
     val rootNavOptions = navOptions {
         // ...
     }

     LaunchedEffect(state) {
+        // Clear focus before navigating to prevent Activity leaks on emulator
         when (state) {

Then trigger focus clearing within the LaunchedEffect before navigation, or use a separate LaunchedEffect that tracks state changes.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/utils/RootUtils.kt (1)

19-27: Document @Serializable requirement and InternalSerializationApi usage.

Both functions use InternalSerializationApi, which has no stability guarantees and may change or be removed in future kotlinx.serialization versions. Additionally, serializer() throws SerializationException if the class is not annotated with @Serializable. Update the KDoc to document these requirements and limitations.

📝 Suggested documentation improvement
 /**
  * Gets the route string for an object.
+ *
+ * `@throws` kotlinx.serialization.SerializationException if the object's class is not annotated with `@Serializable`
  */
 `@OptIn`(InternalSerializationApi::class)
 fun <T : Any> T.toObjectNavigationRoute(): String = this::class.toObjectKClassNavigationRoute()

 /**
  * Gets the route string for a [KClass] of an object.
+ *
+ * Note: Uses internal serialization APIs with no stability guarantees.
+ *
+ * `@throws` kotlinx.serialization.SerializationException if the class is not annotated with `@Serializable`
  */
 `@OptIn`(InternalSerializationApi::class)
 fun <T : Any> KClass<T>.toObjectKClassNavigationRoute(): String =
     this.serializer().descriptor.serialName
cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt (1)

17-19: Consider adding explicit return type for public API clarity.

Since koinConfiguration() is a public function, adding an explicit return type improves API discoverability and documentation:

-fun koinConfiguration() = koinApplication {
+fun koinConfiguration(): KoinApplication = koinApplication {
     modules(KoinModules.allModules)
 }

This would require adding import org.koin.core.KoinApplication to the imports.

gradle/libs.versions.toml (2)

42-42: Consider updating Jackson to a newer patch version.

"Branch is closed since it is not a Long-Term-Support (LTS) version: no more patch releases planned" and importantly "2.19.3 (BROKEN, do not use) 2.19.4 (29-Oct-2025)". Version 2.19.2 is valid but 2.19.4 fixes issues in the 2.19.x line. Alternatively, "Current version 2.20.1" is the latest stable.

Suggested update
-jacksonCore = "2.19.2"
+jacksonCore = "2.19.4"

Or for latest stable:

-jacksonCore = "2.19.2"
+jacksonCore = "2.20.1"

46-46: Dependency versions verified and available, but consider updating GitHub API and Commons Codec to latest releases.

All three versions are confirmed in Maven Central and stable:

  • OkHttp 4.12.0 (Oct 2023) ✓
  • GitHub API 1.329 ✓ — newer versions available: 1.330, 2.0-rc.5
  • Commons Codec 1.19.0 ✓ — newer version available: 1.20.0 (Oct 2025)

Consider upgrading GitHub API to 1.330 and Commons Codec to 1.20.0 for the latest bug fixes and features.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ddb9a2 and 3642c7d.

📒 Files selected for processing (45)
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt
  • cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
  • cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt
  • cmp-navigation/build.gradle.kts
  • cmp-navigation/src/androidMain/kotlin/cmp/navigation/rootnav/RootNavScreen.android.kt
  • cmp-navigation/src/commonMain/composeResources/values/strings.xml
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigation.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigationViewModel.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavNavigation.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavScreen.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavViewModel.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/splash/SplashNavigation.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/splash/SplashScreen.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/ui/KptRootScaffold.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/ui/ScaffoldNavigationData.kt
  • cmp-navigation/src/commonMain/kotlin/cmp/navigation/utils/RootUtils.kt
  • cmp-navigation/src/nonAndroidMain/kotlin/cmp/navigation/rootnav/RootNavScreen.nonAndroid.kt
  • cmp-shared/build.gradle.kts
  • cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt
  • cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt
  • cmp-shared/src/nativeMain/kotlin/org/mifos/shared/ViewController.kt
  • core/data/src/androidMain/kotlin/org/mifospay/core/data/di/AndroidPlatformDependentDataModule.kt
  • core/data/src/androidMain/kotlin/org/mifospay/core/data/di/AndroidPlatformModule.kt
  • core/data/src/androidMain/kotlin/org/mifospay/core/data/util/ConnectivityManagerNetworkMonitor.kt
  • core/data/src/commonMain/kotlin/org/mifospay/core/data/di/PlatformDependentDataModule.kt
  • core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt
  • core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/NetworkMonitor.kt
  • core/data/src/desktopMain/kotlin/org/mifospay/core/data/JsPlatformDependentDataModule.kt
  • core/data/src/jsMain/kotlin/org/mifospay/core/data/JsPlatformDependentDataModule.kt
  • core/data/src/nativeMain/kotlin/org/mifospay/core/data/NativePlatformDependentDataModule.kt
  • core/data/src/wasmJsMain/kotlin/org/mifospay/core/data/JsPlatformDependentDataModule.kt
  • core/model/src/commonMain/kotlin/org/mifospay/core/model/AuthState.kt
  • core/ui/src/commonMain/kotlin/org/mifospay/core/ui/NavigationItem.kt
  • core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptBottomBar.kt
  • core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationBarItem.kt
  • core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRail.kt
  • core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRailItem.kt
  • core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptPullToRefreshState.kt
  • core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptScaffold.kt
  • gradle/libs.versions.toml
  • settings.gradle.kts
✅ Files skipped from review due to trivial changes (2)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
  • core/data/src/wasmJsMain/kotlin/org/mifospay/core/data/JsPlatformDependentDataModule.kt
🧰 Additional context used
🧬 Code graph analysis (9)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptBottomBar.kt (1)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationBarItem.kt (1)
  • KptNavigationBarItem (31-70)
cmp-navigation/src/commonMain/kotlin/cmp/navigation/splash/SplashNavigation.kt (1)
cmp-navigation/src/commonMain/kotlin/cmp/navigation/splash/SplashScreen.kt (1)
  • SplashScreen (19-28)
cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/ImageLoaderExt.kt (1)
  • LocalImageLoaderProvider (91-96)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptScaffold.kt (1)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptPullToRefreshState.kt (1)
  • rememberKptPullToRefreshState (37-48)
cmp-navigation/src/commonMain/kotlin/cmp/navigation/ui/KptRootScaffold.kt (3)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptPullToRefreshState.kt (1)
  • rememberKptPullToRefreshState (37-48)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptBottomBar.kt (1)
  • KptBottomBar (22-46)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRail.kt (1)
  • KptNavigationRail (32-71)
cmp-shared/src/nativeMain/kotlin/org/mifos/shared/ViewController.kt (2)
cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt (1)
  • initKoin (21-26)
cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt (1)
  • SharedApp (21-42)
cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigationViewModel.kt (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt (1)
  • sendEvent (97-99)
cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigation.kt (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/NavGraphBuilderExtensions.kt (1)
  • composableWithStayTransitions (45-60)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRail.kt (1)
core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRailItem.kt (1)
  • KptNavigationRailItem (29-65)
🪛 GitHub Actions: PR Checks
cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavViewModel.kt

[error] 15-22: spotlessKotlinCheck failed: format violations detected in RootNavViewModel.kt. The diff shows an import order issue (added import 'org.mifospay.core.data.repository.UserDataRepository'). Run './gradlew :cmp-navigation:spotlessApply' to fix formatting.

🪛 LanguageTool
cmp-android/dependencies/prodReleaseRuntimeClasspath.txt

[uncategorized] ~337-~337: The official name of this software platform is spelled with a capital “H”.
Context: ...io.github.alexzhirkevich:qrose:1.0.1 io.github.vinceglb:filekit-coil-android:0.10.0-be...

(GITHUB)


[uncategorized] ~338-~338: The official name of this software platform is spelled with a capital “H”.
Context: ...b:filekit-coil-android:0.10.0-beta04 io.github.vinceglb:filekit-coil:0.10.0-beta04 io....

(GITHUB)


[uncategorized] ~339-~339: The official name of this software platform is spelled with a capital “H”.
Context: ....vinceglb:filekit-coil:0.10.0-beta04 io.github.vinceglb:filekit-compose-android:0.8.8 ...

(GITHUB)


[uncategorized] ~340-~340: The official name of this software platform is spelled with a capital “H”.
Context: ...nceglb:filekit-compose-android:0.8.8 io.github.vinceglb:filekit-compose:0.8.8 io.githu...

(GITHUB)

🔇 Additional comments (45)
core/data/src/nativeMain/kotlin/org/mifospay/core/data/NativePlatformDependentDataModule.kt (1)

16-16: LGTM!

The import path update correctly aligns with the NetworkMonitor package reorganization from data.util to repository. This is consistent with the broader refactoring mentioned in the PR.

cmp-navigation/src/commonMain/composeResources/values/strings.xml (1)

13-16: LGTM!

The navigation labels and connectivity message are appropriate for the module's purpose.

core/model/src/commonMain/kotlin/org/mifospay/core/model/AuthState.kt (1)

15-25: Sealed class structure looks good.

The use of a sealed class for modeling authentication state is idiomatic Kotlin and enables exhaustive when expressions. Using data object for the singleton states (Uninitialized, Unauthenticated) is the correct modern Kotlin approach.

cmp-shared/src/nativeMain/kotlin/org/mifos/shared/ViewController.kt (1)

16-28: LGTM - correct iOS entry point structure.

The use of the configure block for Koin initialization ensures DI is set up before composition begins. The no-op callbacks are appropriate placeholders for native iOS, where these platform-specific behaviors may need native implementations later.

Consider adding a brief comment noting that the empty lambdas are intentional placeholders to help future contributors understand the design intent.

cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt (1)

30-41: The function getDefaultImageLoader does not exist in the codebase.

The code imports and uses getDefaultImageLoader(LocalPlatformContext.current), but this function is not defined anywhere in the repository. Similar functions exist in template.core.base.ui.ImageLoaderExt that already handle memoization:

  • rememberImageLoader(context) — returns cached loader or creates a default one
  • rememberDefaultImageLoader(context) — creates and memoizes an ImageLoader with remember(context)

Replace getDefaultImageLoader(LocalPlatformContext.current) with rememberImageLoader(LocalPlatformContext.current) or rememberDefaultImageLoader(LocalPlatformContext.current).

Likely an incorrect or invalid review comment.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigationViewModel.kt (2)

20-32: Well-structured ViewModel with proper flow management.

The isOffline StateFlow correctly:

  • Derives from networkMonitor.isOnline with negation
  • Uses WhileSubscribed(5_000) to allow brief survival during configuration changes
  • Provides sensible initial value

Using Unit as state type is acceptable here since the only exposed state is isOffline.


58-79: Clean action/event sealed class hierarchy.

The sealed class structure properly separates:

  • External actions (HomeTabClick, SettingsTabClick) from internal actions
  • Events with associated tab mapping via abstract tab property

Minor observation: SettingsTabClick triggers NavigateToProfileScreen — verify this naming alignment is intentional (settings navigation leading to profile screen).

core/data/src/androidMain/kotlin/org/mifospay/core/data/util/ConnectivityManagerNetworkMonitor.kt (1)

26-26: LGTM!

The import update correctly aligns with the NetworkMonitor interface relocation to the repository package. The implementation remains unchanged and properly handles network connectivity monitoring.

settings.gradle.kts (1)

42-42: LGTM!

The new cmp-navigation module is correctly included alongside other cmp-* modules, following the established project structure.

core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/NetworkMonitor.kt (1)

10-18: LGTM!

The NetworkMonitor interface relocation to the repository package is a sensible organizational improvement. Placing this interface alongside other repository contracts improves discoverability and aligns with common architecture patterns where monitors/observers sit at the repository layer.

core/data/src/commonMain/kotlin/org/mifospay/core/data/di/PlatformDependentDataModule.kt (1)

13-14: LGTM!

The import update correctly references NetworkMonitor from its new repository package location while TimeZoneMonitor appropriately remains in the util package.

core/data/src/androidMain/kotlin/org/mifospay/core/data/di/AndroidPlatformDependentDataModule.kt (1)

15-16: LGTM!

The import correctly references the NetworkMonitor interface from its new repository package while the concrete implementation ConnectivityManagerNetworkMonitor appropriately remains in the util package. This separation of interface and implementation is a clean architectural pattern.

cmp-navigation/build.gradle.kts (1)

1-55: LGTM!

Build configuration looks appropriate for a multiplatform navigation module. The plugin aliases, dependencies, and compose resource configuration are well-structured.

Minor note: The copyright header uses 2024 while other new files in this PR (like SplashScreen.kt) use 2025. Consider updating for consistency if this is a newly created file.

cmp-shared/build.gradle.kts (1)

27-27: LGTM!

The addition of cmpNavigation as an implementation dependency is appropriate. Using implementation scope correctly keeps navigation internals encapsulated within cmp-shared without leaking to consumers.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/ui/ScaffoldNavigationData.kt (1)

15-20: LGTM!

Clean data class design. Using ImmutableList from kotlinx-collections-immutable is a good choice for Compose stability, and making selectedNavigationItem nullable appropriately handles the case when no item is selected.

core/ui/src/commonMain/kotlin/org/mifospay/core/ui/NavigationItem.kt (1)

15-53: LGTM!

Well-designed interface with comprehensive documentation. The contract covers all necessary aspects for navigation items: icons (selected/unselected states), localized strings, routing information, and test tags. Using StringResource from Jetbrains Compose Resources ensures proper multiplatform localization support.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/splash/SplashNavigation.kt (1)

20-31: LGTM!

The splash navigation setup follows standard Navigation Compose patterns with type-safe routes. The @Serializable annotation on SplashRoute enables type-safe navigation, and the extension functions provide a clean API for registering and navigating to the splash screen.

core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRailItem.kt (1)

29-65: LGTM!

The navigation rail item implementation correctly handles icon selection, label rendering with a minimal indicator pill, and selection state via alwaysShowLabel. The use of a Spacer as a styled label indicator is an intentional design choice for a minimal visual appearance.

core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationRail.kt (1)

59-68: LGTM!

The item rendering correctly applies selection state, click handling, and test tags for each navigation item.

core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationBarItem.kt (1)

31-47: LGTM on the icon and label rendering.

The icon selection logic and label indicator styling are correct and consistent with the navigation rail item pattern.

core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptBottomBar.kt (1)

22-46: LGTM!

The KptBottomBar implementation correctly orchestrates the navigation items, passes selection state, and applies test tags. The tonalElevation = 0.dp provides a flat appearance consistent with modern design patterns.

Note: The selection state will work correctly once the selected = false bug in KptNavigationBarItem is fixed.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavViewModel.kt (1)

45-66: Verify the state transition logic for empty passcode.

Line 56 transitions to UserLocked when passcode.isEmpty(), but this may not match intended behavior. If the user is authenticated but hasn't set a passcode yet, should they be locked out, or should they be prompted to create a passcode?

Also note the TODO at line 50 indicates this logic needs further configuration.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavNavigation.kt (1)

16-28: LGTM!

Clean implementation following standard Jetpack Navigation Compose patterns. The @Serializable annotation enables type-safe navigation, and the extension function properly delegates to RootNavScreen.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/ui/KptRootScaffold.kt (2)

54-156: Well-structured adaptive scaffold implementation.

The responsive layout logic correctly switches between navigation rail (wider screens) and bottom navigation bar (compact screens). The pull-to-refresh integration properly bridges KptPullToRefreshState with Material3's internal state, and the animated visibility transitions provide smooth UX.


158-197: LGTM on helper composables.

Both ScaffoldBottomAppBar and ScaffoldNavigationRail correctly delegate to the shared KptBottomBar and KptNavigationRail components. The z-index handling on the rail ensures proper layering during content transitions.

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt (1)

17-17: Import path update is correct and consistent.

The NetworkMonitor import has been correctly relocated from util to repository package. All 17 references across the codebase consistently use the new import path, and no old references remain.

core/data/src/commonMain/kotlin/org/mifospay/core/data/di/RepositoryModule.kt (1)

26-26: LGTM!

The import path update and new DI binding for NetworkMonitor are consistent with the package reorganization. The binding pattern mirrors the existing TimeZoneMonitor binding on line 103.

Also applies to: 102-102

cmp-android/src/main/kotlin/org/mifospay/MainActivity.kt (1)

31-31: LGTM!

The import path update aligns with the NetworkMonitor relocation to the repository package. Usage remains unchanged.

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt (1)

32-32: LGTM!

The import path update is consistent with the NetworkMonitor package relocation across the codebase.

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)

23-23: LGTM!

The import path update is consistent with the NetworkMonitor package relocation.

cmp-navigation/src/nonAndroidMain/kotlin/cmp/navigation/rootnav/RootNavScreen.nonAndroid.kt (1)

14-16: LGTM!

The no-op actual implementation for non-Android platforms is appropriate. Android requires explicit focus clearing to prevent activity leaks in certain scenarios, while other platforms don't have this concern.

core/data/src/jsMain/kotlin/org/mifospay/core/data/JsPlatformDependentDataModule.kt (1)

16-16: LGTM!

Import path correctly updated to reflect the NetworkMonitor relocation to the repository package.

core/data/src/androidMain/kotlin/org/mifospay/core/data/di/AndroidPlatformModule.kt (1)

17-17: LGTM!

Import path correctly updated to align with the NetworkMonitor package relocation.

core/data/src/desktopMain/kotlin/org/mifospay/core/data/JsPlatformDependentDataModule.kt (1)

16-16: Import update looks correct.

The import path change aligns with the NetworkMonitor package relocation.

cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavScreen.kt (4)

72-81: Incomplete state-to-route mapping returns empty strings.

ShowOnboarding, Auth, and UserLocked states return empty strings, which will cause the route comparison at line 88 to behave unexpectedly if these states are ever reached. Combined with the no-op navigation blocks in the LaunchedEffect (lines 118-122), this could leave the app in an inconsistent navigation state.

If these are intentional placeholders, consider adding a TODO comment or throwing NotImplementedError to make the incomplete status explicit and prevent silent failures.


56-70: Navigation graph structure looks good.

The NavHost setup with transition providers and the split between splashDestination and authenticatedGraph is well-structured. The commented-out destinations (onboarding, auth, userUnlock) are clearly placeholders aligned with the PR's incremental approach.


130-150: Helper functions for route detection and transitions are well-implemented.

The recursive rootLevelRoute() traversal and transition provider selection based on route type provide clean abstractions for the navigation logic.


36-37: Uses experimental Kotlin Atomics API.

ExperimentalAtomicApi is used for AtomicReference. This API remains experimental and may change in future Kotlin releases. The code correctly includes the opt-in annotation.

gradle/libs.versions.toml (4)

206-206: New Room Gradle plugin and Commons Codec library entries are well-formed.

The library declarations follow the correct TOML format. androidx-room-gradle-plugin and commons-codec are properly defined with module coordinates and version references.

Also applies to: 209-209


242-250: Library declarations are well-structured.

The artifact coordinates and version references are correctly specified:

  • Jackson modules correctly share the jacksonCore version reference
  • JUnit Jupiter modules share the junitJupiterApi version reference
  • All group:artifact coordinates match Maven Central conventions

Note: Fix the JUnit version numbers as flagged above before these libraries can be resolved.


380-380: Kermit-Koin integration library added correctly.

The kermit-koin library provides Koin dependency injection integration for Kermit logging, appropriately sharing the kermit version reference with other Kermit libraries.


44-45: The versions specified are valid and exist on Maven Central. JUnit Jupiter API 5.13.4 and JUnit Platform Launcher 1.13.4 are legitimate releases. The release history includes consecutive 5.x versions (5.11.x, 5.12.x, 5.13.x, 5.14.x), not the gap claimed in the original review.

If a version upgrade is desired, 5.14.2 or 6.0.2 are available as newer alternatives, but the current versions are not invalid.

Likely an incorrect or invalid review comment.

cmp-android/dependencies/prodReleaseRuntimeClasspath.txt (3)

1-8: LGTM: Internal module additions align with PR objectives.

The new core-base modules (:cmp-navigation, :core-base:common, :core-base:datastore, :core-base:platform, :core-base:ui) align with the stated goal of adding missing template modules.


1-478: Note: This is a generated dependency file.

This file (prodReleaseRuntimeClasspath.txt) is auto-generated by Gradle. Any dependency changes should be made in the appropriate build configuration files (e.g., build.gradle.kts, version catalogs, or convention plugins), not directly in this file.

The static analysis hints about "GitHub" capitalization are false positives—these are Maven coordinates where lowercase is correct.


251-255: Verify security advisories for newly added dependencies.

Ensure all newly added dependencies are checked for known security vulnerabilities, especially:

  • Google Play libraries (app-update, review)
  • Calf permissions
  • JetBrains Compose material
  • FileKit ecosystem libraries

Run the following script to check for security advisories:

#!/bin/bash
# Check security advisories for key new dependencies

PACKAGES=(
  "com.google.android.play:app-update"
  "com.google.android.play:review"
  "com.mohamedrejeb.calf:calf-permissions"
  "io.github.vinceglb:filekit-core"
  "org.jetbrains.compose.material:material"
)

for package in "${PACKAGES[@]}"; do
  GROUP=$(echo "$package" | cut -d: -f1)
  ARTIFACT=$(echo "$package" | cut -d: -f2)
  echo "Checking $GROUP:$ARTIFACT..."
  
  # Note: GitHub's advisory database primarily covers npm, pip, rubygems
  # For Maven dependencies, consider using OWASP Dependency-Check or Snyk
  echo "Manual review recommended for Maven package: $GROUP:$ARTIFACT"
  echo "---"
done

echo ""
echo "Recommendation: Run OWASP Dependency-Check or Snyk for comprehensive security scanning:"
echo "./gradlew dependencyCheckAnalyze"

Also applies to: 288-289, 434-434

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +312 to +315
io.coil-kt.coil3:coil-compose-android:3.2.0-rc02
io.coil-kt.coil3:coil-compose-core-android:3.2.0
io.coil-kt.coil3:coil-compose-core:3.2.0
io.coil-kt.coil3:coil-compose:3.2.0-rc02
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n cmp-android/dependencies/prodReleaseRuntimeClasspath.txt | sed -n '310,320p'

Repository: openMF/mobile-wallet

Length of output: 607


🏁 Script executed:

#!/bin/bash
# Check Maven Central for available versions of coil-compose packages
curl -s "https://search.maven.org/solrsearch/select?q=g:io.coil-kt.coil3+AND+a:coil-compose-android&rows=50&wt=json" | \
  jq -r '.response.docs[] | .v' | sort -V

echo "---"

curl -s "https://search.maven.org/solrsearch/select?q=g:io.coil-kt.coil3+AND+a:coil-compose-core-android&rows=50&wt=json" | \
  jq -r '.response.docs[] | .v' | sort -V

echo "---"

curl -s "https://search.maven.org/solrsearch/select?q=g:io.coil-kt.coil3+AND+a:coil-compose-core&rows=50&wt=json" | \
  jq -r '.response.docs[] | .v' | sort -V

echo "---"

curl -s "https://search.maven.org/solrsearch/select?q=g:io.coil-kt.coil3+AND+a:coil-compose&rows=50&wt=json" | \
  jq -r '.response.docs[] | .v' | sort -V

Repository: openMF/mobile-wallet

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Try alternative Maven Central search for coil-compose packages
for package in "coil-compose-android" "coil-compose"; do
  echo "Searching for: $package"
  curl -s "https://search.maven.org/solrsearch/select?q=g:%22io.coil-kt.coil3%22+AND+a:%22${package}%22&core=gav&rows=100&wt=json" | \
    jq -r '.response.docs[] | .v' | sort -V | tail -5
  echo "---"
done

Repository: openMF/mobile-wallet

Length of output: 215


🏁 Script executed:

#!/bin/bash
# Alternative approach: use Maven repo API
echo "coil-compose-android versions:"
curl -s "https://repo1.maven.org/maven2/io/coil-kt/coil3/coil-compose-android/maven-metadata.xml" | grep -oP '<version>\K[^<]+' | tail -10

echo ""
echo "coil-compose versions:"
curl -s "https://repo1.maven.org/maven2/io/coil-kt/coil3/coil-compose/maven-metadata.xml" | grep -oP '<version>\K[^<]+' | tail -10

Repository: openMF/mobile-wallet

Length of output: 258


Use stable versions instead of RC versions in production.

The coil-compose and coil-compose-android dependencies are using RC version 3.2.0-rc02, while other Coil libraries use the stable 3.2.0 version. Release candidates should not be used in production runtime classpaths as they may contain undiscovered bugs or undergo API changes before the stable release. Stable version 3.2.0 is available for both packages.

🤖 Prompt for AI Agents
In `@cmp-android/dependencies/prodReleaseRuntimeClasspath.txt` around lines 312 -
315, Replace the RC artifacts with the stable 3.2.0 release: update the entries
for io.coil-kt.coil3:coil-compose-android and io.coil-kt.coil3:coil-compose from
3.2.0-rc02 to 3.2.0 so all Coil artifacts consistently use the stable 3.2.0
version in the runtime classpath.

See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources>
<string name="app_name">KPT Design</string>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update template placeholder app_name to the actual application name.

The value "KPT Design" appears to be a leftover from the kmp-project-template. This should be updated to the actual application name (e.g., "MifosPay" or "Mobile Wallet") to avoid displaying incorrect branding in the UI.

Suggested fix
-    <string name="app_name">KPT Design</string>
+    <string name="app_name">MifosPay</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="app_name">KPT Design</string>
<string name="app_name">MifosPay</string>
🤖 Prompt for AI Agents
In `@cmp-navigation/src/commonMain/composeResources/values/strings.xml` at line
12, Replace the placeholder string value for the resource named "app_name" in
strings.xml (the <string name="app_name"> entry) with the real application name
used by this project (e.g., "MifosPay" or "Mobile Wallet"), ensuring the
displayed app title/branding throughout the UI is correct.

Comment on lines +1 to +35
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
@file:Suppress("MatchingDeclarationName")

package cmp.navigation.authenticatednavbar

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import kotlinx.serialization.Serializable
import template.core.base.ui.composableWithStayTransitions

@Serializable
data object AuthenticatedNavbarRoute

internal fun NavController.navigateToAuthenticatedNavBar(navOptions: NavOptions? = null) {
navigate(route = AuthenticatedNavbarRoute, navOptions = navOptions)
}

internal fun NavGraphBuilder.authenticatedNavbarGraph(
navigateToSettingsScreen: () -> Unit,
) {
composableWithStayTransitions<AuthenticatedNavbarRoute> {
AuthenticatedNavbarNavigationScreen(
navigateToSettingsScreen = navigateToSettingsScreen,
)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify AuthenticatedNavbarNavigationScreen exists in the authenticatednavbar package
ast-grep --pattern 'fun AuthenticatedNavbarNavigationScreen($$$)'

Repository: openMF/mobile-wallet

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for AuthenticatedNavbarNavigationScreen in the codebase using rg
rg "AuthenticatedNavbarNavigationScreen" --type kotlin

Repository: openMF/mobile-wallet

Length of output: 213


🏁 Script executed:

#!/bin/bash
# Broader search for AuthenticatedNavbarNavigationScreen
rg "AuthenticatedNavbarNavigationScreen" --no-type -A 2

Repository: openMF/mobile-wallet

Length of output: 499


🏁 Script executed:

#!/bin/bash
# Search for AuthenticatedNavbarNavigationScreen definition across the entire codebase
rg "AuthenticatedNavbarNavigationScreen" -A 2 -B 2

Repository: openMF/mobile-wallet

Length of output: 774


🏁 Script executed:

#!/bin/bash
# List all files in the authenticatednavbar package directory
find cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar -type f

Repository: openMF/mobile-wallet

Length of output: 282


🏁 Script executed:

#!/bin/bash
# Read AuthenticatedNavbarNavigationViewModel.kt to check for the composable definition
cat -n cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigationViewModel.kt

Repository: openMF/mobile-wallet

Length of output: 3274


AuthenticatedNavbarNavigationScreen composable is not defined.

The file references AuthenticatedNavbarNavigationScreen at line 29, but this composable does not exist in the codebase. It needs to be implemented in this package (typically in a separate file like AuthenticatedNavbarNavigationScreen.kt) before the code will compile.

🤖 Prompt for AI Agents
In
`@cmp-navigation/src/commonMain/kotlin/cmp/navigation/authenticatednavbar/AuthenticatedNavbarNavigation.kt`
around lines 1 - 35, Implement the missing composable
AuthenticatedNavbarNavigationScreen in this package: add a new Kotlin file
(e.g., AuthenticatedNavbarNavigationScreen.kt) and define a `@Composable` function
named AuthenticatedNavbarNavigationScreen that accepts the same parameter used
here (navigateToSettingsScreen: () -> Unit) and composes the authenticated
navbar UI; ensure it is accessible from AuthenticatedNavbarNavigation.kt
(internal or public as needed) and matches the call signature used in
authenticatedNavbarGraph and
composableWithStayTransitions<AuthenticatedNavbarRoute>.

Comment on lines +15 to +21
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import org.mifospay.core.model.AuthState
import org.mifospay.core.data.repository.UserDataRepository
import org.mifospay.core.model.UserData
import template.core.base.ui.BaseViewModel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix import order to resolve pipeline failure.

The pipeline is failing due to spotless format violations. Run ./gradlew :cmp-navigation:spotlessApply to fix the import ordering.

🧰 Tools
🪛 GitHub Actions: PR Checks

[error] 15-22: spotlessKotlinCheck failed: format violations detected in RootNavViewModel.kt. The diff shows an import order issue (added import 'org.mifospay.core.data.repository.UserDataRepository'). Run './gradlew :cmp-navigation:spotlessApply' to fix formatting.

🤖 Prompt for AI Agents
In
`@cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavViewModel.kt`
around lines 15 - 21, Fix the Spotless import-order violation in
RootNavViewModel.kt by reordering the imports to match the project's
Kotlin/Spotless rules (group standard library, third-party, then internal
packages) or simply run the formatter: execute ./gradlew
:cmp-navigation:spotlessApply to automatically correct the import ordering in
the imports block (the lines importing kotlinx.coroutines.flow.*,
org.mifospay.core.*, template.core.base.ui.BaseViewModel).

Comment on lines +29 to +37
init {
userDataRepository.userData.map { userData ->
UserStateUpdateReceive(
authState = AuthState.Authenticated("sample-token"),
userData = userData,
)
}.onEach(::handleAction)
.launchIn(viewModelScope)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Hardcoded authentication token should be removed.

AuthState.Authenticated("sample-token") appears to be placeholder/debug code. This hardcodes authentication state regardless of actual user authentication status, which will bypass proper authentication flow.

The authState should be obtained from the actual authentication source (e.g., from UserDataRepository or a separate AuthRepository).

🐛 Suggested approach
     init {
-        userDataRepository.userData.map { userData ->
+        userDataRepository.userData.map { (authState, userData) ->
             UserStateUpdateReceive(
-                authState = AuthState.Authenticated("sample-token"),
+                authState = authState,
                 userData = userData,
             )
         }.onEach(::handleAction)
             .launchIn(viewModelScope)
     }

Alternatively, if authState isn't actually used in handleUserStateUpdateReceive, consider removing it from UserStateUpdateReceive entirely.

🤖 Prompt for AI Agents
In
`@cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavViewModel.kt`
around lines 29 - 37, Remove the hardcoded
AuthState.Authenticated("sample-token") in the init mapping of
userDataRepository.userData; instead obtain the real auth state from the
appropriate source (e.g., an AuthRepository or from userDataRepository if it
exposes authentication info) and pass that into UserStateUpdateReceive, or if
authState is unused by handleUserStateUpdateReceive/handleAction then remove the
authState field from UserStateUpdateReceive entirely; update the mapping in init
(the lambda that builds UserStateUpdateReceive), references to
UserStateUpdateReceive, and any call sites of handleAction to use the real auth
state or the trimmed payload.

Comment on lines +60 to +61
selected = false,
alwaysShowLabel = isSelected,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: selected is hardcoded to false instead of using isSelected.

The selected parameter is hardcoded to false, which means the NavigationBarItem will never display its selected state styling. This is inconsistent with KptNavigationRailItem, which correctly passes selected = isSelected.

🐛 Proposed fix
-        selected = false,
+        selected = isSelected,
         alwaysShowLabel = isSelected,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
selected = false,
alwaysShowLabel = isSelected,
selected = isSelected,
alwaysShowLabel = isSelected,
🤖 Prompt for AI Agents
In
`@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/bottombar/KptNavigationBarItem.kt`
around lines 60 - 61, KptNavigationBarItem currently hardcodes the
NavigationBarItem parameter selected = false, preventing selection styling;
update the call in KptNavigationBarItem to pass selected = isSelected (same as
KptNavigationRailItem) so the component reflects the selected state, ensuring
alwaysShowLabel remains tied to isSelected as well.

Comment on lines +15 to +27
/**
* Data class representing the pull-to-refresh state and behavior.
*
* @param isRefreshing Indicates whether the content is currently being
* refreshed.
* @param onRefresh Callback triggered when a pull-to-refresh gesture is
* performed.
*/
data class KptPullToRefreshState(
val isEnabled: Boolean,
val isRefreshing: Boolean,
val onRefresh: () -> Unit,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incomplete KDoc: isEnabled parameter is undocumented.

The data class has three fields, but the KDoc only documents isRefreshing and onRefresh. Consider adding documentation for isEnabled.

📝 Suggested documentation fix
 /**
  * Data class representing the pull-to-refresh state and behavior.
  *
+ * `@param` isEnabled Whether pull-to-refresh gesture is enabled.
  * `@param` isRefreshing Indicates whether the content is currently being
  *    refreshed.
  * `@param` onRefresh Callback triggered when a pull-to-refresh gesture is
  *    performed.
  */
🤖 Prompt for AI Agents
In
`@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptPullToRefreshState.kt`
around lines 15 - 27, The KDoc for the KptPullToRefreshState data class is
missing documentation for the isEnabled parameter; update the class KDoc to add
a brief description for isEnabled (e.g., whether pull-to-refresh is
enabled/allowed) alongside the existing entries for isRefreshing and onRefresh
so all constructor parameters are documented.

Comment on lines +29 to +48
/**
* Remembers and returns a [KptPullToRefreshState] instance.
*
* @param isRefreshing Whether the refresh animation should be shown.
* *(Default: `false`)*
* @param onRefresh Callback to execute on pull-to-refresh. *(Default:
* empty lambda)*
*/
@Composable
fun rememberKptPullToRefreshState(
isEnabled: Boolean = false,
isRefreshing: Boolean = false,
onRefresh: () -> Unit = { },
) = remember(isEnabled, isRefreshing, onRefresh) {
KptPullToRefreshState(
isEnabled = isEnabled,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

KDoc for rememberKptPullToRefreshState also missing isEnabled documentation.

Similar to the data class, the isEnabled parameter should be documented.

Otherwise, the implementation correctly uses all three parameters as remember keys, ensuring proper recomposition behavior.

📝 Suggested documentation fix
 /**
  * Remembers and returns a [KptPullToRefreshState] instance.
  *
+ * `@param` isEnabled Whether pull-to-refresh is enabled. *(Default: `false`)*
  * `@param` isRefreshing Whether the refresh animation should be shown.
  *    *(Default: `false`)*
  * `@param` onRefresh Callback to execute on pull-to-refresh. *(Default:
  *    empty lambda)*
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Remembers and returns a [KptPullToRefreshState] instance.
*
* @param isRefreshing Whether the refresh animation should be shown.
* *(Default: `false`)*
* @param onRefresh Callback to execute on pull-to-refresh. *(Default:
* empty lambda)*
*/
@Composable
fun rememberKptPullToRefreshState(
isEnabled: Boolean = false,
isRefreshing: Boolean = false,
onRefresh: () -> Unit = { },
) = remember(isEnabled, isRefreshing, onRefresh) {
KptPullToRefreshState(
isEnabled = isEnabled,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
)
}
/**
* Remembers and returns a [KptPullToRefreshState] instance.
*
* `@param` isEnabled Whether pull-to-refresh is enabled. *(Default: `false`)*
* `@param` isRefreshing Whether the refresh animation should be shown.
* *(Default: `false`)*
* `@param` onRefresh Callback to execute on pull-to-refresh. *(Default:
* empty lambda)*
*/
`@Composable`
fun rememberKptPullToRefreshState(
isEnabled: Boolean = false,
isRefreshing: Boolean = false,
onRefresh: () -> Unit = { },
) = remember(isEnabled, isRefreshing, onRefresh) {
KptPullToRefreshState(
isEnabled = isEnabled,
isRefreshing = isRefreshing,
onRefresh = onRefresh,
)
}
🤖 Prompt for AI Agents
In
`@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptPullToRefreshState.kt`
around lines 29 - 48, The KDoc for rememberKptPullToRefreshState is missing
documentation for the isEnabled parameter; update the function KDoc to add a
line describing isEnabled (e.g., whether pull-to-refresh is enabled, default
false) so all parameters (isEnabled, isRefreshing, onRefresh) are documented;
locate the function rememberKptPullToRefreshState and add the isEnabled
description in its KDoc block to match the data class docs.

Comment on lines +213 to +217
floatingActionButton = {
Box(modifier = Modifier.navigationBarsPadding()) {
floatingActionButton()
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent FAB padding between scaffold variants.

Variant C wraps the FAB with navigationBarsPadding(), but Variants A and B do not. This may cause FABs to be positioned differently depending on which overload is used. Consider aligning the behavior across all variants.

🤖 Prompt for AI Agents
In `@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptScaffold.kt`
around lines 213 - 217, The Variant C implementation of KptScaffold wraps the
floatingActionButton with Modifier.navigationBarsPadding(), but Variants A and B
do not, causing inconsistent FAB positioning; update the other overloads of
KptScaffold (the variants that accept floatingActionButton) to apply the same
Modifier.navigationBarsPadding() to their floatingActionButton Box (or
alternatively remove it from Variant C if consistent removal is desired) so all
floatingActionButton usages use the same padding behavior; look for the
floatingActionButton lambdas in KptScaffold and ensure they consistently wrap
the content with Box(modifier = Modifier.navigationBarsPadding()) or remove that
wrapper from all to match the chosen consistent behavior.

floatingActionButtonPosition = floatingActionButtonPosition,
containerColor = containerColor,
contentColor = contentColor,
contentWindowInsets = contentWindowInsets,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential double application of window insets in Variant C.

contentWindowInsets is passed to Scaffold (line 221) and then reapplied via windowInsetsPadding (line 233). This differs from Variants A/B which set WindowInsets(0.dp) on Scaffold to avoid double padding. This inconsistency may cause unexpected spacing.

🐛 Suggested fix for consistency with other variants
         containerColor = containerColor,
         contentColor = contentColor,
-        contentWindowInsets = contentWindowInsets,
+        contentWindowInsets = WindowInsets(0.dp),
         content = { paddingValues ->

Also applies to: 233-233

🤖 Prompt for AI Agents
In `@core/ui/src/commonMain/kotlin/org/mifospay/core/ui/scaffold/KptScaffold.kt`
at line 221, Variant C is passing contentWindowInsets into KptScaffold/Scaffold
and then also applying windowInsetsPadding to the same content, causing double
insets; update KptScaffold so that when using contentWindowInsets it does not
reapply windowInsetsPadding (make Scaffold receive WindowInsets(0.dp) like
Variants A/B or remove the extra windowInsetsPadding call), adjust references
around contentWindowInsets and the windowInsetsPadding invocation in
KptScaffold/Scaffold to ensure only one application of insets (keep unique
symbols: KptScaffold, contentWindowInsets, windowInsetsPadding, Scaffold).

@biplab1 biplab1 marked this pull request as draft February 17, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant