-
Notifications
You must be signed in to change notification settings - Fork 563
feat(kyc): implement KYC Level 3 review and submit screen #2017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Potential crash if Accessing 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 |
||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
| 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 { | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 1Repository: 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 -20Repository: openMF/mifos-pay Length of output: 1291 🏁 Script executed: rg "^import.*KycLevel" feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.ktRepository: openMF/mifos-pay Length of output: 196 🏁 Script executed: fd "KycLevel" --type f -e kt | head -20Repository: openMF/mifos-pay Length of output: 244 🏁 Script executed: rg "enum class KycLevel|object KycLevel" --type-list | head -20Repository: openMF/mifos-pay Length of output: 450 🏁 Script executed: rg "enum class KycLevel|object KycLevel" -A 2Repository: openMF/mifos-pay Length of output: 382 🏁 Script executed: rg "import.*KycLevel" feature/kyc/src/commonMain/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.ktRepository: 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 🔧 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 |
||||||||||||||||||||||||
| val details = state.kycDetails?.copy(currentLevel = KycLevel.KYC_LEVEL_3.name) | ||||||||||||||||||||||||
| ?: return@launch | ||||||||||||||||||||||||
|
Comment on lines
+44
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: Dialog stuck in loading state if If 🔧 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
Then in the screen:
KycLevel3Dialogs( dialogState = state.dialogState, - onDismiss = { viewModel.trySendAction(KycLevel3Action.NavigateBack) }, + onDismiss = { viewModel.trySendAction(KycLevel3Action.DismissDialog) }, )🤖 Prompt for AI Agents