Skip to content

Conversation

@sahilshivekar
Copy link

@sahilshivekar sahilshivekar commented Dec 29, 2025

Issue Fix

Jira Task: 324

Screen recordings

Recording 1: User with existing passcode clicks "Change Passcode" and goes through the complete flow:

  • Verify old passcode → Create new passcode → Confirm new passcode → Success dialog
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

  • Users can now change their existing passcode from the Settings screen
  • 3-step verification process:
    1. Verify: User must enter their current passcode
    2. Create: User creates a new passcode
    3. Confirm: User confirms the new passcode
  • Success dialog displayed upon completion

2. Dynamic Settings UI

  • Settings button dynamically displays:
    • "Create Passcode" - if user never set a passcode or skipped initially
    • "Change Passcode" - if user has an existing passcode
  • Real-time observation of passcode state for reactive UI updates

3. Intention-Based Navigation

  • Introduced Intention enum with three modes:
    • LOGIN_WITH_PASSCODE: Verify existing passcode during app login
    • CREATE_PASSCODE: Initial passcode setup (with skip option)
    • CHANGE_PASSCODE: Change existing passcode (3-step flow)

4. Skip Passcode Tracking

  • Added hasSkippedPasscodeSetup preference to track user choice
  • Prevents repeated prompting for users who explicitly chose not to use passcode
  • Users who skipped can still create a passcode later via Settings

Summary by CodeRabbit

  • New Features

    • Complete passcode flows: verify on login, create, change, skip setup, plus a change-passcode success dialog.
  • Improvements

    • App now auto-routes after authentication based on passcode state and skipped setup.
    • Settings button now shows "Create Passcode" or "Change Passcode" dynamically.
    • Passcode screens simplified and clearer: unified completion callback, intention-driven steps, and improved step indicator.
  • Documentation

    • Added clarifying docs for passcode and navigation behavior.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

📝 Walkthrough

Walkthrough

Introduces 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

Cohort / File(s) Summary
App Startup Navigation
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
Startup decision now routes based on auth, hasPasscode, and hasSkippedPasscodeSetup to LOGIN_GRAPH, passcodeGraphRoute(Intention...), or MAIN_GRAPH.
ViewModel State Expansion
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt
MainUiState.Success now includes hasPasscode and hasSkippedPasscodeSetup; uiState mapping updated.
Login Flow Simplification
feature/auth/src/commonMain/kotlin/.../LoginScreen.kt, .../LoginViewModel.kt, .../LoginScreenNavigation.kt
Removed post-login passcode navigation callback/event; login now only emits auth state—app-level routing decides passcode path.
Passcode Route Templating
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt
Removed PASSCODE_GRAPH; added templated PASSCODE_GRAPH_ROUTE and helper passcodeGraphRoute(intention: Intention).
NavController Propagation
cmp-shared/src/commonMain/kotlin/.../MifosApp.kt, .../RootNavGraph.kt, .../MifosNavHost.kt
Added rootNavController parameter and threaded it; settings now navigates to passcode with Intention.CHANGE_PASSCODE.
Passcode Nav Graph & Callbacks
cmp-shared/src/commonMain/kotlin/.../PasscodeNavGraph.kt, libs/mifos-passcode/src/.../PasscodeNavigation.kt
Passcode route accepts INTENTION arg; passcodeRoute and navigateToPasscodeScreen accept Intention; callbacks consolidated to onPasscodeFlowComplete; forgot navigates to login.
Intention Enum & Helpers
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt
Added Intention enum (CHANGE_PASSCODE, LOGIN_WITH_PASSCODE, CREATE_PASSCODE) with fromValue, plus INTENTION and PASSCODE_ROUTE.
Passcode State Machine & ViewModel
libs/mifos-passcode/src/commonMain/kotlin/.../PasscodeViewModel.kt
Reworked to multi-step flow (Verify/Create/Confirm) driven by Intention; added per-step buffers, skip action/event, change-success dialog state, and updated events/actions.
Passcode UI Components
libs/mifos-passcode/src/commonMain/kotlin/.../PassCodeScreen.kt, .../component/PasscodeHeader.kt, .../component/PasscodeButton.kt, .../component/PasscodeStepIndicator.kt
Replaced boolean gating with Intention; header/buttons render per intention/step; skip/forgot visibility tied to intention; indicator hidden during Verify; previews updated/added.
Success Dialog Component
libs/mifos-passcode/src/commonMain/kotlin/.../ChangePasscodeSuccessDialog.kt
New internal AlertDialog composable to confirm successful passcode change.
Skip-Passcode Persistence
libs/mifos-passcode/src/commonMain/kotlin/proto/.../PasscodeManager.kt, .../PasscodePreferencesDataSource.kt, .../PasscodePreferencesProto.kt
Added hasSkippedPasscodeSetup field and flows, setSkippedPasscodeSetup/updateSkippedPasscodeSetup APIs; saving passcode resets skipped flag.
DI Scope Change
libs/mifos-passcode/src/commonMain/kotlin/proto/.../PreferenceModule.kt
Switched Settings, PasscodePreferencesDataSource, and PasscodeManager bindings from factory to single.
Settings Integration & Resources
feature/settings/src/commonMain/kotlin/.../SettingsScreen.kt, .../SettingsViewModel.kt, feature/settings/build.gradle.kts, feature/settings/.../strings.xml, libs/mifos-passcode/.../strings.xml
SettingsViewModel depends on PasscodeManager and exposes hasPasscode: StateFlow; SettingsScreen shows "Create" vs "Change"; added dependency on mifosPasscode and new string resources.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through routes and states anew,

Intentions planted, three-step view,
Verify, Create, Confirm — a tidy race,
Skip or change, each finds its place,
A tiny rabbit cheers this passcode space!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.40% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(passcode): implement change passcode flow (MW-324)' accurately summarizes the main feature addition—implementing a change passcode flow. It is concise, clear, and specific to the primary objective of the PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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.value and hasSkippedPasscodeSetup.value inside the map block captures only the current snapshot values. When passcode state changes (e.g., user creates/changes/clears passcode), uiState won't update unless userInfo also 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.combine
libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt (1)

68-72: clearInfo() does not reset passcodeSettings StateFlow.

After removing the key from settings, the passcodeSettings MutableStateFlow still holds the old value. This could cause inconsistent state if clearInfo() 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.ChangePasscode is 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 ManagePasscode to 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 the hasPasscode = true state.

The preview only covers the hasPasscode = false case. 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 viewModel import from androidx.lifecycle.viewmodel.compose appears unused—the code uses koinViewModel on 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 uiState to Success on lines 97-100 is verbose. Consider using Kotlin's smart cast with a when subject:

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 Success check 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.White and Color.Black directly 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: The modifier parameter is not applied to the Box.

The function accepts a modifier parameter but uses Modifier.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 ChangePasscodeSuccessDialog is placed within the content layout between MifosIcon and the passcode input Column. While functionally correct (dialogs render as overlays), placing it at the end of the Scaffold content or outside the Column would 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

📥 Commits

Reviewing files that changed from the base of the PR and between e49f208 and 6ae6074.

📒 Files selected for processing (28)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt
  • feature/settings/build.gradle.kts
  • feature/settings/src/commonMain/composeResources/values/strings.xml
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt
  • libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.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/PasscodeHeader.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt
  • 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/di/PreferenceModule.kt
  • libs/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 mifosPasscode enables 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 NavigateToPasscodeScreen was 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_passcode string is appropriately named and placed alongside the existing feature_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 fromValue helper with a default fallback is practical for handling unexpected route arguments.

One consideration: the default fallback to CREATE_PASSCODE on 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_PASSCODE might 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 single onPasscodeFlowComplete callback 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 NavOptions parameter 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 rootNavController as 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 the activeStep, 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 onNavigateToPasscodeScreen and 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 hasPasscode as a StateFlow enables 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.index correctly 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 hasSkippedPasscodeSetup field 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 rootNavController parameter allows MifosApp to 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 Verify step 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 rootNavController to MifosApp enables 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:

  1. Exposes hasSkippedPasscodeSetup as a reactive StateFlow
  2. Resets the skip flag when a passcode is saved (line 54)
  3. Provides setSkippedPasscodeSetup for explicit updates

This 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 hasSkippedPasscodeSetup flow and updateSkippedPasscodeSetup function 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:

  1. Uses query parameter syntax for the intention argument
  2. Provides a clean helper function to build routes with specific intentions
  3. 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 when expression 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 factory to single is the correct fix. With factory, 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 onPasscodeFlowComplete callback simplifies the API surface. The navigation logic for onForgotButton (to login) and onSkipButton/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 EventsEffect correctly maps the three event types to appropriate callbacks. PasscodeSetupSkipped triggering onSkipButton() maintains consistency with the skip flow.


109-114: LGTM! Skip button correctly dispatches to ViewModel.

Delegating the skip action to the ViewModel via PasscodeAction.SkipPasscodeSetup allows 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 enterKey correctly selects the appropriate buffer based on activeStep and triggers processing when the passcode is complete.


203-219: LGTM! Well-structured flow routing.

The processCompletedPasscode function 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 PasscodeState data class with the new intention and isChangePasscodeSuccessful properties supports all flow variations cleanly.

Comment on lines +96 to 109
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
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find RootNavGraph implementation
rg -n "fun RootNavGraph" --type kotlin -A 20

Repository: 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 f

Repository: 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 5

Repository: 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 -130

Repository: 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 5

Repository: 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)

  1. 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]

  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]

  3. 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]

  4. 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]

  5. Keep a single rememberNavController() hoisted at a top-level composable (or passed via coordinator) so navigation state survives recomposition and configuration changes. [1][6]

  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]

  7. 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]
  8. 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.

Comment on lines 40 to 78
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,
)
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +225 to +228
private fun onDismissChangePasscodeSuccessDialog() {
mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = true) }
sendEvent(PasscodeEvent.PasscodeConfirmed)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from 1c37870 to 468aa86 Compare December 29, 2025 08:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (1)

27-35: Non-reactive flow composition breaks real-time passcode updates.

The uiState flow only reacts to changes in userInfo because it uses map on 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 unless userInfo also 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 reactive startDestination issue flagged in the previous review.

This code still uses a reactive startDestination which was flagged as problematic in the previous review. The NavHost's startDestination should be fixed (e.g., LOGIN_GRAPH), and navigation decisions should be handled via LaunchedEffect with navController.navigate(). This prevents NavHost graph re-creation and back-stack issues.

Based on the previous review, the correct pattern is:

  1. Set startDestination = LOGIN_GRAPH (fixed value)
  2. Add a LaunchedEffect(uiState) that watches state changes
  3. Programmatically navigate using navController.navigate(target) with proper popUpTo handling
  4. 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: hasPasscode uninitialized when checking.

The getIntention() method runs synchronously in init and checks state.hasPasscode on line 70. However, observePasscodeRepository() launches an asynchronous coroutine to collect the hasPasscode flow. When line 70 executes, state.hasPasscode will still be false (the default) because the flow hasn't emitted yet.

Impact: For CHANGE_PASSCODE intention, the condition !state.hasPasscode will incorrectly evaluate to true even 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 set isChangePasscodeSuccessful to false when dismissing.

Line 226 sets isChangePasscodeSuccessful = true, but this is redundant—the flag is already true (which is why the dialog is visible). The dismiss action should reset it to false to 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.White and Color.Black which 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 using SharingStarted.WhileSubscribed() instead of Eagerly.

The hasSkippedPasscodeSetup StateFlow uses SharingStarted.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 Verify step. While the explanation for Verify is helpful, adding brief descriptions for Create and Confirm would 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1c37870 and 468aa86.

📒 Files selected for processing (28)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt
  • feature/settings/build.gradle.kts
  • feature/settings/src/commonMain/composeResources/values/strings.xml
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt
  • libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.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/PasscodeHeader.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt
  • 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/di/PreferenceModule.kt
  • libs/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 Success state 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 of mifosPasscode dependency 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 factory to single is essential for maintaining reactive flows across multiple screens. The documentation clearly explains the rationale—using factory would 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 rootNavController parameter enables proper navigation wiring at the app level, supporting the intention-based passcode navigation flow.


163-163: LGTM!

Correctly forwards the rootNavController to MifosNavHost for navigation handling.

feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt (1)

44-53: LGTM!

The hasPasscode property exposure enables reactive Settings UI updates (showing "Change Passcode" vs "Create Passcode"). The documentation clearly explains the use case, and it correctly leverages the singleton PasscodeManager to maintain consistent state.

libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt (2)

49-57: LGTM!

Correctly resets hasSkippedPasscodeSetup to false when saving a passcode, ensuring the skip flag is cleared when the user actively sets a passcode.


59-61: LGTM!

The setSkippedPasscodeSetup function 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_GRAPH constant 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 Intention enum 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 passcodeRoute definition 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 onPasscodeConfirm and onPasscodeRejected into a single onPasscodeFlowComplete callback 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 PasscodeConfirmed to the unified onPasscodeFlowComplete callback, simplifying the public API.


92-92: LGTM! Skip event properly handled.

The PasscodeSetupSkipped event correctly triggers the onSkipButton callback, 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 ChangePasscodeSuccessDialog is 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 enterKey method correctly selects the appropriate buffer (verifyPasscode, createPasscode, or confirmPasscode) based on activeStep, ensuring inputs are kept separate across the multi-step flow.


203-218: LGTM! Intention-driven flow logic is sound.

The processCompletedPasscode method 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 verifyPasscode buffer 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_passcode is 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 hasPasscode parameter with a sensible default value.


67-68: Good reactive state observation for dynamic UI updates.

Collecting hasPasscode as state enables the UI to reactively update when passcode status changes. The SettingsViewModel properly exposes hasPasscode as a StateFlow, and the implementation correctly uses collectAsStateWithLifecycle() 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 single navigateToPasscodeScreen call remains in the codebase (in settings for the CHANGE_PASSCODE flow), 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.

Comment on lines 15 to 19
enum class Step(var index: Int) {
Create(0),
Confirm(1),
Verify(0),
Create(1),
Confirm(2),
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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.kt

Repository: openMF/mobile-wallet

Length of output: 737


🏁 Script executed:

# Search for usages of .index across the codebase
rg -n --type kotlin '\.index\b' -C 2

Repository: openMF/mobile-wallet

Length of output: 2382


🏁 Script executed:

# Search for assignments to .index (mutable mutations)
rg -n --type kotlin '\.index\s*=' -C 2

Repository: 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 -50

Repository: 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 var is 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.

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from a13601d to c74d216 Compare December 29, 2025 11:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 .value on StateFlows inside the map block captures a snapshot at the time userInfo emits, 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 until userInfo emits 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: Reactive startDestination causes NavHost re-creation and back-stack issues.

This is the same issue previously flagged. Computing navDestination reactively and using it as startDestination (line 116) causes Compose Navigation to re-create the NavHost graph whenever uiState changes, leading to back-stack anomalies and navigation glitches.

Solution: Use a fixed startDestination (e.g., LOGIN_GRAPH) and add a LaunchedEffect that watches uiState to programmatically navigate using navController.navigate() with proper popUpTo handling. 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: hasPasscode may not be initialized when getIntention() checks it.

This issue was flagged in a previous review and remains unaddressed. observePasscodeRepository() launches an async coroutine to collect hasPasscode, but getIntention() runs synchronously immediately after. When line 70 checks !state.hasPasscode, the value will still be false (the default) because the flow hasn't emitted yet.

Impact: For CHANGE_PASSCODE intention, the condition !state.hasPasscode will be true even 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 set isChangePasscodeSuccessful to false when dismissing.

This issue was flagged in a previous review and remains unaddressed. Setting isChangePasscodeSuccessful = true on line 226 is incorrect—it's already true (which is why the dialog was visible). This should be set to false to 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 uiState to Success three 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_GRAPH

Note: This refactor would be unnecessary if the reactive startDestination is replaced with the LaunchedEffect pattern 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 PasscodeStepIndicatorPreview function is marked public (default visibility), but the component it previews is internal. 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 between fromValue and route argument.

Intention.fromValue() defaults to CREATE_PASSCODE (line 40), but the navArgument default is LOGIN_WITH_PASSCODE (line 56). This inconsistency could cause confusion if fromValue is called with an unknown string—it would return CREATE_PASSCODE instead of the expected LOGIN_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.

ChangePasscodeSuccessDialog is placed inside the main Column layout. While AlertDialog typically 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 the Scaffold content or as a sibling to the Scaffold:

🔎 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 with hasPasscode = true would 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

📥 Commits

Reviewing files that changed from the base of the PR and between 468aa86 and c74d216.

📒 Files selected for processing (28)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt
  • feature/settings/build.gradle.kts
  • feature/settings/src/commonMain/composeResources/values/strings.xml
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt
  • libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.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/PasscodeHeader.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt
  • 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/di/PreferenceModule.kt
  • libs/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 NavigateToPasscodeScreen was 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 hasPasscode and hasSkippedPasscodeSetup fields to MainUiState.Success correctly 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 passcodeNavGraph registration and rootNavController propagation to MifosApp correctly 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 hasSkippedPasscodeSetup flow and setSkippedPasscodeSetup function 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/onPasscodeRejected into onPasscodeFlowComplete simplifies the API. The EventsEffect properly 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 StringBuilder buffers (verifyPasscode, createPasscode, confirmPasscode) based on activeStep cleanly 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 hasPasscode state is correctly passed to the content composable following unidirectional data flow.


98-102: LGTM! Function signature correctly extended.

The hasPasscode parameter 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 hasPasscode state provides clear user guidance. The unified SettingsAction.ChangePasscode action is the correct design pattern here. The navigation layer routes to the passcode screen with Intention.CHANGE_PASSCODE, and the PasscodeScreenViewModel adapts 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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines 88 to 91
Box(
modifier = modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +112 to +117
private fun onSkipPasscodeSetup() {
viewModelScope.launch {
passcodeRepository.setSkippedPasscodeSetup(true)
}
sendEvent(PasscodeEvent.PasscodeSetupSkipped)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant