diff --git a/cmp-navigation/build.gradle.kts b/cmp-navigation/build.gradle.kts index 4bef2eaa..11e316f6 100644 --- a/cmp-navigation/build.gradle.kts +++ b/cmp-navigation/build.gradle.kts @@ -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) diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt index 25ad882b..1ae6529e 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/di/KoinModules.kt @@ -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 @@ -43,6 +44,7 @@ object KoinModules { includes( HomeModule, SettingsModule, + OnboardingModule, ) } diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavScreen.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavScreen.kt index b4cd4394..0641158f 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavScreen.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/rootnav/RootNavScreen.kt @@ -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 @@ -63,7 +66,7 @@ fun RootNavScreen( popExitTransition = { toExitTransition()(this) }, ) { splashDestination() -// onboardingDestination() + onboardingDestination() // authNavGraph(navController) authenticatedGraph(navController) // userUnlockDestination() @@ -71,7 +74,7 @@ fun RootNavScreen( val targetRoute = when (state) { // SetLanguageRoute - RootNavState.ShowOnboarding -> "" + RootNavState.ShowOnboarding -> OnBoardingRoute // AuthGraphRoute RootNavState.Auth -> "" RootNavState.Splash -> SplashRoute @@ -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( diff --git a/core/model/src/commonMain/kotlin/org/mifos/core/model/UserData.kt b/core/model/src/commonMain/kotlin/org/mifos/core/model/UserData.kt index bcefe830..45693137 100644 --- a/core/model/src/commonMain/kotlin/org/mifos/core/model/UserData.kt +++ b/core/model/src/commonMain/kotlin/org/mifos/core/model/UserData.kt @@ -40,7 +40,7 @@ data class UserData( isPasscodeEnabled = false, isBiometricsEnabled = false, showOnboarding = false, - firstTimeUser = false, + firstTimeUser = true, enableScreenCapture = false, ) } diff --git a/feature/onboarding/.gitignore b/feature/onboarding/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/onboarding/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts new file mode 100644 index 00000000..36c6604e --- /dev/null +++ b/feature/onboarding/build.gradle.kts @@ -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" + } +} diff --git a/feature/onboarding/consumer-rules.pro b/feature/onboarding/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingModule.kt b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingModule.kt new file mode 100644 index 00000000..1064ce1e --- /dev/null +++ b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingModule.kt @@ -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) +} diff --git a/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingRoute.kt b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingRoute.kt new file mode 100644 index 00000000..7d85955a --- /dev/null +++ b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingRoute.kt @@ -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 { + OnboardingScreen() + } +} diff --git a/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingScreen.kt b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingScreen.kt new file mode 100644 index 00000000..27a18176 --- /dev/null +++ b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnBoardingScreen.kt @@ -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), + ) + } + } +} diff --git a/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnboardingViewmodel.kt b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnboardingViewmodel.kt new file mode 100644 index 00000000..58e28b5f --- /dev/null +++ b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/OnboardingViewmodel.kt @@ -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 = _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) + } + } +} diff --git a/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/components/OnBoardingScreenPage.kt b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/components/OnBoardingScreenPage.kt new file mode 100644 index 00000000..12639e73 --- /dev/null +++ b/feature/onboarding/src/commonMain/kotlin/org/mifos/feature/onboarding/components/OnBoardingScreenPage.kt @@ -0,0 +1,150 @@ +/* + * 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.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.feature.onboarding.TOTAL_PAGES + +@Composable +fun OnBoardingScreenPage( + onNext: () -> Unit, + currentPage: Int, + title: String, + description: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 36.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Box( + modifier = Modifier.fillMaxWidth().weight(1f), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = title, + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = description, + style = MaterialTheme.typography.bodyLarge + .copy(color = MaterialTheme.colorScheme.onSurfaceVariant), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + OnboardingTextBlock( + modifier = Modifier, + onNext = onNext, + currentPage = currentPage, + totalPages = TOTAL_PAGES, + ) + } +} + +@Composable +fun OnboardingTextBlock( + onNext: () -> Unit, + currentPage: Int, + modifier: Modifier = Modifier, + totalPages: Int = TOTAL_PAGES, +) { + Row( + modifier = modifier.fillMaxWidth().padding(vertical = 32.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row { + repeat(totalPages) { index -> + Box( + modifier = Modifier + .size(12.dp) + .background( + color = if (index == currentPage - 1) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) + }, + shape = CircleShape, + ), + ) + if (index < totalPages - 1) Spacer(Modifier.width(8.dp)) + } + } + + Button( + modifier = Modifier, + onClick = onNext, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), + shape = MaterialTheme.shapes.medium, + ) { + Text( + text = "Next", + ) + } + } +} + +@Preview +@Composable +fun PreviewOnboardingTextBlock() { + OnboardingTextBlock( + onNext = {}, + currentPage = 2, + totalPages = 5, + ) +} + +@Preview +@Composable +fun PreviewOnBoardingScreenPage() { + OnBoardingScreenPage( + onNext = { /* ... */ }, + currentPage = 1, + title = "Welcome", + description = "This is onboarding", + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0604fe30..859d19ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -112,6 +112,8 @@ androidPackageNamespace = "cmp.android.app" #Room room = "2.7.2" sqliteBundled = "2.5.2" +espressoCore = "3.7.0" +material = "1.13.0" [libraries] android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } @@ -330,6 +332,8 @@ roborazzi-accessibility-check = { group = "io.github.takahirom.roborazzi", name aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } aboutlibraries-compose-core = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutLibraries" } aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [bundles] androidx-compose-ui-test = [ diff --git a/settings.gradle.kts b/settings.gradle.kts index 550417c6..6f16fad9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -73,6 +73,7 @@ include(":core:ui") include(":feature:home") include(":feature:profile") include(":feature:settings") +include(":feature:onboarding") include(":core-base:analytics") include(":core-base:common") @@ -88,4 +89,5 @@ check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { Java Home: [${System.getProperty("java.home")}] https://developer.android.com/build/jdks#jdk-config-in-studio """.trimIndent() -} \ No newline at end of file +} +