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

    • Intent-driven 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 and skipped-setup state.
    • Settings shows "Create Passcode" or "Change Passcode" dynamically.
    • Unified passcode UX: single completion flow, contextual Forgot/Skip actions, clearer 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

Routes and passcode flows were reworked to an intention-driven multi-step model (Verify/Create/Confirm). Passcode routing moved from login to app-startup decision logic; skip-state persisted; view models, navigation APIs, UI components, DI scopes, and resources updated to support the new flows.

Changes

Cohort / File(s) Summary
App Startup Navigation
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
Start destination now chosen from auth + hasPasscode + hasSkippedPasscodeSetup, routing 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 emits auth only—app-level routing handles passcode navigation.
Passcode Route Templating
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt
Removed PASSCODE_GRAPH; added templated PASSCODE_GRAPH_ROUTE and passcodeGraphRoute(intention: Intention) helper.
NavController Propagation
cmp-shared/src/commonMain/kotlin/.../MifosApp.kt, .../RootNavGraph.kt, .../MifosNavHost.kt
rootNavController: NavHostController propagated into MifosApp/MifosNavHost; settings now navigates to passcode via Intention.CHANGE_PASSCODE.
Passcode Nav Graph & Callbacks
cmp-shared/src/commonMain/kotlin/.../PasscodeNavGraph.kt, libs/mifos-passcode/src/.../PasscodeNavigation.kt
Passcode route takes 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 templated PASSCODE_ROUTE.
Passcode State Machine & ViewModel
libs/mifos-passcode/src/commonMain/kotlin/.../PasscodeViewModel.kt
Reworked to Verify/Create/Confirm steps driven by Intention; per-step buffers added; skip action/event and change-success dialog state introduced; events/actions updated.
Passcode UI Components
libs/mifos-passcode/src/commonMain/kotlin/.../PassCodeScreen.kt, .../component/PasscodeHeader.kt, .../component/PasscodeButton.kt, .../component/PasscodeStepIndicator.kt
Components now take Intention instead of booleans; header/buttons/skip/forgot visibility and step indicator behavior updated; previews added/adjusted.
Success Dialog Component
libs/mifos-passcode/src/commonMain/kotlin/.../ChangePasscodeSuccessDialog.kt
New internal composable ChangePasscodeSuccessDialog(visible, onDismiss) for change-success confirmation.
Skip-Passcode Persistence
libs/mifos-passcode/src/commonMain/kotlin/proto/.../PasscodeManager.kt, .../PasscodePreferencesDataSource.kt, .../PasscodePreferencesProto.kt
Added hasSkippedPasscodeSetup flag/flow, setSkippedPasscodeSetup/updateSkippedPasscodeSetup APIs; saving passcode resets skipped flag; proto default updated.
DI Scope Change
libs/mifos-passcode/src/commonMain/kotlin/proto/.../PreferenceModule.kt
Changed bindings from factory to single for Settings, PasscodePreferencesDataSource, and PasscodeManager.
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 now depends on PasscodeManager and exposes hasPasscode; UI shows "Create" vs "Change"; added dependency on mifosPasscode and new strings.

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 | ✅ 2 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(passcode): implement change passcode flow (MW-324)' accurately describes the main objective: implementing a new change passcode flow feature as part of the MW-324 issue, and is concise and clear.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@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.

@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.

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

🤖 Fix all issues with AI agents
In
@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt:
- Around line 226-232: The ViewModel sets isChangePasscodeSuccessful = true in
validateAndSavePasscode() but never clears it, so when dismissing the success
dialog in onDismissChangePasscodeSuccessDialog() you should reset
isChangePasscodeSuccessful back to false before sending
PasscodeEvent.PasscodeConfirmed; locate the boolean property
isChangePasscodeSuccessful in the PasscodeViewModel, update
onDismissChangePasscodeSuccessDialog() to clear that flag (and any related UI
state) and then call sendEvent(PasscodeEvent.PasscodeConfirmed) to ensure the
success state does not persist across navigations or reused ViewModel instances.
- Around line 244-257: In validateExistingPasscode(), when a passcode is
rejected you clear verifyPasscode and reset currentPasscodeInput but do not
reset filledDots, leaving the UI showing filled dots; update the
mutableStateFlow.update call (used to set currentPasscodeInput = "") to also
reset filledDots (e.g., filledDots = 0 or empty as appropriate) so the state
reflects the cleared input—apply this change in the reject branch (and consider
doing it after verifyPasscode.clear in the success path as well) to ensure the
UI is consistent.
🧹 Nitpick comments (3)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)

95-108: Avoid repeated smart casting for better readability.

Lines 97-99 cast uiState as Success three times. Extract the cast once to improve code clarity and efficiency.

♻️ Refactor to eliminate 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
+        val successState = uiState as Success
+        val userData = successState.userData
+        val hasPasscode = successState.hasPasscode
+        val hasSkippedPasscodeSetup = successState.hasSkippedPasscodeSetup
         when {
             !userData.authenticated -> LOGIN_GRAPH
             hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE)
             hasSkippedPasscodeSetup -> MAIN_GRAPH
             else -> passcodeGraphRoute(Intention.CREATE_PASSCODE)
         }
     }
     else -> LOGIN_GRAPH
 }
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (2)

124-179: Consider extracting duplicated buffer selection logic.

The when (state.activeStep) pattern for selecting the current passcode buffer is repeated across enterKey, deleteKey, and deleteAllKeys. Consider extracting to a computed property for DRY.

♻️ Suggested refactor
+    private val currentPasscodeBuffer: StringBuilder
+        get() = when (state.activeStep) {
+            Step.Verify -> verifyPasscode
+            Step.Create -> createPasscode
+            Step.Confirm -> confirmPasscode
+        }
+
     private fun enterKey(key: String) {
         if (state.filledDots >= PASSCODE_LENGTH) return
-
-        val currentPasscode = when (state.activeStep) {
-            Step.Verify -> verifyPasscode
-            Step.Create -> createPasscode
-            Step.Confirm -> confirmPasscode
-        }
-
-        currentPasscode.append(key)
+        currentPasscodeBuffer.append(key)
         // ... rest of function uses currentPasscodeBuffer

208-224: LGTM with minor suggestion.

The flow logic is correct for all documented intention/step combinations. Consider adding an else branch with logging for defensive debugging in case of unexpected state combinations.

♻️ Optional: add defensive else branch
                 state.intention == Intention.CHANGE_PASSCODE && state.activeStep == Step.Confirm -> validateAndSavePasscode()
+                else -> {
+                    // Log unexpected state for debugging
+                    // timber.w("Unexpected passcode state: intention=${state.intention}, step=${state.activeStep}")
+                }
             }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c74d216 and 04b02ac.

📒 Files selected for processing (2)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (2)
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)
🔇 Additional comments (10)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (3)

22-22: LGTM! Clean imports for the new navigation flow.

The new imports support the intention-based navigation routing introduced in this PR.

Also applies to: 30-31


82-94: Excellent documentation of the navigation decision tree.

The multi-line comment clearly explains the routing logic and helps future maintainers understand the complex state-driven navigation flow.


100-105: The logic correctly ensures mutual exclusivity. When a passcode is saved via savePasscode(), hasSkippedPasscodeSetup is explicitly reset to false, which is properly documented in the PasscodeManager class. The flags cannot both be true simultaneously because creating or changing a passcode always clears the skip flag. No changes needed.

libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (7)

28-38: LGTM!

Good separation of concerns with per-step passcode buffers. Using separate StringBuilder instances prevents cross-contamination between Verify/Create/Confirm steps.


40-83: Well-structured intention-based initialization.

The flow correctly handles all three intentions and the edge case where a user who skipped initial passcode setup can still create one via "Change Passcode". The KDoc comment explains the reasoning clearly.


97-122: LGTM!

Clean action dispatch. The skip tracking via setSkippedPasscodeSetup(true) is important for UX—prevents repeatedly prompting users who deliberately chose not to use a passcode.


259-281: LGTM!

Clean step transitions with proper UI state reset for each new step.


283-308: LGTM!

Good UX differentiation—showing a success dialog for passcode change provides clearer feedback, while initial setup remains streamlined. The KDoc explains the rationale well.


310-320: LGTM!

Correct partial reset—clears input buffers while preserving flow state (intention, activeStep). The isChangePasscodeSuccessful reset issue was addressed in a previous comment.


323-366: LGTM!

Well-structured state, events, and actions for the intention-driven passcode flow. The Parcelize annotation ensures state can survive configuration changes.

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from 04b02ac to 90060f2 Compare January 8, 2026 07: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: 0

🧹 Nitpick comments (8)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt (2)

24-24: Nice addition of preview support!

The preview import and preview function enable design-time visualization of the component, which improves developer experience.

💡 Optional: Consider adding preview variants for different states

To better showcase the component's behavior across different steps, consider adding preview variants:

@Preview
@Composable
fun PasscodeStepIndicatorPreview() {
    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
        // Create step
        PasscodeStepIndicator(activeStep = Step.Create)
        
        // Confirm step
        PasscodeStepIndicator(activeStep = Step.Confirm)
    }
}

This would show how the indicator progresses through the flow.

Also applies to: 74-78


56-56: Consider adding a descriptive animation label.

The animation label is currently an empty string. While functional, a descriptive label like "stepColor" or "stepIndicatorColor" would improve debugging and help with animation inspection tooling.

♻️ Suggested improvement
 val stepColor =
-    animateColorAsState(if (isActiveStep) blueTint else Color.Gray, label = "")
+    animateColorAsState(if (isActiveStep) blueTint else Color.Gray, label = "stepColor")
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt (1)

187-198: Consider streamlining inline comments.

The inline comments are helpful but quite verbose (7 lines) and reference internal VM behavior details. This couples the navigation layer to implementation details that might change.

♻️ Proposed streamlined version
-            // When user clicks "Change Passcode" in settings, navigate to passcode screen
-            // with CHANGE_PASSCODE intention to start the 3-step change flow:
-            // 1. Verify old passcode
-            // 2. Create new passcode
-            // 3. Confirm new passcode
-            // The PasscodeScreenViewModel will change the intention from CHANGE_PASSCODE to CREATE_PASSCODE if there is already a passcode set
-            // (this is done to avoid the navigation layer needing to check if a passcode exists)
+            // Navigate to passcode screen to start change flow (Verify → Create → Confirm)
             onChangePasscode = {
                 rootNavController.navigateToPasscodeScreen(
                     intention = Intention.CHANGE_PASSCODE,
                 )
             },
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (1)

27-35: Consider combining flows for reactive passcode state.

The current implementation reads passcodeManager state as snapshots (.value) within the userInfo flow mapping. If passcode state changes independently, the UI won't update until userInfo emits again, potentially showing stale passcode status.

♻️ Proposed reactive flow combination
-    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),
     )

Add import at the top:

import kotlinx.coroutines.flow.combine
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (2)

53-53: Redundant var with mutableStateOf.

Using var alongside remember { mutableStateOf(...) } is redundant. The MutableState holder already provides mutability via .value. This pattern can cause confusion.

♻️ Suggested fix
-    var showErrorDialog = remember { mutableStateOf<Boolean>(false) }
+    val showErrorDialog = remember { mutableStateOf(false) }

Or use delegate syntax for cleaner access:

var showErrorDialog by remember { mutableStateOf(false) }

95-108: Simplify repeated casting with smart cast.

The uiState is cast to Success three times within the same block. Use Kotlin's smart casting or destructuring to avoid repetition.

♻️ 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 (userData, hasPasscode, hasSkippedPasscodeSetup) = uiState
             when {
                 !userData.authenticated -> LOGIN_GRAPH
                 hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE)
                 hasSkippedPasscodeSetup -> MAIN_GRAPH
                 else -> passcodeGraphRoute(Intention.CREATE_PASSCODE)
             }
         }
         else -> LOGIN_GRAPH
     }

This requires adding a componentN() destructuring operator to Success, or simply rely on smart casting since the is Success check already narrows the type.

libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)

118-121: Consider moving dialog outside the layout Column.

ChangePasscodeSuccessDialog (which renders an AlertDialog) is placed inside the Column layout. While AlertDialog uses a popup window and may render correctly, placing dialogs outside layout containers is the conventional pattern in Compose for clarity and to avoid potential measurement issues.

♻️ Suggested placement

Move the dialog outside the Scaffold content, similar to how PasscodeMismatchedDialog is handled in PasscodeView:

     Scaffold(
         modifier = modifier
             .fillMaxSize(),
     ) { paddingValues ->
+        ChangePasscodeSuccessDialog(
+            visible = state.isChangePasscodeSuccessful,
+            onDismiss = { viewModel.trySendAction(PasscodeAction.DismissChangePasscodeSuccessDialog) },
+        )
+
         Column(
             ...
         ) {
             ...
-            ChangePasscodeSuccessDialog(
-                visible = state.isChangePasscodeSuccessful,
-                onDismiss = { viewModel.trySendAction(PasscodeAction.DismissChangePasscodeSuccessDialog) },
-            )
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)

61-83: State access after async update could be clearer.

After mutableStateFlow.update { ... } completes on line 71, you access state.intention on line 73. While this works because update is synchronous within the coroutine and state is a getter for stateFlow.value, the pattern could be clearer by capturing the intention value locally.

♻️ Suggested refactor for clarity
     private fun getIntention() {
         viewModelScope.launch {
             val hasPasscode = passcodeRepository.hasPasscode.first()
+            val intention = savedStateHandle.get<String>(INTENTION)
+                ?.let(Intention::fromValue)
+                ?: Intention.LOGIN_WITH_PASSCODE
+
             mutableStateFlow.update {
                 it.copy(
-                    intention = savedStateHandle.get<String>(INTENTION)
-                        ?.let(Intention::fromValue)
-                        ?: Intention.LOGIN_WITH_PASSCODE,
+                    intention = intention,
                     hasPasscode = hasPasscode,
                 )
             }
             if (
-                state.intention == Intention.CREATE_PASSCODE ||
-                (state.intention == Intention.CHANGE_PASSCODE && !state.hasPasscode)
+                intention == Intention.CREATE_PASSCODE ||
+                (intention == Intention.CHANGE_PASSCODE && !hasPasscode)
             ) {
                 mutableStateFlow.update {
                     it.copy(
                         activeStep = Step.Create,
                     )
                 }
             }
         }
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04b02ac and 90060f2.

📒 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 (11)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
  • feature/settings/src/commonMain/composeResources/values/strings.xml
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt
🧰 Additional context used
🧬 Code graph analysis (3)
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/MifosPayApp.kt (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)
  • passcodeGraphRoute (31-33)
🔇 Additional comments (26)
feature/settings/build.gradle.kts (1)

26-26: LGTM! Dependency addition aligns with the PR objectives.

The addition of mifosPasscode dependency enables the Settings feature to observe passcode state and display dynamic UI labels ("Change Passcode" or "Create Passcode"). The dependency reference is syntactically correct, appropriately scoped to commonMain, and introduces no circular dependencies—mifosPasscode has no reverse dependency on feature.settings.

feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt (1)

139-149: LGTM! Clean removal of passcode navigation from login flow.

The documentation clearly explains that passcode navigation is now handled at the app level after authentication state changes. The LoginEvent sealed class is properly updated with no references to passcode navigation, and the handleLoginResult method correctly handles success without triggering any navigation events.

feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt (2)

57-70: Excellent documentation explaining the bug fix and architectural improvement.

The documentation clearly explains both the technical issue (duplicate navigation calls causing two PasscodeScreenViewModel instances) and the solution (app-level navigation handling). This provides valuable context for future maintainers.


72-93: App-level navigation correctly handles all post-login passcode scenarios.

The app-level navigation in MifosPayApp.kt (lines 95-108) properly implements all three post-login scenarios:

  • User with existing passcode → navigates to passcode verification (LOGIN_WITH_PASSCODE)
  • User who skipped passcode → navigates directly to main app
  • User needing passcode setup → navigates to passcode creation (CREATE_PASSCODE)

The implementation extracts hasPasscode and hasSkippedPasscodeSetup from the UI state and routes accordingly. The separation of concerns is clean—LoginScreen focuses on authentication while app-level navigation handles all post-login passcode flows.

feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt (1)

21-43: LGTM! Navigation builder properly updated.

The removal of onNavigateToPasscodeScreen callback is clean and consistent with the app-level navigation refactor. The function signature and LoginScreen invocation are properly updated, with clear documentation explaining the change.

libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt (3)

29-38: Excellent documentation!

The KDoc clearly explains the component's behavior, including why the indicator is hidden during Verify and how the step highlighting works. This will help future maintainers understand the design decisions.


44-71: Good UX decision to conditionally show the indicator.

Hiding the step indicator during the Verify phase is appropriate since users don't need progress indication during login or old passcode verification. The indicator appropriately shows only for the Create and Confirm steps where progress tracking is meaningful.


54-54: The code is correct. STEPS_COUNT = 2 matches the highlighting logic: the indicator displays 2 boxes for Step.Create (index 1) and Step.Confirm (index 2), with step + 1 == activeStep.index properly highlighting the active step. The Verify step (index 0) is not included in the visible indicator.

libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml (2)

21-21: LGTM! String resources are clear and consistent.

The new string resources follow the established library_mifos_passcode_* naming pattern and are appropriately worded for their use cases in the passcode flows.

Also applies to: 24-24


22-22: No action required. The message "Your passcode has been updated" is correctly scoped to the change-passcode flow only. Initial passcode creation (CREATE_PASSCODE intention) follows a different code path and does not display this success dialog; instead, it emits PasscodeEvent.PasscodeConfirmed directly without showing a dialog.

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt (1)

94-94: LGTM! Root navigation controller enables cross-graph passcode flow.

The addition of rootNavController parameter correctly enables navigation to the passcode screen from the main navigation graph, supporting the intention-based passcode flow described in the PR.

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

18-24: LGTM! Clear documentation of skip-state behavior.

The KDoc clearly explains the auto-reset behavior when a passcode is saved, which is important context for consumers of this API.


43-47: LGTM! Consistent flow setup.

The hasSkippedPasscodeSetup flow follows the same pattern as existing flows in this class with appropriate initial value and sharing strategy.


49-61: LGTM! Logical auto-reset behavior.

Setting hasSkippedPasscodeSetup = false when saving a passcode is correct—once a user actively sets a passcode, they are no longer in a "skipped" state. The new setSkippedPasscodeSetup method is a clean delegation to the data source.

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

46-47: LGTM! Consistent flow mapping.

The hasSkippedPasscodeSetup flow follows the established pattern for exposing settings properties as reactive streams.


56-66: LGTM! Correct selective update implementation.

The method correctly uses copy() to update only the skip flag while preserving other preferences, and properly updates both persistent storage and in-memory state.

libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt (1)

15-18: The Step enum reordering is safe—no code in the codebase relies on enum ordinals. All usages are by explicit name matching (when expressions, equality checks), not ordinal values. The explicit index property is also unused.

Likely an incorrect or invalid review comment.

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt (1)

47-51: Breaking API change: Success data class signature expanded, but all consumers already updated.

The Success data class now requires three parameters (userData, hasPasscode, hasSkippedPasscodeSetup) instead of one. The single constructor call in MifosPayViewModel.kt:30 already uses the new signature, and the detailed consumer in MifosPayApp.kt correctly accesses all three new properties. No further updates needed.

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)

19-33: LGTM!

The intention-based route building is well-structured. Private constants for implementation details with a public templated route and a helper function follows good encapsulation practices. The KDoc documentation is helpful.

libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt (2)

35-58: LGTM!

The refactor from boolean hasPassCode to Intention enum makes the visibility logic more explicit and self-documenting. The KDoc accurately describes the button's context.


66-89: LGTM!

Consistent with PasscodeSkipButton, using Intention for visibility gating is cleaner than the previous boolean approach.

libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)

65-93: LGTM on the event handling and callback consolidation.

Consolidating onPasscodeConfirm and onPasscodeRejected into onPasscodeFlowComplete simplifies the API surface. The event-to-action mapping is clear and correctly handles all three event types.

libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (4)

124-147: LGTM on step-based buffer management.

The switch to step-specific buffers (verifyPasscode, createPasscode, confirmPasscode) prevents cross-contamination between flow steps. The when expression correctly selects the appropriate buffer based on activeStep.


208-224: LGTM on intention-based flow routing.

The processCompletedPasscode function correctly handles all combinations of intention and step. The branching logic is well-documented and covers:

  • CREATE_PASSCODE: Create → Confirm → Save
  • LOGIN_WITH_PASSCODE: Verify only
  • CHANGE_PASSCODE: Verify → Create → Confirm → Save

244-257: Verify buffer cleared only on success path.

On line 252, verifyPasscode.clear() is called only when validation succeeds. On the rejection path (line 254), the buffer is not cleared here—it's handled later when resetState() is called via the UI flow (dialog dismiss → restart). This is correct but relies on the UI calling restart.

Consider whether clearing the buffer immediately on rejection would be more defensive:

         } else {
+            verifyPasscode.clear()
             sendEvent(PasscodeEvent.PasscodeRejected)
         }

334-343: LGTM on state model updates.

The PasscodeState data class is well-structured with:

  • intention for flow-type tracking
  • isChangePasscodeSuccessful for dialog visibility
  • Default activeStep = Step.Verify aligns with the most common entry point

The KDoc documentation clearly explains each property's purpose.

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