Skip to content

Comments

Fix build failures across all KMP targets (JVM, JS, WASM, iOS)#2608

Open
Arinyadav1 wants to merge 11 commits intoopenMF:developmentfrom
Arinyadav1:Fix-build-failures
Open

Fix build failures across all KMP targets (JVM, JS, WASM, iOS)#2608
Arinyadav1 wants to merge 11 commits intoopenMF:developmentfrom
Arinyadav1:Fix-build-failures

Conversation

@Arinyadav1
Copy link
Contributor

@Arinyadav1 Arinyadav1 commented Feb 12, 2026

Fixes - Jira-#644

image image image

Summary by CodeRabbit

  • New Features

    • Web: language preference persisted to browser storage; improved locale handling.
    • Web/WASM/Desktop: initial UI scaffolding added for PDF preview, client lists/charges, groups, path-tracking, permission dialogs, image/pdf helpers, and camera launcher stubs.
    • New cross-platform currency formatter API surface.
  • Bug Fixes

    • Removed redundant CAMERA and legacy external-storage permissions from Android manifest.
  • Chores

    • Removed iOS x64 target.
    • Upgraded paging dependencies to v3.4.0.

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Multiplatform refactor: enabled JS/wasm targets, removed iosX64, added a KMP base library convention plugin, refactored Room expect/actual annotations, removed a DB migration and downgraded VERSION, introduced many JS/WASM/desktop/native stub implementations and FileKit/storage adjustments, and bumped paging/build settings.

Changes

Cohort / File(s) Summary
Build & Conventions
build-logic/convention/build.gradle.kts, build-logic/convention/src/main/kotlin/...CMPFeatureConventionPlugin.kt, build-logic/convention/src/main/kotlin/KMPCoreBaseLibraryConventionPlugin.kt, gradle/libs.versions.toml, settings.gradle.kts
Added KMPCoreBaseLibraryConventionPlugin and plugin alias; switched KotlinCompile -> compilerOptions; added lib/plugin entries; bumped paging; included :cmp-web.
KMP targets & DSL
build-logic/convention/src/main/kotlin/KMPRoomConventionPlugin.kt, .../KotlinMultiplatform.kt, cmp-desktop/build.gradle.kts, cmp-shared/build.gradle.kts, cmp-web/build.gradle.kts
Removed iosX64, enabled js(IR) and wasmJs targets; renamed moduleNameoutputModuleName; moved resources to commonMain; simplified JVM DSL.
Run config & web app
.run/cmp-web-js.run.xml, cmp-web/src/jsMain/kotlin/Application.kt, cmp-web/src/wasmJsMain/kotlin/Main.kt
Changed Gradle run task to :cmp-web:jsBrowserDevelopmentRun, added run metadata; added browser localStorage language persistence and related imports.
Room / core-base database API
core-base/database/src/commonMain/.../Room.kt, .../jsMain/.../Room.jsMain.kt, .../wasmJsMain/.../Room.wasmJsMain.kt, .../nonJsCommon/.../Room.nonJsCommon.kt
Converted Room expect annotations to require opt-in (@OptIn(ExperimentalMultiplatform) / @OptionalExpectation), expanded ForeignKey signature, removed TypeConverters/BuiltInTypeConverters surface, added JS/WASM actuals.
Database version & migrations
core/database/src/*/kotlin/com/mifos/room/MifosDatabase.kt, core/database/src/*/di/*
Changed VERSION 2→1, removed MIGRATION_1_2 and all addMigrations() calls, switched annotation source to template.core.base.database.
DAO update annotations
core/database/src/commonMain/kotlin/com/mifos/room/dao/*.kt
Added onConflict = OnConflictStrategy.NONE to multiple @Update annotations (CenterDao, ClientDao, ColumnValueDao, GroupsDao, LoanDao, SavingsDao).
Type converters & entities
core/database/src/commonMain/.../CustomTypeConverters.kt, .../LoanAccountEntity.kt
Added Currency JSON converters; renamed SavingAccountCurrency converters; changed status collate to UNDEFINED.
Platform DI & modules
core/data/src/*/kotlin/com/mifos/core/data/di/*, core/data/src/*/kotlin/com/mifos/core/data/util/*
Introduced Desktop/JS/WASM PlatformDependentDataModule implementations and Koin platform modules; removed old wasm DataModule file.
JS/WASM DB & Koin stubs
core/database/src/jsMain/..., core/database/src/wasmJsMain/...
Replaced TODO getters with empty Koin module { } for PlatformSpecificDatabaseModule; removed multiple JS/WASM MifosDatabase stubs.
FileKit / storage & web fallbacks
core/common/src/commonMain/.../FileKitUtil.kt, core/common/src/jsMain/.../CurrencyFormatter.js.kt, core/common/src/wasmJsMain/...
Removed/disabled appCache/appInternalStorage helpers and file-write features (TODOs); added CurrencyFormatter stubs for JS/WASM.
Core UI & design system stubs
core/ui/src/*/kotlin/...ImageUtil*, core/ui/src/*/kotlin/...ShareUtils*, core/designsystem/src/*/kotlin/...MifosPermissionBox*
Added ImageUtil and ShareUtils restartApplication stubs for JS/WASM; added PermissionBox and permission helper stubs.
Client feature platform implementations
feature/client/src/{jsMain,wasmJsMain,desktopMain,nativeMain}/kotlin/...
Added many actual composable stubs and utilities (ClientList, ShowClientCharge, ChargesListContent, Pinpoint, PdfViewer, PhoneNumberUtil, PlatformCameraLauncher, OpenPdfWithDefaultExternalApp) with empty/TODO bodies.
Client API & routing changes
feature/client/src/commonMain/kotlin/...
Changed downloadDocumentAndCache return type PlatformFileString; migrated Feature/RecordType usage to String in routes/params; commented out document write/open flows pending migration.
Image & file handling
feature/client/src/*/kotlin/...
Disabled compression/write logic and removed CMPImagePickNCropDialog usage; added writeFileToCache helper on Android; replaced file path assignments with TODOs.
Multiple feature scaffolds & serializers
feature modules (center, groups, path-tracking, savings, report, data-table, etc.)
Added numerous JS/WASM/desktop/native actual composable stubs and switched some Json.encodeToString calls to explicit serializers.
Android app & manifest
cmp-android/build.gradle.kts, cmp-android/src/main/AndroidManifest.xml, cmp-android/dependencies/*.txt, cmp-android/src/main/kotlin/cmp/android/app/*
Changed default KEYSTORE_ALIAS to mifos; removed CAMERA and WRITE_EXTERNAL_STORAGE permissions; pruned many test/tooling deps; upgraded paging; minor activity formatting changes.
Misc & utilities
core-base/analytics/..., core/common/src/commonMain/.../MapDeserializer.kt, core/common/src/commonMain/.../Utils.kt
Added AnalyticsHelper.timeExecution overload (opt-in ExperimentalTime); removed fallback else in MapDeserializer.read; small date formatting tweak.

Sequence Diagram(s)

(omitted — changes are broad refactors and many small stubs; no single new multi-component runtime flow introduced)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • biplab1
  • therajanmaurya
  • itsPronay

"🐰
I hop through code and stitch each part,
Web and wasm get placeholders to start,
Room now asks politely to opt in,
Plugins and targets shuffled with a grin,
Stubs await your magic to make them smart."

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% 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 title 'Fix build failures across all KMP targets (JVM, JS, WASM, iOS)' accurately summarizes the main objective of the pull request and is directly related to the changes throughout the codebase.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
Copy link
Contributor

biplab1 commented Feb 17, 2026

@Arinyadav1 I hope you have figured out the FileKit part, since it will break several screens as those parts have been commented out. Please make sure that it is resolved in the next PR. Also, ensure that the image cropping issue is taken care of in another PR immediately after that.

@Arinyadav1
Copy link
Contributor Author

@biplab1 Yeah sure after merge this PR I will fix that issue also

@Arinyadav1
Copy link
Contributor Author

@biplab1 please review it again

@biplab1
Copy link
Contributor

biplab1 commented Feb 18, 2026

@Arinyadav1 Some of the previously raised issues are still unresolved. Kindly address them, or explicitly comment if a fix is not required. Please notify me once this is done.

@Arinyadav1
Copy link
Contributor Author

@biplab1 I have fixed all that issues you can check and anyone is left please mention me with issue, I have comment on all issues..

@Arinyadav1
Copy link
Contributor Author

@biplab1 please review it again I have done the changes

@biplab1
Copy link
Contributor

biplab1 commented Feb 18, 2026

@Arinyadav1 Is every target building fine after the updates?

@Arinyadav1
Copy link
Contributor Author

@biplab1 Yeah Working fine in all targets

# Conflicts:
#	cmp-android/dependencies/demoDebugRuntimeClasspath.txt
#	cmp-android/dependencies/demoReleaseRuntimeClasspath.txt
#	cmp-android/dependencies/prodDebugRuntimeClasspath.txt
#	cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
#	core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt
#	core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/Room.nonJsCommon.kt
#	core/database/src/androidMain/kotlin/com/mifos/room/MifosDatabase.kt
#	core/database/src/androidMain/kotlin/com/mifos/room/di/DatabaseModule.android.kt
#	core/database/src/desktopMain/kotlin/com/mifos/room/di/DatabaseModule.desktop.kt
#	core/database/src/jsMain/kotlin/com/mifos/room/di/DatabaseModule.common.js.kt
#	core/database/src/jsMain/kotlin/com/mifos/room/utils/GetCurrentTimeInMillis.js.kt
#	core/database/src/nativeMain/kotlin/com/mifos/room/di/PlatformSpecificModule.kt
#	core/database/src/wasmJsMain/kotlin/com/mifos/room/di/DatabaseModule.common.wasmJs.kt
#	core/database/src/wasmJsMain/kotlin/com/mifos/room/utils/GetCurrentTimeInMillis.wasmJs.kt
#	core/ui/src/desktopMain/kotlin/com/mifos/core/ui/util/ShareUtils.desktop.kt
#	feature/client/src/wasmJsMain/kotlin/com/mifos/feature/client/utils/OpenPdfInDefaultExternalApp.wasmJs.kt
#	gradle.properties
#	gradle/libs.versions.toml
#	gradle/wrapper/gradle-wrapper.properties
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: 10

Note

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

Caution

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

⚠️ Outside diff range comments (22)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDocuments/ClientDocumentsViewModel.kt (2)

200-228: ⚠️ Potential issue | 🔴 Critical

Loading dialog is never dismissed on download success — UI is permanently stuck.

DataState.Loading triggers loadingDialogState() (line 201), but the entire DataState.Success block is now a no-op comment. nullDialogState() is commented out, so the loading spinner is never cleared after a successful download. The user is left with a frozen, unresponsive loading state.

Even though the FileKit integration is being deferred, nullDialogState() must remain active in the success branch to keep the dialog lifecycle correct.

🐛 Minimal fix — dismiss the dialog even while the rest is deferred
                            is DataState.Success -> {
                                // TODO: writeFileToCache return type has changed because currently not support all
                                //  KMP target that's why currently comment out this it will fix in next PR
+                               nullDialogState()
//                                documentSelectAndUploadRepository.updateEntityDocument(platformFile = dataState.data)
-//                                nullDialogState()
//                                if (dataState.data.extension == "pdf") {
//                                    sendAction(ClientDocumentsActions.OpenExternalPdfViewer(dataState.data))
//                                } else {
//                                    sendEvent(ClientDocumentsEvents.OnViewDocument)
//                                }
                            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDocuments/ClientDocumentsViewModel.kt`
around lines 200 - 228, The success branch currently does nothing and never
dismisses the loading UI; inside the is DataState.Success branch in
ClientDocumentsViewModel (the block handling DataState.Success), call
nullDialogState() to dismiss the loading dialog (before or immediately after any
further actions) and then continue with the existing
sendEvent(ClientDocumentsEvents.OnViewDocument) if appropriate; leave the rest
of the deferred file-handling lines commented out but ensure nullDialogState()
is executed so the spinner is cleared on download success.

107-109: ⚠️ Potential issue | 🟡 Minor

OpenExternalPdfViewer handler and previewPdfInExternalApp are now unreachable dead code.

The only site that dispatches ClientDocumentsActions.OpenExternalPdfViewer (previously line 210 in the commented-out block) is disabled. previewPdfInExternalApp (lines 230–240) and the OpenExternalPdfViewer branch in handleAction can never be reached. Similarly, ClientDocumentsEvents.OnViewDocument (line 311) can never be sent.

These can remain as stubs if the follow-up PR will restore the functionality, but it's worth tracking explicitly so they aren't forgotten.

Do you want me to open a tracking issue for restoring full FileKit/document-viewing support in the follow-up PR?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDocuments/ClientDocumentsViewModel.kt`
around lines 107 - 109, ClientDocumentsActions.OpenExternalPdfViewer,
previewPdfInExternalApp, and the ClientDocumentsEvents.OnViewDocument emission
are currently dead/unreachable because the only dispatcher was disabled; either
remove these unreachable branches and helper function from
ClientDocumentsViewModel or explicitly retain them as stubs with a clear TODO
and tracking reference so they aren't lost. Update the handleAction switch (the
OpenExternalPdfViewer branch) and the previewPdfInExternalApp method: if you
keep them, add a TODO comment linking to a tracking issue (or open one now) and
mark the methods/events as intentionally noop/stub; if you remove them, also
remove any related imports/usages (e.g., references to
ClientDocumentsEvents.OnViewDocument) to avoid unused code. Ensure you
update/tests or comments to reflect which approach was taken.
feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt (1)

352-368: ⚠️ Potential issue | 🟠 Major

Image upload is silently broken and selectedImagePath now permanently returns null.

Since both selectedImagePath = file.path assignments are commented out, selectedImagePath (Line 347) stays null for the entire composition lifetime. This has two cascading regressions:

  1. No visual feedbackClientImageSection(selectedImagePath = selectedImagePath) (Line 470) always renders the placeholder, so users see no indication that their image was picked.

  2. Upload never triggeredhandleSubmitClick passes selectedImagePath (always null) to setFileForUpload.invoke(...) (Line 794). The callback checks filePath?.let { createClientWithImage = true } (Lines 221–224), so createClientWithImage is never set to true. When SetClientId is reached (Lines 232–238), the screen always falls to navigateBack() instead of calling uploadImage(uiState.id). The PlatformFile stored in the ViewModel via onImageSelected(file) is therefore silently discarded on every target.

The cleanest minimal fix is to decouple the upload trigger from the now-unavailable path. Since onImageSelected already propagates the PlatformFile to the ViewModel, introduce a dedicated flag to drive createClientWithImage:

🛠️ Proposed fix – decouple upload trigger from path
     var selectedImagePath by rememberSaveable { mutableStateOf<String?>(null) }
+    var imageSelected by rememberSaveable { mutableStateOf(false) }
     val galleryLauncher = rememberFilePickerLauncher(
         type = FileKitType.Image,
     ) { file ->
         file?.let {
-//            TODO: path not support in kmp all targets
-//            selectedImagePath = file.path
+            // TODO: display selected image preview (path API unavailable cross-platform)
+            imageSelected = true
             onImageSelected(file)
         }
     }

     val cameraLauncher = rememberPlatformCameraLauncher { file ->
         file?.let {
-//            TODO: path not support in kmp all targets
-//            selectedImagePath = file.path
+            // TODO: display selected image preview (path API unavailable cross-platform)
+            imageSelected = true
             onImageSelected(file)
         }
     }

Then replace the setFileForUpload call-site in the Submit onClick with:

-                    setFileForUpload,
+                    imageSelected,

And update handleSubmitClick signature/body accordingly:

-    selectedImagePath: String?,
-    setFileForUpload: (filePath: String?) -> Unit,
+    imageSelected: Boolean,
+    setFileForUpload: (selected: Boolean) -> Unit,
-    setFileForUpload.invoke(selectedImagePath)
+    setFileForUpload.invoke(imageSelected)

And the lambda in the stateful overload:

-    setFileForUpload = { filePath ->
-        filePath?.let {
-            createClientWithImage = true
-        }
-    },
+    setFileForUpload = { selected ->
+        if (selected) createClientWithImage = true
+    },

For image preview, the ViewModel could expose the PlatformFile (or a ByteArray) as a StateFlow so ClientImageSection can render it without relying on a file path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt`
around lines 352 - 368, selectedImagePath is never set because both
selectedImagePath = file.path lines in the rememberFilePickerLauncher and
rememberPlatformCameraLauncher callbacks are commented out, so the UI always
shows a placeholder and upload logic never triggers; instead, modify the flow so
onImageSelected(file) also flips a dedicated flag (e.g., imageSelectedForUpload
or hasSelectedImage) in the ViewModel/state when invoked from
rememberFilePickerLauncher and rememberPlatformCameraLauncher, update
setFileForUpload.invoke(...) call-site in the Submit onClick to pass that flag
(or call a new setCreateClientWithImage(true) method) rather than relying on a
file path, and adjust handleSubmitClick, createClientWithImage handling, and
ClientImageSection to read the ViewModel/state-provided flag and/or PlatformFile
(exposed as StateFlow or ByteArray) so previews and upload trigger correctly
without depending on file.path; use the existing symbols
rememberFilePickerLauncher, rememberPlatformCameraLauncher, selectedImagePath,
onImageSelected(file), setFileForUpload.invoke, handleSubmitClick,
createClientWithImage, and ClientImageSection to locate and implement these
changes.
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt (1)

163-176: ⚠️ Potential issue | 🟠 Major

"Upload New Photo" button leads to a silent UX dead-end — disable it or add a placeholder dialog.

When the user taps "Upload New Photo" (Line 166), ClientProfileEditAction.OnUploadNewPhotoClick sets dialogState = ShowUploadOptions in the ViewModel. However, the ShowUploadOptions branch in ClientProfileEditDialogs (Line 229) is completely empty—all UI code is commented out. No dialog is shown, and there is no mechanism to dismiss the state. The user sees no feedback and cannot proceed, leaving them stuck.

Since the picker migration to crop-krop-ui is deferred, either disable the button or display a temporary placeholder:

Option 1: Disable the button
                MifosTextButton(
                    text = { Text(stringResource(Res.string.upload_new_photo)) },
                    onClick = {
                        onAction(ClientProfileEditAction.OnUploadNewPhotoClick)
                    },
                    modifier = Modifier.fillMaxWidth(),
+                   enabled = false, // TODO(KMP): Re-enable once migrated to crop-krop-ui
                    leadingIcon = {
                        Icon(
                            painter = painterResource(Res.drawable.arrow_up),
                            contentDescription = null,
                            modifier = Modifier.size(DesignToken.sizes.iconAverage),
                        )
                    },
                )
Option 2: Show a temporary dialog
        ClientProfileEditState.DialogState.ShowUploadOptions -> {
            // TODO(KMP): Migrate from cmp-image-pick-n-crop to crop-krop-ui in follow-up PR.
+           MifosBasicDialog(
+               visibilityState = BasicDialogState.Shown(
+                   title = "Feature Unavailable",
+                   message = "Photo upload will be available in an upcoming update.",
+               ),
+               onDismissRequest = { onAction(ClientProfileEditAction.DismissModalBottomSheet) },
+               onConfirm = { onAction(ClientProfileEditAction.DismissModalBottomSheet) },
+               confirmText = stringResource(Res.string.ok),
+           )
        }

Also consider removing the 25 lines of commented-out code (lines 232–256) to reduce noise; the TODO comment already documents the migration intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`
around lines 163 - 176, The "Upload New Photo" button triggers
ClientProfileEditAction.OnUploadNewPhotoClick which sets dialogState =
ShowUploadOptions but ClientProfileEditDialogs has an empty ShowUploadOptions
branch, leaving users in a dead-end; fix by either disabling the MifosTextButton
(set enabled=false or omit onClick) in ClientProfileEditProfileScreen until the
picker migration is complete, or implement a simple placeholder dialog in
ClientProfileEditDialogs for the ShowUploadOptions case that shows a dismissible
message and clears dialogState on dismissal; also remove the large block of
commented-out picker migration code in ClientProfileEditDialogs to reduce noise
and rely on the existing TODO to document future work.
core/database/src/jsMain/kotlin/com/mifos/room/utils/GetCurrentTimeInMillis.js.kt (1)

12-14: ⚠️ Potential issue | 🟠 Major

TODO() stubs will throw NotImplementedError at runtime on the JS and WebAssembly targets.

The PR aims to fix build failures across all KMP targets. Both jsMain and wasmJsMain implementations currently compile without error but throw NotImplementedError whenever getCurrentTimeInMillis() is called at runtime. Any database or time-dependent feature on these JS-based targets will crash.

kotlin.js.Date is available in Kotlin/JS and provides millisecond-precision timestamps with zero additional dependencies:

🛠️ Proposed implementation for JS and WebAssembly targets
+import kotlin.js.Date
+
 actual fun getCurrentTimeInMillis(): Long {
-    TODO("Not yet implemented")
+    return Date().getTime().toLong()
 }

Apply this fix to both:

  • core/database/src/jsMain/kotlin/com/mifos/room/utils/GetCurrentTimeInMillis.js.kt
  • core/database/src/wasmJsMain/kotlin/com/mifos/room/utils/GetCurrentTimeInMillis.wasmJs.kt
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/database/src/jsMain/kotlin/com/mifos/room/utils/GetCurrentTimeInMillis.js.kt`
around lines 12 - 14, Replace the TODO stub in the actual function
getCurrentTimeInMillis() so it returns the current epoch millis using
kotlin.js.Date instead of throwing NotImplementedError; update both
implementations (the one in GetCurrentTimeInMillis.js.kt and the one in
GetCurrentTimeInMillis.wasmJs.kt) to construct kotlin.js.Date() and return its
getTime().toLong() (ensuring the return type Long) so JS and WASM targets no
longer crash at runtime.
core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/ShareUtils.js.kt (1)

56-57: ⚠️ Potential issue | 🟡 Minor

openUrl is a no-op on JS but functional on WASM — likely a regression.

The WASM counterpart implements openUrl as window.open(url) (ShareUtils.wasmJs.kt line 57), and kotlinx.browser.window is already imported here (line 13). This JS target should have the same behavior.

Suggested fix
     actual fun openUrl(url: String) {
+        window.open(url)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/jsMain/kotlin/com/mifos/core/ui/util/ShareUtils.js.kt` around
lines 56 - 57, The openUrl function in ShareUtils.js.kt is currently a no-op;
implement it to call the browser window opening API to match the WASM
implementation: inside actual fun openUrl(url: String) call the imported
kotlinx.browser.window.open(url) so the JS target opens the URL just like
ShareUtils.wasmJs.kt does. Ensure the call is made in the function body of
openUrl and keep the existing import of kotlinx.browser.window.
core/common/src/commonMain/kotlin/com/mifos/core/common/utils/FileKitUtil.kt (1)

146-149: ⚠️ Potential issue | 🟠 Major

Complete takePhotoIfSupported implementations for JS and WASM targets with proper error handling.

Two platform source sets are incomplete:

  • core/common/src/jsMain/kotlin/com/mifos/core/common/utils/FileKitUtil.js.kt (line 15): TODO
  • core/common/src/wasmJsMain/kotlin/com/mifos/core/common/utils/FileKitUtil.wasmJs.kt (line 15): TODO

Follow the pattern established in desktopMain (which correctly emits DataState.Error(IllegalStateException("Platform not supported"))). This ensures callers receive a meaningful error state on unsupported platforms rather than a thrown NotImplementedError at runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/common/src/commonMain/kotlin/com/mifos/core/common/utils/FileKitUtil.kt`
around lines 146 - 149, The JS and WASM platform implementations of expect fun
takePhotoIfSupported() are left as TODOs and should match the desktopMain
behavior by returning a Flow that emits a single DataState.Error with an
IllegalStateException("Platform not supported") (instead of throwing
NotImplementedError); update
core/common/src/jsMain/kotlin/com/mifos/core/common/utils/FileKitUtil.js.kt and
core/common/src/wasmJsMain/kotlin/com/mifos/core/common/utils/FileKitUtil.wasmJs.kt
to implement takePhotoIfSupported() to emit
DataState.Error(IllegalStateException("Platform not supported")) for
PlatformFile? using the same Flow construction pattern used in desktopMain so
callers receive a meaningful error state.
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt (1)

53-65: ⚠️ Potential issue | 🟡 Minor

Feature.valueOf() lacks error handling — add validation or explicit fallback.

Line 56 calls Feature.valueOf(route.feature) without error handling. While the current call site (ClientIdentifiersListScreen) always passes a valid Feature.name value, any misconfigured navigation path or future deep link passing an invalid string will crash the screen in init with IllegalArgumentException.

The second concern in the original review about state.feature being read after mutableStateFlow.update{} is not an issue: BaseViewModel.state is a synchronous getter backed directly by mutableStateFlow.value, so the read is safe.

Consider explicitly handling the enum parsing:

Suggested approach
 init {
+    val parsedFeature = try {
+        Feature.valueOf(route.feature)
+    } catch (e: IllegalArgumentException) {
+        Feature.ADD_IDENTIFIER  // or throw with more context
+    }
     mutableStateFlow.update {
         it.copy(
             clientId = route.clientId,
-            feature = Feature.valueOf(route.feature),
+            feature = parsedFeature,
         )
     }
-    if (state.feature == Feature.ADD_IDENTIFIER) {
+    if (parsedFeature == Feature.ADD_IDENTIFIER) {
         getIdentifiersOptionsAndObserveNetwork()
     } else {
         viewModelScope.launch {
             getDocumentId()
         }
     }
 }

Alternatively, validate at the navigation layer to ensure only valid Feature names are passed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt`
around lines 53 - 65, The code calls Feature.valueOf(route.feature) without
validation which can throw IllegalArgumentException for invalid strings; update
ClientIdentifiersAddUpdateViewModel to safely parse the enum (e.g., use
Feature.values().firstOrNull { it.name == route.feature } or runCatching) and
choose a clear fallback (log/error state and bail out or default to a safe
Feature), then call mutableStateFlow.update with the validated feature and
proceed to either getIdentifiersOptionsAndObserveNetwork() or launch
getDocumentId() based on that validated value; ensure any fallback path records
the error (or navigates away) rather than letting the exception crash init.
core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/loans/LoanAccountEntity.kt (1)

73-77: ⚠️ Potential issue | 🟠 Major

Fix collate = UNDEFINED on line 73 — should use CollationSequence.UNSPECIFIED.

Line 73 incorrectly uses UNDEFINED (from ColumnInfoTypeAffinity) for the collate parameter, while line 76 correctly uses UNSPECIFIED (from CollationSequence). The collate parameter specifies a collation sequence (BINARY, NOCASE, RTRIM, etc.), not a type affinity. Both constants happen to have the same numeric value (1), so the code compiles and runs, but it's semantically incorrect and inconsistent. The status field should match the loanType field's usage:

Proposed fix
-    `@ColumnInfo`(index = true, name = INHERIT_FIELD_NAME, typeAffinity = UNDEFINED, collate = UNDEFINED, defaultValue = VALUE_UNSPECIFIED)
+    `@ColumnInfo`(index = true, name = INHERIT_FIELD_NAME, typeAffinity = UNDEFINED, collate = UNSPECIFIED, defaultValue = VALUE_UNSPECIFIED)
     val status: LoanStatusEntity? = null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/database/src/commonMain/kotlin/com/mifos/room/entities/accounts/loans/LoanAccountEntity.kt`
around lines 73 - 77, In LoanAccountEntity change the collate argument for the
status ColumnInfo to use CollationSequence.UNSPECIFIED (not UNDEFINED); update
the ColumnInfo on the status property (val status: LoanStatusEntity? = null) so
its collate parameter matches the loanType field by importing/using
CollationSequence.UNSPECIFIED for the collate value.
core/database/src/androidMain/kotlin/com/mifos/room/MifosDatabase.kt (1)

156-158: ⚠️ Potential issue | 🔴 Critical

ClientTypeConverters removal leaves LoanAccountEntity fields without required type converters.

ClientTypeConverters provided converters for LoanAccountEntity (used in ClientAccounts.loanAccounts, GroupAccounts.loanAccounts, and CenterAccounts.loanAccounts/memberLoanAccounts). Removing it without adding equivalent converters to CustomTypeConverters will cause runtime failures when attempting to serialize or deserialize these entity lists. Either restore ClientTypeConverters to the @TypeConverters annotation or add fromLoanAccountEntity and toLoanAccountEntity converter methods to CustomTypeConverters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/database/src/androidMain/kotlin/com/mifos/room/MifosDatabase.kt` around
lines 156 - 158, The `@TypeConverters` annotation on MifosDatabase removed
ClientTypeConverters which provided serialization for LoanAccountEntity used by
ClientAccounts.loanAccounts, GroupAccounts.loanAccounts and
CenterAccounts.memberLoanAccounts; restore functionality by either re-adding
ClientTypeConverters to the `@TypeConverters` list or implement equivalent
converter methods named fromLoanAccountEntity and toLoanAccountEntity inside
CustomTypeConverters that handle converting LoanAccountEntity lists to/from a
persistable format so those entity fields can be serialized/deserialized at
runtime.
core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt (1)

305-310: ⚠️ Potential issue | 🟠 Major

Upsert annotation uses @Suppress instead of @OptionalExpectation, inconsistent with 15 other annotations.

Upsert is the only annotation in this file that still uses the legacy @Suppress("NO_ACTUAL_FOR_EXPECT") pattern. All other annotations lacking actuals on JS/WASM (Dao, Query, Insert, Delete, Update, Entity, Transaction, ColumnInfo, Embedded, Relation, TypeConverter, Database, Ignore, DatabaseView) have been migrated to @OptIn(ExperimentalMultiplatform::class) @OptionalExpectation``. With @OptionalExpectation, the compiler properly handles missing actuals by dropping the annotation on unsupported platforms; with `@Suppress`, the warning is merely silenced without semantic handling.

Proposed fix
-@Suppress("NO_ACTUAL_FOR_EXPECT")
+@OptIn(ExperimentalMultiplatform::class)
+@OptionalExpectation
 `@Target`(AnnotationTarget.FUNCTION)
 `@Retention`(AnnotationRetention.BINARY)
 expect annotation class Upsert(
     val entity: KClass<*>,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`
around lines 305 - 310, Replace the legacy suppression on the Upsert expect
annotation with the modern optional-expect mechanism: in the Upsert declaration
(expect annotation class Upsert), remove `@Suppress`("NO_ACTUAL_FOR_EXPECT") and
add `@OptIn`(ExperimentalMultiplatform::class) and `@OptionalExpectation` so the
compiler will drop the annotation on unsupported platforms rather than merely
silencing warnings.
core/common/build.gradle.kts (1)

46-50: ⚠️ Potential issue | 🟡 Minor

Remove unused filekit.compose (v0.8.8) and redundant filekit.dialogs.

Lines 46–50 declare five FileKit dependencies, but two are problematic:

  1. Line 48 (libs.filekit.compose v0.8.8): This old pre-v0.10 artifact is not imported anywhere in the codebase. All FileKit usage comes from v0.12.0 modules (filekit-core, filekit-dialogs, filekit-dialogs-compose). Remove this unused dependency.

  2. Line 50 (libs.filekit.dialogs): Redundant. The filekit-dialogs-compose dependency on line 49 already transitively includes filekit-dialogs. Explicitly declaring it is unnecessary.

Recommended action: Remove lines 48 and 50. Keep filekit.core, filekit.coil, and filekit.dialog.compose (v0.12.0).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/common/build.gradle.kts` around lines 46 - 50, Remove the unused and
redundant FileKit dependencies: delete the dependency entry referencing
libs.filekit.compose (the old v0.8.8 artifact) and remove the explicit
libs.filekit.dialogs declaration since filekit.dialogs is transitively provided
by filekit.dialog.compose; keep the existing implementations of
libs.filekit.core, libs.filekit.coil, and libs.filekit.dialog.compose so only
those three remain in the implementation block.
cmp-android/build.gradle.kts (1)

99-104: ⚠️ Potential issue | 🟡 Minor

Duplicate dependencyGuard blocks — consolidate into one.

dependencyGuard { ... } is declared twice in the same file. In Gradle, the extension is configured additively across blocks, but having two separate declarations is confusing and risks divergence (e.g., the prodReleaseRuntimeClasspath entry appears in both blocks with different options).

♻️ Proposed consolidation
-dependencyGuard {
-    configuration("demoDebugRuntimeClasspath")
-    configuration("demoReleaseRuntimeClasspath")
-    configuration("prodDebugRuntimeClasspath")
-    configuration("prodReleaseRuntimeClasspath")
-}
-
-// ... rest of dependencies block ...
-
-dependencyGuard {
-    configuration("prodReleaseRuntimeClasspath") {
-        modules = true
-        tree = true
-    }
-}
+dependencyGuard {
+    configuration("demoDebugRuntimeClasspath")
+    configuration("demoReleaseRuntimeClasspath")
+    configuration("prodDebugRuntimeClasspath")
+    configuration("prodReleaseRuntimeClasspath") {
+        modules = true
+        tree = true
+    }
+}

Also applies to: 152-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmp-android/build.gradle.kts` around lines 99 - 104, The file declares
duplicate dependencyGuard blocks which should be consolidated into a single
configuration to avoid confusion and divergent entries; locate the multiple
dependencyGuard { ... } blocks (they reference configurations like
"demoDebugRuntimeClasspath", "demoReleaseRuntimeClasspath",
"prodDebugRuntimeClasspath", and "prodReleaseRuntimeClasspath") and merge their
configuration(...) lines into one dependencyGuard block so each runtimeClasspath
entry appears only once and in a single place.
cmp-web/src/wasmJsMain/kotlin/Main.kt (2)

57-57: ⚠️ Potential issue | 🟡 Minor

Replace document.body!! with a null-safe guard.

document.body can technically be null (e.g., before <body> is parsed or in headless environments), causing an unhandled NullPointerException that crashes the WASM entry point.

🛡️ Proposed fix
-        ComposeViewport(document.body!!) {
+        val body = document.body ?: return@onWasmReady
+        ComposeViewport(body) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmp-web/src/wasmJsMain/kotlin/Main.kt` at line 57, The call to
ComposeViewport(document.body!!) can throw an NPE because document.body may be
null; update the WASM entry to null-check the body before passing it to
ComposeViewport (e.g., guard with if (document.body == null) return or obtain a
safe element via document.getElementsByTagName("body").firstOrNull() and
return/log if null) so that ComposeViewport is only invoked with a non-null DOM
element; adjust Main.kt accordingly around the ComposeViewport invocation.

58-89: ⚠️ Potential issue | 🟠 Major

localeVersion is never incremented — locale changes have no visible effect on WASM.

key(localeVersion) is intended to force a full recomposition of SharedApp when the locale changes, but localeVersion is never mutated inside handleAppLocale. It stays 0 forever, so key() never changes and Compose never restarts the subtree. Additionally, window.location.reload() is commented out with no alternative applied. The net effect: the user selects a language, localStorage is updated, but the UI is never refreshed — the locale change is silently swallowed.

🐛 Proposed fix — increment `localeVersion` inside the lambda
 handleAppLocale = { languageTag ->
     if (languageTag != null) {
         localStorage.setItem("app_language", languageTag)
         document.documentElement?.setAttribute("lang", languageTag)
     } else {
         localStorage.removeItem("app_language")
         val browserLang = window.navigator.language
         document.documentElement?.setAttribute("lang", browserLang)
     }
+    localeVersion++   // triggers key() recomposition
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmp-web/src/wasmJsMain/kotlin/Main.kt` around lines 58 - 89, The locale
change handler never updates the remembered localeVersion, so key(localeVersion)
never changes and SharedApp is not recomposed; inside the handleAppLocale lambda
update the remembered state (localeVersion) after storing/removing localStorage
and setting document.documentElement (e.g., increment localeVersion) so
key(localeVersion) will rerun the composable; if you want a full page reload
instead, either uncomment and use window.location.reload() or perform the
increment+reload decision there, but do not leave localeVersion unchanged.
core-base/database/build.gradle.kts (2)

1-22: ⚠️ Potential issue | 🟡 Minor

Duplicate copyright block — lines 1–9 and 12–20 are identical.

The file carries two consecutive copyright headers. One should be removed.

🔧 Remove the duplicate header
-/*
- * 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/android-client/blob/master/LICENSE.md
- */
 import org.jetbrains.compose.compose
 
 /*
  * Copyright 2025 Mifos Initiative
  ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/build.gradle.kts` around lines 1 - 22, Remove the
duplicated copyright header at the top of the file so only one copy remains:
locate the two identical license comment blocks preceding the import statement
and the plugins { alias(libs.plugins.kmp.core.base.library.convention) }
declaration and delete the second block (the one immediately above the plugins
block), leaving a single copyright header followed by import
org.jetbrains.compose.compose and the plugins block.

10-10: ⚠️ Potential issue | 🟡 Minor

Unused import org.jetbrains.compose.compose.

No compose.* symbol is referenced in this file (the kotlin.sourceSets blocks only use libs.androidx.room.runtime). This import is a leftover that should be removed.

🔧 Remove the unused import
-import org.jetbrains.compose.compose
-
 /*
  * Copyright 2025 Mifos Initiative
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/build.gradle.kts` at line 10, Remove the unused import
org.jetbrains.compose.compose at the top of the build.gradle.kts file—it's not
referenced anywhere (no compose.* symbols are used; the kotlin.sourceSets only
reference libs.androidx.room.runtime), so delete that single import line to
clean up the file.
cmp-web/src/jsMain/kotlin/Application.kt (2)

36-61: ⚠️ Potential issue | 🟠 Major

localeVersion is never incremented — key(localeVersion) never forces recomposition.

localeVersion is declared as mutable state and used as the argument to key() to trigger a full recomposition of SharedApp on locale change. However, handleAppLocale never reassigns localeVersion, so the key stays at 0 permanently. The key(localeVersion) block is effectively dead code: after a locale change, Compose UI strings will not update (only the DOM lang attribute is patched directly). The commented-out window.location.reload() at line 57 was the original working approach; the replacement recomposition mechanism is incomplete.

🐛 Proposed fix — increment `localeVersion` at the end of the lambda
                 handleAppLocale = { languageTag ->
                     if (languageTag != null) {
                         localStorage.setItem("app_language", languageTag)
                         document.documentElement?.setAttribute("lang", languageTag)
                     } else {
                         localStorage.removeItem("app_language")
                         val browserLang = window.navigator.language
                         document.documentElement?.setAttribute("lang", browserLang)
                     }
-                    // Reload page to apply language changes (required for web)
-                    // Note: This will reload the page, and locale selection depends on browser settings
-                    // window.location.reload()
+                    localeVersion++
                 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmp-web/src/jsMain/kotlin/Application.kt` around lines 36 - 61, The key-based
recomposition never occurs because localeVersion (declared with var
localeVersion by remember { mutableStateOf(0) }) is never changed; update the
handleAppLocale lambda (the one passed into SharedApp) to increment
localeVersion after applying localStorage/document changes so that
key(localeVersion) will change and force a full recomposition of SharedApp
(e.g., localeVersion += 1 or localeVersion++ at the end of handleAppLocale).

16-22: ⚠️ Potential issue | 🟡 Minor

Stale comment — this is the JS entry point, not WebAssembly.

The docblock says "The entry point of the WebAssembly Compose application", but this file lives in jsMain. The wasmJs target would have its own separate entry point. Update the comment to read "The entry point of the Kotlin/JS Compose application."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmp-web/src/jsMain/kotlin/Application.kt` around lines 16 - 22, The
file-level docblock incorrectly calls this the "WebAssembly Compose
application"; update the comment to say "The entry point of the Kotlin/JS
Compose application." and remove or replace any other WebAssembly-specific
phrasing so it correctly describes this JS entry point; you can find the comment
near the top of Application.kt alongside references to onWasmReady, the Compose
viewport creation, and the SharedApp composable—edit that comment text only.
gradle/libs.versions.toml (2)

39-39: ⚠️ Potential issue | 🟡 Minor

Remove the orphaned version entry cmpImagePickNCrop at line 39.

The library definition at line 200 is already commented out, but the corresponding version entry (cmpImagePickNCrop = "1.1.2") remains in the [versions] section and serves no purpose. Since no active build.gradle.kts files reference this library, cleanup is needed to remove dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle/libs.versions.toml` at line 39, Remove the dead version entry named
cmpImagePickNCrop from the [versions] section in gradle/libs.versions.toml; it's
orphaned because the corresponding library definition is commented out and no
build.gradle.kts references it, so delete the line cmpImagePickNCrop = "1.1.2"
to clean up unused versions.

139-140: ⚠️ Potential issue | 🟡 Minor

Remove unused filekit-compose:0.8.8 dependency declarations.

filekit-compose:0.8.8 is declared in 7 build.gradle files but is not imported or used anywhere in the codebase. All compose file-picking functionality has been migrated to the new filekit-dialogs-compose:0.12.0 API. The old artifact is dead code and can be safely removed.

🛠️ Cleanup — remove all filekit-compose declarations

Remove from gradle/libs.versions.toml:

-fileKitCompose = "0.8.8"

Remove from gradle/libs.versions.toml (versions.bundles or [libraries]):

-filekit-compose = { group = "io.github.vinceglb", name = "filekit-compose", version.ref = "fileKitCompose" }

Remove from all build.gradle.kts files:

-implementation(libs.filekit.compose)

Affected modules: feature/document, feature/client, core/ui, core/common, core-base/ui, cmp-android.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle/libs.versions.toml` around lines 139 - 140, Remove the unused
fileKitCompose declaration and all references to the old artifact: delete the
fileKitCompose = "0.8.8" entry from gradle/libs.versions.toml and remove any
usages or bundle entries named filekit-compose in the versions.bundles or
[libraries] sections; update affected build.gradle.kts files in modules
feature/document, feature/client, core/ui, core/common, core-base/ui, and
cmp-android to stop importing or using the old artifact and, where needed,
replace with the new filekit-dialogs-compose:0.12.0 alias (or remove the
dependency entirely if no replacement is required). Ensure no remaining
references to filekit-compose exist in the repo (dependencies, bundles, or
import aliases) before committing.
core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/PerformanceTracker.kt (1)

270-284: ⚠️ Potential issue | 🔴 Critical

timePerformance / timePerformanceSync create throwaway PerformanceTracker instances — accumulated metrics are inaccessible, and timing is broken due to fixed currentTime.

Each call to performanceTracker() constructs a fresh instance. While stopTimer() logs individual events to analytics, the internal performanceMetrics state is discarded, making getPerformanceStats() and logPerformanceSummary() unreachable for any tracker obtained via these wrappers.

More critically, line 267 defines currentTime as a fixed Long value evaluated once at module load, not a dynamic function. This causes all duration calculations (currentTime - startTime) to be zero or near-zero, rendering the entire timing system broken—affecting not only these convenience functions but any direct PerformanceTracker usage.

Fix required:

  • Change line 267 from private val currentTime = ... to a computed property or function that returns the current time on each access.
  • Consider whether timePerformance / timePerformanceSync should accept an optional tracker parameter to support accumulated stats, or document that they are single-shot only.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/PerformanceTracker.kt`
around lines 270 - 284, The convenience wrappers timePerformance and
timePerformanceSync call performanceTracker() which creates a fresh
PerformanceTracker each time (losing accumulated metrics from performanceMetrics
and making getPerformanceStats()/logPerformanceSummary() unusable), and the
module-level private val currentTime is evaluated once at load causing zero
durations; change currentTime into a function or computed property (e.g., fun
currentTime() = System.nanoTime()/SystemClock.elapsedRealtimeMillis() as
appropriate) so each measurement uses the real current time, and either update
timePerformance/timePerformanceSync to accept an optional PerformanceTracker
parameter (or reuse a shared instance) so callers can accumulate metrics instead
of always getting a throwaway tracker; ensure stopTimer(), performanceMetrics,
getPerformanceStats(), and logPerformanceSummary() continue to operate on the
same tracker instance when provided.

@biplab1
Copy link
Contributor

biplab1 commented Feb 19, 2026

@Arinyadav1 Can you please address the suggestions by @coderabbitai (relevant to your PR)? Please notify me after you are done.

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

Caution

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

⚠️ Outside diff range comments (2)
core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt (2)

520-524: ⚠️ Potential issue | 🟡 Minor

Doc example references @TypeConverters (plural) which has been removed.

The TypeConverter KDoc at line 522–523 shows @TypeConverters(DateConverters::class), but the TypeConverters (plural) annotation was removed in this change set. The example will confuse users since only @TypeConverter (singular, method-level) exists now.

Proposed fix — remove or update the stale reference
- * // Register in database
- * `@Database`(...)
- * `@TypeConverters`(DateConverters::class)
- * abstract class MyDatabase : RoomDatabase()
+ * // Register converters according to your platform's database configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`
around lines 520 - 524, The KDoc example still shows the removed plural
annotation `@TypeConverters(DateConverters::class)` which will confuse readers;
update the example in Room.kt to remove that line and instead reference the
correct singular usage: mention that conversion methods should be annotated with
`@TypeConverter` inside a `DateConverters` class (or similar) and then used by
the database. Locate the example block (the lines showing `@Database(...)` and
`@TypeConverters(DateConverters::class)`) and delete or replace the
`@TypeConverters(...)` line with a short note like “implement conversion methods
in DateConverters annotated with `@TypeConverter`” referencing the
`DateConverters` type and the `@TypeConverter` annotation.

305-310: 🛠️ Refactor suggestion | 🟠 Major

Migrate Upsert annotation from @Suppress("NO_ACTUAL_FOR_EXPECT") to @OptionalExpectation.

Upsert is inconsistent with Insert, Update, and Delete, which all use @OptIn(ExperimentalMultiplatform::class) + @OptionalExpectation. Since no platform-specific actual implementations of Upsert exist, it must be updated to match the pattern used by its siblings to ensure consistent behavior across targets.

Proposed fix
-@Suppress("NO_ACTUAL_FOR_EXPECT")
+@OptIn(ExperimentalMultiplatform::class)
+@OptionalExpectation
 `@Target`(AnnotationTarget.FUNCTION)
 `@Retention`(AnnotationRetention.BINARY)
 expect annotation class Upsert(
     val entity: KClass<*>,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`
around lines 305 - 310, The Upsert annotation is incorrectly marked with
`@Suppress`("NO_ACTUAL_FOR_EXPECT") instead of the module's pattern; replace the
`@Suppress` and its import with `@OptIn`(ExperimentalMultiplatform::class) and
annotate Upsert with `@OptionalExpectation` so it matches Insert/Update/Delete;
update the file-level annotations/imports for ExperimentalMultiplatform and
OptionalExpectation and keep the expect annotation class Upsert(val entity:
KClass<*>) unchanged.
🧹 Nitpick comments (1)
core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt (1)

153-163: ForeignKey is missing @OptIn(ExperimentalMultiplatform::class) / @OptionalExpectation — intentional but worth a brief inline comment.

Unlike the directly-applied annotations, ForeignKey can't use @OptionalExpectation because it's referenced as a type in Entity's constructor (val foreignKeys: Array<ForeignKey>), so it must be resolvable on every target. The file-level @Suppress("KotlinNoActualForExpect") covers it. A one-line comment here (and on Index, Junction, AutoMigration) explaining why this subset doesn't carry @OptionalExpectation would save future readers from treating it as an oversight.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`
around lines 153 - 163, Add a one-line inline comment above the expect
annotation class ForeignKey (and similarly above Index, Junction, AutoMigration)
explaining that these annotation types intentionally do not carry
`@OptionalExpectation/`@OptIn(ExperimentalMultiplatform::class) because they are
referenced as types (e.g., Entity.val foreignKeys: Array<ForeignKey>) and
therefore must be resolvable on every target; reference the existing file-level
`@Suppress`("KotlinNoActualForExpect") as the mechanism that covers them to
prevent future maintainers from treating the omission as an oversight.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`:
- Around line 128-137: The doc example in Room.kt incorrectly references
ForeignKey.CASCADE; update the example to use ForeignKeyAction.CASCADE (the
cascade constant is defined on ForeignKeyAction) so the annotation reads using
ForeignKeyAction.CASCADE for onDelete; ensure the import/usage in the example
matches the symbol ForeignKeyAction and not ForeignKey.

---

Outside diff comments:
In
`@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`:
- Around line 520-524: The KDoc example still shows the removed plural
annotation `@TypeConverters(DateConverters::class)` which will confuse readers;
update the example in Room.kt to remove that line and instead reference the
correct singular usage: mention that conversion methods should be annotated with
`@TypeConverter` inside a `DateConverters` class (or similar) and then used by
the database. Locate the example block (the lines showing `@Database(...)` and
`@TypeConverters(DateConverters::class)`) and delete or replace the
`@TypeConverters(...)` line with a short note like “implement conversion methods
in DateConverters annotated with `@TypeConverter`” referencing the
`DateConverters` type and the `@TypeConverter` annotation.
- Around line 305-310: The Upsert annotation is incorrectly marked with
`@Suppress`("NO_ACTUAL_FOR_EXPECT") instead of the module's pattern; replace the
`@Suppress` and its import with `@OptIn`(ExperimentalMultiplatform::class) and
annotate Upsert with `@OptionalExpectation` so it matches Insert/Update/Delete;
update the file-level annotations/imports for ExperimentalMultiplatform and
OptionalExpectation and keep the expect annotation class Upsert(val entity:
KClass<*>) unchanged.

---

Nitpick comments:
In
`@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`:
- Around line 153-163: Add a one-line inline comment above the expect annotation
class ForeignKey (and similarly above Index, Junction, AutoMigration) explaining
that these annotation types intentionally do not carry
`@OptionalExpectation/`@OptIn(ExperimentalMultiplatform::class) because they are
referenced as types (e.g., Entity.val foreignKeys: Array<ForeignKey>) and
therefore must be resolvable on every target; reference the existing file-level
`@Suppress`("KotlinNoActualForExpect") as the mechanism that covers them to
prevent future maintainers from treating the omission as an oversight.

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.

🧹 Nitpick comments (2)
core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt (2)

305-310: Upsert is the only function-level expect annotation still using the old @Suppress("NO_ACTUAL_FOR_EXPECT") pattern.

Every other directly-applicable expect annotation class with function targets in this file (Query, Insert, Update, Delete) has been migrated to @OptIn(ExperimentalMultiplatform::class) + @OptionalExpectation. No actual Upsert implementation exists in the codebase, making the modern pattern semantically correct and safe to adopt.

Migrating Upsert eliminates this inconsistency and allows the file-level @file:Suppress("KotlinNoActualForExpect") (line 10) to be removed.

♻️ Proposed refactor
-@Suppress("NO_ACTUAL_FOR_EXPECT")
+@OptIn(ExperimentalMultiplatform::class)
+@OptionalExpectation
 `@Target`(AnnotationTarget.FUNCTION)
 `@Retention`(AnnotationRetention.BINARY)
 expect annotation class Upsert(
     val entity: KClass<*>,
 )

Remove the file-level suppression:

-@file:Suppress("KotlinNoActualForExpect")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`
around lines 305 - 310, The Upsert expect annotation should be migrated from
using the old `@Suppress`("NO_ACTUAL_FOR_EXPECT") pattern to the modern
optional-expectation approach used by Query/Insert/Update/Delete: annotate the
expect annotation class Upsert with `@OptIn`(ExperimentalMultiplatform::class) and
`@OptionalExpectation`, remove the per-function `@Suppress`, and then you can drop
the file-level `@file`:Suppress("KotlinNoActualForExpect"); update the declaration
of Upsert (the expect annotation class with val entity: KClass<*>) to match the
other function-level expect annotations so the codebase is consistent and
compiles without an actual implementation.

153-163: Add default values to ForeignKey annotation parameters to align with Room's API.

The onDelete, onUpdate, and deferred parameters should have defaults matching androidx.room.ForeignKey: onDelete = ForeignKeyAction.NO_ACTION, onUpdate = ForeignKeyAction.NO_ACTION, and deferred = false. This aligns with Room's actual API design and provides a cleaner annotation interface for potential new use cases.

🛠️ Proposed fix
 expect annotation class ForeignKey(
     val entity: KClass<*>,
     val parentColumns: Array<String>,
     val childColumns: Array<String>,
-    val onDelete: Int,
-    val onUpdate: Int,
-    val deferred: Boolean,
+    val onDelete: Int = ForeignKeyAction.NO_ACTION,
+    val onUpdate: Int = ForeignKeyAction.NO_ACTION,
+    val deferred: Boolean = false,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`
around lines 153 - 163, The ForeignKey expect-annotation is missing defaults for
onDelete, onUpdate, and deferred; update the declaration of expect annotation
class ForeignKey to set onDelete = androidx.room.ForeignKeyAction.NO_ACTION,
onUpdate = androidx.room.ForeignKeyAction.NO_ACTION, and deferred = false so it
matches Room's API defaults (leave entity, parentColumns, childColumns
required). Ensure you reference the ForeignKey annotation and its parameters
(onDelete, onUpdate, deferred) when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`:
- Around line 128-137: The example was corrected to use ForeignKeyAction.CASCADE
instead of the incorrect ForeignKey.CASCADE; verify and replace any remaining
usages of ForeignKey.CASCADE with ForeignKeyAction.CASCADE (e.g., in the `@Entity`
foreignKeys block referencing ForeignKey, parentColumns, childColumns and
onDelete) so all examples consistently reference ForeignKeyAction.CASCADE.

---

Nitpick comments:
In
`@core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt`:
- Around line 305-310: The Upsert expect annotation should be migrated from
using the old `@Suppress`("NO_ACTUAL_FOR_EXPECT") pattern to the modern
optional-expectation approach used by Query/Insert/Update/Delete: annotate the
expect annotation class Upsert with `@OptIn`(ExperimentalMultiplatform::class) and
`@OptionalExpectation`, remove the per-function `@Suppress`, and then you can drop
the file-level `@file`:Suppress("KotlinNoActualForExpect"); update the declaration
of Upsert (the expect annotation class with val entity: KClass<*>) to match the
other function-level expect annotations so the codebase is consistent and
compiles without an actual implementation.
- Around line 153-163: The ForeignKey expect-annotation is missing defaults for
onDelete, onUpdate, and deferred; update the declaration of expect annotation
class ForeignKey to set onDelete = androidx.room.ForeignKeyAction.NO_ACTION,
onUpdate = androidx.room.ForeignKeyAction.NO_ACTION, and deferred = false so it
matches Room's API defaults (leave entity, parentColumns, childColumns
required). Ensure you reference the ForeignKey annotation and its parameters
(onDelete, onUpdate, deferred) when making the change.

@Arinyadav1
Copy link
Contributor Author

@biplab1 please do the final review

@Arinyadav1
Copy link
Contributor Author

@biplab1 please review this one

@sonarqubecloud
Copy link

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.

Caution

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

⚠️ Outside diff range comments (2)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt (2)

1288-1307: ⚠️ Potential issue | 🟡 Minor

isAddressTypeIdValid boundary check allows unselected address type (id = 0) to pass silently.

The guard is addressTypeId < 0, but selectedAddressTypeId is initialised to 0 (line 313). When the address section is enabled and the user never picks an address type, isAddressTypeIdValid(0, …) returns true, submitting an address payload with addressTypeId = 0. The check should be <= 0.

🛡️ Proposed fix
-        addressTypeId < 0 -> {
+        addressTypeId <= 0 -> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt`
around lines 1288 - 1307, The boundary check in isAddressTypeIdValid incorrectly
allows addressTypeId == 0 (the unselected default) to pass; update the guard in
isAddressTypeIdValid to treat non-positive ids as invalid (change the condition
from addressTypeId < 0 to addressTypeId <= 0) so that selectedAddressTypeId
initialized to 0 will trigger the snackbar via scope.launch and return false
when no type is chosen.

353-369: ⚠️ Potential issue | 🟠 Major

Commenting out selectedImagePath assignment silently breaks both image preview and upload.

Two regressions stem from this change:

  1. Image preview always shows placeholderselectedImagePath remains null after image selection, so ClientImageSection (line 471) always renders the placeholder image instead of the selected image.

  2. Image upload is never triggeredsetFileForUpload.invoke(selectedImagePath) (line 795) always receives null, so the lambda at lines 222–225 never sets createClientWithImage = true. The SetClientId state handler (lines 233–239) then always calls navigateBack() instead of uploadImage(uiState.id), even though viewmodel.updateSelectedImage(file) has stored the file in the ViewModel.

The ViewModel holds the picked file but the UI layer has no way to observe or use it.

Suggested approach:

Replace the String? preview state with a PlatformFile? state variable:

Proposed fix sketch
-    var selectedImagePath by rememberSaveable { mutableStateOf<String?>(null) }
+    var selectedPlatformFile by remember { mutableStateOf<PlatformFile?>(null) }

In the launchers:

     val galleryLauncher = rememberFilePickerLauncher(type = FileKitType.Image) { file ->
         file?.let {
-//            TODO: path not support in kmp all targets
-//            selectedImagePath = file.path
             onImageSelected(file)
+            selectedPlatformFile = file
         }
     }

Pass selectedPlatformFile to ClientImageSection for preview (filekit-coil is already available in the project and can integrate with Coil 3's rememberAsyncImagePainter). For the upload trigger, derive it from the presence of a file rather than from a path:

-    setFileForUpload = { filePath ->
-        filePath?.let { createClientWithImage = true }
-    },
+    setFileForUpload = { _ ->
+        if (selectedPlatformFile != null) createClientWithImage = true
+    },

When removing an image (line 425), also reset selectedPlatformFile = null.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt`
around lines 353 - 369, The UI stopped showing previews and triggering uploads
because selectedImagePath (String?) was commented out and UI no longer observes
the picked file from the gallery/camera; fix by replacing the String? preview
state with a PlatformFile? (e.g., selectedPlatformFile) and update the
galleryLauncher and cameraLauncher callbacks (where onImageSelected(file) is
called) to set selectedPlatformFile = file and still call
viewmodel.updateSelectedImage(file); pass selectedPlatformFile into
ClientImageSection for preview rendering; change the upload trigger
(setFileForUpload invocation) to derive from selectedPlatformFile presence (not
a path string) and ensure the image-remove handler also sets
selectedPlatformFile = null so preview and upload flow work end-to-end.
🧹 Nitpick comments (1)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt (1)

89-89: MaterialTheme should be replaced with KptTheme per project convention.

Several usages across this file (colorScheme.outline at line 966, colorScheme.surface / colorScheme.secondary at lines 991, 1010, 1021, 1031, and typography.bodyLarge at lines 1003, 1015, 1025, 1035) reference MaterialTheme directly. The project convention is to use KptTheme instead.

♻️ Import to add
-import androidx.compose.material3.MaterialTheme
+import template.core.base.designsystem.theme.KptTheme

Then replace all MaterialTheme.colorScheme.* / MaterialTheme.typography.* references with KptTheme.colorScheme.* / KptTheme.typography.* accordingly.

Based on learnings from PR #2610: In Kotlin files under the android-client module, replace MaterialTheme references with KptTheme imported from template.core.base.designsystem.theme.KptTheme.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt`
at line 89, Replace the MaterialTheme import and usages with the project's
KptTheme: remove the androidx.compose.material3.MaterialTheme import and add
import template.core.base.designsystem.theme.KptTheme, then update all
occurrences of MaterialTheme.colorScheme.outline,
MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.secondary and
MaterialTheme.typography.bodyLarge in CreateNewClientScreen (e.g., the places
referencing colorScheme.outline, surface, secondary and typography.bodyLarge) to
use KptTheme.colorScheme.* and KptTheme.typography.bodyLarge respectively so the
file follows the android-client convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt`:
- Around line 1288-1307: The boundary check in isAddressTypeIdValid incorrectly
allows addressTypeId == 0 (the unselected default) to pass; update the guard in
isAddressTypeIdValid to treat non-positive ids as invalid (change the condition
from addressTypeId < 0 to addressTypeId <= 0) so that selectedAddressTypeId
initialized to 0 will trigger the snackbar via scope.launch and return false
when no type is chosen.
- Around line 353-369: The UI stopped showing previews and triggering uploads
because selectedImagePath (String?) was commented out and UI no longer observes
the picked file from the gallery/camera; fix by replacing the String? preview
state with a PlatformFile? (e.g., selectedPlatformFile) and update the
galleryLauncher and cameraLauncher callbacks (where onImageSelected(file) is
called) to set selectedPlatformFile = file and still call
viewmodel.updateSelectedImage(file); pass selectedPlatformFile into
ClientImageSection for preview rendering; change the upload trigger
(setFileForUpload invocation) to derive from selectedPlatformFile presence (not
a path string) and ensure the image-remove handler also sets
selectedPlatformFile = null so preview and upload flow work end-to-end.

---

Nitpick comments:
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt`:
- Line 89: Replace the MaterialTheme import and usages with the project's
KptTheme: remove the androidx.compose.material3.MaterialTheme import and add
import template.core.base.designsystem.theme.KptTheme, then update all
occurrences of MaterialTheme.colorScheme.outline,
MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.secondary and
MaterialTheme.typography.bodyLarge in CreateNewClientScreen (e.g., the places
referencing colorScheme.outline, surface, secondary and typography.bodyLarge) to
use KptTheme.colorScheme.* and KptTheme.typography.bodyLarge respectively so the
file follows the android-client convention.

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