Skip to content

feat(passcode): Implement change passcode flow#1949

Closed
sahilshivekar wants to merge 1 commit intoopenMF:developmentfrom
sahilshivekar:feature/mw-324-change-passcode
Closed

feat(passcode): Implement change passcode flow#1949
sahilshivekar wants to merge 1 commit intoopenMF:developmentfrom
sahilshivekar:feature/mw-324-change-passcode

Conversation

@sahilshivekar
Copy link
Copy Markdown
Contributor

@sahilshivekar sahilshivekar commented Dec 29, 2025

Issue Fix

Jira Task: 324

Screen recordings

Before

the passcode change flow does not proceed

video_1280.mp4

After

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
mw-324-change-passcode-from-settings.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

mw-324-create-passcode-from-settings.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

    • Passcode flow supports Login, Create and Change intents; users can skip initial setup and routing respects that.
    • Settings shows "Create Passcode" or "Change Passcode" based on state.
    • Added confirmation dialog and new passcode-related strings.
  • Refactor

    • Passcode flow unified into an intent-driven single completion flow; UI components updated to intent-based behavior.
  • Chores

    • Integrated passcode library as a project dependency.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 29, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Navigation and passcode flows were refactored to use an Intention-driven passcode route and arg, consolidating passcode callbacks into a single completion callback, adding a Verify/Create/Confirm step model, tracking skipped-setup state, and threading a root NavHostController through app navigation.

Changes

Cohort / File(s) Summary
Navigation routing & host
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt, cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt, cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt, cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
Replaced static PASSCODE_GRAPH with templated PASSCODE_GRAPH_ROUTE and added passcodeGraphRoute(intention). Passcode nav now carries an INTENTION navArg; MifosNavHost and MifosApp now accept/rootNavController to navigate to passcode routes.
App startup & ViewModel
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt, cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt
MainUiState.Success extended with hasPasscode and hasSkippedPasscodeSetup; startup routing computes start destination using these flags and routes to LOGIN_GRAPH, passcodeGraphRoute(Intention...), or MAIN_GRAPH accordingly.
Login flow
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.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
Removed navigateToPasscodeScreen wiring and NavigateToPasscodeScreen event from login flow; login no longer directly initiates passcode navigation.
Passcode library — navigation & API
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt, libs/mifos-passcode/src/commonMain/kotlin/proto/.../PasscodeManager.kt
Added Intention enum and INTENTION/PASSCODE_ROUTE constants; nav graph now declares INTENTION arg with default; NavController.navigateToPasscodeScreen accepts an Intention; passcode route signature simplified to onPasscodeFlowComplete; PasscodeManager exposes hasSkippedPasscodeSetup and setter.
Passcode UI & components
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt, .../component/PasscodeButton.kt, .../component/PasscodeHeader.kt, .../component/PasscodeStepIndicator.kt, .../component/ChangePasscodeSuccessDialog.kt
Consolidated passcode callbacks into onPasscodeFlowComplete; components switched from Boolean-based gating to Intention-based gating; added ChangePasscodeSuccessDialog; step indicator hidden for Verify; previews added/updated.
Passcode viewmodel & step model
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt, .../utility/Step.kt
Introduced Verify step (Verify, Create, Confirm), per-step input buffers, intention initialization from nav args, new actions/events (SkipPasscodeSetup, DismissChangePasscodeSuccessDialog, PasscodeSetupSkipped), and multi-branch processing for flows.
Passcode persistence & DI
libs/mifos-passcode/src/commonMain/kotlin/proto/.../PasscodePreferencesDataSource.kt, .../PasscodeManager.kt, .../model/PasscodePreferencesProto.kt, .../di/PreferenceModule.kt
Added hasSkippedPasscodeSetup flow and update method; PasscodePreferencesProto gained hasSkippedPasscodeSetup; DI registrations for Settings, DataSource, Manager changed from factory to single.
Settings feature & resources
feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt, feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt, feature/settings/build.gradle.kts, feature/settings/src/commonMain/composeResources/values/strings.xml, libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml
SettingsViewModel injected with PasscodeManager and exposes hasPasscode StateFlow; SettingsScreen consumes hasPasscode to toggle Create/Change label; added dependency on mifosPasscode and new string resources in settings and passcode lib.
Misc — dependency tree
cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt
Added project dependency :libs:mifos-passcode to runtime classpath.

Sequence Diagram(s)

sequenceDiagram
    participant App as MifosPayApp
    participant VM as MifosPayViewModel
    participant PM as PasscodeManager
    participant Root as RootNavController
    participant PassNav as PasscodeNavGraph
    participant PVM as PasscodeViewModel
    participant Screen as PasscodeScreen

    App->>VM: request uiState
    VM->>PM: read hasPasscode & hasSkippedPasscodeSetup
    PM-->>VM: return flags
    VM-->>App: MainUiState.Success(userData, hasPasscode, hasSkippedPasscodeSetup)

    alt not authenticated
        App->>Root: navigate to LOGIN_GRAPH
    else hasPasscode
        App->>Root: navigate to passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE)
    else skipped setup
        App->>Root: navigate to MAIN_GRAPH
    else create flow
        App->>Root: navigate to passcodeGraphRoute(Intention.CREATE_PASSCODE)
    end

    Root->>PassNav: start passcode graph (INTENTION arg)
    PassNav->>PVM: initialize with intention
    PVM->>Screen: render passcode UI
    Screen->>PVM: user input (Verify/Create/Confirm)
    PVM->>PM: validate/save as needed
    PVM-->>Screen: emit completion
    Screen->>PassNav: onPasscodeFlowComplete()
    PassNav->>Root: navigateToMainGraph()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • therajanmaurya

Poem

🐇 I hopped through routes and tiny keys,

Verify, Create, Confirm with ease.
Intentions set, the paths align,
A passcode dance—secure and fine.
Thump-thump—finished! Onward to the main.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.17% 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 title 'feat(passcode): Implement change passcode flow' is a clear, specific, and accurate summary of the main changes in the PR, which focus on implementing the change passcode functionality and related navigation flows.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

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

27-35: Fix reactivity issue: passcode state changes won't trigger UI updates.

Reading passcodeManager.hasPasscode.value and hasSkippedPasscodeSetup.value inside the map block captures only the current snapshot values. When passcode state changes (e.g., user creates/changes/clears passcode), uiState won't update unless userInfo also changes, causing the UI and navigation logic to use stale passcode data.

🔎 Proposed fix using combine to merge all flows
-    val uiState: StateFlow<MainUiState> = userDataRepository.userInfo.map {
-        val hasPasscode = passcodeManager.hasPasscode.value
-        val hasSkippedPasscodeSetup = passcodeManager.hasSkippedPasscodeSetup.value
-        MainUiState.Success(it, hasPasscode, hasSkippedPasscodeSetup)
-    }.stateIn(
+    val uiState: StateFlow<MainUiState> = combine(
+        userDataRepository.userInfo,
+        passcodeManager.hasPasscode,
+        passcodeManager.hasSkippedPasscodeSetup,
+    ) { userInfo, hasPasscode, hasSkippedPasscodeSetup ->
+        MainUiState.Success(userInfo, hasPasscode, hasSkippedPasscodeSetup)
+    }.stateIn(
         scope = viewModelScope,
         initialValue = MainUiState.Loading,
         started = SharingStarted.WhileSubscribed(5_000),
     )

You'll also need to add the import:

import kotlinx.coroutines.flow.combine
libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt (1)

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

After removing the key from settings, the passcodeSettings MutableStateFlow still holds the old value. This could cause inconsistent state if clearInfo() is called and then the flows are observed before a new value is written.

🔎 Proposed fix
     suspend fun clearInfo() {
         withContext(dispatcher) {
             settings.remove(PASSCODE_INFO_KEY)
+            passcodeSettings.value = PasscodePreferencesProto.DEFAULT
         }
     }
🧹 Nitpick comments (9)
feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt (2)

142-151: Dynamic passcode action label works correctly.

The conditional logic cleanly toggles between "Change Passcode" and "Create Passcode" based on state. Note that the action SettingsAction.ChangePasscode is used in both cases—consider whether this action name still accurately describes the intent when the user has no passcode (it triggers creation, not change).

Optional: Rename action for semantic clarity

If desired, rename the action to something more generic like ManagePasscode to better reflect that it handles both creation and change scenarios:

- onAction(SettingsAction.ChangePasscode)
+ onAction(SettingsAction.ManagePasscode)

This is purely a naming consideration—the current implementation works correctly.


246-252: Consider adding a preview for the hasPasscode = true state.

The preview only covers the hasPasscode = false case. Adding another preview would help visualize both UI states during development.

Add preview for hasPasscode = true
@Preview
@Composable
private fun SettingsScreenWithPasscodePreview() {
    SettingsScreenContent(
        onAction = {},
        hasPasscode = true,
    )
}
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (2)

19-19: Potentially unused import.

The viewModel import from androidx.lifecycle.viewmodel.compose appears unused—the code uses koinViewModel on line 49. If not needed elsewhere, consider removing it.

-import androidx.lifecycle.viewmodel.compose.viewModel

83-109: Excellent documentation of the navigation decision tree.

The KDoc comment clearly explains each branch of the navigation logic, making the code self-documenting and easier to maintain.

However, the repeated casting of uiState to Success on lines 97-100 is verbose. Consider using Kotlin's smart cast with a when subject:

Refactor to avoid repeated casts
     val navDestination = when {
-        uiState is Success -> {
-            val userData = (uiState as Success).userData
-            val hasPasscode = (uiState as Success).hasPasscode
-            val hasSkippedPasscodeSetup = (uiState as Success).hasSkippedPasscodeSetup
+        uiState is Success -> with(uiState) {
             when {
-                !userData.authenticated -> LOGIN_GRAPH
+                !this.userData.authenticated -> LOGIN_GRAPH
                 hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE)
                 hasSkippedPasscodeSetup -> MAIN_GRAPH
                 else -> passcodeGraphRoute(Intention.CREATE_PASSCODE)
             }
         }
         else -> LOGIN_GRAPH
     }

Or extract to a local variable after the is Success check since smart cast applies.

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

240-243: Hardcoded string should use a string resource for i18n.

The text "Don't have an account yet? " is hardcoded. For consistency with the rest of the file (which uses stringResource), consider moving this to a string resource.

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

29-47: Hardcoded colors break dark theme support.

Using Color.White and Color.Black directly bypasses Material theming. This will cause poor contrast or visibility issues in dark mode.

🔎 Proposed fix using theme colors
         AlertDialog(
             shape = MaterialTheme.shapes.large,
-            containerColor = Color.White,
+            containerColor = MaterialTheme.colorScheme.surface,
             title = {
                 Text(
                     text = stringResource(Res.string.library_mifos_passcode_passcode_changed_message),
-                    color = Color.Black,
+                    color = MaterialTheme.colorScheme.onSurface,
                 )
             },
             confirmButton = {
                 TextButton(onClick = onDismiss) {
                     Text(
                         text = stringResource(Res.string.library_mifos_passcode_done),
-                        color = Color.Black,
+                        color = MaterialTheme.colorScheme.primary,
                     )
                 }
             },
             onDismissRequest = onDismiss,
         )
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt (1)

88-89: The modifier parameter is not applied to the Box.

The function accepts a modifier parameter but uses Modifier.fillMaxWidth() directly instead. This prevents callers from customizing the header's layout.

🔎 Proposed fix
     Box(
-        modifier = Modifier.fillMaxWidth(),
+        modifier = modifier.fillMaxWidth(),
         contentAlignment = Alignment.Center,
     ) {
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)

118-121: Consider repositioning the dialog for semantic clarity.

The ChangePasscodeSuccessDialog is placed within the content layout between MifosIcon and the passcode input Column. While functionally correct (dialogs render as overlays), placing it at the end of the Scaffold content or outside the Column would be semantically cleaner.

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

112-117: Consider sending the event after the coroutine completes for consistency.

sendEvent(PasscodeEvent.PasscodeSetupSkipped) is called before the coroutine persisting the skip state completes. If navigation happens immediately, there's a small window where the preference might not be saved.

🔎 Proposed fix: emit event inside the coroutine
 private fun onSkipPasscodeSetup() {
     viewModelScope.launch {
         passcodeRepository.setSkippedPasscodeSetup(true)
+        sendEvent(PasscodeEvent.PasscodeSetupSkipped)
     }
-    sendEvent(PasscodeEvent.PasscodeSetupSkipped)
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 Files selected for processing (28)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/RootNavGraph.kt
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt
  • feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/navigation/LoginScreenNavigation.kt
  • feature/settings/build.gradle.kts
  • feature/settings/src/commonMain/composeResources/values/strings.xml
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt
  • feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt
  • libs/mifos-passcode/src/commonMain/composeResources/values/strings.xml
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt
  • libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt
  • libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt
💤 Files with no reviewable changes (1)
  • cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/LoginNavGraph.kt
🧰 Additional context used
🧬 Code graph analysis (4)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt (1)
  • passcodeGraphRoute (31-33)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt (1)
  • ChangePasscodeSuccessDialog (23-49)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt (1)
  • sendEvent (97-99)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (1)
  • passcodeRoute (47-67)
🔇 Additional comments (33)
feature/settings/build.gradle.kts (1)

26-26: Dependency addition is appropriate.

Adding mifosPasscode enables the Settings feature to access passcode state (hasPasscode) and navigation utilities, which aligns with the dynamic "Create Passcode" / "Change Passcode" UI requirement.

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

139-149: Good documentation of the architectural change.

The comment clearly explains why NavigateToPasscodeScreen was removed and where that responsibility now lives. This decoupling allows app-level navigation to make routing decisions based on both authentication state and passcode status, which is cleaner than having the login screen handle passcode navigation directly.

feature/settings/src/commonMain/composeResources/values/strings.xml (1)

18-18: String resource follows conventions.

The new feature_settings_create_passcode string is appropriately named and placed alongside the existing feature_settings_change_passcode, supporting the dynamic UI that toggles between these based on passcode state.

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

21-24: String resources support the change passcode flow.

The new strings appropriately support the success dialog and the "Create new Passcode" step in the change flow. They follow the library_mifos_passcode_ naming convention used by other recent strings in this file.

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

67-68: Proper lifecycle-aware state observation.

Using collectAsStateWithLifecycle() for the passcode state ensures the UI updates reactively when the passcode status changes, which is especially important when returning from the passcode creation flow.

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

32-42: Intention enum is well-designed with clear documentation.

The KDoc clearly explains each intention's purpose and flow. The fromValue helper with a default fallback is practical for handling unexpected route arguments.

One consideration: the default fallback to CREATE_PASSCODE on unknown values means an attacker or bug causing an invalid intention would route users to passcode creation. Verify this is the safest default—LOGIN_WITH_PASSCODE might be more conservative since it requires verification of an existing passcode.


47-67: Route definition correctly uses optional query parameter.

The route pattern $PASSCODE_SCREEN?$INTENTION={$INTENTION} with a default value ensures backward compatibility. The single onPasscodeFlowComplete callback simplifies the API compared to separate success/failure callbacks—this works because the ViewModel now handles rejection internally with retry logic.


69-72: Navigation helper correctly constructs the parameterized route.

The function properly encodes the intention value into the route string. The optional NavOptions parameter provides flexibility for callers.

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

94-94: Correct use of separate NavController for passcode navigation.

Passing rootNavController as a separate parameter enables navigation to the passcode screen which exists outside the main navigation graph. This is the right architectural approach.


187-198: Correct the inverted logic in the comment.

The comment states the ViewModel changes intention "if there is already a passcode set," but the actual code (PasscodeViewModel.kt, line 70) checks !state.hasPasscode, meaning it activates when NO passcode exists. Additionally, the ViewModel changes the activeStep, not the intention itself. Update the comment to reflect: the ViewModel starts at the Create step directly (skipping Verify) when the user navigates to "Change Passcode" but never set up a passcode initially.

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

21-28: LGTM! Navigation refactoring is well-documented.

The removal of onNavigateToPasscodeScreen and the explanatory comment clearly communicate that passcode navigation is now handled at the app level, aligning with the PR's intention-based navigation approach.

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

29-58: LGTM! Intent-based rendering improves clarity.

The shift from boolean-based to intention-based gating makes the button visibility logic more explicit and maintainable. The documentation clearly explains when the skip button appears.


60-89: LGTM! Consistent with skip button refactoring.

The forgot button correctly appears only during login, with clear documentation explaining the behavior.

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

44-53: LGTM! Reactive passcode state exposure.

Exposing hasPasscode as a StateFlow enables the Settings UI to reactively display "Create Passcode" or "Change Passcode" based on the current passcode state. The comprehensive documentation clearly explains the purpose.

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

29-72: LGTM! Step indicator logic correctly implements single-step highlighting.

The changes appropriately:

  • Hide the indicator during the Verify step (when no creation progress needs to be shown)
  • Highlight only the current step in the 2-step creation flow (Create → Confirm)
  • Provide clear documentation explaining the behavior

The active step calculation step + 1 == activeStep.index correctly highlights step 0 during Create(1) and step 1 during Confirm(2).


74-78: LGTM! Preview function aids development.

libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt (1)

14-31: LGTM! Skip tracking prevents repeated prompts.

The hasSkippedPasscodeSetup field appropriately distinguishes between users who never encountered passcode setup and those who explicitly declined, preventing unwanted re-prompting. The documentation clearly explains this distinction.

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

69-74: LGTM! Root controller enables app-level navigation.

Adding the rootNavController parameter allows MifosApp to coordinate navigation at the application level, which is essential for the intention-based passcode routing introduced in this PR.

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

12-19: LGTM! Three-step flow supports verification.

Adding the Verify step appropriately supports both login verification and the "verify old passcode" step in the change flow. The index reordering is correct.

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

57-70: LGTM on the navigation refactor!

The docblock clearly explains the architectural decision to separate passcode navigation from the login flow. This is a good separation of concerns that prevents duplicate navigation calls.

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

40-46: LGTM!

Passing rootNavController to MifosApp enables proper navigation for the change passcode flow from within the main graph back to the passcode graph.

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

43-61: LGTM on the skip-passcode tracking!

The implementation correctly:

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

This ensures consistent state management across the passcode flows.

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

46-66: LGTM on the skip-passcode tracking implementation!

The hasSkippedPasscodeSetup flow and updateSkippedPasscodeSetup function are correctly implemented with proper dispatcher context switching for thread safety.

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

19-33: LGTM on the intention-based routing!

The implementation correctly:

  1. Uses query parameter syntax for the intention argument
  2. Provides a clean helper function to build routes with specific intentions
  3. Includes clear documentation explaining the purpose
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt (1)

92-127: LGTM on the intention-based header logic!

The when expression cleanly handles all three steps with appropriate text based on the intention. The distinction between "Create Passcode" (initial) and "Create New Passcode" (change flow) provides clear UX differentiation.

libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt (1)

30-33: LGTM on the singleton scope change!

Switching from factory to single is the correct fix. With factory, each injection site would get a new instance with its own StateFlows, breaking reactivity across the app. The IO dispatcher is appropriate for preferences I/O operations.

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (2)

22-37: LGTM! Clear documentation and correct route construction.

The documentation block clearly explains the three intention modes, and the start destination with the intention placeholder correctly inherits the argument from the graph route when navigating to the passcode graph.


39-52: LGTM! Clean callback consolidation.

The unified onPasscodeFlowComplete callback simplifies the API surface. The navigation logic for onForgotButton (to login) and onSkipButton/onPasscodeFlowComplete (to main graph) correctly reflects the intended user flows.

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

79-93: LGTM! Clean event-to-callback mapping.

The EventsEffect correctly maps the three event types to appropriate callbacks. PasscodeSetupSkipped triggering onSkipButton() maintains consistency with the skip flow.


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

Delegating the skip action to the ViewModel via PasscodeAction.SkipPasscodeSetup allows proper persistence of the skip state before navigation occurs.

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

119-142: LGTM! Clean step-based buffer selection.

The refactored enterKey correctly selects the appropriate buffer based on activeStep and triggers processing when the passcode is complete.


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

The processCompletedPasscode function clearly handles all intention/step combinations with appropriate transitions. The documentation accurately describes the flow logic.


330-339: LGTM! State class is well-documented.

The PasscodeState data class with the new intention and isChangePasscodeSuccessful properties supports all flow variations cleanly.

Comment thread cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt
@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from 1c37870 to 468aa86 Compare December 29, 2025 08:44
Copy link
Copy Markdown
Contributor

@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
Copy Markdown
Contributor

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

Comment thread cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt Outdated
Copy link
Copy Markdown
Contributor

@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
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor

@biplab1 biplab1 left a comment

Choose a reason for hiding this comment

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

Some preliminary review comments have been added. Let me know after you done the changes, I will review again.

Comment thread cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavGraph.kt Outdated
Comment thread cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt Outdated
Comment thread cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt Outdated
@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from 5285924 to b925603 Compare January 17, 2026 15:51
Copy link
Copy Markdown
Contributor

@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)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)

293-308: On passcode mismatch at Confirm step, user remains stuck unable to proceed.

When createPasscode and confirmPasscode don't match, resetState() clears both buffers but leaves activeStep at Confirm. Since createPasscode is now empty, any subsequent passcode entry at Confirm cannot match it, blocking the user from completing the flow.

The user must return to Create step to re-enter a new passcode that can be confirmed.

🐛 Proposed fix
     private suspend fun validateAndSavePasscode() {
         if (createPasscode.toString() == confirmPasscode.toString()) {
             passcodeRepository.savePasscode(confirmPasscode.toString())

             if (state.intention == Intention.CHANGE_PASSCODE) {
                 mutableStateFlow.update { it.copy(isChangePasscodeSuccessful = true) }
             } else {
                 sendEvent(PasscodeEvent.PasscodeConfirmed)
             }

             resetState()
         } else {
             sendEvent(PasscodeEvent.PasscodeRejected)
-            resetState()
+            resetStateToCreateStep()
         }
     }

+    private fun resetStateToCreateStep() {
+        mutableStateFlow.update {
+            it.copy(
+                activeStep = Step.Create,
+                currentPasscodeInput = "",
+                filledDots = 0,
+            )
+        }
+        verifyPasscode.clear()
+        createPasscode.clear()
+        confirmPasscode.clear()
+    }
🤖 Fix all issues with AI agents
In
`@libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt`:
- Around line 55-65: The read-modify-write in updateSkippedPasscodeSetup races
with updatePasscodeSettings because both run on MifosDispatchers.IO; introduce a
shared Mutex (e.g., private val passcodeMutex = Mutex()) and wrap the critical
sections of both updateSkippedPasscodeSetup and updatePasscodeSettings with
passcodeMutex.withLock { ... } so the sequence (read passcodeSettings.value,
settings.putPasscodePreference(updated), passcodeSettings.value = updated) is
atomic; ensure the same Mutex guards calls that modify passcodeSettings and
settings.putPasscodePreference to prevent lost updates.
♻️ Duplicate comments (1)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)

82-103: Avoid state‑driven startDestination; navigate via LaunchedEffect instead.

Deriving startDestination from uiState can recreate the NavHost and reset the back stack when state changes (e.g., loading → success). Use a fixed start destination and perform state-driven navigation in a LaunchedEffect with popUpTo.

✅ Suggested pattern
-    val navDestination = when {
+    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(navDestination) {
+        if (navController.currentBackStackEntry?.destination?.route != navDestination) {
+            navController.navigate(navDestination) {
+                popUpTo(navController.graph.id) { inclusive = true }
+                launchSingleTop = true
+            }
+        }
+    }
@@
-            startDestination = navDestination,
+            startDestination = LOGIN_GRAPH,

Please verify with a cold start + state transition that the back stack remains stable.

🧹 Nitpick comments (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)

34-36: Consider moving inline comments to KDoc or removing them.

Based on learnings from past review comments, the project prefers avoiding inline comments. The explanation about the verifyPasscode buffer purpose could be moved to a KDoc on the class or removed if self-explanatory.

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from b925603 to e432d33 Compare January 17, 2026 15:53
Copy link
Copy Markdown
Contributor

@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: 6

Caution

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

⚠️ Outside diff range comments (2)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)

293-320: Mismatch path can trap users on the Confirm step

After rejection, Line 305 calls resetState() which clears createPasscode/confirmPasscode (Lines 318–319) but leaves activeStep at Confirm, so the next attempt can never match. Reset to Create (or Verify for CHANGE_PASSCODE if you want re‑verification) before allowing retry.

🐛 Suggested fix
@@
         } else {
             sendEvent(PasscodeEvent.PasscodeRejected)
-            resetState()
+            resetState()
+            moveToCreateStep()
         }
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (1)

22-41: startDestination base route mismatch with navigation route

The startDestination uses "$PASSCODE_SCREEN?..." (resolves to "passcode_screen?...") while the navigation route uses MifosNavGraph.PASSCODE_GRAPH_ROUTE (resolves to "passcode_graph?..."). In Compose Navigation, the startDestination must reference a valid destination within the graph. The base routes must match.

Fix the mismatch by using "$PASSCODE_GRAPH_BASE?$INTENTION={$INTENTION}" for the startDestination to align with the route definition.

🤖 Fix all issues with AI agents
In `@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt`:
- Around line 27-31: uiState currently maps only userDataRepository.userInfo and
samples passcodeManager.hasPasscode.value and hasSkippedPasscodeSetup.value, so
passcode changes won't trigger emissions; replace the mapping with a combined
flow that observes userDataRepository.userInfo, passcodeManager.hasPasscode, and
passcodeManager.hasSkippedPasscodeSetup (e.g., use combine(userInfo,
hasPasscode, hasSkipped) and map to MainUiState.Success(...)) and then call
stateIn(...) so uiState updates whenever any of those flows change.

In
`@feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt`:
- Around line 57-61: KDoc in LoginScreen.kt contains a duplicated word "the the"
in the comment block above the function/class; update the comment text to remove
the duplicate so it reads "As the app-level navigation logic (MifosPayApp.kt) is
already doing it..." (edit the KDoc near the LoginScreen declaration or the top
comment block) ensuring the rest of the wording remains unchanged.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt`:
- Around line 38-41: The fromValue companion method currently returns
CREATE_PASSCODE when the provided value is invalid, which can bypass
verification; update PasscodeNavigation.Companion.fromValue to default to the
safer LOGIN_WITH_PASSCODE (or throw) instead of CREATE_PASSCODE by changing the
fallback from CREATE_PASSCODE to LOGIN_WITH_PASSCODE (or replacing the fallback
with an explicit error/exception) so Intention.entries.firstOrNull { it.value ==
value } uses LOGIN_WITH_PASSCODE as the safe default.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt`:
- Around line 118-121: The success dialog dismissal currently only sends
PasscodeAction.DismissChangePasscodeSuccessDialog and leaves the user on the
passcode screen; update the dialog's onDismiss to both dismiss and complete the
flow by calling viewModel.onPasscodeFlowComplete() (i.e.,
ChangeChangePasscodeSuccessDialog onDismiss should call
viewModel.trySendAction(PasscodeAction.DismissChangePasscodeSuccessDialog) and
then viewModel.onPasscodeFlowComplete()), or alternatively update the
ViewModel's handler for PasscodeAction.DismissChangePasscodeSuccessDialog to
invoke onPasscodeFlowComplete() so navigation back to settings occurs; look for
ChangePasscodeSuccessDialog, state.isChangePasscodeSuccessful,
PasscodeAction.DismissChangePasscodeSuccessDialog, and
viewModel.onPasscodeFlowComplete to implement the fix.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt`:
- Around line 12-18: The Step enum's index property is declared mutable but
never changed; change the property declaration in enum class Step from var
index: Int to val index: Int to make it immutable, updating the enum definition
(Step.Verify, Step.Create, Step.Confirm) accordingly; verify usages such as in
PasscodeStepIndicator.kt (where activeStep.index is read) still compile and
adjust any tests or callers if they attempted to assign to Step.index (they
shouldn't).

In
`@libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt`:
- Around line 19-34: The KDoc incorrectly states dispatcher usage: update the
comment or the wiring so they match; either change the KDoc to explain that
PasscodePreferencesDataSource uses MifosDispatchers.IO while PasscodeManager is
intentionally constructed with MifosDispatchers.Unconfined, or change the DI
binding in PasscodePreferenceModule so PasscodeManager receives
get(named(MifosDispatchers.IO.name)) instead of Unconfined; reference the
PasscodePreferenceModule, PasscodeManager, PasscodePreferencesDataSource, and
MifosDispatchers.IO/Unconfined symbols when making the change.
♻️ Duplicate comments (3)
libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt (1)

48-64: Concurrent updates can lose state (race).
updatePasscodeSettings and updateSkippedPasscodeSetup can interleave, overwriting each other’s writes. Guard these mutations with a shared lock or consolidate into a single atomic update path.

🔒 Suggested fix with a shared Mutex
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
@@
 class PasscodePreferencesDataSource(
     private val settings: Settings,
     private val dispatcher: CoroutineDispatcher,
 ) {
+    private val updateMutex = Mutex()
@@
     suspend fun updatePasscodeSettings(passcodePreferences: PasscodePreferencesProto) {
         withContext(dispatcher) {
-            settings.putPasscodePreference(passcodePreferences)
-            passcodeSettings.value = passcodePreferences
+            updateMutex.withLock {
+                settings.putPasscodePreference(passcodePreferences)
+                passcodeSettings.value = passcodePreferences
+            }
         }
     }
@@
     suspend fun updateSkippedPasscodeSetup(skipped: Boolean) {
         withContext(dispatcher) {
-            val updated = passcodeSettings.value.copy(hasSkippedPasscodeSetup = skipped)
-            settings.putPasscodePreference(updated)
-            passcodeSettings.value = updated
+            updateMutex.withLock {
+                val updated = passcodeSettings.value.copy(hasSkippedPasscodeSetup = skipped)
+                settings.putPasscodePreference(updated)
+                passcodeSettings.value = updated
+            }
         }
     }
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)

82-103: Avoid dynamic startDestination to prevent NavHost resets.

startDestination is derived from state at Line 82 and passed into RootNavGraph at Line 102. This can recreate the NavHost graph on recomposition, leading to back‑stack anomalies. Prefer a fixed startDestination (or a router/splash) and navigate in a LaunchedEffect when uiState changes (similar to the unauthorized handling).

✅ Suggested adjustment (fixed start + state-driven navigate)
-    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) {
+        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
+        }
+        navController.navigate(navDestination) {
+            popUpTo(navController.graph.id) { inclusive = true }
+            launchSingleTop = true
+        }
+    }
...
-            startDestination = navDestination,
+            startDestination = LOGIN_GRAPH,
Compose Navigation NavHost startDestination dynamic changes best practices
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt (1)

88-91: The modifier parameter is still ignored.

This issue was previously flagged and discussed. The modifier parameter is declared but not used—Line 89 uses Modifier.fillMaxWidth() instead of chaining with the incoming modifier. This breaks the composable's customization contract.

🔧 Proposed fix
     Box(
-        modifier = Modifier.fillMaxWidth(),
+        modifier = modifier.fillMaxWidth(),
         contentAlignment = Alignment.Center,
     ) {

Comment thread feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt Outdated
Copy link
Copy Markdown
Contributor

@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: 4

🤖 Fix all issues with AI agents
In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt`:
- Around line 64-71: There is a leftover merge conflict in
PasscodeStepIndicator.kt around the .background call; remove the conflict
markers (<<<<<<<, =======, >>>>>>>) and pick one shape source (either
KptTheme.shapes.medium or MaterialTheme.shapes.medium) so the shape argument is
unambiguous; if you choose MaterialTheme, add the appropriate import for
androidx.compose.material.MaterialTheme, and if you choose KptTheme ensure its
import remains and any unused import is removed.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt`:
- Around line 21-34: Remove the leftover Git conflict markers surrounding the
KDoc in PasscodeNavigation (the lines containing <<<<<<<, =======, >>>>>>>) and
restore the intended KDoc comment for the enum; ensure the KDoc immediately
precedes the PasscodeNavigation enum declaration and that there are no stray
markers or duplicated blocks so the file compiles cleanly.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt`:
- Around line 34-38: The file PasscodeViewModel.kt contains unresolved Git merge
conflict markers (<<<<<<< HEAD, =======, >>>>>>>) scattered across the file
(e.g., near the comment about a separate StringBuilder for passcode verification
and other blocks); remove all conflict markers and reconcile the two conflicting
variants so the intended Kotlin code/comments remain valid (for example keep the
explanatory comment about the verification StringBuilder), ensuring methods and
properties in the PasscodeViewModel class compile cleanly; search for and delete
all <<<<<<<, =======, and >>>>>>> markers, resolve each block by merging or
choosing the correct lines, and run a build to confirm no remaining conflict
artifacts.

In
`@libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt`:
- Around line 55-61: Remove the leftover git merge conflict markers (<<<<<<<,
=======, >>>>>>>) in PasscodePreferencesDataSource.kt and restore the intended
comment/block around the hasSkippedPasscodeSetup update; specifically edit the
PasscodePreferencesDataSource file to delete the conflict markers and keep the
documented comment starting with "Updates only the hasSkippedPasscodeSetup
flag." so the method that updates hasSkippedPasscodeSetup (the function handling
the "Skip" action) compiles normally and retains the explanatory KDoc.
♻️ Duplicate comments (2)
libs/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt (1)

62-67: Guard the read‑modify‑write with a mutex to avoid lost updates.
passcodeSettings.value.copy(...) can race with other updates on a multi-threaded dispatcher.

🔒 Suggested fix (shared mutex for both updates)
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 ...
 class PasscodePreferencesDataSource(
     private val settings: Settings,
     private val dispatcher: CoroutineDispatcher,
 ) {
+    private val passcodeMutex = Mutex()
     private val passcodeSettings = MutableStateFlow(
         ...
     )
 ...
     suspend fun updatePasscodeSettings(passcodePreferences: PasscodePreferencesProto) {
         withContext(dispatcher) {
-            settings.putPasscodePreference(passcodePreferences)
-            passcodeSettings.value = passcodePreferences
+            passcodeMutex.withLock {
+                settings.putPasscodePreference(passcodePreferences)
+                passcodeSettings.value = passcodePreferences
+            }
         }
     }
 ...
     suspend fun updateSkippedPasscodeSetup(skipped: Boolean) {
         withContext(dispatcher) {
-            val updated = passcodeSettings.value.copy(hasSkippedPasscodeSetup = skipped)
-            settings.putPasscodePreference(updated)
-            passcodeSettings.value = updated
+            passcodeMutex.withLock {
+                val updated = passcodeSettings.value.copy(hasSkippedPasscodeSetup = skipped)
+                settings.putPasscodePreference(updated)
+                passcodeSettings.value = updated
+            }
         }
     }
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (1)

41-44: Safer fallback for invalid intention still needed.

Line 42 still defaults to CREATE_PASSCODE when the value is invalid, which can bypass verification.

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

78-82: Limit preview composable visibility.

PasscodeStepIndicatorPreview is public in a commonMain library; consider making it internal (or private) so it doesn’t leak into the public API.

♻️ Proposed tweak
 `@Preview`
 `@Composable`
-fun PasscodeStepIndicatorPreview() {
+internal fun PasscodeStepIndicatorPreview() {
     PasscodeStepIndicator(activeStep = Step.Confirm)
 }

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch 4 times, most recently from 6fe0d5e to 398ffe7 Compare January 17, 2026 16:36
@sahilshivekar sahilshivekar requested a review from biplab1 January 17, 2026 17:22
@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from 398ffe7 to 8717dea Compare February 6, 2026 14:18
Copy link
Copy Markdown
Contributor

@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 (2)
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt (1)

22-41: ⚠️ Potential issue | 🟠 Major

Remove popBackStack() before navigateToLogin() and add proper back stack management, or ensure navigateToLogin() includes popUpTo.

When the passcode graph is the root (user's first navigation destination on app startup with hasPasscode = true), popBackStack() cannot remove the root destination and returns false. This leaves the passcode graph in the back stack.

The issue is specifically in onForgotButton (line 28–29): after popBackStack() fails, navigateToLogin() executes without back stack cleanup. Users can then press back from the login screen and return to the passcode screen—they're trapped in a loop instead of progressing to authentication.

The onSkipButton and onPasscodeFlowComplete callbacks avoid this because navigateToMainGraph() uses popUpTo(graph.findStartDestination().id) to clear the stack, making the popBackStack() call redundant but harmless there.

Solution: Either add popUpTo to the navigateToLogin() extension function (similar to navigateToMainGraph()), or remove the redundant popBackStack() calls if navigateToLogin() is updated to handle back stack properly. Alternatively, pass navOptions with popUpTo directly in the callbacks.

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

222-237: ⚠️ Potential issue | 🔴 Critical

Bug: Passcode mismatch leaves user stuck on Confirm step with empty Create buffer.

When createPasscode != confirmPasscode, resetState() clears the three StringBuilder buffers but does not reset activeStep. The user remains on Step.Confirm with an empty createPasscode buffer. Any subsequent confirm entry will be compared against "", causing an infinite mismatch loop.

The user must be moved back to Step.Create on mismatch so they can re-enter both passcodes.

🐛 Proposed fix

Replace resetState() with moveToCreateStep() and explicit buffer clears on mismatch:

         } else {
             sendEvent(PasscodeEvent.PasscodeRejected)
-            resetState()
+            moveToCreateStep()
+            createPasscode.clear()
+            confirmPasscode.clear()
+            verifyPasscode.clear()
         }

This is preferred over modifying resetState() to reset activeStep because resetState() is also called on success paths (line 232) where changing the step would interfere with the state prior to sending the success event.

🤖 Fix all issues with AI agents
In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt`:
- Around line 239-249: When handling a passcode mismatch in
validateAndSavePasscode, explicitly reset activeStep back to the initial step
instead of changing resetState; update the mutableStateFlow in the mismatch
branch of validateAndSavePasscode to set activeStep = Step.Create (or the
appropriate initial Step enum) before/when you clear inputs and call resetState
so the UI leaves Step.Confirm and returns to the create step; do not modify
resetState itself since it's used on success paths where activeStep must remain
unchanged.
🧹 Nitpick comments (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)

165-181: processCompletedPasscode covers all valid intention+step combinations but has no fallback.

The when block has no else branch. If an unexpected combination occurs (e.g., due to a future code change introducing a new step or intention), the passcode input will silently be ignored after the user types 4 digits. Consider adding a logging else branch for debuggability.

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from 8717dea to 97eb4b9 Compare February 7, 2026 03:12
Copy link
Copy Markdown
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)

222-249: ⚠️ Potential issue | 🔴 Critical

Fix: Reset activeStep to Step.Create when passcode mismatch occurs.

When createPasscode and confirmPasscode don't match (line 233), resetState() clears the input buffers and dots but does not reset activeStep back to Step.Create. The user remains stuck on Step.Confirm with empty input. Dismissing the mismatch dialog via restart()resetState() still leaves the step unchanged, making it impossible for the user to re-enter a new passcode.

This affects both CREATE_PASSCODE and CHANGE_PASSCODE flows. Add activeStep = Step.Create to the mismatch branch:

Proposed fix
         } else {
             sendEvent(PasscodeEvent.PasscodeRejected)
+            mutableStateFlow.update { it.copy(activeStep = Step.Create) }
             resetState()
         }
🧹 Nitpick comments (3)
feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/SettingsScreen.kt (1)

141-147: Consider extracting the string resource selection for readability.

The inline ternary on Line 142 is quite long. Minor nit — could extract to a local val for readability, but not blocking.

Optional refactor
+            val passcodeTitle = if (hasPasscode) {
+                stringResource(Res.string.feature_settings_change_passcode)
+            } else {
+                stringResource(Res.string.feature_settings_create_passcode)
+            }
+
             SettingsCardItem(
-                title = if (hasPasscode) stringResource(Res.string.feature_settings_change_passcode) else stringResource(Res.string.feature_settings_create_passcode),
+                title = passcodeTitle,
                 icon = vectorResource(Res.drawable.outline_pin),
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)

82-95: Avoid repeated explicit casts; use when with a subject variable.

uiState is cast to Success three times on lines 84–86. Using Kotlin's when with a captured subject eliminates the repetition and is more idiomatic.

♻️ 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
-            when {
-                !userData.authenticated -> LOGIN_GRAPH
-                hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE)
-                hasSkippedPasscodeSetup -> MAIN_GRAPH
-                else -> passcodeGraphRoute(Intention.CREATE_PASSCODE)
-            }
+    val navDestination = when (val state = uiState) {
+        is Success -> when {
+            !state.userData.authenticated -> LOGIN_GRAPH
+            state.hasPasscode -> passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE)
+            state.hasSkippedPasscodeSetup -> MAIN_GRAPH
+            else -> passcodeGraphRoute(Intention.CREATE_PASSCODE)
         }
-        else -> LOGIN_GRAPH
-    }
+        else -> LOGIN_GRAPH
+     }
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt (1)

64-68: Preview function visibility should match the component's internal visibility.

PasscodeStepIndicator is internal, but PasscodeStepIndicatorPreview is public. In a library module this unnecessarily expands the public API surface. Consider marking it internal (or private) to keep it consistent.

♻️ Suggested fix
 `@Preview`
 `@Composable`
-fun PasscodeStepIndicatorPreview() {
+internal fun PasscodeStepIndicatorPreview() {
     PasscodeStepIndicator(activeStep = Step.Confirm)
 }

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch 2 times, most recently from 120257f to 817511b Compare February 19, 2026 10:20
@sahilshivekar sahilshivekar changed the title feat(passcode): implement change passcode flow (MW-324) feat(passcode): Implement change passcode flow Feb 19, 2026
Copy link
Copy Markdown
Contributor

@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/MifosPayApp.kt (1)

53-53: ⚠️ Potential issue | 🟡 Minor

Use val instead of var for showErrorDialog.

The MutableState object is never reassigned—only its .value is mutated. Using var is misleading and could allow accidental reassignment, which would silently break the remember contract.

Proposed fix
-    var showErrorDialog = remember { mutableStateOf<Boolean>(false) }
+    val showErrorDialog = remember { mutableStateOf(false) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt` at line
53, The variable showErrorDialog is declared with var but its MutableState
returned from remember is never reassigned—only its .value is mutated; change
the declaration from var showErrorDialog = remember {
mutableStateOf<Boolean>(false) } to use val (val showErrorDialog = remember {
mutableStateOf<Boolean>(false) }) to prevent accidental reassignment and
preserve the remember contract in MifosPayApp.
🧹 Nitpick comments (6)
feature/settings/build.gradle.kts (1)

26-26: Consider grouping project dependencies separately from external library dependencies.

projects.libs.mifosPasscode is currently interleaved between compose.* and libs.koin.* entries. Grouping all projects.* dependencies together improves readability and aligns with common Gradle convention in multi-module KMP builds.

♻️ Suggested dependency ordering
 commonMain.dependencies {
     implementation(compose.ui)
     implementation(compose.foundation)
     implementation(compose.material3)
     implementation(compose.components.resources)
     implementation(compose.components.uiToolingPreview)
-    implementation(projects.libs.mifosPasscode)
     implementation(libs.koin.compose.viewmodel)
     implementation(libs.koin.compose)
+    implementation(projects.libs.mifosPasscode)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/settings/build.gradle.kts` at line 26, The dependency
implementation(projects.libs.mifosPasscode) is interleaved with external
libraries (compose.* and libs.koin.*); move this implementation line so all
project module dependencies (projects.*) are grouped together in the
dependencies block, placing implementation(projects.libs.mifosPasscode) with
other projects entries and keeping compose.* and libs.koin.* together to improve
readability and follow Gradle conventions.
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt (1)

165-181: processCompletedPasscode: unhandled when branches silently no-op.

LOGIN_WITH_PASSCODE paired with Step.Create or Step.Confirm matches no branch, so passcode input is silently swallowed. While these states should be unreachable in the current navigation design, an explicit else guard would surface logic errors immediately during future refactors.

♻️ Proposed defensive guard
             state.intention == Intention.CHANGE_PASSCODE && state.activeStep == Step.Confirm -> validateAndSavePasscode()
+
+            else -> {
+                // This combination is not a valid state; log or assert in debug builds.
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt`
around lines 165 - 181, processCompletedPasscode currently uses a non-exhaustive
when over state.intention/state.activeStep so some combinations (e.g.,
Intention.LOGIN_WITH_PASSCODE with Step.Create or Step.Confirm) silently no-op;
add a defensive else branch to the when in processCompletedPasscode that surface
unexpected combinations by logging an error and/or throwing an
IllegalStateException (include the current state values) so future navigation
regressions fail-fast; reference the processCompletedPasscode method and the
state properties (state.intention, state.activeStep) and the enum symbols
Intention.LOGIN_WITH_PASSCODE, Step.Create, Step.Confirm when implementing the
guard.
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt (1)

29-47: Mixed theming: MaterialTheme.shapes used alongside KptTheme.colorScheme.

Line 30 uses MaterialTheme.shapes.large while all colors come from KptTheme.colorScheme. If KptTheme exposes its own shape system, use KptTheme.shapes.large for consistency and to avoid divergence if the design token changes.

♻️ Proposed fix
-            shape = MaterialTheme.shapes.large,
+            shape = KptTheme.shapes.large,

If KptTheme doesn't expose shapes, remove the unused import androidx.compose.material3.MaterialTheme.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt`
around lines 29 - 47, The AlertDialog mixes MaterialTheme.shapes.large with
KptTheme.colorScheme; update the shape usage to the KptTheme shape system by
replacing MaterialTheme.shapes.large with KptTheme.shapes.large in the
ChangePasscodeSuccessDialog AlertDialog call (or, if KptTheme does not expose
shapes, remove the unused import of androidx.compose.material3.MaterialTheme and
keep shapes omitted/defaulted) so theming is consistent across shape and color
tokens.
cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt (1)

83-86: Avoid repeated uiState as Success casts.

The success state is cast three separate times. Bind it once and destructure.

Suggested refactor
     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
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt` around
lines 83 - 86, In the Success branch of the uiState when-check, avoid repeating
the cast by binding the cast result once: create a local val (e.g., success =
uiState as Success) and then read success.userData, success.hasPasscode, and
success.hasSkippedPasscodeSetup (or destructure those properties) instead of
casting uiState three times; update the pattern in the Success branch where
uiState is referenced to use that single bound variable.
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt (1)

107-107: Use named argument for clarity.

The second positional argument to PasscodeToolbar is not labeled, reducing readability.

Suggested fix
-            PasscodeToolbar(activeStep = state.activeStep, state.hasPasscode)
+            PasscodeToolbar(activeStep = state.activeStep, hasPasscode = state.hasPasscode)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt`
at line 107, PasscodeToolbar is called with an unlabeled second positional
argument which reduces readability; change the call to pass the second parameter
using a named argument (e.g., PasscodeToolbar(activeStep = state.activeStep,
hasPasscode = state.hasPasscode)) so the meaning of state.hasPasscode is
explicit; update the invocation of PasscodeToolbar in PassCodeScreen to use the
named parameter.
libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt (1)

58-61: Reorder parameters: place required intention before optional options.

The call site uses named arguments (intention = Intention.CHANGE_PASSCODE), so reordering is safe. This aligns with Kotlin conventions where required parameters precede optional ones, improving consistency and readability.

Suggested change
-fun NavController.navigateToPasscodeScreen(options: NavOptions? = null, intention: Intention) {
+fun NavController.navigateToPasscodeScreen(intention: Intention, options: NavOptions? = null) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt`
around lines 58 - 61, The function signature
NavController.navigateToPasscodeScreen currently declares the optional
NavOptions? parameter before the required Intention parameter; reorder the
parameters so the required intention: Intention comes first and the optional
options: NavOptions? comes after (i.e., fun
NavController.navigateToPasscodeScreen(intention: Intention, options:
NavOptions? = null)), then update the function body to build the route using the
intention parameter as before and verify/adjust all call sites (they already use
named arguments like intention = Intention.CHANGE_PASSCODE) to match the new
parameter order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt`:
- Around line 36-39: The current onPasscodeFlowComplete handler always calls
navController.popBackStack() then navController.navigateToMainGraph(), which
causes a CHANGE_PASSCODE flow started from Settings to return to main home
instead of back to Settings; modify the handler (onPasscodeFlowComplete /
onChangePasscodeFlowComplete) to accept or inspect the originating flow type
(e.g., CHANGE_PASSCODE) and conditionally skip the navigateToMainGraph() call
when the flow was started from Settings (or alternatively expose an intent
parameter to the callback so the caller can decide); update the call sites so
the passcode flow provides that intent/flow enum and ensure popBackStack() is
still called but navigateToMainGraph() is only invoked for flows that should go
to the main graph.

---

Outside diff comments:
In `@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt`:
- Line 53: The variable showErrorDialog is declared with var but its
MutableState returned from remember is never reassigned—only its .value is
mutated; change the declaration from var showErrorDialog = remember {
mutableStateOf<Boolean>(false) } to use val (val showErrorDialog = remember {
mutableStateOf<Boolean>(false) }) to prevent accidental reassignment and
preserve the remember contract in MifosPayApp.

---

Duplicate comments:
In `@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt`:
- Around line 82-95: The current reactive computation of navDestination from
uiState causes NavHost graph re-creation; replace this by using a fixed
startDestination (e.g., LOGIN_GRAPH) in NavHost and remove the navDestination
variable, then drive navigation imperatively inside a LaunchedEffect keyed on
uiState: inspect uiState (Success and its fields userData, hasPasscode,
hasSkippedPasscodeSetup) and call navController.navigate(...) to the appropriate
routes (passcodeGraphRoute(Intention.LOGIN_WITH_PASSCODE),
passcodeGraphRoute(Intention.CREATE_PASSCODE), MAIN_GRAPH, or LOGIN_GRAPH) with
proper popUpTo behavior to clear the back stack as needed (same semantics as the
existing unauthorized handling). Ensure you reference and update the same
symbols used now: uiState, navController, passcodeGraphRoute, Intention,
MAIN_GRAPH, and LOGIN_GRAPH, and remove the reactive startDestination usage so
NavHost is stable.

In `@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt`:
- Around line 27-31: Replace the current mapping that reads
passcodeManager.hasPasscode.value and .hasSkippedPasscodeSetup.value inside
userDataRepository.userInfo.map with a reactive combine so uiState updates when
any source changes: use combine(userDataRepository.userInfo,
passcodeManager.hasPasscode, passcodeManager.hasSkippedPasscodeSetup) and map
the combined triple into MainUiState.Success(...) before calling stateIn; ensure
you keep the same uiState declaration and stateIn usage so all three flows
(userInfo, hasPasscode, hasSkippedPasscodeSetup) drive updates.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt`:
- Around line 27-30: The fromValue function currently falls back to
CREATE_PASSCODE when the input is invalid, which can mistakenly route users into
the create flow; change the fallback to LOGIN_WITH_PASSCODE so malformed or
unknown values default to the safer verification path by returning
LOGIN_WITH_PASSCODE instead of CREATE_PASSCODE in the companion object fromValue
implementation that uses Intention.entries.firstOrNull { it.value == value }.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt`:
- Around line 118-121: The success dialog dismissal currently only dispatches
PasscodeAction.DismissChangePasscodeSuccessDialog from
ChangePasscodeSuccessDialog which hides the dialog but never finishes the flow;
update the dismissal handler to also trigger the passcode flow completion by
invoking the existing onPasscodeFlowComplete path (either dispatch a new action
that the ViewModel maps to calling onPasscodeFlowComplete() or call the
ViewModel method directly) so that when ChangePasscodeSuccessDialog’s onDismiss
runs it both hides the dialog and completes the passcode flow (refer to
ChangePasscodeSuccessDialog, PasscodeAction.DismissChangePasscodeSuccessDialog,
viewModel.trySendAction and onPasscodeFlowComplete).

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt`:
- Around line 12-16: Change the Step enum to make the index immutable and remove
the redundant constructor parameter: update "enum class Step(var index: Int)" to
use a val or remove the index parameter entirely since the enum's ordinal
already provides the same values; then in PasscodeStepIndicator (where you
reference activeStep.index, e.g., at the use in PasscodeStepIndicator.kt)
replace activeStep.index with activeStep.ordinal and remove any
constructor/property that stored the duplicate index so the enum is simplified
and immutable.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt`:
- Around line 233-235: The mismatch branch in validateAndSavePasscode() sends
PasscodeEvent.PasscodeRejected and calls resetState() but leaves activeStep at
Step.Confirm, locking the UI; modify that branch so after sending
PasscodeEvent.PasscodeRejected you explicitly set activeStep = Step.Create
(before or after calling resetState()) so the user is returned to the
create-passcode step; keep resetState() unchanged because it’s also used on the
success path.

---

Nitpick comments:
In `@cmp-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayApp.kt`:
- Around line 83-86: In the Success branch of the uiState when-check, avoid
repeating the cast by binding the cast result once: create a local val (e.g.,
success = uiState as Success) and then read success.userData,
success.hasPasscode, and success.hasSkippedPasscodeSetup (or destructure those
properties) instead of casting uiState three times; update the pattern in the
Success branch where uiState is referenced to use that single bound variable.

In `@feature/settings/build.gradle.kts`:
- Line 26: The dependency implementation(projects.libs.mifosPasscode) is
interleaved with external libraries (compose.* and libs.koin.*); move this
implementation line so all project module dependencies (projects.*) are grouped
together in the dependencies block, placing
implementation(projects.libs.mifosPasscode) with other projects entries and
keeping compose.* and libs.koin.* together to improve readability and follow
Gradle conventions.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/ChangePasscodeSuccessDialog.kt`:
- Around line 29-47: The AlertDialog mixes MaterialTheme.shapes.large with
KptTheme.colorScheme; update the shape usage to the KptTheme shape system by
replacing MaterialTheme.shapes.large with KptTheme.shapes.large in the
ChangePasscodeSuccessDialog AlertDialog call (or, if KptTheme does not expose
shapes, remove the unused import of androidx.compose.material3.MaterialTheme and
keep shapes omitted/defaulted) so theming is consistent across shape and color
tokens.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt`:
- Around line 58-61: The function signature
NavController.navigateToPasscodeScreen currently declares the optional
NavOptions? parameter before the required Intention parameter; reorder the
parameters so the required intention: Intention comes first and the optional
options: NavOptions? comes after (i.e., fun
NavController.navigateToPasscodeScreen(intention: Intention, options:
NavOptions? = null)), then update the function body to build the route using the
intention parameter as before and verify/adjust all call sites (they already use
named arguments like intention = Intention.CHANGE_PASSCODE) to match the new
parameter order.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt`:
- Line 107: PasscodeToolbar is called with an unlabeled second positional
argument which reduces readability; change the call to pass the second parameter
using a named argument (e.g., PasscodeToolbar(activeStep = state.activeStep,
hasPasscode = state.hasPasscode)) so the meaning of state.hasPasscode is
explicit; update the invocation of PasscodeToolbar in PassCodeScreen to use the
named parameter.

In
`@libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt`:
- Around line 165-181: processCompletedPasscode currently uses a non-exhaustive
when over state.intention/state.activeStep so some combinations (e.g.,
Intention.LOGIN_WITH_PASSCODE with Step.Create or Step.Confirm) silently no-op;
add a defensive else branch to the when in processCompletedPasscode that surface
unexpected combinations by logging an error and/or throwing an
IllegalStateException (include the current state values) so future navigation
regressions fail-fast; reference the processCompletedPasscode method and the
state properties (state.intention, state.activeStep) and the enum symbols
Intention.LOGIN_WITH_PASSCODE, Step.Create, Step.Confirm when implementing the
guard.

@sahilshivekar sahilshivekar force-pushed the feature/mw-324-change-passcode branch from 817511b to 705a743 Compare February 19, 2026 11:25
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.

2 participants