Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmp-navigation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ kotlin {
implementation(projects.feature.home)
implementation(projects.feature.profile)
implementation(projects.feature.settings)
implementation(projects.feature.onboarding)

//put your multiplatform dependencies here
implementation(compose.material3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.koin.dsl.module
import org.mifos.core.data.di.DataModule
import org.mifos.core.datastore.di.DatastoreModule
import org.mifos.feature.home.di.HomeModule
import org.mifos.feature.onboarding.OnboardingModule
import org.mifos.feature.settings.SettingsModule
import template.core.base.analytics.di.analyticsModule
import template.core.base.common.di.CommonModule
Expand All @@ -43,6 +44,7 @@ object KoinModules {
includes(
HomeModule,
SettingsModule,
OnboardingModule,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import cmp.navigation.splash.splashDestination
import cmp.navigation.ui.rememberKptNavController
import cmp.navigation.utils.toObjectNavigationRoute
import org.koin.compose.viewmodel.koinViewModel
import org.mifos.feature.onboarding.OnBoardingRoute
import org.mifos.feature.onboarding.navigateToOnBoarding
import org.mifos.feature.onboarding.onboardingDestination
import template.core.base.ui.NonNullEnterTransitionProvider
import template.core.base.ui.NonNullExitTransitionProvider
import template.core.base.ui.RootTransitionProviders
Expand Down Expand Up @@ -63,15 +66,15 @@ fun RootNavScreen(
popExitTransition = { toExitTransition()(this) },
) {
splashDestination()
// onboardingDestination()
onboardingDestination()
// authNavGraph(navController)
authenticatedGraph(navController)
// userUnlockDestination()
}

val targetRoute = when (state) {
// SetLanguageRoute
RootNavState.ShowOnboarding -> ""
RootNavState.ShowOnboarding -> OnBoardingRoute
// AuthGraphRoute
RootNavState.Auth -> ""
RootNavState.Splash -> SplashRoute
Expand Down Expand Up @@ -117,7 +120,7 @@ fun RootNavScreen(
// navController.navigateToAuthGraph(rootNavOptions)
RootNavState.Auth -> {}
// navController.navigateToSetLanguage(rootNavOptions)
RootNavState.ShowOnboarding -> {}
RootNavState.ShowOnboarding -> navController.navigateToOnBoarding(rootNavOptions)
// navController.navigateToUserUnlock(rootNavOptions)
RootNavState.UserLocked -> {}
is RootNavState.UserUnlocked -> navController.navigateToAuthenticatedGraph(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ data class UserData(
isPasscodeEnabled = false,
isBiometricsEnabled = false,
showOnboarding = false,
firstTimeUser = false,
firstTimeUser = true,
enableScreenCapture = false,
)
}
Expand Down
1 change: 1 addition & 0 deletions feature/onboarding/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
39 changes: 39 additions & 0 deletions feature/onboarding/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
alias(libs.plugins.cmp.feature.convention)
}

android {
namespace = "org.mifos.feature.onboarding"
}

kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.data)
implementation(projects.core.datastore)
implementation(projects.core.model)
implementation(projects.coreBase.ui)

implementation(compose.ui)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
}
}
}

compose {
resources {
packageOfResClass = "org.mifos.feature.onboarding.generated.resources"
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package org.mifos.feature.onboarding

import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module

val OnboardingModule = module {
viewModelOf(::OnboardingViewmodel)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package org.mifos.feature.onboarding

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import kotlinx.serialization.Serializable
import template.core.base.ui.composableWithStayTransitions

@Serializable
data object OnBoardingRoute

fun NavController.navigateToOnBoarding(navOptions: NavOptions? = null) = navigate(OnBoardingRoute, navOptions)

fun NavGraphBuilder.onboardingDestination() {
composableWithStayTransitions<OnBoardingRoute> {
OnboardingScreen()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package org.mifos.feature.onboarding

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.koin.compose.viewmodel.koinViewModel
import org.mifos.feature.onboarding.components.OnBoardingScreenPage

const val TOTAL_PAGES = 2

@Composable
fun OnboardingScreen(
modifier: Modifier = Modifier,
viewmodel: OnboardingViewmodel = koinViewModel(),
) {
val currentPage by viewmodel.currentPage.collectAsStateWithLifecycle()

Scaffold(
containerColor = MaterialTheme.colorScheme.background,
) {
when (currentPage) {
1 -> OnBoardingScreenPage(
onNext = viewmodel::onNextPage,
currentPage = currentPage,
title = "Welcome",
description = "Thank you for using our template for creating your project",
modifier = modifier.padding(it),
)
2 -> OnBoardingScreenPage(
onNext = viewmodel::onNextPage,
currentPage = currentPage,
title = "Get Started",
description = "You can now start using your project",
modifier = modifier.padding(it),
)
}
Comment on lines +34 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add an else branch to handle unexpected page values.

The when expression only handles pages 1 and 2 without an else branch. If currentPage somehow becomes invalid (e.g., 0 or > TOTAL_PAGES), nothing will render, leaving a blank screen.

🔎 Suggested fix with else branch
     Scaffold(
         containerColor = MaterialTheme.colorScheme.background,
     ) {
         when (currentPage) {
             1 -> OnBoardingScreenPage(
                 onNext = viewmodel::onNextPage,
                 currentPage = currentPage,
                 title = "Welcome",
                 description = "Thank you for using our template for creating your project",
                 modifier = modifier.padding(it),
             )
             2 -> OnBoardingScreenPage(
                 onNext = viewmodel::onNextPage,
                 currentPage = currentPage,
                 title = "Get Started",
                 description = "You can now start using your project",
                 modifier = modifier.padding(it),
             )
+            else -> {
+                // Fallback for unexpected page values
+                OnBoardingScreenPage(
+                    onNext = viewmodel::onNextPage,
+                    currentPage = 1,
+                    title = "Welcome",
+                    description = "Thank you for using our template for creating your project",
+                    modifier = modifier.padding(it),
+                )
+            }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
when (currentPage) {
1 -> OnBoardingScreenPage(
onNext = viewmodel::onNextPage,
currentPage = currentPage,
title = "Welcome",
description = "Thank you for using our template for creating your project",
modifier = modifier.padding(it),
)
2 -> OnBoardingScreenPage(
onNext = viewmodel::onNextPage,
currentPage = currentPage,
title = "Get Started",
description = "You can now start using your project",
modifier = modifier.padding(it),
)
}
when (currentPage) {
1 -> OnBoardingScreenPage(
onNext = viewmodel::onNextPage,
currentPage = currentPage,
title = "Welcome",
description = "Thank you for using our template for creating your project",
modifier = modifier.padding(it),
)
2 -> OnBoardingScreenPage(
onNext = viewmodel::onNextPage,
currentPage = currentPage,
title = "Get Started",
description = "You can now start using your project",
modifier = modifier.padding(it),
)
else -> {
// Fallback for unexpected page values
OnBoardingScreenPage(
onNext = viewmodel::onNextPage,
currentPage = 1,
title = "Welcome",
description = "Thank you for using our template for creating your project",
modifier = modifier.padding(it),
)
}
}
🤖 Prompt for AI Agents
In
feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingScreen.kt
around lines 34–49, the when on currentPage only handles 1 and 2 so an
unexpected value leaves the UI blank; add an else branch that handles invalid
pages (e.g., log the unexpected value and render a safe default
OnBoardingScreenPage such as page 1 or a generic fallback message, or reset
currentPage via the viewmodel) so the UI never renders empty and unexpected
values are recoverable.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package org.mifos.feature.onboarding

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.mifos.core.datastore.UserPreferencesRepository

class OnboardingViewmodel(
private val preferenceRepository: UserPreferencesRepository,
) : ViewModel() {

private val _currentPage = MutableStateFlow(1)
val currentPage: StateFlow<Int> = _currentPage.asStateFlow()

private val totalPages = TOTAL_PAGES

fun onNextPage() {
if (_currentPage.value < totalPages) {
_currentPage.value += 1
} else {
updateOnboardingStatus()
}
}

private fun updateOnboardingStatus() {
viewModelScope.launch {
preferenceRepository.setFirstTimeState(false)
}
}
Comment on lines +29 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling and navigation callback after onboarding completion.

The updateOnboardingStatus() function uses a fire-and-forget pattern with no error handling. If setFirstTimeState() fails, the user won't be notified, and the onboarding state won't be persisted. Additionally, there's no mechanism to navigate the user away from the onboarding screen after completion.

Consider:

  1. Adding error handling for the repository call
  2. Providing a callback or event to trigger navigation after successful persistence
  3. Showing a loading state during the save operation
🔎 Suggested approach with completion callback
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
 class OnboardingViewmodel(
     private val preferenceRepository: UserPreferencesRepository,
 ) : ViewModel() {
 
     private val _currentPage = MutableStateFlow(1)
     val currentPage: StateFlow<Int> = _currentPage.asStateFlow()
+    
+    private val _onboardingComplete = MutableSharedFlow<Boolean>()
+    val onboardingComplete: SharedFlow<Boolean> = _onboardingComplete.asSharedFlow()
 
     private val totalPages = TOTAL_PAGES
 
     fun onNextPage() {
         if (_currentPage.value < totalPages) {
             _currentPage.value += 1
         } else {
             updateOnboardingStatus()
         }
     }
 
     private fun updateOnboardingStatus() {
         viewModelScope.launch {
-            preferenceRepository.setFirstTimeState(false)
+            try {
+                preferenceRepository.setFirstTimeState(false)
+                _onboardingComplete.emit(true)
+            } catch (e: Exception) {
+                // Handle error - maybe emit false or show error state
+                _onboardingComplete.emit(false)
+            }
         }
     }
 }

Then in the screen, collect onboardingComplete to trigger navigation.

🤖 Prompt for AI Agents
In
feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnboardingViewmodel.kt
around lines 29-41, update updateOnboardingStatus() to handle errors, loading
state, and emit a completion event: add a loading StateFlow/LiveData (e.g.,
_isSaving -> isSaving), an error StateFlow/LiveData (e.g., _saveError ->
saveError) and a one-time completion event (e.g., _onboardingComplete as
SharedFlow or SingleLiveEvent), then in viewModelScope.launch set isSaving true,
call preferenceRepository.setFirstTimeState(false) inside try/catch, on success
emit onboardingComplete event and clear error, on failure capture and emit the
error message/exception to saveError, and finally set isSaving false; update
onNextPage to trigger updateOnboardingStatus() when finishing and the UI should
collect isSaving/saveError/onboardingComplete to show a loading indicator,
display errors, and navigate on completion.

}
Loading
Loading