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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,101 @@
*/
package org.mifospay.feature.kyc

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.koin.compose.viewmodel.koinViewModel
import org.mifospay.core.designsystem.component.MifosScaffold
import org.mifospay.core.designsystem.component.*
import org.mifospay.core.ui.MifosProgressIndicator
import org.mifospay.core.ui.utils.EventsEffect
import template.core.base.designsystem.theme.KptTheme

// TODO:: Implement KYC Level 3 screen
@Composable
internal fun KYCLevel3Screen(
navigateBack: () -> Unit,
onKycComplete: () -> Unit,
modifier: Modifier = Modifier,
viewModel: KYCLevel3ViewModel = koinViewModel(),
) {
KYCLevel3ScreenContent(
modifier = modifier,
navigateBack = navigateBack,
val state by viewModel.stateFlow.collectAsStateWithLifecycle()

EventsEffect(viewModel) { event ->
when (event) {
KycLevel3Event.OnNavigateBack -> navigateBack()
KycLevel3Event.OnKycComplete -> onKycComplete()
}
}

KycLevel3Dialogs(
dialogState = state.dialogState,
onDismiss = { viewModel.trySendAction(KycLevel3Action.NavigateBack) },
)
Comment on lines +40 to 43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

UX concern: Error dialog dismissal navigates away from screen.

When the user dismisses an error dialog, they're navigated back to the previous screen. This prevents retry attempts and may frustrate users who want to fix the issue and resubmit. Typically, dismissing an error dialog should just close the dialog while keeping the user on the current screen.

💡 Suggested UX improvement

Add a dedicated dismiss action to clear the dialog state:

+// In KycLevel3Action
+sealed interface KycLevel3Action {
+    data object NavigateBack : KycLevel3Action
+    data object ConfirmAndSubmit : KycLevel3Action
+    data object DismissDialog : KycLevel3Action
+    sealed interface Internal : KycLevel3Action {
+        data class HandleLevel1Result(val result: DataState<KYCLevel1Details?>) : Internal
+    }
+}
+
+// In KYCLevel3ViewModel.handleAction
+when (action) {
+    KycLevel3Action.NavigateBack -> sendEvent(KycLevel3Event.OnNavigateBack)
+    KycLevel3Action.ConfirmAndSubmit -> handleConfirmSubmit()
+    KycLevel3Action.DismissDialog -> mutableStateFlow.update { it.copy(dialogState = null) }
+    is HandleLevel1Result -> handleLevel1Result(action)
+}

Then in the screen:

     KycLevel3Dialogs(
         dialogState = state.dialogState,
-        onDismiss = { viewModel.trySendAction(KycLevel3Action.NavigateBack) },
+        onDismiss = { viewModel.trySendAction(KycLevel3Action.DismissDialog) },
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt`
around lines 40 - 43, The current onDismiss for KycLevel3Dialogs calls
viewModel.trySendAction(KycLevel3Action.NavigateBack) which navigates away on
any dialog dismissal; add a new action (e.g., KycLevel3Action.DismissDialog or
ClearDialog) to the KycLevel3Action sealed class, handle it in the ViewModel to
clear state.dialogState, and change the KycLevel3Dialogs call to use onDismiss =
{ viewModel.trySendAction(KycLevel3Action.DismissDialog) } so dismissing the
error simply clears the dialog without navigating away.

}

@Composable
fun KYCLevel3ScreenContent(
modifier: Modifier = Modifier,
navigateBack: () -> Unit,
) {
MifosScaffold(
topBarTitle = "Review & Submit",
backPress = navigateBack,
backPress = { viewModel.trySendAction(KycLevel3Action.NavigateBack) },
modifier = modifier,
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = "KYC Level 3",
)
) { padding ->
if (state.isLoading) {
MifosProgressIndicator(modifier = Modifier.fillMaxSize())
} else {
LazyColumn(
modifier = Modifier.fillMaxSize().padding(padding),
contentPadding = PaddingValues(KptTheme.spacing.md),
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
) {
state.kycDetails?.let { details ->
item { SectionHeader("Personal Details") }
item { ReviewRow("First Name", details.firstName) }
item { ReviewRow("Last Name", details.lastName) }
item { ReviewRow("Mobile Number", details.mobileNo) }
item { ReviewRow("Date of Birth", details.dob) }
item { SectionHeader("Address") }
item { ReviewRow("Address Line 1", details.addressLine1) }
item { ReviewRow("Address Line 2", details.addressLine2) }
}
item {
MifosButton(
onClick = { viewModel.trySendAction(KycLevel3Action.ConfirmAndSubmit) },
modifier = Modifier.fillMaxWidth(),
) { Text("Confirm & Submit") }
}
}
}
}
}

@Composable
private fun ReviewRow(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(text = label, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(text = value, style = MaterialTheme.typography.bodyMedium)
}
HorizontalDivider(modifier = Modifier.padding(top = KptTheme.spacing.sm))
}

@Composable
private fun SectionHeader(title: String) {
Text(text = title, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = KptTheme.spacing.xs))
}

@Composable
private fun KycLevel3Dialogs(dialogState: KycLevel3State.DialogState?, onDismiss: () -> Unit) {
when (dialogState) {
is KycLevel3State.DialogState.Loading -> MifosLoadingDialog(LoadingDialogState.Shown)
is KycLevel3State.DialogState.Error -> MifosBasicDialog(
visibilityState = BasicDialogState.Shown(message = dialogState.message),
onDismissRequest = onDismiss,
)
null -> Unit
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,98 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
* ...license header...
*/
package org.mifospay.feature.kyc

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import org.mifospay.core.common.DataState
import org.mifospay.core.data.repository.KycLevelRepository
import org.mifospay.core.datastore.UserPreferencesRepository
import org.mifospay.core.model.kyc.KYCLevel1Details
import org.mifospay.core.ui.utils.BaseViewModel
import org.mifospay.feature.kyc.KycLevel3Action.Internal.HandleLevel1Result

// TODO: Implement KYC Level3 View Model
class KYCLevel3ViewModel : ViewModel()
class KYCLevel3ViewModel(
private val kycLevelRepository: KycLevelRepository,
private val userPreferencesRepository: UserPreferencesRepository,
) : BaseViewModel<KycLevel3State, KycLevel3Event, KycLevel3Action>(
initialState = KycLevel3State(
clientId = requireNotNull(userPreferencesRepository.clientId.value),
),
Comment on lines +22 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Potential crash if clientId is null at ViewModel initialization.

Accessing userPreferencesRepository.clientId.value with requireNotNull during ViewModel construction will crash the app if the client ID hasn't been set yet. This creates a fragile initialization dependency.

Consider deferring clientId access or handling the null case gracefully.

🛡️ Proposed safer initialization
-) : BaseViewModel<KycLevel3State, KycLevel3Event, KycLevel3Action>(
-    initialState = KycLevel3State(
-        clientId = requireNotNull(userPreferencesRepository.clientId.value),
-    ),
-) {
+) : BaseViewModel<KycLevel3State, KycLevel3Event, KycLevel3Action>(
+    initialState = KycLevel3State(
+        clientId = userPreferencesRepository.clientId.value ?: 0L,
+    ),
+) {
     init {
+        if (state.clientId == 0L) {
+            mutableStateFlow.update { 
+                it.copy(
+                    isLoading = false,
+                    dialogState = KycLevel3State.DialogState.Error("Client ID not found")
+                )
+            }
+            return
+        }
         kycLevelRepository.fetchKYCLevel1Details(state.clientId)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt`
around lines 22 - 24, The ViewModel currently force-unwraps clientId in
KYCLevel3ViewModel initialization (initialState = KycLevel3State(clientId =
requireNotNull(userPreferencesRepository.clientId.value))), which can crash if
clientId is not set; change this to a safe initialization by not using
requireNotNull — set KycLevel3State.clientId to a nullable or default value
(e.g., null or empty string) and defer real clientId usage to when it becomes
available (subscribe/observe userPreferencesRepository.clientId and update the
state), and handle null/default in any methods that use clientId (refer to
KYCLevel3ViewModel, KycLevel3State, and userPreferencesRepository.clientId).

) {
init {
kycLevelRepository.fetchKYCLevel1Details(state.clientId)
.onEach { sendAction(HandleLevel1Result(it)) }
.launchIn(viewModelScope)
}

override fun handleAction(action: KycLevel3Action) {
when (action) {
KycLevel3Action.NavigateBack -> sendEvent(KycLevel3Event.OnNavigateBack)
KycLevel3Action.ConfirmAndSubmit -> handleConfirmSubmit()
is HandleLevel1Result -> handleLevel1Result(action)
}
}

private fun handleConfirmSubmit() {
mutableStateFlow.update { it.copy(dialogState = KycLevel3State.DialogState.Loading) }
// KYC Level 3 marks the current level as complete — update level field
viewModelScope.launch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the missing import and check if launch is used elsewhere in the codebase

# Check current imports in the file
echo "=== Current imports in KYCLevel3ViewModel.kt ==="
rg -n "^import" feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt

# Search for correct import pattern in other ViewModels
echo -e "\n=== Examples of launch usage in other ViewModels ==="
rg -n "import kotlinx.coroutines.launch" --type=kt -g '*ViewModel*' -C 1

Repository: openMF/mifos-pay

Length of output: 731


🏁 Script executed:

cat -n feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt | sed -n '40,50p'

Repository: openMF/mifos-pay

Length of output: 805


🏁 Script executed:

cat -n feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt | sed -n '1,30p'

Repository: openMF/mifos-pay

Length of output: 1389


🏁 Script executed:

rg "KycLevel" feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt -n | head -20

Repository: openMF/mifos-pay

Length of output: 1291


🏁 Script executed:

rg "^import.*KycLevel" feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt

Repository: openMF/mifos-pay

Length of output: 196


🏁 Script executed:

fd "KycLevel" --type f -e kt | head -20

Repository: openMF/mifos-pay

Length of output: 244


🏁 Script executed:

rg "enum class KycLevel|object KycLevel" --type-list | head -20

Repository: openMF/mifos-pay

Length of output: 450


🏁 Script executed:

rg "enum class KycLevel|object KycLevel" -A 2

Repository: openMF/mifos-pay

Length of output: 382


🏁 Script executed:

rg "import.*KycLevel" feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt

Repository: openMF/mifos-pay

Length of output: 120


🏁 Script executed:

cat -n feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt | grep -A 5 "enum class KycLevel"

Repository: openMF/mifos-pay

Length of output: 276


🏁 Script executed:

rg "KycLevel\." -n feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/ | grep -v "KycLevel3\|KycLevelRepository"

Repository: openMF/mifos-pay

Length of output: 660


Multiple missing imports cause compilation errors.

The code uses viewModelScope.launch (line 43) and KycLevel.KYC_LEVEL_3 (line 44) but neither launch nor KycLevel are imported. Both are required for compilation.

🔧 Add missing imports
 import androidx.lifecycle.viewModelScope
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
 import org.mifospay.core.common.DataState
 import org.mifospay.core.data.repository.KycLevelRepository
 import org.mifospay.core.datastore.UserPreferencesRepository
 import org.mifospay.core.model.kyc.KYCLevel1Details
 import org.mifospay.core.ui.utils.BaseViewModel
+import org.mifospay.feature.kyc.KycLevel
 import org.mifospay.feature.kyc.KycLevel3Action.Internal.HandleLevel1Result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt`
at line 43, Add the missing imports required for using viewModelScope.launch and
the KycLevel enum: import the coroutine extension (kotlinx.coroutines.launch) so
viewModelScope.launch resolves, and import the KycLevel enum/type that defines
KYC_LEVEL_3 (the package where KycLevel is declared in this module) so
KycLevel.KYC_LEVEL_3 compiles; update the KYCLevel3ViewModel imports to include
these symbols.

val details = state.kycDetails?.copy(currentLevel = KycLevel.KYC_LEVEL_3.name)
?: return@launch
Comment on lines +44 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Major: Dialog stuck in loading state if kycDetails is null.

If kycDetails is null when the user clicks submit, the function returns early without updating dialogState, leaving the loading dialog visible indefinitely. This creates a poor user experience where the UI appears frozen.

🔧 Handle the null case properly
     private fun handleConfirmSubmit() {
         mutableStateFlow.update { it.copy(dialogState = KycLevel3State.DialogState.Loading) }
         // KYC Level 3 marks the current level as complete — update level field
         viewModelScope.launch {
             val details = state.kycDetails?.copy(currentLevel = KycLevel.KYC_LEVEL_3.name)
-                ?: return@launch
+                ?: run {
+                    mutableStateFlow.update {
+                        it.copy(dialogState = KycLevel3State.DialogState.Error(
+                            "KYC details not available. Please try again."
+                        ))
+                    }
+                    return@launch
+                }
             val result = kycLevelRepository.updateKYCLevel1Details(state.clientId, details)
📝 Committable suggestion

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

Suggested change
val details = state.kycDetails?.copy(currentLevel = KycLevel.KYC_LEVEL_3.name)
?: return@launch
val details = state.kycDetails?.copy(currentLevel = KycLevel.KYC_LEVEL_3.name)
?: run {
mutableStateFlow.update {
it.copy(dialogState = KycLevel3State.DialogState.Error(
"KYC details not available. Please try again."
))
}
return@launch
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt`
around lines 44 - 45, In KYCLevel3ViewModel, the early return when
state.kycDetails is null (the line referencing state.kycDetails?.copy(...))
leaves dialogState stuck in loading; update the null branch to first set
dialogState to a non-loading state (e.g., dismiss the dialog or set an error
state/message) before returning so the UI stops showing the spinner—locate the
submit/launch block that references state.kycDetails and ensure dialogState is
updated (cleared or set to an error) whenever state.kycDetails is null instead
of simply returning.

val result = kycLevelRepository.updateKYCLevel1Details(state.clientId, details)
when (result) {
is DataState.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(KycLevel3Event.OnKycComplete)
}
is DataState.Error -> {
mutableStateFlow.update {
it.copy(dialogState = KycLevel3State.DialogState.Error(
result.exception.message ?: "Submission failed"
))
}
}
else -> Unit
}
}
}

private fun handleLevel1Result(action: HandleLevel1Result) {
when (action.result) {
is DataState.Success -> mutableStateFlow.update {
it.copy(kycDetails = action.result.data, isLoading = false)
}
is DataState.Loading -> mutableStateFlow.update { it.copy(isLoading = true) }
else -> mutableStateFlow.update { it.copy(isLoading = false) }
}
}
}

data class KycLevel3State(
val clientId: Long,
val kycDetails: KYCLevel1Details? = null,
val isLoading: Boolean = true,
val dialogState: DialogState? = null,
) {
sealed interface DialogState {
data object Loading : DialogState
data class Error(val message: String) : DialogState
}
}

sealed interface KycLevel3Event {
data object OnNavigateBack : KycLevel3Event
data object OnKycComplete : KycLevel3Event
}

sealed interface KycLevel3Action {
data object NavigateBack : KycLevel3Action
data object ConfirmAndSubmit : KycLevel3Action
sealed interface Internal : KycLevel3Action {
data class HandleLevel1Result(val result: DataState<KYCLevel1Details?>) : Internal
}
}
Loading