Skip to content

Conversation

@amanna13
Copy link

@amanna13 amanna13 commented Dec 31, 2025

Fixes - Jira-#597

  • Run the static analysis check ./gradlew check or ci-prepush.sh to make sure you didn't break anything

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


This PR syncs missing root files and folders from KMP project template into Mifos X Field Officer

  • Added core-base module structure from KMP template
  • Update settings.gradle to include core-base module(disabled now, pending build-logic setup)
  • Added/Updated root level scripts and metadata

Note

core-base module from the kmp-template are currently added, but commented out in settings.gradle, as they depend on KMP conventtion plugins and version-catalog entries that will be introduced in the follow-up epic tasks. ( Check: Jira-#596 )

Note

The existing detekt configuration in android-client was kept as is. The template detekt.yml is significantly more extensive and depends on build-logic and modules that will be introduced in later epic steps. ( Check: Jira-#596 )

Summary by CodeRabbit

Release Notes

  • New Features

    • Added analytics module with type-safe event tracking, performance monitoring, and batch logging capabilities.
    • Introduced complete design system featuring responsive layouts, animations, theming system, and reusable components.
    • Added network abstraction layer with built-in error handling and result wrapping.
    • Implemented cross-platform managers for intents, app reviews, and updates.
    • Added MVVM architecture foundation with state and event management.
  • Documentation

    • Comprehensive READMEs added for all core modules with usage examples and architecture guides.
  • Configuration & Tooling

    • Added keystore and secrets management scripts for secure credential handling.

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

…emplate

- Added core-base module structure from KMP template
- Update settings.gradle to include core-base module(disabled now, pending build-logic setup)
- Added/Updated root level scripts and metadata
@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

Establishes foundational Kotlin Multiplatform infrastructure: new core modules (analytics, common, database, designsystem, network, platform, ui), multiplatform build configurations, Compose UI components and theming system, platform abstractions and implementations, networking client setup, and utility scripts for keystore management and directory synchronization. Primarily configuration, architecture setup, and utility APIs without active business feature logic.

Changes

Cohort / File(s) Summary
Git & Build Configuration
.gitignore, core-base/*/build.gradle.kts, core-base/*/.gitignore, settings.gradle.kts
Added build artifact ignores, Gradle Kotlin DSL configs for KMP modules with plugin aliases, Android namespaces, and multiplatform sourceSets; commented out disabled modules in settings.
Analytics Module
core-base/analytics/*
Complete analytics library with type-safe event model (AnalyticsEvent, Param, ParamKeys), multiple platform implementations (Firebase, Stub, NoOp, Test), validation utilities, performance tracking, UI helpers, Compose integration (LocalAnalyticsHelper, TrackScreenView, trackClick modifier), and extensive extension functions for timed events, batching, and builders.
Common Module
core-base/common/src/*
Added DataState sealed type for async UI states with Flow extension (asDataStateFlow), DataStateExtensions for mapping/combining, image Base64/data-URI utilities, Parcelize multiplatform abstraction (expect/actual across platforms), DispatcherManager interface and implementations per platform.
Database Module
core-base/database/src/*
Room abstraction layer with expect/actual annotations (Dao, Query, Entity, TypeConverter), OnConflictStrategy constants, platform-specific AppDatabaseFactory implementations (Android, Desktop, Native with directory resolution), and typealias bridges to Room APIs.
Design System Module
core-base/designsystem/src/commonMain/*
Comprehensive theming system (KptTheme, KptColorScheme, KptTypography, KptShapes, KptSpacing, KptElevation implementations), Material3 integration (KptMaterialTheme with dark mode support), animation specs and transitions, layout components (Grid, Flow, Masonry, Sidebar, SplitPane, ResponsiveLayout, AdaptiveScaffolds), card/snackbar/topappbar components, and state holder pattern.
Network Module
core-base/network/src/*
Ktor-based HTTP client factory (expect/actual per platform: OkHttp, Darwin, Js), default client setup with auth/logging/timeouts, NetworkResult/NetworkError types for response wrapping, and ResultSuspendConverterFactory for Ktorfit integration.
Platform Module
core-base/platform/src/*
Cross-platform manager interfaces (IntentManager, AppReviewManager, AppUpdateManager, GarbageCollectionManager) with Android implementations (Play Review/Update APIs, ShareIntent, Settings), CompositionLocal provisioning (LocalManagerProvider), AppContext abstraction, and MimeType enum with extension resolution.
UI Module
core-base/ui/src/*
BaseViewModel with unidirectional data flow (state/events/actions), lifecycle-aware EventsEffect, Coil image loader integration, navigation transition builders (slide/push/fade), shared element/animated visibility scopes, string extensions, jank tracking (Android-specific), ReportDrawnWhen lifecycle, and platform-specific ShareUtils (text/image sharing, URL/phone/email/SMS).
Scripts & Templates
keystore-manager.sh, sync-dirs.sh, secrets.env.template
Bash scripts for Android keystore generation and GitHub secrets management, upstream directory synchronization with exclusions and dry-run support; template env file with placeholders for keystores, certificates, and credentials.
Documentation
.gitignore root, CODE_OF_CONDUCT.md, core-base/*/README.md
Updated .gitignore with keystore/secrets patterns, reflowed CoC attribution, and comprehensive module READMEs documenting architecture, APIs, usage, platform support, and integration guidance.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Reasoning: Extensive multimodule foundation with mixed homogeneity—many platform-specific implementations follow repeating patterns (reducing per-file complexity), but high overall heterogeneity across distinct modules (analytics, database, design system, platform abstractions) requiring separate reasoning per domain. Includes dense logic areas (validation, state management, layout composition), numerous new public APIs, build configuration, and scripts. Scope and breadth demand comprehensive review despite some pattern repetition.

Suggested Reviewers

  • biplab1
  • sam-arth07
  • revanthkumarJ

Poem

🐰 A warren of modules, now standing so tall,
Analytics and themes answer the call,
Databases humming, networks ablaze,
The platform's foundation set through the maze!
Multiplatform magic from Android to web,
Each build block aligned in this KMP ebb.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.01% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: syncing root-level scripts, files, and metadata from a KMP project template.
✨ 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.

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: 17

Note

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

🟠 Major comments (26)
core-base/platform/src/androidMain/kotlin/template/core/base/platform/utils/AndroidBuildUtils.kt-10-10 (1)

10-10: Replace placeholder package name before module integration.

The package name uses template as the root namespace, which is a placeholder from the KMP project template. This pattern occurs across the entire core-base module (95+ files). Before enabling the core-base module, refactor all package declarations from template.core.base.* to com.mifos.core.base.* to match the project's package structure.

core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/ComponentStateHolder.kt-19-48 (1)

19-48: Remove or revise the thread-safety claim in the KDoc.

Line 22 states "This class provides a thread-safe way to hold and update state values," but mutableStateOf is not thread-safe. It is designed exclusively for the single-threaded Compose runtime and must only be accessed from the main/UI thread. This misleading claim could encourage unsafe concurrent access patterns and should be removed or clarified.

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

12-12: Package name retains "template" prefix from source.

The package name template.core.base.ui appears to be a placeholder from the kmp-project-template. Before enabling the core-base module in settings.gradle, ensure this is renamed to align with the Mifos X Field Officer project namespace (e.g., org.mifos.core.base.ui or similar).

core-base/network/build.gradle.kts-15-17 (1)

15-17: Update the template placeholder namespace to match the project.

The namespace "template.core.base.network" appears to be a placeholder from the KMP template. It should be updated to align with the project's actual package structure (e.g., "com.mifos.core.base.network" or "org.mifos.core.base.network").

🔎 Proposed fix
 android {
-    namespace = "template.core.base.network"
+    namespace = "com.mifos.core.base.network"
 }

Note: Adjust the namespace prefix (com.mifos) to match your project's established package naming convention.

core-base/ui/src/commonMain/kotlin/template/core/base/ui/ImageLoaderExt.kt-53-65 (1)

53-65: Make DebugLogger conditional on build type.

The DebugLogger() is unconditionally enabled, which means verbose logging will run in production builds. This can impact performance and potentially leak information in production logs.

🔎 Suggested approach

Consider making the logger conditional based on build type:

 fun getDefaultImageLoader(context: PlatformContext): ImageLoader = ImageLoader
     .Builder(context)
-    .logger(DebugLogger())
+    .apply {
+        // Only enable debug logging in debug builds
+        if (BuildConfig.DEBUG) {
+            logger(DebugLogger())
+        }
+    }
     .networkCachePolicy(CachePolicy.ENABLED)
     .memoryCachePolicy(CachePolicy.ENABLED)

Alternatively, pass the logger as a parameter to allow configuration at the call site, or use a platform-specific expectation to determine whether debug logging should be enabled.

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

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

80-80: Replace println with structured logging.

Using println for error and diagnostic messages is not production-ready. These statements lack log levels, structure, and proper output channels. Since Kermit is already in use elsewhere in the project (see KtorHttpClient.kt), integrate it here for consistent, configurable logging.

🔎 Recommended approach

Add Kermit logger instance to the class and replace println statements:

+import co.touchlab.kermit.Logger
+
 class ResultSuspendConverterFactory : Converter.Factory {
+    private val logger = Logger.withTag("ResultSuspendConverterFactory")
+
     override fun suspendResponseConverter(
         typeData: TypeData,
         ktorfit: Ktorfit,
     ): Converter.SuspendResponseConverter<HttpResponse, *>? {
         if (typeData.typeInfo.type == NetworkResult::class) {
             val successType = typeData.typeArgs.first().typeInfo
             return object :
                 Converter.SuspendResponseConverter<HttpResponse, NetworkResult<Any, NetworkError>> {
                 override suspend fun convert(result: KtorfitResult): NetworkResult<Any, NetworkError> {
                     return when (result) {
                         is KtorfitResult.Failure -> {
-                            println("Failure: " + result.throwable.message)
+                            logger.e(result.throwable) { "Request failed" }
                             NetworkResult.Error(NetworkError.UNKNOWN)
                         }
                         is KtorfitResult.Success -> {
                             val status = result.response.status.value
                             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}")
+                                        logger.e(e) { "Serialization failed" }
                                         NetworkResult.Error(NetworkError.SERIALIZATION)
                                     }
                                 }
                                 400 -> NetworkResult.Error(NetworkError.BAD_REQUEST)
                                 401 -> NetworkResult.Error(NetworkError.UNAUTHORIZED)
                                 404 -> NetworkResult.Error(NetworkError.NOT_FOUND)
                                 408 -> NetworkResult.Error(NetworkError.REQUEST_TIMEOUT)
                                 429 -> NetworkResult.Error(NetworkError.TOO_MANY_REQUESTS)
                                 in 500..599 -> NetworkResult.Error(NetworkError.SERVER)
                                 else -> {
-                                    println("Status code $status")
+                                    logger.w { "Unhandled status code: $status" }
                                     NetworkResult.Error(NetworkError.UNKNOWN)
                                 }
                             }
                         }
                     }
                 }
             }
         }
         return null
     }
 }

Also applies to: 95-95, 107-107

core-base/network/src/commonMain/kotlin/template/core/base/network/KtorHttpClient.kt-144-148 (1)

144-148: Fix overly broad host matching in the logging filter.

The request.url.host.contains(host) check at line 146 performs substring matching, which can match unintended hosts. For example, if loggableHosts includes "api", it will match both "api.example.com" (intended) and "maliciousapi.com" (unintended). This could lead to logging sensitive requests to unauthorized hosts or failing to log legitimate ones.

🔎 Proposed fix using equality or suffix matching

Option 1: Exact equality (if hosts are fully qualified)

         filter { request ->
             loggableHosts.any { host ->
-                request.url.host.contains(host)
+                request.url.host == host
             }
         }

Option 2: Domain suffix matching (if hosts are domain patterns)

         filter { request ->
             loggableHosts.any { host ->
-                request.url.host.contains(host)
+                request.url.host == host || request.url.host.endsWith(".$host")
             }
         }

Choose based on whether loggableHosts contains exact hostnames or domain patterns.

core-base/database/src/desktopMain/kotlin/template/core/base/database/AppDatabaseFactory.kt-104-114 (1)

104-114: Handle potential null APPDATA and unchecked directory creation.

Two potential issues:

  1. System.getenv("APPDATA") can return null on Windows in unusual configurations, causing an NPE when passed to File().
  2. mkdirs() can return false if directory creation fails, but this is silently ignored.
🔎 Proposed fix
         val os = System.getProperty("os.name").lowercase()
         val userHome = System.getProperty("user.home")
         val appDataDir = when {
-            os.contains("win") -> File(System.getenv("APPDATA"), "MifosDatabase")
+            os.contains("win") -> {
+                val appData = System.getenv("APPDATA") ?: userHome
+                File(appData, "MifosDatabase")
+            }
             os.contains("mac") -> File(userHome, "Library/Application Support/MifosDatabase")
             else -> File(userHome, ".local/share/MifosDatabase")
         }

         if (!appDataDir.exists()) {
-            appDataDir.mkdirs()
+            require(appDataDir.mkdirs()) { "Failed to create database directory: $appDataDir" }
         }
core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt-55-79 (1)

55-79: Remove the non-standard entity parameter from the Insert annotation.

The Insert annotation includes an entity: KClass<*> parameter that does not exist in Room's standard @Insert API. Room's @Insert annotation has only one parameter: onConflict. Keeping this extra parameter creates API incompatibility and will confuse developers familiar with Room. Remove the entity parameter to align with Room's established API contract.

core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt-21-26 (1)

21-26: shareText implementation incorrectly saves text as an image file.

The function uses FileKit.saveImageToGallery with text bytes and a .txt filename, which is semantically incorrect. saveImageToGallery is designed for image data, not text files. This will likely fail or produce unexpected results.

Consider using a file picker/saver API appropriate for text files instead.

core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt-28-38 (1)

28-38: shareImage may produce corrupt image files.

image.asSkiaBitmap().readPixels() returns raw pixel data (RGBA bytes), not an encoded PNG. Saving this directly with a .png extension will create a file that image viewers cannot open.

The pixel data needs to be encoded to PNG format before saving. Consider using Skia's encoding APIs to properly encode the image.

core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt-32-38 (1)

32-38: Inconsistent placeholder implementations will cause runtime crashes.

Lines 33 and 37 use TODO("Not yet implemented") which throws NotImplementedError at runtime, while other methods use commented // TODO and silently do nothing. This inconsistency means calling shareFile(fileUri, mimeType, extraText) or shareImage(title, image) on non-Android platforms will crash the app unexpectedly.

For consistency with other placeholder methods, either comment these TODOs or document that these operations are intentionally unsupported on non-Android platforms.

🔎 Proposed fix for consistent no-op behavior
 override fun shareFile(fileUri: String, mimeType: MimeType, extraText: String) {
-    TODO("Not yet implemented")
+    // TODO("Not yet implemented")
 }

 override suspend fun shareImage(title: String, image: ImageBitmap) {
-    TODO("Not yet implemented")
+    // TODO("Not yet implemented")
 }

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

keystore-manager.sh-638-646 (1)

638-646: sed -i without backup suffix is not portable to macOS.

Line 638 uses sed -i without a backup suffix, which works on GNU sed (Linux) but fails on BSD sed (macOS). In contrast, update_fastlane_config (line 572) correctly uses sed -i.bak.

🔎 Proposed fix for macOS compatibility
         # Use sed to update the signing configuration
-        sed -i \
+        sed -i.bak \
             -e "s|storeFile = file(System.getenv(\"KEYSTORE_PATH\") ?: \".*\")|storeFile = file(System.getenv(\"KEYSTORE_PATH\") ?: \"../keystores/$keystore_name\")|" \
             -e "s|storePassword = System.getenv(\"KEYSTORE_PASSWORD\") ?: \".*\"|storePassword = System.getenv(\"KEYSTORE_PASSWORD\") ?: \"$keystore_password\"|" \
             -e "s|keyAlias = System.getenv(\"KEYSTORE_ALIAS\") ?: \".*\"|keyAlias = System.getenv(\"KEYSTORE_ALIAS\") ?: \"$key_alias\"|" \
             -e "s|keyPassword = System.getenv(\"KEYSTORE_ALIAS_PASSWORD\") ?: \".*\"|keyPassword = System.getenv(\"KEYSTORE_ALIAS_PASSWORD\") ?: \"$key_password\"|" \
             "$gradle_file"
+
+        # Remove the backup file
+        rm -f "$gradle_file.bak"

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

core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/PerformanceTracker.kt-231-233 (1)

231-233: Variable shadows top-level currentTime property.

Line 232 declares a local val currentTime that shadows the top-level property. While this happens to work because the shadowing creates a fresh evaluation in the local scope, it's confusing and fragile. After fixing the top-level currentTime to be a getter, this shadowing should be removed:

     fun markAppForeground() {
-        val currentTime = currentTime
-        lastForegroundTime = currentTime
+        lastForegroundTime = currentTime
         val bgTime = backgroundTime
core-base/analytics/src/androidProd/kotlin/template.core.base.analytics/di/AnalyticsModule.kt-10-12 (1)

10-12: Fix inconsistent source set directory structure in androidProd.

The @file:Suppress("InvalidPackageDeclaration") is necessary because androidProd uses a non-standard directory structure with dots in folder names (template.core.base.analytics/di/) instead of the standard slash-based structure (template/core/base/analytics/di/) used by other source sets like androidDemo and commonMain. To eliminate this suppression and align with Kotlin conventions, rename the androidProd directory from template.core.base.analytics/ to template/core/base/analytics/.

core-base/platform/src/androidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.android.kt-12-14 (1)

12-14: Remove explicit garbage collection calls or document specific use case.

Explicit calls to Runtime.getRuntime().gc() are strongly discouraged in Android applications per official Android Developers guidance. They can trigger expensive full garbage collections and often worsen performance. The current implementation compounds this concern by making 10 repeated GC attempts in a loop, which is even more problematic than a single call.

If this is intentional for a specific, measured use case (e.g., memory testing, controlled server-side scenarios), document the reasoning and measurement results. Otherwise, reconsider this abstraction entirely and focus on reducing allocation churn instead.

core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt-29-30 (1)

29-30: appScope creates a new CoroutineScope on every access.

Using get() = CoroutineScope(...) creates a fresh scope each time the property is accessed. This defeats the purpose of a shared application scope — coroutines launched in different accesses won't share the same SupervisorJob and won't be cancelled together.

Proposed fix using lazy initialization
-    override val appScope: CoroutineScope
-        get() = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+    override val appScope: CoroutineScope by lazy {
+        CoroutineScope(SupervisorJob() + Dispatchers.Default)
+    }

Alternatively, use direct initialization:

override val appScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowRow.kt-26-27 (1)

26-27: horizontalArrangement and verticalAlignment parameters are unused.

These parameters are declared but never applied in the layout logic. Items are always placed sequentially from x=0 without arrangement spacing, and cross-axis positioning ignores vertical alignment.

🔎 Suggested approach

Apply horizontalArrangement when calculating x positions within each row, and use verticalAlignment to align items within rows of varying heights. Alternatively, if these features are not yet needed, consider removing the parameters to avoid misleading the API consumers.

core-base/common/src/androidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt-29-30 (1)

29-30: appScope creates a new scope on every access.

Using a getter with SupervisorJob() creates a fresh CoroutineScope each time appScope is accessed. This defeats the purpose of having a shared application-wide scope for long-running operations.

🔎 Proposed fix - use lazy initialization
-    override val appScope: CoroutineScope
-        get() = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+    override val appScope: CoroutineScope by lazy {
+        CoroutineScope(SupervisorJob() + Dispatchers.Default)
+    }
core-base/platform/src/androidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt-224-242 (1)

224-242: FileOutputStream not closed properly on error paths.

The FileOutputStream is created on line 231 but flush() or close() could throw, leaving the stream open. Use Kotlin's use extension for automatic resource management.

🔎 Proposed fix using use{} block
     private suspend fun saveImage(image: Bitmap, context: Context): Uri? {
         return withContext(Dispatchers.IO) {
             try {
                 val imagesFolder = File(context.cacheDir, "images")
                 imagesFolder.mkdirs()
                 val file = File(imagesFolder, "shared_image.png")

-                val stream = FileOutputStream(file)
-                image.compress(Bitmap.CompressFormat.PNG, 100, stream)
-                stream.flush()
-                stream.close()
+                FileOutputStream(file).use { stream ->
+                    image.compress(Bitmap.CompressFormat.PNG, 100, stream)
+                    stream.flush()
+                }

                 FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
             } catch (e: IOException) {
                 Log.d("saving bitmap", "saving bitmap error ${e.message}")
                 null
             }
         }
     }
core-base/platform/src/androidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt-95-102 (1)

95-102: Missing FLAG_ACTIVITY_NEW_TASK when starting activity from non-Activity context.

When context is an Application or Service context (not Activity), calling startActivity without FLAG_ACTIVITY_NEW_TASK will crash on some Android versions. Consider adding the flag for safety.

🔎 Proposed fix
         } else {
             val newUri = if (androidUri.scheme == null) {
                 androidUri.buildUpon().scheme("https").build()
             } else {
                 androidUri.normalizeScheme()
             }
-            startActivity(Intent(Intent.ACTION_VIEW, newUri))
+            startActivity(Intent(Intent.ACTION_VIEW, newUri).apply {
+                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            })
         }
core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptTopAppBarConfiguration.kt-116-116 (1)

116-116: Typo in property name: onNavigationIonClick should be onNavigationIconClick.

The property name onNavigationIonClick appears to be missing the "c" in "Icon". This is in the public API and should be fixed before release to avoid breaking changes later.

🔎 Proposed fix
-    val onNavigationIonClick: (() -> Unit)? = null,
+    val onNavigationIconClick: (() -> Unit)? = null,

Also update line 246 in the builder:

-        onNavigationIonClick = onNavigationClick,
+        onNavigationIconClick = onNavigationClick,

And update the KDoc on line 97:

- * @param onNavigationIonClick Optional click handler for the navigation icon
+ * @param onNavigationIconClick Optional click handler for the navigation icon
core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt-88-106 (1)

88-106: FileOutputStream resource leak - same issue as IntentManagerImpl.

The FileOutputStream should use use{} block to ensure proper cleanup. This is duplicated code from IntentManagerImpl.saveImage.

🔎 Proposed fix
     private suspend fun saveImage(image: Bitmap, context: Context): Uri? {
         return withContext(Dispatchers.IO) {
             try {
                 val imagesFolder = File(context.cacheDir, "images")
                 imagesFolder.mkdirs()
                 val file = File(imagesFolder, "shared_image.png")

-                val stream = FileOutputStream(file)
-                image.compress(Bitmap.CompressFormat.PNG, 100, stream)
-                stream.flush()
-                stream.close()
+                FileOutputStream(file).use { stream ->
+                    image.compress(Bitmap.CompressFormat.PNG, 100, stream)
+                    stream.flush()
+                }

                 FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
             } catch (e: IOException) {
                 Log.d("saving bitmap", "saving bitmap error ${e.message}")
                 null
             }
         }
     }
core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableListDetailScaffold.kt-258-261 (1)

258-261: Non-null assertion item.id!! may throw NPE.

The PaneScaffoldItem<T> interface allows T to be nullable, but line 260 uses item.id!! which will crash if id is null. Consider constraining the type or handling nullability.

🔎 Proposed fix - use safe key or require non-null id
         itemsIndexed(
             items = items,
-            key = { _, item -> item.id!! },
+            key = { index, item -> item.id ?: index },
         ) { index, item ->

Alternatively, constrain the interface:

interface PaneScaffoldItem<T : Any> {
    val id: T
}
core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptAnimationSpecs.kt-105-123 (1)

105-123: emphasizedEasing and standardEasing do not match Material Design 3 specifications.

Both use CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f), but according to MD3 specs:

  • standardEasing should be CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
  • emphasizedEasing should match one of the MD3 emphasized approximations: either decelerate (0.05f, 0.7f, 0.1f, 1.0f) or accelerate (0.3f, 0.0f, 0.8f, 0.15f)

The file claims to follow Material Design 3 guidelines but these values deviate from the spec. Correct these to match the intended easing curves, or update the documentation if using custom values.

core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptTopAppBar.kt-382-394 (1)

382-394: KptMediumTopAppBar and KptLargeTopAppBar ignore the onNavigationIconClick parameter.

Both functions accept onNavigationIconClick as a parameter but never use it - they always call the simpler KptTopAppBar(title, modifier, variant) overload which doesn't support navigation.

🔎 Proposed fix
 @Composable
 fun KptMediumTopAppBar(
     title: String,
     onNavigationIconClick: (() -> Unit)? = null,
     modifier: Modifier = Modifier,
-) = KptTopAppBar(title, modifier, TopAppBarVariant.Medium)
+) = onNavigationIconClick?.let {
+    KptTopAppBar(
+        title = title,
+        onNavigationIconClick = it,
+        modifier = modifier,
+        variant = TopAppBarVariant.Medium,
+    )
+} ?: KptTopAppBar(title, modifier, TopAppBarVariant.Medium)

 @Composable
 fun KptLargeTopAppBar(
     title: String,
     onNavigationIconClick: (() -> Unit)? = null,
     modifier: Modifier = Modifier,
-) = KptTopAppBar(title, modifier, TopAppBarVariant.Large)
+) = onNavigationIconClick?.let {
+    KptTopAppBar(
+        title = title,
+        onNavigationIconClick = it,
+        modifier = modifier,
+        variant = TopAppBarVariant.Large,
+    )
+} ?: KptTopAppBar(title, modifier, TopAppBarVariant.Large)

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

🟡 Minor comments (48)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/Transition.kt-63-67 (1)

63-67: Documentation refers to "slide transitions" but describes push transitions.

The KDoc for DEFAULT_PUSH_TRANSITION_TIME_MS is copy-pasted from DEFAULT_SLIDE_TRANSITION_TIME_MS and incorrectly says "slide transitions."

 /**
- * The default transition time (in milliseconds) for all slide transitions in the
+ * The default transition time (in milliseconds) for all push transitions in the
  * [TransitionProviders].
  */
 const val DEFAULT_PUSH_TRANSITION_TIME_MS: Int = 350
core-base/ui/src/commonMain/kotlin/template/core/base/ui/NavGraphBuilderExtensions.kt-8-8 (1)

8-8: Minor typo in license header.

"See See" should be just "See".

- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
core-base/ui/src/commonMain/kotlin/template/core/base/ui/Transition.kt-8-8 (1)

8-8: Minor typo in license header.

Same typo as in NavGraphBuilderExtensions.kt: "See See" should be "See".

- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
core-base/ui/src/commonMain/kotlin/template/core/base/ui/SharedElementExt.kt-10-10 (1)

10-10: Remove ExperimentalSharedTransitionApi opt-in annotations if using Compose Animation 1.10+.

The SharedTransitionApi was stabilized in androidx.compose.animation:1.10.0 (released December 3, 2025). If your project is on this version or later, the @file:OptIn(ExperimentalSharedTransitionApi::class) annotation (lines 10, 15) is no longer required and should be removed.

core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptComponent.kt-8-8 (1)

8-8: Typo: duplicate word "See".

The license comment has "See See" which should be just "See".

🔎 Proposed fix
- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
sync-dirs.sh-312-316 (1)

312-316: Unreachable cleanup after handle_error.

handle_error calls exit 1, so cleanup_temp_dirs on line 314 will never execute. Consider using a trap for cleanup or calling cleanup before the error handler.

Proposed fix using trap

Add a trap at the beginning of the script (after line 22):

# Cleanup trap for unexpected exits
trap 'find . -depth -type d -name "temp_*" -exec rm -rf {} + 2>/dev/null; rm -rf temp_files temp_root' EXIT

Then simplify the error block:

             git checkout "$temp_branch" -- "$dir" || {
-                handle_error "Failed to sync $dir"
-                cleanup_temp_dirs
+                handle_error "Failed to sync $dir"
             }

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

sync-dirs.sh-149-204 (1)

149-204: Unused parameter check_dir and potential prefix matching issue.

  1. The check_dir parameter (line 151) is declared but never used in the function body, which suggests either dead code or a missing implementation.

  2. Line 183: The pattern [[ "$full_path" == "$dir"* ]] could match unintentionally if directory names share prefixes (e.g., a hypothetical exclusion key cmp would match paths starting with cmp-android). Consider using "$dir/"* to ensure proper directory boundary matching.

Proposed fix
 is_excluded() {
-    local check_dir=$1
-    local full_path=$2
-    local check_type=$3  # 'file' or 'dir'
+    local full_path=$1
+    local check_type=$2  # 'file' or 'dir'
 
     # Remove ./ from the beginning of the path if it exists
     full_path="${full_path#./}"

And update the call site at line 327:

-    if is_excluded "$(dirname "$file")" "$file" "file"; then
+    if is_excluded "$file" "file"; then

For the prefix matching, consider:

-        if [[ "$full_path" == "$dir"* ]]; then
+        if [[ "$full_path" == "$dir/"* || "$full_path" == "$dir" ]]; then

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

core-base/common/src/commonMain/kotlin/template/core/base/common/DataState.kt-8-8 (1)

8-8: Typo: Duplicate "See" in comment.

The line reads "See See" which appears to be a copy-paste error.

🔎 Proposed fix
- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
core-base/common/src/commonMain/kotlin/template/core/base/common/DataStateExtensions.kt-8-8 (1)

8-8: Typo: Duplicate "See" in comment.

Same typo as in DataState.kt.

🔎 Proposed fix
- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
core-base/common/src/commonMain/kotlin/template/core/base/common/ImageExtension.kt-91-102 (1)

91-102: Edge case: Empty MIME type returns empty string instead of null.

For data URIs like "data:;base64,..." (valid per RFC 2397, defaulting to text/plain), extractMimeTypeFromDataUri returns an empty string rather than null. This behavior may be unexpected given the nullable return type typically signals invalidity.

Consider either:

  • Returning null for empty MIME types to maintain consistency with the nullable return type semantics, or
  • Documenting that empty strings are valid and represent the default MIME type
🔎 Proposed fix to return null for empty MIME types
 fun String.extractMimeTypeFromDataUri(): String? {
     val dataUriPrefix = "data:"
     val base64Prefix = ";base64,"
 
     return takeIf { it.startsWith(dataUriPrefix) && it.contains(base64Prefix) }
         ?.substringAfter(dataUriPrefix)
         ?.substringBefore(base64Prefix)
+        ?.takeIf { it.isNotEmpty() }
 }
core-base/database/src/androidMain/kotlin/template/core/base/database/AppDatabaseFactory.kt-8-8 (1)

8-8: Fix typo in license header.

"See See" should be "See" (duplicate word).

core-base/database/src/nativeMain/kotlin/template/core/base/database/AppDatabaseFactory.kt-8-8 (1)

8-8: Fix typo in license header.

"See See" should be "See" (duplicate word).

core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/Room.nonJsCommon.kt-8-8 (1)

8-8: Fix typo in license header.

"See See" should be "See" (duplicate word).

core-base/database/src/desktopMain/kotlin/template/core/base/database/AppDatabaseFactory.kt-8-8 (1)

8-8: Fix typo in license header.

"See See" should be "See" (duplicate word).

core-base/database/src/commonMain/kotlin/template/core/base/database/TypeConverter.kt-8-8 (1)

8-8: Fix typo in license header.

"See See" should be "See" (duplicate word).

🔎 Proposed fix
- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
core-base/database/README.md-191-196 (1)

191-196: Minor grammatical improvement.

Per static analysis, "iOS/macOS specific" should be hyphenated as a compound adjective.

🔎 Proposed fix
 - Leverages Kotlin/Native C interop for platform APIs
-- Requires iOS/macOS specific Room dependencies
+- Requires iOS/macOS-specific Room dependencies
core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/TypeConverter.nonJsCommon.kt-8-8 (1)

8-8: Fix typo in license header.

"See See" should be "See" (duplicate word).

🔎 Proposed fix
- * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
core-base/database/build.gradle.kts-1-20 (1)

1-20: Remove duplicate copyright header and unused import.

The file contains a duplicate copyright header (lines 1-9 and 12-20). Additionally, line 10 imports org.jetbrains.compose.compose which is unused in this build file.

🔎 Proposed fix
-/*
- * 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/kmp-project-template/blob/main/LICENSE
- */
-import org.jetbrains.compose.compose
-
 /*
  * Copyright 2025 Mifos Initiative
  *
core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt-218-236 (1)

218-236: Clarify the documentation for OnConflictStrategy.NONE.

The OnConflictStrategy.NONE constant is a standard Room constant (not custom), so no removal is needed. However, the documentation comment is inaccurate. According to Room's documentation, NONE (the default) has runtime behavior identical to ABORT—it rolls back the transaction when conflicts occur.

Update the comment from:

/** No conflict resolution strategy specified (may cause exceptions) */
const val NONE = 0

to something like:

/** Default strategy (no ON CONFLICT clause). Runtime behavior same as ABORT (rolls back transaction) */
const val NONE = 0

This prevents confusion about the actual behavior.

core-base/platform/build.gradle.kts-1-20 (1)

1-20: Remove duplicate copyright header.

The copyright header appears twice (lines 1-10 and lines 12-20). Remove one of the duplicate blocks.

🔎 Proposed fix
 /*
  * 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/kmp-project-template/blob/main/LICENSE
  */
 import org.gradle.kotlin.dsl.implementation
 
-/*
- * 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/kmp-project-template/blob/main/LICENSE
- */
 plugins {
     alias(libs.plugins.kmp.library.convention)
core-base/ui/build.gradle.kts-1-20 (1)

1-20: Remove duplicate license header.

The license header appears twice (lines 1-9 and lines 12-20), with an import statement sandwiched between them. Remove the duplicate header block at lines 12-20.

🔎 Proposed fix
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+
 /*
  * 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/kmp-project-template/blob/main/LICENSE
  */
-import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
-
-/*
- * 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/kmp-project-template/blob/main/LICENSE
- */
 plugins {
     alias(libs.plugins.kmp.library.convention)
     alias(libs.plugins.jetbrainsCompose)
core-base/platform/README.md-73-76 (1)

73-76: Truncated documentation text.

Lines 75-76 appear to be incomplete:

  • Line 75 ends with "android.content.Context")
  • Line 76 ends abruptly with "- \AppContext.activity`: Provides access to the current activity (e.g., `android`"

Please complete this documentation.

core-base/designsystem/README.md-30-30 (1)

30-30: Fix documentation formatting issues.

Address the following markdown formatting issues for better readability:

  1. Lines 30 & 331: Add language specifiers to fenced code blocks (e.g., text or appropriate language)
  2. Line 341: Replace emphasis with a proper heading (e.g., ## Built with ❤️ for the Kotlin Multiplatform community)
🔎 Proposed fixes

For line 30:

-```
+```text
 designsystem/

For line 331:

-```
+```text
 Copyright 2025 Mifos Initiative

For line 341:

-**Built with ❤️ for the Kotlin Multiplatform community**
+## Built with ❤️ for the Kotlin Multiplatform community

Also applies to: 331-331, 341-341

core-base/common/build.gradle.kts-47-47 (1)

47-47: Add newline at end of file.

The file is missing a newline at the end, which is a common convention to prevent issues with some tools and diff utilities.

🔎 Proposed fix
         jsMain.dependencies {
             api(libs.jb.kotlin.stdlib.js)
             api(libs.jb.kotlin.dom)
         }
     }
 }
+
core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt-19-21 (1)

19-21: Track or remove the TODO comment.

The TODO comment suggests implementing a custom review flow, but this is the non-Android stub where platform-specific review flows may not be applicable. Consider whether this TODO is necessary for non-Android targets, or if it should be removed.

Would you like me to open an issue to track this task, or shall we remove the TODO if custom review flow isn't needed for non-Android platforms?

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

1-17: Copyright year inconsistency.

The copyright year is 2024 while other files in this PR use 2025. Consider updating for consistency.

🔎 Proposed fix
 /*
- * Copyright 2024 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
  *
  * This Source Code Form is subject to the terms of the Mozilla Public

The marker interface design is appropriate for its purpose.

core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSplitPane.kt-34-36 (1)

34-36: Unused parameters: minLeftWidth, minRightWidth, resizable.

These parameters are declared but never referenced in the implementation. Either:

  1. Remove them if resize functionality is not planned, or
  2. Implement the resize logic (drag handle on divider) and min-width constraints.

This appears to be incomplete functionality synced from the template.

Would you like me to help implement the resize functionality, or should these parameters be removed for now?

core-base/platform/src/androidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt-96-98 (1)

96-98: Incorrect Log.d parameter order.

Log.d(String tag, String msg, Throwable tr) expects tag first, then message. The current call has them reversed.

Proposed fix
                 }.addOnFailureListener {
-                    Log.d("Unable to update app!", "UpdateManager", it)
+                    Log.e("UpdateManager", "Unable to update app!", it)
                 }

Also changed to Log.e since this is an error condition.

core-base/ui/src/commonMain/kotlin/template/core/base/ui/EventsEffect.kt-29-42 (1)

29-42: Misleading parameter name and missing key dependency.

Two issues:

  1. Parameter lifecycleOwner has type Lifecycle, not LifecycleOwner — consider renaming to lifecycle for clarity.
  2. LaunchedEffect(key1 = Unit) won't restart if viewModel changes. If the same composable can receive different viewModels, the effect will continue observing the stale one.
Proposed fix
 @Composable
 fun <E> EventsEffect(
     viewModel: BaseViewModel<*, E, *>,
-    lifecycleOwner: Lifecycle = LocalLifecycleOwner.current.lifecycle,
+    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
     handler: suspend (E) -> Unit,
 ) {
-    LaunchedEffect(key1 = Unit) {
+    LaunchedEffect(key1 = viewModel) {
         viewModel.eventFlow
             .filter {
                 it is BackgroundEvent ||
-                    lifecycleOwner.currentState.isAtLeast(Lifecycle.State.RESUMED)
+                    lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
             }
             .onEach { handler.invoke(it) }
             .launchIn(this)
     }
 }
core-base/platform/src/androidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt-116-136 (1)

116-136: Missing error handling in checkForResumeUpdateState.

Unlike checkForAppUpdate, this method has no .addOnFailureListener. If the update info fetch fails, errors will be silently swallowed.

Proposed fix
     override fun checkForResumeUpdateState() {
         manager
             .appUpdateInfo
             .addOnSuccessListener { appUpdateInfo ->
                 if (appUpdateInfo.updateAvailability()
                     == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
                 ) {
                     // If an in-app update is already running, resume the update.
                     manager.startUpdateFlowForResult(
                         /* p0 = */
                         appUpdateInfo,
                         /* p1 = */
                         activity,
                         /* p2 = */
                         updateOptions,
                         /* p3 = */
                         UPDATE_MANAGER_REQUEST_CODE,
                     )
                 }
+            }.addOnFailureListener {
+                Log.e("UpdateManager", "Failed to check resume update state", it)
             }
     }
core-base/ui/src/commonMain/kotlin/template/core/base/ui/EventsEffect.kt-53-66 (1)

53-66: Same issues as the first overload: misleading name and static key.

Apply the same fixes for consistency: rename lifecycleOwnerlifecycle, and use eventFlow as the LaunchedEffect key so the effect restarts when the flow reference changes.

Proposed fix
 @Composable
 fun <E> EventsEffect(
     eventFlow: Flow<E>,
-    lifecycleOwner: Lifecycle = LocalLifecycleOwner.current.lifecycle,
+    lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
     handler: suspend (E) -> Unit,
 ) {
-    LaunchedEffect(key1 = Unit) {
+    LaunchedEffect(key1 = eventFlow) {
         eventFlow
             .filter {
                 it is BackgroundEvent ||
-                    lifecycleOwner.currentState.isAtLeast(Lifecycle.State.RESUMED)
+                    lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
             }
             .onEach { handler.invoke(it) }
             .launchIn(this)
     }
 }
core-base/platform/src/androidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt-85-94 (1)

85-94: Deprecated API: startUpdateFlowForResult with request code.

This overload is deprecated in Play Core library. The modern approach uses ActivityResultLauncher<IntentSenderRequest> with AppUpdateOptions, or the Task-based startUpdateFlow() API. Update to use the AppUpdateOptions-based overloads.

core-base/platform/src/commonMain/kotlin/template/core/base/platform/LocalManagerProviders.kt-55-60 (1)

55-60: Documentation typo: "circumstance manager" should be "app update manager".

The KDoc comment incorrectly refers to "circumstance manager" instead of "app update manager."

🔎 Proposed fix
 /**
- * Provides access to the circumstance manager throughout the app.
+ * Provides access to the app update manager throughout the app.
  */
 val LocalAppUpdateManager: ProvidableCompositionLocal<AppUpdateManager> = compositionLocalOf {
core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsEvent.kt-268-277 (1)

268-277: Documentation claims IllegalArgumentException but implementation silently truncates.

The KDoc for Param (line 238) and withParam (line 70) states @throws IllegalArgumentException if validation constraints are violated, but the invoke factory silently truncates values and uses a fallback key instead of throwing. This is a documentation-behavior mismatch.

Either update the documentation to reflect the defensive behavior, or throw exceptions as documented. The current defensive approach is actually preferable for analytics (avoids crashes), so updating the docs is recommended:

🔎 Proposed doc fix for Param class
- * @throws IllegalArgumentException if validation constraints are violated
+ * Keys exceeding 40 characters are truncated; blank keys fall back to "unknown_param".
+ * Values exceeding 100 characters are truncated.

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

core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/UiHelpers.kt-40-52 (1)

40-52: Potential duplicate screen view logging when additionalParams is provided.

When additionalParams is not empty, both logScreenView (line 41) and logEvent with Types.SCREEN_VIEW (lines 44-51) are called, potentially resulting in two screen view events being logged for the same navigation.

🔎 Proposed fix - consolidate into single event
     LaunchedEffect(screenName) {
-        analytics.logScreenView(screenName, sourceScreen)
-        // Log additional params if provided
         if (additionalParams.isNotEmpty()) {
             analytics.logEvent(
                 Types.SCREEN_VIEW,
                 mapOf(
                     ParamKeys.SCREEN_NAME to screenName,
                     *additionalParams.toList().toTypedArray(),
                 ).plus(sourceScreen?.let { mapOf(ParamKeys.SOURCE_SCREEN to it) } ?: emptyMap()),
             )
+        } else {
+            analytics.logScreenView(screenName, sourceScreen)
         }
     }
core-base/platform/src/androidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt-54-56 (1)

54-56: Incorrect Log.e parameter order.

Log.e(tag, message) expects the tag as the first parameter, but the code passes the message first. This will result in misleading log output.

🔎 Proposed fix
             } else {
-                Log.e("Failed to launch review flow.", task.exception?.message.toString())
+                Log.e("AppReviewManager", "Failed to launch review flow: ${task.exception?.message}")
             }
core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt-69-82 (1)

69-82: Incomplete URL encoding may cause malformed URLs.

The encode() function only handles spaces and newlines, but URL encoding requires escaping many more characters (&, =, ?, #, %, +, etc.). Consider using a proper URL encoding utility to prevent malformed URLs or unexpected behavior.

core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptMaterialTheme.kt-72-78 (1)

72-78: KDoc parameter documentation mismatch.

The documentation references @param darkTheme twice with different meanings:

  • Line 73: Boolean flag for dark theme
  • Line 74: Describes it as "KptThemeProvider for dark theme"

The actual parameter is named darkThemeProvider (line 83), but the doc says @param darkTheme.

🔎 Proposed fix
 /**
  * KptMaterialTheme with dark theme support.
  * Provides automatic light/dark theme switching with Material3 integration.
  *
  * @param darkTheme Whether to use dark theme. Defaults to system preference.
  * @param lightTheme KptThemeProvider for light theme
- * @param darkTheme KptThemeProvider for dark theme
+ * @param darkThemeProvider KptThemeProvider for dark theme
  * @param content The composable content that will have access to both KptTheme and MaterialTheme
  *
  * @sample KptMaterialThemeWithDarkModeExample
  */
core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt-84-96 (1)

84-96: SMS URL uses incorrect query separator.

The SMS URI should use ? to start the query string, not &. The format sms:$number&body= may not work correctly on iOS.

🔎 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)
         }
     }
core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt-22-30 (1)

22-30: keyWindow is deprecated in iOS 13+; use the scenes API instead.

UIApplication.sharedApplication().keyWindow is deprecated in iOS 13 and later. For iOS 13+, access the key window via UIApplication.shared.connectedScenes:

val currentViewController = UIApplication.shared.connectedScenes
    .compactMap { ($0 as? UIWindowScene)?.keyWindow }
    .last?.rootViewController

Or for broader compatibility (iOS 13+):

val currentViewController = UIApplication.shared.connectedScenes
    .compactMap { $0 as? UIWindowScene }
    .flatMap { $0.windows }
    .first { $0.isKeyWindow }?.rootViewController
core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableSupportingPaneScaffold.kt-59-103 (1)

59-103: Consider using the stable BackHandler API from androidx.activity.compose instead of androidx.compose.ui.backhandler.BackHandler.

The androidx.compose.ui.backhandler.BackHandler is experimental and requires the ExperimentalComposeUiApi opt-in. For production Android code, the stable androidx.activity.compose.BackHandler (from androidx.activity:activity-compose) is the recommended approach. If multiplatform support is a requirement, the experimental API is acceptable, but be aware of the stability implications across platform-specific implementations.

core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt-108-116 (1)

108-116: Redundant let block does not use its parameter.

Line 110 uses url.let { url.toUri() } but ignores the it parameter, making the let unnecessary.

🔎 Proposed fix
     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)
     }
core-base/platform/src/androidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt-56-63 (1)

56-63: Unsafe cast may throw ClassCastException.

The intent as Intent cast on line 59 will throw ClassCastException if caller passes a non-Intent object. Since the interface accepts Any, consider adding a type check or documenting the expected type contract.

🔎 Proposed fix with type validation
     override fun startActivity(intent: Any) {
+        if (intent !is Intent) {
+            Log.w("IntentManagerImpl", "startActivity called with non-Intent: ${intent::class}")
+            return
+        }
         try {
             Log.d("IntentManagerImpl", "Starting activity: $intent")
-            context.startActivity(intent as Intent)
+            context.startActivity(intent)
         } catch (_: ActivityNotFoundException) {
             // no-op
         }
     }
core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/ValidationUtils.kt-295-308 (1)

295-308: Validation error logging could exceed parameter limits.

The analytics_validation_error event includes errors parameter with joined error messages. If the original event had many errors, this could exceed MAX_PARAM_VALUE_LENGTH (100 chars). The error event itself should be validated/sanitized.

🔎 Proposed fix
                 if (logValidationErrors) {
                     delegate.logEvent(
                         AnalyticsEvent(
                             "analytics_validation_error",
                             listOf(
-                                Param(key = "original_event_type", value = event.type),
+                                Param(
+                                    key = "original_event_type", 
+                                    value = event.type.take(AnalyticsValidator.MAX_PARAM_VALUE_LENGTH)
+                                ),
                                 Param(
                                     key = "errors",
-                                    value = validationResult.errorMessages.joinToString("; "),
+                                    value = validationResult.errorMessages.joinToString("; ")
+                                        .take(AnalyticsValidator.MAX_PARAM_VALUE_LENGTH),
                                 ),
                             ),
                         ),
                     )
                 }
core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/theme/KptColorSchemeImpl.kt-246-299 (1)

246-299: KptColorSchemeBuilder is missing several color properties from KptColorSchemeImpl.

The builder exposes only 22 color properties, but KptColorSchemeImpl has 37 (missing: scrim, inverseSurface, inverseOnSurface, inversePrimary, surfaceDim, surfaceBright, surfaceContainerLowest, surfaceContainerLow, surfaceContainer, surfaceContainerHigh, surfaceContainerHighest, surfaceTint).

Users cannot customize these colors via the builder DSL.

🔎 Suggested additions to KptColorSchemeBuilder

Add the missing properties to the builder class:

var scrim: Color = Color(0xFF000000)
var inverseSurface: Color = Color(0xFF313033)
var inverseOnSurface: Color = Color(0xFFF4EFF4)
var inversePrimary: Color = Color(0xFFD0BCFF)
var surfaceDim: Color = Color(0xFFDAD6DC)
var surfaceBright: Color = Color(0xFFFFFBFE)
var surfaceContainerLowest: Color = Color(0xFFFFFFFF)
var surfaceContainerLow: Color = Color(0xFFF3EFF4)
var surfaceContainer: Color = Color(0xFFE7E0EC)
var surfaceContainerHigh: Color = Color(0xFFDAD6DC)
var surfaceContainerHighest: Color = Color(0xFFCFC8D0)
var surfaceTint: Color = Color(0xFF6750A4)

And include them in the build() function.

core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/theme/KptColorSchemeImpl.kt-301-336 (1)

301-336: KptTypographyBuilder defaults differ from KptTypographyImpl defaults.

The builder's default TextStyle values only set fontWeight and fontSize, but KptTypographyImpl includes lineHeight and letterSpacing. This means typography built via the DSL won't match the default typography unless explicitly configured.

Consider either copying the full defaults from KptTypographyImpl or delegating to KptTypographyImpl() for initial values.

core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptThemeExtensions.kt-295-297 (1)

295-297: Inconsistent use of this.primary instead of this.surfaceTint for surfaceTint.

Line 297 maps surfaceTint to this.primary, but KptColorScheme has a dedicated surfaceTint property. This could lead to incorrect colors if someone customizes surfaceTint separately from primary.

🔎 Proposed fix
-        surfaceTint = this.primary,
+        surfaceTint = this.surfaceTint,
core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptTopAppBar.kt-77-89 (1)

77-89: Fix typo: onNavigationIonClick should be onNavigationIconClick

The property name in KptTopAppBarConfiguration is misspelled (missing 'c' in "Icon"), causing API inconsistency. The public function overloads in KptTopAppBar.kt use the correct spelling onNavigationIconClick as parameters but must map them to the misspelled property.

Fix the property definition in KptTopAppBarConfiguration and all 8 usages in KptTopAppBar.kt (lines 80, 81, 178, 200, 222, 244, 311, 346).

@biplab1
Copy link
Contributor

biplab1 commented Jan 1, 2026

@amanna13 If you are looking to contribute to mobile projects at Mifos, please go through this document Welcome to the Mifos Mobile Apps Community to get started.

@amanna13
Copy link
Author

amanna13 commented Jan 1, 2026

@biplab1 Thanks for sharing this! I already gone thru the guide and have joined the Slack as well.

This PR focuses only on syncing missing root level files/folders and keeping behavior unchanged. The initial PR is necessarily large, but changes are mostly additive and mirror the template structure. Hence I intentionally kept it as one single PR instead of splitting it.

Please let me know if there’s anything specific you’d like me to do next for this PR. Thanks!

@biplab1
Copy link
Contributor

biplab1 commented Jan 2, 2026

@amanna13 Have you registered for the daily standup meeting? There is a link in the document itself. If you don't find it, let me know in the Slack mobile channel.

@amanna13
Copy link
Author

amanna13 commented Jan 2, 2026

@amanna13 Have you registered for the daily standup meeting? There is a link in the document itself. If you don't find it, let me know in the Slack mobile channel.

Yess !! I'm already in the meet.

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.

2 participants