-
Notifications
You must be signed in to change notification settings - Fork 515
feat(passcode): implement change passcode flow (MW-324) #1949
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: development
Are you sure you want to change the base?
feat(passcode): implement change passcode flow (MW-324) #1949
Conversation
📝 WalkthroughWalkthroughIntroduces an intention-driven passcode flow (Verify/Create/Confirm), moves passcode routing from the login layer to app-startup decision logic, persists a skip-passcode flag, consolidates passcode callbacks, and updates navigation, view models, DI scopes, and UI components to use the new Intention routing. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App Startup
participant VM as MifosPayViewModel
participant Decider as MifosPayApp (Decision)
participant NavHost as MifosNavHost
participant Passcode as PasscodeModule
participant Main as Main Graph
App->>VM: request auth + passcode state
VM-->>App: MainUiState.Success(user, hasPasscode, hasSkippedPasscodeSetup)
App->>Decider: evaluate state
alt Not authenticated
Decider->>NavHost: navigate(LOGIN_GRAPH)
else Authenticated & hasPasscode
Decider->>NavHost: navigate(passcodeGraphRoute(LOGIN_WITH_PASSCODE))
NavHost->>Passcode: start Verify flow
else Authenticated & no passcode & not skipped
Decider->>NavHost: navigate(passcodeGraphRoute(CREATE_PASSCODE))
NavHost->>Passcode: start Create → Confirm flow
else Authenticated & skipped
Decider->>NavHost: navigate(MAIN_GRAPH)
NavHost->>Main: show main app
end
Note over Passcode,NavHost: onPasscodeFlowComplete -> NavHost navigates to MAIN_GRAPH
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (1)
27-35: Fix reactivity issue: passcode state changes won't trigger UI updates.Reading
passcodeManager.hasPasscode.valueandhasSkippedPasscodeSetup.valueinside themapblock captures only the current snapshot values. When passcode state changes (e.g., user creates/changes/clears passcode),uiStatewon't update unlessuserInfoalso changes, causing the UI and navigation logic to use stale passcode data.🔎 Proposed fix using combine to merge all flows
- val uiState: StateFlow<MainUiState> = userDataRepository.userInfo.map { - val hasPasscode = passcodeManager.hasPasscode.value - val hasSkippedPasscodeSetup = passcodeManager.hasSkippedPasscodeSetup.value - MainUiState.Success(it, hasPasscode, hasSkippedPasscodeSetup) - }.stateIn( + val uiState: StateFlow<MainUiState> = combine( + userDataRepository.userInfo, + passcodeManager.hasPasscode, + passcodeManager.hasSkippedPasscodeSetup, + ) { userInfo, hasPasscode, hasSkippedPasscodeSetup -> + MainUiState.Success(userInfo, hasPasscode, hasSkippedPasscodeSetup) + }.stateIn( scope = viewModelScope, initialValue = MainUiState.Loading, started = SharingStarted.WhileSubscribed(5_000), )You'll also need to add the import:
import kotlinx.coroutines.flow.combinelibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt (1)
68-72:clearInfo()does not resetpasscodeSettingsStateFlow.After removing the key from settings, the
passcodeSettingsMutableStateFlow still holds the old value. This could cause inconsistent state ifclearInfo()is called and then the flows are observed before a new value is written.🔎 Proposed fix
suspend fun clearInfo() { withContext(dispatcher) { settings.remove(PASSCODE_INFO_KEY) + passcodeSettings.value = PasscodePreferencesProto.DEFAULT } }
🧹 Nitpick comments (9)
feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt (2)
142-151: Dynamic passcode action label works correctly.The conditional logic cleanly toggles between "Change Passcode" and "Create Passcode" based on state. Note that the action
SettingsAction.ChangePasscodeis used in both cases—consider whether this action name still accurately describes the intent when the user has no passcode (it triggers creation, not change).Optional: Rename action for semantic clarity
If desired, rename the action to something more generic like
ManagePasscodeto better reflect that it handles both creation and change scenarios:- onAction(SettingsAction.ChangePasscode) + onAction(SettingsAction.ManagePasscode)This is purely a naming consideration—the current implementation works correctly.
246-252: Consider adding a preview for thehasPasscode = truestate.The preview only covers the
hasPasscode = falsecase. Adding another preview would help visualize both UI states during development.Add preview for hasPasscode = true
@Preview @Composable private fun SettingsScreenWithPasscodePreview() { SettingsScreenContent( onAction = {}, hasPasscode = true, ) }cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (2)
19-19: Potentially unused import.The
viewModelimport fromandroidx.lifecycle.viewmodel.composeappears unused—the code useskoinViewModelon line 49. If not needed elsewhere, consider removing it.-import androidx.lifecycle.viewmodel.compose.viewModel
83-109: Excellent documentation of the navigation decision tree.The KDoc comment clearly explains each branch of the navigation logic, making the code self-documenting and easier to maintain.
However, the repeated casting of
uiStatetoSuccesson lines 97-100 is verbose. Consider using Kotlin's smart cast with awhensubject:Refactor to avoid repeated casts
val navDestination = when { - uiState is Success -> { - val userData = (uiState as Success).userData - val hasPasscode = (uiState as Success).hasPasscode - val hasSkippedPasscodeSetup = (uiState as Success).hasSkippedPasscodeSetup + uiState is Success -> with(uiState) { when { - !userData.authenticated -> LOGIN_GRAPH + !this.userData.authenticated -> LOGIN_GRAPH hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE) hasSkippedPasscodeSetup -> MAIN_GRAPH else -> passcodeGraphRoute(Intention.CREATE_PASSCODE) } } else -> LOGIN_GRAPH }Or extract to a local variable after the
is Successcheck since smart cast applies.feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt (1)
240-243: Hardcoded string should use a string resource for i18n.The text "Don't have an account yet? " is hardcoded. For consistency with the rest of the file (which uses
stringResource), consider moving this to a string resource.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt (1)
29-47: Hardcoded colors break dark theme support.Using
Color.WhiteandColor.Blackdirectly bypasses Material theming. This will cause poor contrast or visibility issues in dark mode.🔎 Proposed fix using theme colors
AlertDialog( shape = MaterialTheme.shapes.large, - containerColor = Color.White, + containerColor = MaterialTheme.colorScheme.surface, title = { Text( text = stringResource(Res.string.library_mifos_passcode_passcode_changed_message), - color = Color.Black, + color = MaterialTheme.colorScheme.onSurface, ) }, confirmButton = { TextButton(onClick = onDismiss) { Text( text = stringResource(Res.string.library_mifos_passcode_done), - color = Color.Black, + color = MaterialTheme.colorScheme.primary, ) } }, onDismissRequest = onDismiss, )libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt (1)
88-89: Themodifierparameter is not applied to the Box.The function accepts a
modifierparameter but usesModifier.fillMaxWidth()directly instead. This prevents callers from customizing the header's layout.🔎 Proposed fix
Box( - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) {libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)
118-121: Consider repositioning the dialog for semantic clarity.The
ChangePasscodeSuccessDialogis placed within the content layout betweenMifosIconand the passcode inputColumn. While functionally correct (dialogs render as overlays), placing it at the end of theScaffoldcontent or outside theColumnwould be semantically cleaner.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)
112-117: Consider sending the event after the coroutine completes for consistency.
sendEvent(PasscodeEvent.PasscodeSetupSkipped)is called before the coroutine persisting the skip state completes. If navigation happens immediately, there's a small window where the preference might not be saved.🔎 Proposed fix: emit event inside the coroutine
private fun onSkipPasscodeSetup() { viewModelScope.launch { passcodeRepository.setSkippedPasscodeSetup(true) + sendEvent(PasscodeEvent.PasscodeSetupSkipped) } - sendEvent(PasscodeEvent.PasscodeSetupSkipped) }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.ktfeature/settings/build.gradle.ktsfeature/settings/src/commonMain/composeResources/values/strings.xmlfeature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.ktfeature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.ktlibs/mifos-passcode/src/commonMain/composeResources/values/strings.xmllibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt
💤 Files with no reviewable changes (1)
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
🧰 Additional context used
🧬 Code graph analysis (4)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)
passcodeGraphRoute(31-33)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt (1)
ChangePasscodeSuccessDialog(23-49)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt (1)
sendEvent(97-99)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (1)
passcodeRoute(47-67)
🔇 Additional comments (33)
feature/settings/build.gradle.kts (1)
26-26: Dependency addition is appropriate.Adding
mifosPasscodeenables the Settings feature to access passcode state (hasPasscode) and navigation utilities, which aligns with the dynamic "Create Passcode" / "Change Passcode" UI requirement.feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt (1)
139-149: Good documentation of the architectural change.The comment clearly explains why
NavigateToPasscodeScreenwas removed and where that responsibility now lives. This decoupling allows app-level navigation to make routing decisions based on both authentication state and passcode status, which is cleaner than having the login screen handle passcode navigation directly.feature/settings/src/commonMain/composeResources/values/strings.xml (1)
18-18: String resource follows conventions.The new
feature_settings_create_passcodestring is appropriately named and placed alongside the existingfeature_settings_change_passcode, supporting the dynamic UI that toggles between these based on passcode state.libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml (1)
21-24: String resources support the change passcode flow.The new strings appropriately support the success dialog and the "Create new Passcode" step in the change flow. They follow the
library_mifos_passcode_naming convention used by other recent strings in this file.feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt (1)
67-68: Proper lifecycle-aware state observation.Using
collectAsStateWithLifecycle()for the passcode state ensures the UI updates reactively when the passcode status changes, which is especially important when returning from the passcode creation flow.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (3)
32-42: Intention enum is well-designed with clear documentation.The KDoc clearly explains each intention's purpose and flow. The
fromValuehelper with a default fallback is practical for handling unexpected route arguments.One consideration: the default fallback to
CREATE_PASSCODEon unknown values means an attacker or bug causing an invalid intention would route users to passcode creation. Verify this is the safest default—LOGIN_WITH_PASSCODEmight be more conservative since it requires verification of an existing passcode.
47-67: Route definition correctly uses optional query parameter.The route pattern
$PASSCODE_SCREEN?$INTENTION={$INTENTION}with a default value ensures backward compatibility. The singleonPasscodeFlowCompletecallback simplifies the API compared to separate success/failure callbacks—this works because the ViewModel now handles rejection internally with retry logic.
69-72: Navigation helper correctly constructs the parameterized route.The function properly encodes the intention value into the route string. The optional
NavOptionsparameter provides flexibility for callers.cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt (2)
94-94: Correct use of separate NavController for passcode navigation.Passing
rootNavControlleras a separate parameter enables navigation to the passcode screen which exists outside the main navigation graph. This is the right architectural approach.
187-198: Correct the inverted logic in the comment.The comment states the ViewModel changes intention "if there is already a passcode set," but the actual code (PasscodeViewModel.kt, line 70) checks
!state.hasPasscode, meaning it activates when NO passcode exists. Additionally, the ViewModel changes theactiveStep, not the intention itself. Update the comment to reflect: the ViewModel starts at the Create step directly (skipping Verify) when the user navigates to "Change Passcode" but never set up a passcode initially.feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt (1)
21-28: LGTM! Navigation refactoring is well-documented.The removal of
onNavigateToPasscodeScreenand the explanatory comment clearly communicate that passcode navigation is now handled at the app level, aligning with the PR's intention-based navigation approach.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt (2)
29-58: LGTM! Intent-based rendering improves clarity.The shift from boolean-based to intention-based gating makes the button visibility logic more explicit and maintainable. The documentation clearly explains when the skip button appears.
60-89: LGTM! Consistent with skip button refactoring.The forgot button correctly appears only during login, with clear documentation explaining the behavior.
feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt (1)
44-53: LGTM! Reactive passcode state exposure.Exposing
hasPasscodeas aStateFlowenables the Settings UI to reactively display "Create Passcode" or "Change Passcode" based on the current passcode state. The comprehensive documentation clearly explains the purpose.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt (2)
29-72: LGTM! Step indicator logic correctly implements single-step highlighting.The changes appropriately:
- Hide the indicator during the Verify step (when no creation progress needs to be shown)
- Highlight only the current step in the 2-step creation flow (Create → Confirm)
- Provide clear documentation explaining the behavior
The active step calculation
step + 1 == activeStep.indexcorrectly highlights step 0 during Create(1) and step 1 during Confirm(2).
74-78: LGTM! Preview function aids development.libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt (1)
14-31: LGTM! Skip tracking prevents repeated prompts.The
hasSkippedPasscodeSetupfield appropriately distinguishes between users who never encountered passcode setup and those who explicitly declined, preventing unwanted re-prompting. The documentation clearly explains this distinction.cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt (1)
69-74: LGTM! Root controller enables app-level navigation.Adding the
rootNavControllerparameter allowsMifosAppto coordinate navigation at the application level, which is essential for the intention-based passcode routing introduced in this PR.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt (1)
12-19: LGTM! Three-step flow supports verification.Adding the
Verifystep appropriately supports both login verification and the "verify old passcode" step in the change flow. The index reordering is correct.feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt (1)
57-70: LGTM on the navigation refactor!The docblock clearly explains the architectural decision to separate passcode navigation from the login flow. This is a good separation of concerns that prevents duplicate navigation calls.
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt (1)
40-46: LGTM!Passing
rootNavControllertoMifosAppenables proper navigation for the change passcode flow from within the main graph back to the passcode graph.libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt (1)
43-61: LGTM on the skip-passcode tracking!The implementation correctly:
- Exposes
hasSkippedPasscodeSetupas a reactive StateFlow- Resets the skip flag when a passcode is saved (line 54)
- Provides
setSkippedPasscodeSetupfor explicit updatesThis ensures consistent state management across the passcode flows.
libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt (1)
46-66: LGTM on the skip-passcode tracking implementation!The
hasSkippedPasscodeSetupflow andupdateSkippedPasscodeSetupfunction are correctly implemented with proper dispatcher context switching for thread safety.cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)
19-33: LGTM on the intention-based routing!The implementation correctly:
- Uses query parameter syntax for the intention argument
- Provides a clean helper function to build routes with specific intentions
- Includes clear documentation explaining the purpose
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt (1)
92-127: LGTM on the intention-based header logic!The
whenexpression cleanly handles all three steps with appropriate text based on the intention. The distinction between "Create Passcode" (initial) and "Create New Passcode" (change flow) provides clear UX differentiation.libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt (1)
30-33: LGTM on the singleton scope change!Switching from
factorytosingleis the correct fix. Withfactory, each injection site would get a new instance with its own StateFlows, breaking reactivity across the app. The IO dispatcher is appropriate for preferences I/O operations.cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (2)
22-37: LGTM! Clear documentation and correct route construction.The documentation block clearly explains the three intention modes, and the start destination with the intention placeholder correctly inherits the argument from the graph route when navigating to the passcode graph.
39-52: LGTM! Clean callback consolidation.The unified
onPasscodeFlowCompletecallback simplifies the API surface. The navigation logic foronForgotButton(to login) andonSkipButton/onPasscodeFlowComplete(to main graph) correctly reflects the intended user flows.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (2)
79-93: LGTM! Clean event-to-callback mapping.The
EventsEffectcorrectly maps the three event types to appropriate callbacks.PasscodeSetupSkippedtriggeringonSkipButton()maintains consistency with the skip flow.
109-114: LGTM! Skip button correctly dispatches to ViewModel.Delegating the skip action to the ViewModel via
PasscodeAction.SkipPasscodeSetupallows proper persistence of the skip state before navigation occurs.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (3)
119-142: LGTM! Clean step-based buffer selection.The refactored
enterKeycorrectly selects the appropriate buffer based onactiveStepand triggers processing when the passcode is complete.
203-219: LGTM! Well-structured flow routing.The
processCompletedPasscodefunction clearly handles all intention/step combinations with appropriate transitions. The documentation accurately describes the flow logic.
330-339: LGTM! State class is well-documented.The
PasscodeStatedata class with the newintentionandisChangePasscodeSuccessfulproperties supports all flow variations cleanly.
| val navDestination = when { | ||
| uiState is Success -> { | ||
| val userData = (uiState as Success).userData | ||
| val hasPasscode = (uiState as Success).hasPasscode | ||
| val hasSkippedPasscodeSetup = (uiState as Success).hasSkippedPasscodeSetup | ||
| when { | ||
| !userData.authenticated -> LOGIN_GRAPH | ||
| hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE) | ||
| hasSkippedPasscodeSetup -> MAIN_GRAPH | ||
| else -> passcodeGraphRoute(Intention.CREATE_PASSCODE) | ||
| } | ||
| } | ||
| else -> LOGIN_GRAPH | ||
| } |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find RootNavGraph implementation
rg -n "fun RootNavGraph" --type kotlin -A 20Repository: openMF/mobile-wallet
Length of output: 2434
🏁 Script executed:
#!/bin/bash
# Also check the full context of MifosPayApp.kt around the cited lines
fd "MifosPayApp.kt" --type fRepository: openMF/mobile-wallet
Length of output: 187
🏁 Script executed:
#!/bin/bash
# Search for how startDestination is being used
rg -n "startDestination" --type kotlin -B 2 -A 5Repository: openMF/mobile-wallet
Length of output: 6571
🏁 Script executed:
#!/bin/bash
# Get the full MifosPayApp.kt file to understand the context
cat -n cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt | head -130Repository: openMF/mobile-wallet
Length of output: 5804
🏁 Script executed:
#!/bin/bash
# Check if there's any LaunchedEffect or navigation handling for state changes
rg -n "LaunchedEffect|SideEffect|DisposableEffect" cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt -A 5Repository: openMF/mobile-wallet
Length of output: 501
🌐 Web query:
Compose Navigation NavHost startDestination dynamic changes best practices
💡 Result:
Short best-practices for changing NavHost startDestination dynamically (Jetpack Compose)
-
Prefer a fixed startDestination; don’t re-create/change NavHost startDestination at runtime. Replacing the NavHost to change startDestination causes graph resets and surprising back-stack behavior. [1][2]
-
Use a small “router”/splash composable as the fixed start destination that decides where to go (onboarding, login, home) and immediately navigates (and pops itself) based on app state. This preserves a single NavHost and a stable graph. [2][4]
-
Fetch decision data outside the NavHost (DI/ViewModel/secure storage) and surface it to the router via state. Avoid IO/long work inside Compose orchestration; perform it in a ViewModel and expose a State/Flow. [4][1]
-
When navigating to the chosen destination, clear or pop the router from the back stack so users can’t navigate back to it (use popUpTo + inclusive). Example:
navController.navigate("home") {
popUpTo("router") { inclusive = true }
launchSingleTop = true
}
Use saveState/restoreState when appropriate. [7] -
Keep a single rememberNavController() hoisted at a top-level composable (or passed via coordinator) so navigation state survives recomposition and configuration changes. [1][6]
-
Encapsulate routes as sealed classes/constants and pass navigation via callbacks (or a navigation API) to keep screens testable and decoupled from NavController. [6][8]
-
For auth/onboarding flows consider:
- Persisting a “seen onboarding / logged in” flag and reading it on cold start from ViewModel/DI. [3][4]
- Showing a dedicated splash/router only until decision is made; don’t branch the whole UI into two separate NavHost instances unless you truly need isolated graphs. [2][5]
-
Test edge cases: process death, deep links, configuration changes and ensure your decision logic and popUpTo behavior produce the expected back stack.
Sources: official guidance and community solutions on conditional start destinations and routing patterns. [1] [2] [3] [4] [7]
References
[1] Kotlin docs / Compose Navigation overview.
[2] Community guides: “Conditional navigation with Navigation Compose” / Mantel.
[3] StackOverflow discussion: changing NavHost startDestination at runtime.
[4] Articles on single-NavHost router pattern and state-driven routing.
[7] Navigation Compose docs / navController navigate options (popUpTo, launchSingleTop, saveState).
If you want, I can show a minimal router sample Compose implementation with nav code.
Replace reactive startDestination with fixed value and LaunchedEffect navigation.
The startDestination parameter should remain fixed (e.g., LOGIN_GRAPH). Instead, add a LaunchedEffect watching uiState to programmatically navigate to the correct destination using navController.navigate() with appropriate popUpTo handling. This prevents Compose Navigation from recomposing the NavHost graph with different root destinations, which causes back-stack anomalies and navigation glitches. The pattern for handling authorization state (lines 57–61) demonstrates the correct approach.
🤖 Prompt for AI Agents
In cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt around
lines 96-109, the code computes a reactive navDestination and uses it as NavHost
startDestination which causes NavHost graph re-creation and back-stack glitches;
instead set startDestination to a fixed value (e.g., LOGIN_GRAPH) and remove the
reactive startDestination logic, then add a LaunchedEffect keyed on uiState that
computes the intended destination (same when-logic currently in lines 96-109)
and calls navController.navigate(target) with proper popUpTo and inclusive flags
to clear/replace the stack as needed (mirror the authorization handling used at
lines 57-61), ensuring navigation is done programmatically rather than by
changing startDestination.
| init { | ||
| observePasscodeRepository() | ||
| getIntention() | ||
| } | ||
|
|
||
| /** | ||
| * Determines the intention (purpose) for which the passcode screen was opened. | ||
| * | ||
| * This function retrieves the intention from savedStateHandle (passed via navigation), | ||
| * and sets the initial step accordingly: | ||
| * | ||
| * - LOGIN_WITH_PASSCODE: Start at Verify step (default) | ||
| * - CREATE_PASSCODE: Start at Create step (skip Verify) | ||
| * - CHANGE_PASSCODE: Start at Verify step normally, BUT if the user never set a passcode | ||
| * (they skipped it initially), start at Create step instead. | ||
| * | ||
| * This handles the edge case where a user who skipped passcode setup initially | ||
| * can still use the "Change Passcode" button to set one up for the first time. | ||
| * (This is done so that the settings screen won't need to check if a passcode exists during navigation in navigation layer.) | ||
| */ | ||
| private fun getIntention() { | ||
| mutableStateFlow.update { | ||
| it.copy( | ||
| intention = savedStateHandle.get<String>(INTENTION) | ||
| ?.let(Intention::fromValue) | ||
| ?: Intention.LOGIN_WITH_PASSCODE, | ||
| ) | ||
| } | ||
| if ( | ||
| state.intention == Intention.CREATE_PASSCODE || | ||
| (state.intention == Intention.CHANGE_PASSCODE && !state.hasPasscode) // in case if user have skipped setting the passcode initially, then the "Change passcode" button should directly allow him to create a new passcode | ||
| ) { | ||
| mutableStateFlow.update { | ||
| it.copy( | ||
| activeStep = Step.Create, | ||
| ) | ||
| } | ||
| } | ||
| } |
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.
Race condition: hasPasscode may not be initialized when checking.
getIntention() runs synchronously in init, but observePasscodeRepository() launches an async coroutine to collect hasPasscode. When the check on line 70 runs, state.hasPasscode will still be false (the default) because the flow hasn't emitted yet.
This means for CHANGE_PASSCODE intention, the condition !state.hasPasscode will be true even when the user has a passcode, causing the Verify step to be skipped—allowing passcode change without verification.
🔎 Proposed fix: await the first hasPasscode value before checking
init {
observePasscodeRepository()
- getIntention()
+ viewModelScope.launch {
+ // Wait for hasPasscode to be loaded before determining flow
+ val hasPasscode = passcodeRepository.hasPasscode.first()
+ mutableStateFlow.update { it.copy(hasPasscode = hasPasscode) }
+ getIntention()
+ }
}Alternatively, restructure getIntention() to be a suspend function that awaits the first hasPasscode value:
-private fun getIntention() {
+private suspend fun getIntention() {
+ val hasPasscode = passcodeRepository.hasPasscode.first()
mutableStateFlow.update {
it.copy(
intention = savedStateHandle.get<String>(INTENTION)
?.let(Intention::fromValue)
?: Intention.LOGIN_WITH_PASSCODE,
+ hasPasscode = hasPasscode,
)
}
if (
state.intention == Intention.CREATE_PASSCODE ||
(state.intention == Intention.CHANGE_PASSCODE && !state.hasPasscode)
) {Committable suggestion skipped: line range outside the PR's diff.
| private fun onDismissChangePasscodeSuccessDialog() { | ||
| mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = true) } | ||
| sendEvent(PasscodeEvent.PasscodeConfirmed) | ||
| } |
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.
Bug: Should set isChangePasscodeSuccessful to false when dismissing.
Setting isChangePasscodeSuccessful = true is redundant—it's already true (which is why the dialog was visible). While navigation via PasscodeConfirmed makes this moot, the state update is incorrect and could cause issues if the flow is modified later.
🔎 Proposed fix
private fun onDismissChangePasscodeSuccessDialog() {
- mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = true) }
+ mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = false) }
sendEvent(PasscodeEvent.PasscodeConfirmed)
}📝 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.
| private fun onDismissChangePasscodeSuccessDialog() { | |
| mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = true) } | |
| sendEvent(PasscodeEvent.PasscodeConfirmed) | |
| } | |
| private fun onDismissChangePasscodeSuccessDialog() { | |
| mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = false) } | |
| sendEvent(PasscodeEvent.PasscodeConfirmed) | |
| } |
🤖 Prompt for AI Agents
In
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt
around lines 225 to 228, the dismiss handler incorrectly sets
isChangePasscodeSuccessful = true (it should be set to false since the dialog
was visible because the flag was true); change the state update to set
isChangePasscodeSuccessful = false and then
sendEvent(PasscodeEvent.PasscodeConfirmed) so the success flag is cleared when
the dialog is dismissed.
1c37870 to
468aa86
Compare
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.
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 (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (1)
27-35: Non-reactive flow composition breaks real-time passcode updates.The
uiStateflow only reacts to changes inuserInfobecause it usesmapon that single flow. Lines 28-29 read passcode state via.value, which captures a snapshot but doesn't create a reactive subscription. When passcode state changes (e.g., user creates/changes passcode), the UI won't update unlessuserInfoalso happens to change.This violates the PR requirement: "The UI observes passcode state in real time for reactive updates."
🔎 Proposed fix using combine() for reactive composition
- val uiState: StateFlow<MainUiState> = userDataRepository.userInfo.map { - val hasPasscode = passcodeManager.hasPasscode.value - val hasSkippedPasscodeSetup = passcodeManager.hasSkippedPasscodeSetup.value - MainUiState.Success(it, hasPasscode, hasSkippedPasscodeSetup) + val uiState: StateFlow<MainUiState> = combine( + userDataRepository.userInfo, + passcodeManager.hasPasscode, + passcodeManager.hasSkippedPasscodeSetup + ) { userInfo, hasPasscode, hasSkippedPasscodeSetup -> + MainUiState.Success(userInfo, hasPasscode, hasSkippedPasscodeSetup) }.stateIn( scope = viewModelScope, initialValue = MainUiState.Loading,Don't forget to import
combine:+import kotlinx.coroutines.flow.combine
♻️ Duplicate comments (3)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)
96-109: Address the reactivestartDestinationissue flagged in the previous review.This code still uses a reactive
startDestinationwhich was flagged as problematic in the previous review. The NavHost'sstartDestinationshould be fixed (e.g.,LOGIN_GRAPH), and navigation decisions should be handled viaLaunchedEffectwithnavController.navigate(). This prevents NavHost graph re-creation and back-stack issues.Based on the previous review, the correct pattern is:
- Set
startDestination = LOGIN_GRAPH(fixed value)- Add a
LaunchedEffect(uiState)that watches state changes- Programmatically navigate using
navController.navigate(target)with properpopUpTohandling- Follow the authorization handling pattern already used in lines 57-61
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (2)
40-78: Race condition:hasPasscodeuninitialized when checking.The
getIntention()method runs synchronously ininitand checksstate.hasPasscodeon line 70. However,observePasscodeRepository()launches an asynchronous coroutine to collect thehasPasscodeflow. When line 70 executes,state.hasPasscodewill still befalse(the default) because the flow hasn't emitted yet.Impact: For
CHANGE_PASSCODEintention, the condition!state.hasPasscodewill incorrectly evaluate totrueeven when a passcode exists, causing the Verify step to be skipped and allowing passcode changes without verification.🔎 Proposed fix: await hasPasscode before checking
init { - observePasscodeRepository() - getIntention() + viewModelScope.launch { + // Wait for hasPasscode to be loaded before determining flow + val hasPasscode = passcodeRepository.hasPasscode.first() + mutableStateFlow.update { it.copy(hasPasscode = hasPasscode) } + getIntention() + } + observePasscodeRepository() }
225-228: Bug: Should setisChangePasscodeSuccessfultofalsewhen dismissing.Line 226 sets
isChangePasscodeSuccessful = true, but this is redundant—the flag is alreadytrue(which is why the dialog is visible). The dismiss action should reset it tofalseto clear the success state.🔎 Proposed fix
private fun onDismissChangePasscodeSuccessDialog() { - mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = true) } + mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = false) } sendEvent(PasscodeEvent.PasscodeConfirmed) }
🧹 Nitpick comments (3)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt (1)
29-47: Consider using MaterialTheme.colorScheme instead of hardcoded colors.The dialog uses hardcoded
Color.WhiteandColor.Blackwhich won't respect theming or dark mode preferences. Consider using theme-aware colors.🔎 Proposed refactor to use theme colors
AlertDialog( shape = MaterialTheme.shapes.large, - containerColor = Color.White, + containerColor = MaterialTheme.colorScheme.surface, title = { Text( text = stringResource(Res.string.library_mifos_passcode_passcode_changed_message), - color = Color.Black, + color = MaterialTheme.colorScheme.onSurface, ) }, confirmButton = { TextButton(onClick = onDismiss) { Text( text = stringResource(Res.string.library_mifos_passcode_done), - color = Color.Black, + color = MaterialTheme.colorScheme.primary, ) } }, onDismissRequest = onDismiss, )libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt (1)
43-47: Consider usingSharingStarted.WhileSubscribed()instead ofEagerly.The
hasSkippedPasscodeSetupStateFlow usesSharingStarted.Eagerly, which keeps the flow active even when there are no collectors. Since this is a UI-related state,SharingStarted.WhileSubscribed(5000)would be more appropriate—it stops collection after 5 seconds of no subscribers, reducing unnecessary work.🔎 Proposed refactor
val hasSkippedPasscodeSetup = source.hasSkippedPasscodeSetup.stateIn( scope = coroutineScope, - started = SharingStarted.Eagerly, + started = SharingStarted.WhileSubscribed(5000), initialValue = false, )libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt (1)
12-14: Consider documenting all enum values for consistency.The KDoc currently only documents the
Verifystep. While the explanation forVerifyis helpful, adding brief descriptions forCreateandConfirmwould improve documentation completeness and maintainability.📝 Suggested documentation improvement
/** + * Represents the steps in the passcode flow. + * * @property Verify Verifying existing passcode (used in LOGIN and CHANGE intentions) + * @property Create Creating a new passcode (used in CREATE and CHANGE intentions) + * @property Confirm Confirming the newly created passcode */ enum class Step(var index: Int) {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.ktfeature/settings/build.gradle.ktsfeature/settings/src/commonMain/composeResources/values/strings.xmlfeature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.ktfeature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.ktlibs/mifos-passcode/src/commonMain/composeResources/values/strings.xmllibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt
💤 Files with no reviewable changes (1)
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
🚧 Files skipped from review as they are similar to previous changes (9)
- libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt
- libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
- libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt
- feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt
- libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt
- libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt
- feature/settings/src/commonMain/composeResources/values/strings.xml
🧰 Additional context used
🧬 Code graph analysis (4)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)
passcodeGraphRoute(31-33)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt (1)
sendEvent(97-99)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (1)
passcodeRoute(47-67)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt (1)
ChangePasscodeSuccessDialog(23-49)
🔇 Additional comments (29)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (2)
37-42: LGTM!The logout logic correctly clears both user data and passcode state in the appropriate scope.
47-51: LGTM!The expanded
Successstate structure cleanly carries user and passcode metadata, aligning with the PR's intention-driven passcode flow requirements.feature/settings/build.gradle.kts (1)
26-26: LGTM! The addition ofmifosPasscodedependency is correct and enables the settings module to interact with passcode components for the "Create/Change Passcode" functionality.The library exists with proper structure, has no circular dependencies, and is correctly referenced in the commonMain scope for cross-platform usage.
libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml (1)
21-24: LGTM!The new string resources appropriately support the change passcode flow and success dialog.
libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt (1)
19-34: LGTM!The change from
factorytosingleis essential for maintaining reactive flows across multiple screens. The documentation clearly explains the rationale—usingfactorywould create new instances with separate flows, breaking reactivity. The switch to IO dispatcher is also more appropriate for preference persistence operations.cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt (2)
73-73: LGTM!The addition of the
rootNavControllerparameter enables proper navigation wiring at the app level, supporting the intention-based passcode navigation flow.
163-163: LGTM!Correctly forwards the
rootNavControllertoMifosNavHostfor navigation handling.feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt (1)
44-53: LGTM!The
hasPasscodeproperty exposure enables reactive Settings UI updates (showing "Change Passcode" vs "Create Passcode"). The documentation clearly explains the use case, and it correctly leverages the singletonPasscodeManagerto maintain consistent state.libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt (2)
49-57: LGTM!Correctly resets
hasSkippedPasscodeSetuptofalsewhen saving a passcode, ensuring the skip flag is cleared when the user actively sets a passcode.
59-61: LGTM!The
setSkippedPasscodeSetupfunction provides a clean interface for updating the skip flag.cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)
19-33: LGTM!The refactoring from a fixed
PASSCODE_GRAPHconstant to an intention-driven routing system is well-designed. The templated route and builder function enable multiple passcode flows (LOGIN_WITH_PASSCODE, CREATE_PASSCODE, CHANGE_PASSCODE) while maintaining clean navigation semantics.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt (2)
29-58: LGTM! Clean refactor to intention-based visibility.The shift from boolean flags to
Intentionenum provides clearer semantics and better maintainability. The KDoc accurately describes when the Skip button appears.
60-89: LGTM! Correct intention-based gating.The Forgot button correctly appears only during login flows. The KDoc clearly communicates this behavior.
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (3)
22-37: LGTM! Intention-based routing properly configured.The start destination correctly includes the intention placeholder, allowing the graph route to pass intention through to the passcode screen. This aligns with the
passcodeRoutedefinition in PasscodeNavigation.kt.
40-47: LGTM! Correct navigation for forgot passcode.The change to navigate to login screen (instead of main graph) when user forgets their passcode makes sense - it provides a fallback authentication path.
48-51: LGTM! Unified callback simplifies API.Consolidating
onPasscodeConfirmandonPasscodeRejectedinto a singleonPasscodeFlowCompletecallback simplifies the navigation surface and aligns with the intention-driven flow where the screen internally handles different outcomes.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (4)
69-82: LGTM! Unified callback correctly wired.The event handling correctly maps
PasscodeConfirmedto the unifiedonPasscodeFlowCompletecallback, simplifying the public API.
92-92: LGTM! Skip event properly handled.The
PasscodeSetupSkippedevent correctly triggers theonSkipButtoncallback, completing the skip flow.
109-114: LGTM! Skip button correctly integrated.The button now uses intention-based gating and triggers the ViewModel action, properly encapsulating the skip logic within the ViewModel.
118-121: LGTM! Success dialog properly integrated.The
ChangePasscodeSuccessDialogis correctly wired to state and triggers the dismiss action through the ViewModel.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (3)
122-142: LGTM! Per-step buffer selection correctly implemented.The
enterKeymethod correctly selects the appropriate buffer (verifyPasscode,createPasscode, orconfirmPasscode) based onactiveStep, ensuring inputs are kept separate across the multi-step flow.
203-218: LGTM! Intention-driven flow logic is sound.The
processCompletedPasscodemethod correctly orchestrates the passcode flow based on intention and step, handling all three flows (CREATE, LOGIN, CHANGE) appropriately.
240-253: LGTM! Dual-purpose validation correctly implemented.The validation logic correctly uses the
verifyPasscodebuffer and branches appropriately: for CHANGE_PASSCODE it advances to Create step, while for LOGIN_WITH_PASSCODE it completes the flow.feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt (4)
34-34: Required resource import added correctly.The import for
feature_settings_create_passcodeis necessary for the dynamic button title feature.
142-151: Excellent UX: Dynamic button title based on passcode state.The conditional title ("Change Passcode" vs "Create Passcode") provides clear guidance to users based on their current passcode configuration. The comment effectively explains the feature's purpose.
251-251: Preview correctly updated with new parameter.The preview properly supplies the
hasPasscodeparameter with a sensible default value.
67-68: Good reactive state observation for dynamic UI updates.Collecting
hasPasscodeas state enables the UI to reactively update when passcode status changes. The SettingsViewModel properly exposeshasPasscodeas a StateFlow, and the implementation correctly usescollectAsStateWithLifecycle()for lifecycle-aware state observation.feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt (1)
57-70: Excellent documentation of the architectural refactor and bug fix.The documentation clearly explains:
- The bug that was fixed (double navigation to passcode graph)
- The root cause (redundant navigation calls)
- The new responsibility separation
This is verified by the app-level navigation logic in
MifosPayApp.kt(lines 57-95), which centrally determines the post-login destination based on authentication and passcode state. Only a singlenavigateToPasscodeScreencall remains in the codebase (in settings for theCHANGE_PASSCODEflow), confirming the double-navigation issue is fully resolved and LoginScreen no longer handles passcode navigation.feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt (1)
139-149: Documentation clearly explains the architectural change.The app-level navigation properly handles all passcode routing scenarios. Authentication state changes are observed in MifosPayViewModel, which determines the navigation destination based on authentication status, existing passcode, and skip preferences. Post-login passcode verification (LOGIN_WITH_PASSCODE), first-time setup (CREATE_PASSCODE), and change flows (CHANGE_PASSCODE) are all correctly routed through the PasscodeNavGraph.
| enum class Step(var index: Int) { | ||
| Create(0), | ||
| Confirm(1), | ||
| Verify(0), | ||
| Create(1), | ||
| Confirm(2), | ||
| } |
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.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the complete Step.kt file to understand the full context
cat -n libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.ktRepository: openMF/mobile-wallet
Length of output: 737
🏁 Script executed:
# Search for usages of .index across the codebase
rg -n --type kotlin '\.index\b' -C 2Repository: openMF/mobile-wallet
Length of output: 2382
🏁 Script executed:
# Search for assignments to .index (mutable mutations)
rg -n --type kotlin '\.index\s*=' -C 2Repository: openMF/mobile-wallet
Length of output: 46
🏁 Script executed:
# Also check for any imports or references to Step class to understand usage patterns
rg -n --type kotlin 'Step\.' -C 1 | head -50Repository: openMF/mobile-wallet
Length of output: 6098
Use Kotlin's built-in ordinal property instead of the custom mutable index parameter.
The var index: Int parameter duplicates Kotlin's built-in ordinal property and introduces unnecessary complexity:
- The explicit index values (0, 1, 2) match the ordinal values exactly
- The property is only accessed once in the codebase (PasscodeStepIndicator.kt:54), where it's compared with a loop counter
- The mutable
varis never reassigned, making mutability unnecessary
Replace activeStep.index with activeStep.ordinal in PasscodeStepIndicator.kt:54, then remove the custom parameter entirely:
-enum class Step(var index: Int) {
- Verify(0),
- Create(1),
- Confirm(2),
+enum class Step {
+ Verify,
+ Create,
+ Confirm,
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt
around lines 15 to 19, the enum defines a redundant mutable property `var index:
Int` with explicit values; remove the parameter and its assignments so the enum
becomes plain enum entries (Verify, Create, Confirm) and update the usage at
PasscodeStepIndicator.kt:54 to use the built-in `ordinal` property (replace
`activeStep.index` with `activeStep.ordinal`), ensuring no other references to
`index` remain before deleting the property.
a13601d to
c74d216
Compare
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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (1)
27-35: Fix reactivity issue: passcode state changes won't update UI.Using
.valueon StateFlows inside themapblock captures a snapshot at the timeuserInfoemits, but won't react to independent changes in passcode state. If a user creates or changes their passcode via Settings, the UI won't reflect this untiluserInfoemits again for an unrelated reason.Use
combine()to properly observe all three flows together for reactive updates.🔎 Proposed fix using combine for reactive state
- val uiState: StateFlow<MainUiState> = userDataRepository.userInfo.map { - val hasPasscode = passcodeManager.hasPasscode.value - val hasSkippedPasscodeSetup = passcodeManager.hasSkippedPasscodeSetup.value - MainUiState.Success(it, hasPasscode, hasSkippedPasscodeSetup) - }.stateIn( + val uiState: StateFlow<MainUiState> = combine( + userDataRepository.userInfo, + passcodeManager.hasPasscode, + passcodeManager.hasSkippedPasscodeSetup + ) { userInfo, hasPasscode, hasSkippedPasscodeSetup -> + MainUiState.Success(userInfo, hasPasscode, hasSkippedPasscodeSetup) + }.stateIn( scope = viewModelScope, initialValue = MainUiState.Loading, started = SharingStarted.WhileSubscribed(5_000),Don't forget to add the import:
import kotlinx.coroutines.flow.combine
♻️ Duplicate comments (3)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)
96-109: Critical: ReactivestartDestinationcauses NavHost re-creation and back-stack issues.This is the same issue previously flagged. Computing
navDestinationreactively and using it asstartDestination(line 116) causes Compose Navigation to re-create the NavHost graph wheneveruiStatechanges, leading to back-stack anomalies and navigation glitches.Solution: Use a fixed
startDestination(e.g.,LOGIN_GRAPH) and add aLaunchedEffectthat watchesuiStateto programmatically navigate usingnavController.navigate()with properpopUpTohandling. The unauthorized handling pattern (lines 57-61) demonstrates the correct approach.🔎 Recommended fix pattern
- /** - * Determines the initial navigation destination based on authentication and passcode state. - * - * Navigation decision tree: - * 1. If still loading -> LOGIN_GRAPH (temporary until we know auth state) - * 2. If not authenticated -> LOGIN_GRAPH - * 3. If authenticated AND has passcode -> PASSCODE_GRAPH with LOGIN_WITH_PASSCODE intention - * (user needs to verify their passcode to access the app) - * 4. If authenticated AND skipped passcode setup -> MAIN_GRAPH - * (user previously chose to skip, respect that choice) - * 5. If authenticated but no passcode and not skipped -> PASSCODE_GRAPH with CREATE_PASSCODE intention - * (first-time setup, user can create or skip) - */ - val navDestination = when { - uiState is Success -> { - val userData = (uiState as Success).userData - val hasPasscode = (uiState as Success).hasPasscode - val hasSkippedPasscodeSetup = (uiState as Success).hasSkippedPasscodeSetup - when { - !userData.authenticated -> LOGIN_GRAPH - hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE) - hasSkippedPasscodeSetup -> MAIN_GRAPH - else -> passcodeGraphRoute(Intention.CREATE_PASSCODE) - } - } - else -> LOGIN_GRAPH - } + LaunchedEffect(uiState) { + if (uiState is Success) { + val successState = uiState as Success + val userData = successState.userData + val hasPasscode = successState.hasPasscode + val hasSkippedPasscodeSetup = successState.hasSkippedPasscodeSetup + + val targetDestination = when { + !userData.authenticated -> LOGIN_GRAPH + hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE) + hasSkippedPasscodeSetup -> MAIN_GRAPH + else -> passcodeGraphRoute(Intention.CREATE_PASSCODE) + } + + navController.navigate(targetDestination) { + popUpTo(navController.graph.id) { + inclusive = true + } + launchSingleTop = true + } + } + } MifosTheme { RootNavGraph( networkMonitor = networkMonitor, timeZoneMonitor = timeZoneMonitor, navHostController = navController, - startDestination = navDestination, + startDestination = LOGIN_GRAPH, modifier = modifier,Based on learnings from past review and web search results on Compose Navigation best practices.
Also applies to: 116-116
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (2)
40-43: Race condition:hasPasscodemay not be initialized whengetIntention()checks it.This issue was flagged in a previous review and remains unaddressed.
observePasscodeRepository()launches an async coroutine to collecthasPasscode, butgetIntention()runs synchronously immediately after. When line 70 checks!state.hasPasscode, the value will still befalse(the default) because the flow hasn't emitted yet.Impact: For
CHANGE_PASSCODEintention, the condition!state.hasPasscodewill betrueeven when the user has a passcode, causing the Verify step to be incorrectly skipped—allowing passcode change without verification.🔎 Proposed fix: await hasPasscode before determining flow
init { observePasscodeRepository() - getIntention() + viewModelScope.launch { + // Wait for hasPasscode to be loaded before determining flow + val hasPasscode = passcodeRepository.hasPasscode.first() + mutableStateFlow.update { it.copy(hasPasscode = hasPasscode) } + getIntention() + } }
225-228: Bug: Should setisChangePasscodeSuccessfultofalsewhen dismissing.This issue was flagged in a previous review and remains unaddressed. Setting
isChangePasscodeSuccessful = trueon line 226 is incorrect—it's alreadytrue(which is why the dialog was visible). This should be set tofalseto properly reset the state.🔎 Proposed fix
private fun onDismissChangePasscodeSuccessDialog() { - mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = true) } + mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = false) } sendEvent(PasscodeEvent.PasscodeConfirmed) }
🧹 Nitpick comments (5)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)
98-100: Consider extracting the Success cast to reduce repetition.The code casts
uiStatetoSuccessthree times (lines 98-100). Extracting this to a local variable would improve readability and reduce redundancy.🔎 Suggested refactor
val navDestination = when { uiState is Success -> { - val userData = (uiState as Success).userData - val hasPasscode = (uiState as Success).hasPasscode - val hasSkippedPasscodeSetup = (uiState as Success).hasSkippedPasscodeSetup + val successState = uiState as Success + val userData = successState.userData + val hasPasscode = successState.hasPasscode + val hasSkippedPasscodeSetup = successState.hasSkippedPasscodeSetup when { !userData.authenticated -> LOGIN_GRAPHNote: This refactor would be unnecessary if the reactive
startDestinationis replaced with theLaunchedEffectpattern as recommended above.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt (1)
74-78: Preview function should be private.The
PasscodeStepIndicatorPreviewfunction is markedpublic(default visibility), but the component it previews isinternal. Preview functions for internal components should also be private to avoid polluting the public API surface.🔎 Proposed fix
@Preview @Composable -fun PasscodeStepIndicatorPreview() { +private fun PasscodeStepIndicatorPreview() { PasscodeStepIndicator(activeStep = Step.Confirm) }libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (1)
38-41: Inconsistent default intention betweenfromValueand route argument.
Intention.fromValue()defaults toCREATE_PASSCODE(line 40), but thenavArgumentdefault isLOGIN_WITH_PASSCODE(line 56). This inconsistency could cause confusion iffromValueis called with an unknown string—it would returnCREATE_PASSCODEinstead of the expectedLOGIN_WITH_PASSCODE.Consider aligning the defaults:
🔎 Proposed fix
companion object { fun fromValue(value: String): Intention = - Intention.entries.firstOrNull { it.value == value } ?: CREATE_PASSCODE + Intention.entries.firstOrNull { it.value == value } ?: LOGIN_WITH_PASSCODE }libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)
118-121: Dialog placement within Column may cause layout shifts.
ChangePasscodeSuccessDialogis placed inside the mainColumnlayout. WhileAlertDialogtypically renders as a popup overlay, placing it inline within the column structure is unconventional and could cause unexpected behavior on some platforms.Consider moving the dialog outside the
Column, either after theScaffoldcontent or as a sibling to theScaffold:🔎 Suggested placement
Scaffold(...) { paddingValues -> Column(...) { // ... column content without dialog } } ChangePasscodeSuccessDialog( visible = state.isChangePasscodeSuccessful, onDismiss = { viewModel.trySendAction(PasscodeAction.DismissChangePasscodeSuccessDialog) }, )feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt (1)
248-253: Consider adding a preview for the alternative state.The current preview tests
hasPasscode = false(showing "Create Passcode"). Adding a second preview withhasPasscode = truewould improve visual testing coverage for both UI states.💡 Suggested additional preview
@Preview @Composable private fun SettingsScreenPreviewWithPasscode() { SettingsScreenContent( onAction = {}, hasPasscode = true, ) }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.ktcmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.ktfeature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.ktfeature/settings/build.gradle.ktsfeature/settings/src/commonMain/composeResources/values/strings.xmlfeature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.ktfeature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.ktlibs/mifos-passcode/src/commonMain/composeResources/values/strings.xmllibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.ktlibs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.ktlibs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt
💤 Files with no reviewable changes (1)
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
🚧 Files skipped from review as they are similar to previous changes (13)
- feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt
- libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt
- libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
- feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt
- libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt
- feature/settings/build.gradle.kts
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
- libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt
- libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt
- libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt
- cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt
🧰 Additional context used
🧬 Code graph analysis (3)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)
passcodeGraphRoute(31-33)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)
PasscodeScreen(65-171)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt (1)
sendEvent(97-99)
🔇 Additional comments (14)
feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt (1)
139-144: LGTM! Clear documentation of architectural change.The documentation effectively explains why
NavigateToPasscodeScreenwas removed and clarifies that passcode navigation is now centralized at the app level. This helps future maintainers understand the design decision.cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (1)
47-51: Data class signature updated appropriately.The addition of
hasPasscodeandhasSkippedPasscodeSetupfields toMainUiState.Successcorrectly exposes the passcode state needed for the dynamic Settings UI feature described in the PR objectives.feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt (1)
57-70: Excellent documentation of navigation refactoring.The documentation thoroughly explains the double-navigation bug that was fixed and clearly describes the new separation of concerns where login handles authentication while app-level logic handles passcode routing. This provides valuable context for future maintainers.
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt (1)
38-46: LGTM!The
passcodeNavGraphregistration androotNavControllerpropagation toMifosAppcorrectly enable intention-based passcode navigation from the main graph. This cleanly separates passcode routing from the login flow.libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt (1)
43-61: LGTM!The
hasSkippedPasscodeSetupflow andsetSkippedPasscodeSetupfunction provide a clean API for tracking skip state. The automatic reset of the skip flag when saving a passcode (line 54) ensures consistent state—users who eventually create a passcode are no longer considered "skipped."libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml (1)
21-24: LGTM!The new string resources appropriately support the change-passcode flow with clear, user-friendly messaging. The naming follows the existing
library_mifos_passcode_prefix pattern used for newer strings in this file.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)
65-93: LGTM on the unified callback and event handling.The consolidation of
onPasscodeConfirm/onPasscodeRejectedintoonPasscodeFlowCompletesimplifies the API. TheEventsEffectproperly handles the three event types (PasscodeConfirmed,PasscodeRejected,PasscodeSetupSkipped) with appropriate responses.libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)
119-142: LGTM on step-based passcode buffer handling.The switch to per-step
StringBuilderbuffers (verifyPasscode,createPasscode,confirmPasscode) based onactiveStepcleanly separates input across the multi-step flow and prevents cross-contamination between steps.feature/settings/src/commonMain/composeResources/values/strings.xml (1)
18-18: LGTM! String resource addition is correct.The new string resource follows the existing naming convention and integrates well with the dynamic passcode UI flow.
feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt (5)
34-34: LGTM! Import correctly added.The import of the new string resource is properly placed and follows the existing pattern.
67-68: LGTM! State collection follows Compose best practices.The lifecycle-aware state collection ensures the UI updates reactively when the passcode state changes, with proper lifecycle handling.
89-94: LGTM! State propagation is correct.The
hasPasscodestate is correctly passed to the content composable following unidirectional data flow.
98-102: LGTM! Function signature correctly extended.The
hasPasscodeparameter addition is correct, and requiring it without a default ensures explicit state provision from callers.
142-151: LGTM! Dynamic passcode button implementation is correct.The conditional label switching between "Change Passcode" and "Create Passcode" based on
hasPasscodestate provides clear user guidance. The unifiedSettingsAction.ChangePasscodeaction is the correct design pattern here. The navigation layer routes to the passcode screen withIntention.CHANGE_PASSCODE, and thePasscodeScreenViewModeladapts the intention based on whether a passcode already exists, avoiding the need for the navigation layer to check passcode state.
| import androidx.compose.runtime.remember | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
| import androidx.lifecycle.viewmodel.compose.viewModel |
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.
Remove unused import.
The viewModel import from androidx.lifecycle.viewmodel.compose is not used anywhere in this file. The code uses koinViewModel() instead (line 49).
🔎 Proposed fix
-import androidx.lifecycle.viewmodel.compose.viewModel📝 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.
| import androidx.lifecycle.viewmodel.compose.viewModel |
🤖 Prompt for AI Agents
In cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt around
line 19, the import "androidx.lifecycle.viewmodel.compose.viewModel" is unused;
remove that import line so the file relies on koinViewModel() as used later
(line 49) and then run a quick compile/check to ensure no other references
remain.
| Box( | ||
| modifier = modifier.fillMaxWidth(), | ||
| modifier = Modifier.fillMaxWidth(), | ||
| contentAlignment = Alignment.Center, | ||
| ) { |
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.
The modifier parameter is not applied to the Box.
The modifier parameter passed to PasscodeHeader is ignored. Line 89 uses Modifier.fillMaxWidth() directly instead of chaining with the provided modifier. This breaks the composable's customization contract.
🔎 Proposed fix
Box(
- modifier = Modifier.fillMaxWidth(),
+ modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {📝 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.
| Box( | |
| modifier = modifier.fillMaxWidth(), | |
| modifier = Modifier.fillMaxWidth(), | |
| contentAlignment = Alignment.Center, | |
| ) { | |
| Box( | |
| modifier = modifier.fillMaxWidth(), | |
| contentAlignment = Alignment.Center, | |
| ) { |
🤖 Prompt for AI Agents
In
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt
around lines 88 to 91, the passed modifier parameter is ignored because the Box
uses Modifier.fillMaxWidth() directly; update the Box to apply the incoming
modifier chained with fillMaxWidth (e.g., use modifier.fillMaxWidth() or
modifier.then(Modifier.fillMaxWidth())) so callers' modifier customizations are
respected.
| private fun onSkipPasscodeSetup() { | ||
| viewModelScope.launch { | ||
| passcodeRepository.setSkippedPasscodeSetup(true) | ||
| } | ||
| sendEvent(PasscodeEvent.PasscodeSetupSkipped) | ||
| } |
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.
Event sent before skip preference is persisted.
sendEvent(PasscodeEvent.PasscodeSetupSkipped) is called outside the viewModelScope.launch block, meaning it fires before setSkippedPasscodeSetup(true) completes. If navigation happens immediately on the event, the skip flag might not be persisted yet.
🔎 Proposed fix: send event after persistence completes
private fun onSkipPasscodeSetup() {
viewModelScope.launch {
passcodeRepository.setSkippedPasscodeSetup(true)
+ sendEvent(PasscodeEvent.PasscodeSetupSkipped)
}
- sendEvent(PasscodeEvent.PasscodeSetupSkipped)
}📝 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.
| private fun onSkipPasscodeSetup() { | |
| viewModelScope.launch { | |
| passcodeRepository.setSkippedPasscodeSetup(true) | |
| } | |
| sendEvent(PasscodeEvent.PasscodeSetupSkipped) | |
| } | |
| private fun onSkipPasscodeSetup() { | |
| viewModelScope.launch { | |
| passcodeRepository.setSkippedPasscodeSetup(true) | |
| sendEvent(PasscodeEvent.PasscodeSetupSkipped) | |
| } | |
| } |
🤖 Prompt for AI Agents
In
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt
around lines 112 to 117, the PasscodeSetupSkipped event is sent before the
coroutine that persists the skip preference completes; move the sendEvent call
inside the viewModelScope.launch (after calling setSkippedPasscodeSetup(true))
or otherwise await the repository call so the preference is persisted before
emitting the event, ensuring navigation triggered by the event sees the updated
persisted state.
Issue Fix
Jira Task: 324
Screen recordings
Recording 1: User with existing passcode clicks "Change Passcode" and goes through the complete flow:
change_passcode_correctly_working.mp4
Recording 2: User logs in, skips passcode setup, then navigates to settings showing "Create Passcode" button and goes through the complete flow Create passcode → Confirm passcode → Success dialog
create_passcode_instead_of_change_passcode_if_no_passcode_set.mp4
Description
Key Features
1. Change Passcode Flow
2. Dynamic Settings UI
3. Intention-Based Navigation
Intentionenum with three modes:LOGIN_WITH_PASSCODE: Verify existing passcode during app loginCREATE_PASSCODE: Initial passcode setup (with skip option)CHANGE_PASSCODE: Change existing passcode (3-step flow)4. Skip Passcode Tracking
hasSkippedPasscodeSetuppreference to track user choiceSummary by CodeRabbit
New Features
Improvements
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.