diff --git a/.gitignore b/.gitignore index a4228d2..d1429a8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,9 @@ captures !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings -# Google Service -composeApp/google-service.json -**/google-service.json -google-service.json +# Google Services +#composeApp/google-services.json +#**/google-services.json +#google-services.json # Project exclude paths \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index e69e0f7..7ee770b 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -78,6 +78,7 @@ kotlin { implementation(compose.components.uiToolingPreview) implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) + //adaptive implementation(libs.adaptive) implementation(libs.adaptive.layout) @@ -90,24 +91,24 @@ kotlin { implementation(libs.koin.compose.viewmodel.navigation) api(libs.koin.annotations) + //ktor implementation(libs.ktor.client.core) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.logging) implementation(libs.bundles.coil) - implementation(libs.kotlinx.datetime) - implementation(libs.androidx.datastore.preferences) - implementation(libs.calf.file.picker) + //navigation + implementation(libs.navigation.compose) + implementation(libs.kotlinx.serialization.json) } commonTest.dependencies { implementation(libs.kotlin.test) - implementation(libs.kotlinx.coroutines.test) implementation(libs.ktor.client.mock) implementation(libs.koin.test) diff --git a/composeApp/src/commonMain/composeResources/values-ar/string.xml b/composeApp/src/commonMain/composeResources/values-ar/string.xml index 0dc7e89..fe30da7 100644 --- a/composeApp/src/commonMain/composeResources/values-ar/string.xml +++ b/composeApp/src/commonMain/composeResources/values-ar/string.xml @@ -6,7 +6,7 @@ الفريق يحاول حلها حاليا, جرب مرة اخرى. حسناً - مرحبًا بك في صنعة 👋 + مرحبًا بك في كرافتو 👋 أدخل رقم هاتفك للمتابعة +٢٠ ٠٠٠ - ٠٠٠ - ٠٠٠٠ الشروط والأحكام diff --git a/composeApp/src/commonMain/composeResources/values/string.xml b/composeApp/src/commonMain/composeResources/values/string.xml index a8033d4..5756f83 100644 --- a/composeApp/src/commonMain/composeResources/values/string.xml +++ b/composeApp/src/commonMain/composeResources/values/string.xml @@ -7,7 +7,7 @@ Ok - Welcome to San3a 👋 + Welcome to Crafto 👋 Enter your phone number to\n continue +20 000 - 000 - 0000 Terms and Conditions diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index e767ad8..50a0f78 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -1,17 +1,25 @@ package org.example.project +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.screens.setup.craftsmansetup.CraftsmanSetupScreen -import org.example.project.presentation.screens.splash.SplashScreen +import org.example.project.presentation.navigation.CraftoNavGraph import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @Preview fun App() { AppTheme { - //OnboardingScreen() - SplashScreen { } - CraftsmanSetupScreen() + val navController = rememberNavController() + CraftoNavGraph( + navController = navController, + modifier = Modifier + .fillMaxSize() + .background(AppTheme.craftoColors.background.screen) + .navigationBarsPadding()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt index b4ec49b..d0cbbb2 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt @@ -8,13 +8,11 @@ import org.example.project.data.remote.network.ApiConstants.Endpoints.ONBOARDING import org.example.project.data.remote.network.wrapApiCall import org.example.project.domain.entity.OnboardingItem import org.example.project.domain.repository.OnboardingRepository -import org.koin.core.annotation.Provided -import org.koin.core.annotation.Single -@Single(binds = [OnboardingRepository::class]) + class OnboardingRepositoryImp( - @Provided private val httpClient: HttpClient + private val httpClient: HttpClient ) : OnboardingRepository { override suspend fun getOnboardingData(): List { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt index aec8671..5649de8 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt @@ -1,6 +1,7 @@ package org.example.project.data.repository import org.example.project.data.local.datasource.StorageLocalDataSource +import org.example.project.domain.entity.UserType import org.example.project.domain.repository.UserPreferences class UserPreferencesImpl( @@ -9,6 +10,8 @@ class UserPreferencesImpl( companion object { private const val KEY_USER_ID = "user_id" + private const val KEY_USER_TYPE = "user_type" + private const val KEY_IS_FIRST_TIME = "is_first_time" } override suspend fun getUserId(): String? { @@ -22,4 +25,30 @@ class UserPreferencesImpl( override suspend fun clearUserId() { storage.remove(KEY_USER_ID) } + + override suspend fun getUserType(): UserType? { + return storage.getString(KEY_USER_TYPE)?.let { + try { + UserType.valueOf(it) + } catch (e: IllegalArgumentException) { + null + } + } + } + + override suspend fun setUserType(userType: UserType) { + storage.saveString(KEY_USER_TYPE, userType.name) + } + + override suspend fun clearUserType() { + storage.remove(KEY_USER_TYPE) + } + + override suspend fun isFirstTime(): Boolean { + return storage.getBoolean(KEY_IS_FIRST_TIME) ?: true + } + + override suspend fun setFirstTime(isFirstTime: Boolean) { + storage.saveBoolean(KEY_IS_FIRST_TIME, isFirstTime) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt index 4ab09b4..a948c51 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -11,10 +11,12 @@ import org.example.project.data.remote.datasource.CraftsmanRemoteDataSourceImpl import org.example.project.data.repository.CategoryRepositoryImpl import org.example.project.data.repository.CraftsmanRepositoryImpl import org.example.project.data.repository.LocationRepositoryImpl +import org.example.project.data.repository.OnboardingRepositoryImp import org.example.project.data.service.ValidationServiceImpl import org.example.project.domain.repository.CategoryRepository import org.example.project.domain.repository.CraftsmanRepository import org.example.project.domain.repository.LocationRepository +import org.example.project.domain.repository.OnboardingRepository import org.example.project.domain.service.ValidationService import org.koin.dsl.module @@ -27,6 +29,9 @@ val dataModule = module { userPreferences = get() ) } + single { + OnboardingRepositoryImp(get()) + } single { categorySeed } single { CategoryMemoryDataSource(get()) } single { CategoryRepositoryImpl(get()) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt index 70c9a8c..74abecb 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt @@ -8,6 +8,10 @@ import org.example.project.domain.usecase.craftsman.GetCraftsmanStatusUseCase import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase import org.example.project.domain.usecase.craftsman.UploadProfilePictureUseCase import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase +import org.example.project.domain.usecase.session.ClearUserSessionUseCase +import org.example.project.domain.usecase.session.GetUserSessionUseCase +import org.example.project.domain.usecase.session.MarkOnboardingCompleteUseCase +import org.example.project.domain.usecase.session.SaveUserTypeUseCase import org.koin.dsl.module val domainModule = module { @@ -19,4 +23,9 @@ val domainModule = module { factory { DeleteCraftsmanAccountUseCase(get()) } factory { GetCategoriesUseCase(get())} factory { UploadProfilePictureUseCase(get(),get()) } + + factory { GetUserSessionUseCase(get()) } + factory { SaveUserTypeUseCase(get()) } + factory { MarkOnboardingCompleteUseCase(get()) } + factory { ClearUserSessionUseCase(get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt index 9a1adec..e5d40cb 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt @@ -1,5 +1,7 @@ package org.example.project.di +import org.example.project.presentation.screens.auth.UserTypeSelectionViewModel +import org.example.project.presentation.screens.onboarding.OnboardingViewModel import org.example.project.presentation.screens.setup.craftsmansetup.CraftsmanSetupViewModel import org.example.project.presentation.screens.setup.location.LocationViewModel import org.koin.core.module.dsl.viewModel @@ -15,5 +17,7 @@ val presentationModule = module { uploadProfilePictureUseCase = get(), ) } - viewModel {LocationViewModel(get())} + viewModel { LocationViewModel(get()) } + viewModel { OnboardingViewModel(get(), get()) } + viewModel { UserTypeSelectionViewModel(get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt index e38cf21..f2bec9c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt @@ -8,7 +8,6 @@ fun initKoin(config: KoinAppDeclaration? = null) { startKoin { config?.invoke(this) modules( - //CraftoModule().module, networkModule, dataModule, domainModule, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/UserSession.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/UserSession.kt new file mode 100644 index 0000000..a697ae4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/UserSession.kt @@ -0,0 +1,7 @@ +package org.example.project.domain.entity + +data class UserSession( + val userId: String?, + val userType: UserType?, + val isFirstTime: Boolean +) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/UserType.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/UserType.kt new file mode 100644 index 0000000..8897a23 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/UserType.kt @@ -0,0 +1,6 @@ +package org.example.project.domain.entity + +enum class UserType { + CUSTOMER, + CRAFTSMAN +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/UserPreferences.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/UserPreferences.kt index 7564fd0..17a04b6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/UserPreferences.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/UserPreferences.kt @@ -1,7 +1,18 @@ package org.example.project.domain.repository +import org.example.project.domain.entity.UserType + interface UserPreferences { suspend fun getUserId(): String? suspend fun setUserId(userId: String) suspend fun clearUserId() + + //TODO: Separate UserType to its own Repository + suspend fun getUserType(): UserType? + suspend fun setUserType(userType: UserType) + suspend fun clearUserType() + + // Add these + suspend fun isFirstTime(): Boolean + suspend fun setFirstTime(isFirstTime: Boolean) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/ClearUserSessionUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/ClearUserSessionUseCase.kt new file mode 100644 index 0000000..69cd850 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/ClearUserSessionUseCase.kt @@ -0,0 +1,12 @@ +package org.example.project.domain.usecase.session + +import org.example.project.domain.repository.UserPreferences + +class ClearUserSessionUseCase( + private val userPreferences: UserPreferences +) { + suspend operator fun invoke() { + userPreferences.clearUserId() + userPreferences.clearUserType() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/GetUserSessionUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/GetUserSessionUseCase.kt new file mode 100644 index 0000000..e367f19 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/GetUserSessionUseCase.kt @@ -0,0 +1,16 @@ +package org.example.project.domain.usecase.session + +import org.example.project.domain.entity.UserSession +import org.example.project.domain.repository.UserPreferences + +class GetUserSessionUseCase( + private val userPreferences: UserPreferences +) { + suspend operator fun invoke(): UserSession { + return UserSession( + userId = userPreferences.getUserId(), + userType = userPreferences.getUserType(), + isFirstTime = userPreferences.isFirstTime() + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/MarkOnboardingCompleteUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/MarkOnboardingCompleteUseCase.kt new file mode 100644 index 0000000..44f490b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/MarkOnboardingCompleteUseCase.kt @@ -0,0 +1,11 @@ +package org.example.project.domain.usecase.session + +import org.example.project.domain.repository.UserPreferences + +class MarkOnboardingCompleteUseCase( + private val userPreferences: UserPreferences +) { + suspend operator fun invoke() { + userPreferences.setFirstTime(false) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/SaveUserTypeUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/SaveUserTypeUseCase.kt new file mode 100644 index 0000000..e0f0565 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/session/SaveUserTypeUseCase.kt @@ -0,0 +1,12 @@ +package org.example.project.domain.usecase.session + +import org.example.project.domain.entity.UserType +import org.example.project.domain.repository.UserPreferences + +class SaveUserTypeUseCase( + private val userPreferences: UserPreferences +) { + suspend operator fun invoke(userType: UserType) { + userPreferences.setUserType(userType) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/BottomNavigation.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/BottomNavigation.kt new file mode 100644 index 0000000..b540d6b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/BottomNavigation.kt @@ -0,0 +1,107 @@ +package org.example.project.presentation.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.* +import org.example.project.domain.entity.UserType +import org.example.project.presentation.designsystem.components.CraftoBottomNavBar +import org.example.project.presentation.designsystem.components.CraftoNavItem +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource + + +enum class BottomNavigationItem( + val icon: DrawableResource, + val selectedIcon: DrawableResource, + val label: String, + val route: NavigationBarDestinations, +) { + Home( + icon = Res.drawable.home_angle, + selectedIcon = Res.drawable.home_angle_1, + label = "Home", + route = NavigationBarDestinations.HomeScreen + ), + MyRequests( // Customer + icon = Res.drawable.clipboard_text, + selectedIcon = Res.drawable.clipboard_text_1, + label = "My Requests", + route = NavigationBarDestinations.MyRequestsScreen + ), + MyJobs( // Craftsman + icon = Res.drawable.clipboard_text, + selectedIcon = Res.drawable.clipboard_text_1, + label = "My Jobs", + route = NavigationBarDestinations.MyJobsScreen + ), + Messages( + icon = Res.drawable.dialog, + selectedIcon = Res.drawable.dialog_1, + label = "Messages", + route = NavigationBarDestinations.MessagesScreen + ), + More( + icon = Res.drawable.user_circle, + selectedIcon = Res.drawable.user_circle_1, + label = "More", + route = NavigationBarDestinations.MoreScreen + ) +} + +fun getBottomNavItems(userType: UserType): List { + return when (userType) { + UserType.CUSTOMER -> listOf( + BottomNavigationItem.Home, + BottomNavigationItem.MyRequests, + BottomNavigationItem.Messages, + BottomNavigationItem.More + ) + UserType.CRAFTSMAN -> listOf( + BottomNavigationItem.Home, + BottomNavigationItem.MyJobs, + BottomNavigationItem.Messages, + BottomNavigationItem.More + ) + } +} + +@Composable +fun CraftoNavBar( + currentRoute: NavigationBarDestinations, + onNavDestinationClicked: (NavigationBarDestinations) -> Unit, + userType: UserType +) { + val items = getBottomNavItems(userType) + val selectedIndex = items.indexOfFirst { it.route == currentRoute } + + CraftoBottomNavBar( + items = items.map { item -> + CraftoNavItem( + label = item.label, + icon = painterResource(item.icon), + selectedIcon = painterResource(item.selectedIcon) + ) + }, + selectedIndex = selectedIndex.coerceAtLeast(0), + onItemSelected = { index -> + val selectedItem = items[index] + if (selectedItem.route != currentRoute) { + onNavDestinationClicked(selectedItem.route) + } + } + ) +} + +@Composable +fun getCurrentNavBarScreen(navController: NavController): NavigationBarDestinations? { + val backStackEntry by navController.currentBackStackEntryAsState() + val currentRoute: String? = backStackEntry?.destination?.route + val matchedEntry: Map.Entry? = + bottomNavBarDestinationsMap.entries.firstOrNull { (route, _) -> + currentRoute != null && route != null && currentRoute.startsWith(route) + } + return matchedEntry?.value +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/CraftoNavGraph.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/CraftoNavGraph.kt new file mode 100644 index 0000000..948110d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/CraftoNavGraph.kt @@ -0,0 +1,150 @@ +package org.example.project.presentation.navigation + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.launch +import org.example.project.domain.entity.UserType +import org.example.project.domain.usecase.session.GetUserSessionUseCase +import org.example.project.presentation.navigation.routes.* +import org.koin.compose.koinInject + +@Composable +fun CraftoNavGraph( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), + getUserSessionUseCase: GetUserSessionUseCase = koinInject() +) { + var userType by remember { mutableStateOf(null) } + var startDestination by remember { mutableStateOf(OnboardingDestination) } + var isLoading by remember { mutableStateOf(true) } + + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + coroutineScope.launch { + try { + val session = getUserSessionUseCase() + userType = session.userType + + startDestination = when { + session.isFirstTime -> { + OnboardingDestination + } + session.userId != null && session.userType != null -> { + NavigationBarDestinations.HomeScreen + } + session.userId != null && session.userType == null -> { + UserTypeSelectionDestination + } + else -> { + OtpRegistrationDestination + } + } + } catch (e: Exception) { + println("Error loading session: ${e.message}") + startDestination = OnboardingDestination + } finally { + isLoading = false + } + } + } + +// if (isLoading || startDestination == null) { +// Box( +// modifier = Modifier.fillMaxSize(), +// contentAlignment = Alignment.Center +// ) { +// CraftoCircularProgressIndicator() +// } +// return +// } + + Scaffold( + modifier = modifier, + bottomBar = { + getCurrentNavBarScreen(navController)?.let { selectedRoute -> + userType?.let { type -> + ShowNavigationBar( + selectedRoute = selectedRoute, + navController = navController, + userType = type + ) + } + } + } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = startDestination, + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None } + ) { + authNavigationGraph(navController, onUserTypeUpdated = { type -> userType = type }) + setupNavigationGraph(navController, onUserTypeUpdated = { type -> userType = type }) + bottomNavigationBarGraph(navController) + //detailsNavigationGraph(navController) + } + } +} + +@Composable +private fun ShowNavigationBar( + selectedRoute: NavigationBarDestinations, + navController: NavHostController, + userType: UserType +) { + CraftoNavBar( + currentRoute = selectedRoute, + onNavDestinationClicked = { route -> + navController.navigate(route) { + popUpTo(NavigationBarDestinations.HomeScreen) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + userType = userType + ) +} + +// ===== Navigation Graph Functions ===== + +fun NavGraphBuilder.authNavigationGraph( + navController: NavHostController, + onUserTypeUpdated: (UserType) -> Unit +) { + onboardingRoute(navController) + otpRegistrationRoute(navController) + userTypeSelectionRoute(navController, onUserTypeUpdated) +} + +fun NavGraphBuilder.setupNavigationGraph( + navController: NavHostController, + onUserTypeUpdated: (UserType) -> Unit +) { + craftsmanSetupRoute(navController, onUserTypeUpdated) + customerSetupRoute(navController, onUserTypeUpdated) +} + +fun NavGraphBuilder.bottomNavigationBarGraph(navController: NavHostController) { + homeRoute(navController) + myRequestsRoute(navController) + myJobsRoute(navController) + messagesRoute(navController) + moreRoute(navController) +} + +//fun NavGraphBuilder.detailsNavigationGraph(navController: NavHostController) { +// requestDetailsRoute(navController) +// jobDetailsRoute(navController) +// messageThreadRoute(navController) +// settingsDetailRoute(navController) +//} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/Destination.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/Destination.kt new file mode 100644 index 0000000..7cdfaa8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/Destination.kt @@ -0,0 +1,49 @@ +package org.example.project.presentation.navigation + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface NavigationBarDestinations { + @Serializable + data object HomeScreen : NavigationBarDestinations + + @Serializable + data object MyRequestsScreen : NavigationBarDestinations // Customer + + @Serializable + data object MyJobsScreen : NavigationBarDestinations // Craftsman + + @Serializable + data object MessagesScreen : NavigationBarDestinations + + @Serializable + data object MoreScreen : NavigationBarDestinations +} + +val bottomNavBarDestinationsMap: Map = mapOf( + NavigationBarDestinations.HomeScreen::class.qualifiedName to NavigationBarDestinations.HomeScreen, + NavigationBarDestinations.MyRequestsScreen::class.qualifiedName to NavigationBarDestinations.MyRequestsScreen, + NavigationBarDestinations.MyJobsScreen::class.qualifiedName to NavigationBarDestinations.MyJobsScreen, + NavigationBarDestinations.MessagesScreen::class.qualifiedName to NavigationBarDestinations.MessagesScreen, + NavigationBarDestinations.MoreScreen::class.qualifiedName to NavigationBarDestinations.MoreScreen, + //SettingsDetailDestination::class.qualifiedName to NavigationBarDestinations.MoreScreen // Keep bottom nav selected +) + + +@Serializable +data object OnboardingDestination + +@Serializable +data object OtpRegistrationDestination + +@Serializable +data object UserTypeSelectionDestination + +@Serializable +data object CraftsmanSetupDestination + +@Serializable +data object CustomerSetupDestination + +//@Serializable +//data class SettingsDetailDestination(val settingType: String) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/AuthRoutes.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/AuthRoutes.kt new file mode 100644 index 0000000..ce3e31a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/AuthRoutes.kt @@ -0,0 +1,67 @@ +package org.example.project.presentation.navigation.routes + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import org.example.project.domain.entity.UserType +import org.example.project.presentation.navigation.CraftsmanSetupDestination +import org.example.project.presentation.navigation.CustomerSetupDestination +import org.example.project.presentation.navigation.OnboardingDestination +import org.example.project.presentation.navigation.OtpRegistrationDestination +import org.example.project.presentation.navigation.UserTypeSelectionDestination +import org.example.project.presentation.screens.auth.UserTypeSelectionScreen +import org.example.project.presentation.screens.onboarding.OnboardingScreen +import org.example.project.presentation.screens.register.RegisterScreen + + +fun NavGraphBuilder.onboardingRoute(navController: NavHostController) { + composable { + OnboardingScreen( + onNavigateToOtp = { + navController.navigate(OtpRegistrationDestination) { + popUpTo(OnboardingDestination) { inclusive = true } + } + } + ) + } +} + +fun NavGraphBuilder.otpRegistrationRoute(navController: NavHostController) { + composable { + RegisterScreen( + onRegistrationComplete = { + navController.navigate(UserTypeSelectionDestination) { + popUpTo(OtpRegistrationDestination) { inclusive = true } + } + }, + ) + } +} + +fun NavGraphBuilder.userTypeSelectionRoute( + navController: NavHostController, + onUserTypeUpdated: (UserType) -> Unit +) { + composable { + UserTypeSelectionScreen( + onNavigateToSetup = { userType -> + onUserTypeUpdated(userType) + + when (userType) { + UserType.CRAFTSMAN -> { + navController.navigate(CraftsmanSetupDestination) { + //popUpTo(UserTypeSelectionDestination) { inclusive = true } + launchSingleTop = true + } + } + UserType.CUSTOMER -> { + navController.navigate(CustomerSetupDestination) { + //popUpTo(UserTypeSelectionDestination) { inclusive = true } + launchSingleTop = true + } + } + } + }, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/BottomNavRoutes.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/BottomNavRoutes.kt new file mode 100644 index 0000000..b00c228 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/BottomNavRoutes.kt @@ -0,0 +1,41 @@ +package org.example.project.presentation.navigation.routes + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import org.example.project.presentation.navigation.NavigationBarDestinations +import org.example.project.presentation.screens.home.HomeScreen +import org.example.project.presentation.screens.messages.MessagesScreen +import org.example.project.presentation.screens.more.MoreScreen +import org.example.project.presentation.screens.myjobs.MyJobsScreen +import org.example.project.presentation.screens.myrequests.MyRequestsScreen + +fun NavGraphBuilder.homeRoute(navController: NavHostController) { + composable { + HomeScreen() + } +} + +fun NavGraphBuilder.myRequestsRoute(navController: NavHostController) { + composable { + MyRequestsScreen() + } +} + +fun NavGraphBuilder.myJobsRoute(navController: NavHostController) { + composable { + MyJobsScreen() + } +} + +fun NavGraphBuilder.messagesRoute(navController: NavHostController) { + composable { + MessagesScreen() + } +} + +fun NavGraphBuilder.moreRoute(navController: NavHostController) { + composable { + MoreScreen() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/SetupRoutes.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/SetupRoutes.kt new file mode 100644 index 0000000..4b8ab53 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/navigation/routes/SetupRoutes.kt @@ -0,0 +1,49 @@ +package org.example.project.presentation.navigation.routes + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import org.example.project.domain.entity.UserType +import org.example.project.presentation.navigation.CraftsmanSetupDestination +import org.example.project.presentation.navigation.CustomerSetupDestination +import org.example.project.presentation.navigation.NavigationBarDestinations +import org.example.project.presentation.screens.setup.craftsmansetup.CraftsmanSetupScreen +import org.example.project.presentation.screens.setup.customersetup.CustomerSetupScreen + +fun NavGraphBuilder.craftsmanSetupRoute( + navController: NavHostController, + onUserTypeUpdated: (UserType) -> Unit +) { + composable { + CraftsmanSetupScreen( + onComplete = { + onUserTypeUpdated(UserType.CRAFTSMAN) + navController.navigate(NavigationBarDestinations.HomeScreen) { + popUpTo(0) { inclusive = true } + } + }, + onClose = { + navController.popBackStack() + } + ) + } +} + +fun NavGraphBuilder.customerSetupRoute( + navController: NavHostController, + onUserTypeUpdated: (UserType) -> Unit +) { + composable { + CustomerSetupScreen( + onComplete = { + onUserTypeUpdated(UserType.CUSTOMER) + navController.navigate(NavigationBarDestinations.HomeScreen) { + popUpTo(0) { inclusive = true } + } + }, + onClose = { + navController.popBackStack() + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionEffect.kt new file mode 100644 index 0000000..5f3938d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionEffect.kt @@ -0,0 +1,7 @@ +package org.example.project.presentation.screens.auth + +import org.example.project.domain.entity.UserType + +sealed interface UserTypeSelectionEffect { + data class NavigateToSetup(val userType: UserType) : UserTypeSelectionEffect +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionInteractionListener.kt new file mode 100644 index 0000000..c702bb0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionInteractionListener.kt @@ -0,0 +1,8 @@ +package org.example.project.presentation.screens.auth + +import org.example.project.domain.entity.UserType + +interface UserTypeSelectionInteractionListener { + fun onUserTypeSelected(userType: UserType) + fun onContinueClick() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionScreen.kt new file mode 100644 index 0000000..c8093d6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionScreen.kt @@ -0,0 +1,129 @@ +package org.example.project.presentation.screens.auth + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.continue_button +import crafto.composeapp.generated.resources.registration_step_1_description +import crafto.composeapp.generated.resources.user_type +import org.example.project.domain.entity.UserType +import org.example.project.presentation.designsystem.components.ButtonState +import org.example.project.presentation.designsystem.components.TextButton +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.example.project.presentation.screens.setup.composable.SetupScreenScaffold +import org.example.project.presentation.screens.setup.composable.page.UserTypeSelectionPage +import org.example.project.presentation.shared.base.ErrorUiState +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun UserTypeSelectionScreen( + viewModel: UserTypeSelectionViewModel = koinViewModel(), + onNavigateToSetup: (UserType) -> Unit, +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when (effect) { + is UserTypeSelectionEffect.NavigateToSetup -> { + onNavigateToSetup(effect.userType) + } + } + } + } + + Box(modifier = Modifier.fillMaxSize()) { + UserTypeSelectionContent( + state = state, + viewModel = viewModel, + ) + + AnimatedVisibility( + visible = state.error != null, + enter = slideInVertically { it } + fadeIn(), + exit = slideOutVertically { it } + fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter) + ) { + state.error?.let { error -> + ErrorSnackbar( + error = error, + onDismiss = viewModel::clearError, + modifier = Modifier.padding(16.dp) + ) + } + } + } +} + +@Composable +private fun UserTypeSelectionContent( + state: UserTypeSelectionUiState, + viewModel: UserTypeSelectionInteractionListener, +) { + SetupScreenScaffold( + currentPageNumber = 1, + totalPages = 1, + title = stringResource(Res.string.user_type), + description = stringResource(Res.string.registration_step_1_description), + nextButtonText = stringResource(Res.string.continue_button), + nextButtonEnabled = state.selectedType != null && !state.isLoading, + nextButtonState = when { + state.isLoading -> ButtonState.LOADING + state.selectedType != null -> ButtonState.Enable + else -> ButtonState.DISABLED + }, + showBackButton = false, + onNextButtonClick = viewModel::onContinueClick + ) { + UserTypeSelectionPage( + selectedType = state.selectedType, + onTypeSelected = viewModel::onUserTypeSelected, + modifier = Modifier.fillMaxSize() + ) + } +} + +@Composable +private fun ErrorSnackbar( + error: ErrorUiState, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + Snackbar( + modifier = modifier, + shape = RoundedCornerShape(8.dp), + containerColor = AppTheme.craftoColors.additional.primaryRed.copy(alpha = 0.95f), + contentColor = AppTheme.craftoColors.button.onPrimary, + action = { + TextButton( + onClick = onDismiss, + text = "Dismiss", + enabled = true, + buttonState = ButtonState.Enable + ) + } + ) { + Text( + text = error.message, + style = AppTheme.textStyle.body.medium, + color = AppTheme.craftoColors.button.onPrimary + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionUiState.kt new file mode 100644 index 0000000..8434970 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionUiState.kt @@ -0,0 +1,11 @@ +package org.example.project.presentation.screens.auth + +import org.example.project.domain.entity.UserType +import org.example.project.presentation.shared.base.BaseScreenState +import org.example.project.presentation.shared.base.ErrorUiState + +data class UserTypeSelectionUiState( + override val isLoading: Boolean = false, + override val error: ErrorUiState? = null, + val selectedType: UserType? = null +) : BaseScreenState diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionViewModel.kt new file mode 100644 index 0000000..0b5b09d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/auth/UserTypeSelectionViewModel.kt @@ -0,0 +1,57 @@ +package org.example.project.presentation.screens.auth + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.example.project.domain.entity.UserType +import org.example.project.domain.usecase.session.SaveUserTypeUseCase +import org.example.project.presentation.shared.base.BaseViewModel +import org.example.project.presentation.shared.base.ErrorUiState + +class UserTypeSelectionViewModel( + private val saveUserTypeUseCase: SaveUserTypeUseCase +) : BaseViewModel( + UserTypeSelectionUiState() +), UserTypeSelectionInteractionListener { + + init { + viewModelScope.launch { + isLoading.collect { loading -> + updateState { it.copy(isLoading = loading) } + } + } + } + + + override fun onUserTypeSelected(userType: UserType) { + updateState { it.copy(selectedType = userType) } + } + + override fun onContinueClick() { + val selectedType = state.value.selectedType + + if (selectedType == null) { + updateState { + it.copy(error = ErrorUiState("Please select a user type")) + } + return + } + + tryToCall( + call = { + saveUserTypeUseCase(selectedType) + selectedType + }, + onSuccess = { type -> + sendNewEffect(UserTypeSelectionEffect.NavigateToSetup(type)) + }, + onError = { error -> + updateState { it.copy(error = error) } + }, + showLoading = true + ) + } + + fun clearError() { + updateState { it.copy(error = null) } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/home/HomeScreen.kt new file mode 100644 index 0000000..10a11fa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/home/HomeScreen.kt @@ -0,0 +1,31 @@ +package org.example.project.presentation.screens.home + +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.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.example.project.presentation.designsystem.textstyle.AppTheme + +@Composable +fun HomeScreen( + +) { + Box( + modifier = Modifier.fillMaxSize().background(AppTheme.craftoColors.brand.primary) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("Home Screen", style = AppTheme.textStyle.title.large, + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/messages/MessagesScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/messages/MessagesScreen.kt new file mode 100644 index 0000000..2c00987 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/messages/MessagesScreen.kt @@ -0,0 +1,31 @@ +package org.example.project.presentation.screens.messages + +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.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.example.project.presentation.designsystem.textstyle.AppTheme + +@Composable +fun MessagesScreen( + +) { + Box( + modifier = Modifier.fillMaxSize().background(AppTheme.craftoColors.brand.primary) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("Messages Screen", style = AppTheme.textStyle.title.large, + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/more/MoreScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/more/MoreScreen.kt new file mode 100644 index 0000000..c8ffeae --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/more/MoreScreen.kt @@ -0,0 +1,31 @@ +package org.example.project.presentation.screens.more + +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.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.example.project.presentation.designsystem.textstyle.AppTheme + +@Composable +fun MoreScreen( + +) { + Box( + modifier = Modifier.fillMaxSize().background(AppTheme.craftoColors.brand.primary) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("More Screen", style = AppTheme.textStyle.title.large, + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/myjobs/MyJobsScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/myjobs/MyJobsScreen.kt new file mode 100644 index 0000000..7419a98 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/myjobs/MyJobsScreen.kt @@ -0,0 +1,31 @@ +package org.example.project.presentation.screens.myjobs + +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.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.example.project.presentation.designsystem.textstyle.AppTheme + +@Composable +fun MyJobsScreen( + +) { + Box( + modifier = Modifier.fillMaxSize().background(AppTheme.craftoColors.brand.primary) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("My Jobs Screen", style = AppTheme.textStyle.title.large, + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/myrequests/MyRequestsScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/myrequests/MyRequestsScreen.kt new file mode 100644 index 0000000..0d54bfb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/myrequests/MyRequestsScreen.kt @@ -0,0 +1,31 @@ +package org.example.project.presentation.screens.myrequests + +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.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.example.project.presentation.designsystem.textstyle.AppTheme + +@Composable +fun MyRequestsScreen( + +) { + Box( + modifier = Modifier.fillMaxSize().background(AppTheme.craftoColors.brand.primary) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("My Requests Screen", style = AppTheme.textStyle.title.large, + textAlign = androidx.compose.ui.text.style.TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt index 957729d..d96c4c5 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -38,7 +39,6 @@ import org.example.project.presentation.designsystem.components.SecondaryButton import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.screens.onboarding.composable.OnboardingIndicator import org.example.project.presentation.screens.onboarding.composable.OnboardingItem -import org.example.project.presentation.screens.onboarding.OnboardingViewModel import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview @@ -46,10 +46,23 @@ import org.koin.compose.viewmodel.koinViewModel @Composable fun OnboardingScreen( - viewModel: OnboardingViewModel = koinViewModel() - + viewModel: OnboardingViewModel = koinViewModel(), + onNavigateToOtp: () -> Unit ) { val state by viewModel.state.collectAsState() + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when (effect) { + OnboardingScreenEffect.NavigateToGetStartedScreen, + OnboardingScreenEffect.NavigateToRegisterScreen -> { + onNavigateToOtp() + } + OnboardingScreenEffect.NavigateToNext -> { + // Internal navigation handled by pager + } + } + } + } OnboardingContent( state = state, interactions = viewModel @@ -98,7 +111,8 @@ private fun OnboardingContent( ) ) } - interactions::onSkipClick +// interactions::onSkipClick + interactions.onSkipClick() }, buttonState = ButtonState.Enable, containerColor = AppTheme.craftoColors.button.secondary, @@ -165,7 +179,8 @@ private fun OnboardingActionsRow( ) } } else { - interactions::onGetStartedClick + //interactions::onGetStartedClick + interactions.onGetStartedClick() } }, buttonState = ButtonState.Enable, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt index f22d328..e76292a 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt @@ -5,14 +5,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import org.example.project.domain.entity.OnboardingItem import org.example.project.domain.repository.OnboardingRepository +import org.example.project.domain.usecase.session.MarkOnboardingCompleteUseCase import org.example.project.presentation.screens.onboarding.model.toUiState import org.example.project.presentation.shared.base.BaseViewModel -import org.koin.android.annotation.KoinViewModel -import org.koin.core.annotation.Provided -@KoinViewModel + class OnboardingViewModel( - @Provided private val repository: OnboardingRepository, + private val repository: OnboardingRepository, + private val markOnboardingCompleteUseCase: MarkOnboardingCompleteUseCase, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) : BaseViewModel(initialState = OnboardingScreenState()), @@ -22,16 +22,17 @@ class OnboardingViewModel( loadData() } - private fun loadData() { tryToCall( call = { updateState { it.copy(loading = true) } repository.getOnboardingData() }, - onSuccess = ::onLoadDataSuccess , - onError = { errorState -> updateState { it.copy(errorMessage = errorState) } }, - dispatcher = ioDispatcher + onSuccess = ::onLoadDataSuccess, + onError = { errorState -> + updateState { it.copy(errorMessage = errorState, loading = false) } + }, + dispatcher = ioDispatcher, ) } @@ -45,6 +46,7 @@ class OnboardingViewModel( } override fun onSkipClick() { + markOnboardingComplete() sendNewEffect(OnboardingScreenEffect.NavigateToGetStartedScreen) } @@ -53,6 +55,22 @@ class OnboardingViewModel( } override fun onGetStartedClick() { + markOnboardingComplete() sendNewEffect(OnboardingScreenEffect.NavigateToRegisterScreen) } + + private fun markOnboardingComplete() { + tryToCall( + call = { + markOnboardingCompleteUseCase() + }, + onSuccess = { + // Do nothing, just continue navigation + }, + onError = { error -> + updateState { it.copy(loading = false) } + println("⚠️ Failed to mark onboarding complete: ${error.message}") + }, + ) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/RegisterScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/RegisterScreen.kt index d319231..7d229e1 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/RegisterScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/RegisterScreen.kt @@ -39,17 +39,17 @@ import org.example.project.presentation.designsystem.components.TextField import org.example.project.presentation.designsystem.textstyle.AppTheme import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun RegisterScreen( modifier: Modifier = Modifier, + onRegistrationComplete: () -> Unit ) { RegisterContent( modifier = modifier, onPrivacyPolicyClick = {}, onTermsClick = {}, - onButtonClick = {} + onButtonClick = { onRegistrationComplete() } ) } @@ -176,10 +176,10 @@ private fun PrivacyAndTextSection( } } -@Preview -@Composable -private fun RegisterScreenPreview() { - AppTheme { - RegisterScreen() - } -} \ No newline at end of file +//@Preview +//@Composable +//private fun RegisterScreenPreview() { +// AppTheme { +// RegisterScreen() +// } +//} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/SetupScreenScaffold.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/SetupScreenScaffold.kt index 04994fa..dfadc50 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/SetupScreenScaffold.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/SetupScreenScaffold.kt @@ -31,10 +31,11 @@ fun SetupScreenScaffold( totalPages: Int = 4, nextButtonText: String = stringResource(Res.string.next), nextButtonEnabled: Boolean = true, + showBackButton: Boolean= true, nextButtonState: ButtonState = ButtonState.Enable, title: String, description: String, - onBackButtonClick: () -> Unit, + onBackButtonClick: () -> Unit = {}, onNextButtonClick: () -> Unit, content: @Composable (() -> Unit) ) { @@ -52,6 +53,7 @@ fun SetupScreenScaffold( totalPages = totalPages, nextButtonText = nextButtonText, nextButtonEnabled = nextButtonEnabled, + showBackButton = showBackButton, nextButtonState = nextButtonState, description = description, onBackButtonClick = onBackButtonClick, @@ -68,6 +70,7 @@ fun SetupScreenScaffold( totalPages = totalPages, nextButtonText = nextButtonText, nextButtonEnabled = nextButtonEnabled, + showBackButton = showBackButton, nextButtonState = nextButtonState, title = title, description = description, @@ -90,6 +93,7 @@ private fun PortraitLayout( nextButtonText: String, nextButtonEnabled: Boolean, nextButtonState: ButtonState, + showBackButton: Boolean = true, description: String, onBackButtonClick: () -> Unit, onNextButtonClick: () -> Unit, @@ -107,7 +111,8 @@ private fun PortraitLayout( AccountSetupTopBar( onBackButtonClick = onBackButtonClick, currentPage = currentPageNumber, - totalPages = totalPages ) + totalPages = totalPages, + showBackButton = showBackButton) TitleDescriptionBox( modifier = Modifier.weight(1f), @@ -141,6 +146,7 @@ private fun LandscapeLayout( nextButtonText: String, nextButtonEnabled: Boolean, nextButtonState: ButtonState, + showBackButton: Boolean, title: String, description: String, onBackButtonClick: () -> Unit, @@ -159,7 +165,8 @@ private fun LandscapeLayout( AccountSetupTopBar( onBackButtonClick = onBackButtonClick, currentPage = currentPageNumber, - totalPages = totalPages) + totalPages = totalPages, + showBackButton = showBackButton) Row( modifier = Modifier .fillMaxWidth() diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/UserTypeSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/UserTypeSelectionPage.kt index 62a8677..a2f01e4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/UserTypeSelectionPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/UserTypeSelectionPage.kt @@ -13,18 +13,19 @@ import crafto.composeapp.generated.resources.customer import crafto.composeapp.generated.resources.customer_description import crafto.composeapp.generated.resources.selection_craftsman import crafto.composeapp.generated.resources.selection_customer +import org.example.project.domain.entity.UserType import org.example.project.presentation.designsystem.components.SelectionCard -import org.example.project.presentation.screens.setup.craftsmansetup.UserType import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @Composable fun UserTypeSelectionPage( selectedType: UserType?, - onTypeSelected: (UserType) -> Unit + onTypeSelected: (UserType) -> Unit, + modifier: Modifier=Modifier ) { Row( - modifier = Modifier + modifier = modifier .fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupInteractionListener.kt index bc33d78..20eebc5 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupInteractionListener.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -5,8 +5,6 @@ import org.example.project.presentation.model.PersonalInfoUiModel import org.example.project.presentation.shared.base.ErrorUiState interface CraftsmanSetupInteractionListener { - fun onUserTypeSelected(userType: UserType) - fun onCategoryToggled(categoryId: Int) fun onPersonalInfoChanged(personalInfo: PersonalInfoUiModel) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupScreen.kt index d9d4369..2faa672 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupScreen.kt @@ -28,16 +28,13 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.identity_verification -import crafto.composeapp.generated.resources.location_hint import crafto.composeapp.generated.resources.personal_info import crafto.composeapp.generated.resources.portfolio_upload -import crafto.composeapp.generated.resources.registration_step_1_description import crafto.composeapp.generated.resources.registration_step_2_description import crafto.composeapp.generated.resources.registration_step_3_description import crafto.composeapp.generated.resources.registration_step_4_description import crafto.composeapp.generated.resources.registration_step_5_description import crafto.composeapp.generated.resources.service_selection -import crafto.composeapp.generated.resources.user_type import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.TextButton import org.example.project.presentation.designsystem.textstyle.AppTheme @@ -46,7 +43,6 @@ import org.example.project.presentation.screens.setup.composable.page.IdentityVe import org.example.project.presentation.screens.setup.composable.page.PersonalInfoPage import org.example.project.presentation.screens.setup.composable.page.PortfolioUploadPage import org.example.project.presentation.screens.setup.composable.page.ServiceSelectionPage -import org.example.project.presentation.screens.setup.composable.page.UserTypeSelectionPage import org.example.project.presentation.shared.base.ErrorUiState import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @@ -134,18 +130,16 @@ fun CraftsmanSetupContent( onNextButtonClick = { when (state.currentStep) { RegistrationStep.IDENTITY_VERIFICATION -> { - if (state.idCardFront != null && state.idCardBack != null) { + if (state.hasUploadedIdCards) viewModel.onUploadIdCards() - } else { - viewModel.onSkipIdentityVerification() - } +// } else { +// viewModel.onSkipIdentityVerification() +// } } else -> viewModel.navigateNext() } }, title = when (state.currentStep) { - RegistrationStep.USER_TYPE -> { - stringResource(Res.string.user_type) } RegistrationStep.SERVICE_SELECTION -> { stringResource(Res.string.service_selection) } @@ -160,9 +154,6 @@ fun CraftsmanSetupContent( } }, description =when (state.currentStep) { - RegistrationStep.USER_TYPE -> { - stringResource(Res.string.registration_step_1_description) - } RegistrationStep.SERVICE_SELECTION -> { stringResource(Res.string.registration_step_2_description) } @@ -187,12 +178,6 @@ fun CraftsmanSetupContent( userScrollEnabled = state.isSwipeEnabled && state.canNavigateNext ) { page -> when (RegistrationStep.fromIndex(page)) { - RegistrationStep.USER_TYPE -> { - UserTypeSelectionPage( - selectedType = state.userType, - onTypeSelected = viewModel::onUserTypeSelected, - ) - } RegistrationStep.SERVICE_SELECTION -> { ServiceSelectionPage( availableCategories = state.availableCategories, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupUiState.kt index ac71fa9..83af7a4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupUiState.kt @@ -12,7 +12,7 @@ data class CraftsmanSetupUiState( override val error: ErrorUiState? = null, val currentPageIndex: Int = 0, - val totalPages: Int = 5, + val totalPages: Int = 4, val canNavigateNext: Boolean = false, val canNavigateBack: Boolean = false, val isSwipeEnabled: Boolean = true, @@ -23,8 +23,6 @@ data class CraftsmanSetupUiState( val uploadedPortfolioUrls: List = emptyList(), val verificationDocuments: VerificationDocuments? = null, - val userType: UserType? = null, - val availableCategories: List = emptyList(), val selectedCategoryIds: Set = emptySet(), @@ -48,12 +46,11 @@ data class CraftsmanSetupUiState( val currentStep: RegistrationStep get() = RegistrationStep.fromIndex(currentPageIndex) - val progress: Float - get() = (currentPageIndex + 1) / totalPages.toFloat() + val hasUploadedIdCards: Boolean + get() = idCardFront != null && idCardBack != null val nextButtonText: String get() = when (currentStep) { - RegistrationStep.USER_TYPE -> "Next" RegistrationStep.SERVICE_SELECTION -> "Next" RegistrationStep.PERSONAL_INFO -> if (isLoading) "Creating Profile..." else "Next" RegistrationStep.PORTFOLIO_UPLOAD -> "Next" @@ -62,19 +59,13 @@ data class CraftsmanSetupUiState( } enum class RegistrationStep(val index: Int) { - USER_TYPE(0), - SERVICE_SELECTION(1), - PERSONAL_INFO(2), - PORTFOLIO_UPLOAD(3), - IDENTITY_VERIFICATION(4); + SERVICE_SELECTION(0), + PERSONAL_INFO(1), + PORTFOLIO_UPLOAD(2), + IDENTITY_VERIFICATION(3); companion object { fun fromIndex(index: Int): RegistrationStep = - entries.firstOrNull { it.index == index } ?: USER_TYPE + entries.firstOrNull { it.index == index } ?: SERVICE_SELECTION } -} - -enum class UserType { - CUSTOMER, - CRAFTSMAN } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupViewModel.kt index 35e6f2b..6c62473 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupViewModel.kt @@ -27,22 +27,6 @@ class CraftsmanSetupViewModel( CraftsmanSetupUiState() ), CraftsmanSetupInteractionListener { - override fun onUserTypeSelected(userType: UserType) { - when (userType) { - UserType.CRAFTSMAN -> { - updateState { - it.copy( - userType = userType, - canNavigateNext = true - ) - } - } - UserType.CUSTOMER -> { - TODO("Implement Customer setup flow – redirect to CustomerSetupScreen when ready") - } - } - } - init { validateCurrentPage() fetchCategories() @@ -106,7 +90,7 @@ class CraftsmanSetupViewModel( updateState { it.copy(error = ErrorUiState("Please upload both ID card images")) } return } - updateState { it.copy(isSwipeEnabled = false) } + updateState { it.copy(isSwipeEnabled = false, isUploadingIdCards = true) } tryToCall( call = { @@ -119,14 +103,18 @@ class CraftsmanSetupViewModel( ) }, onSuccess = { verificationDocs -> - updateState { it.copy(isSwipeEnabled = true) } + updateState { it.copy( + isSwipeEnabled = true, + isUploadingIdCards = false, + verificationDocuments = verificationDocs) } sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) }, onError = { error -> updateState { it.copy( error = error, - isSwipeEnabled = true + isSwipeEnabled = true, + isUploadingIdCards = false ) } }, @@ -135,8 +123,8 @@ class CraftsmanSetupViewModel( } override fun onSkipIdentityVerification() { - //sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) - navigateNext() + sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) + //navigateNext() } override fun onPortfolioImagesAdded(images: List) { @@ -349,7 +337,6 @@ class CraftsmanSetupViewModel( } if (currentIndex < state.value.totalPages - 1 && state.value.canNavigateNext) { - AppLogger.d("Navigation", "Navigating from page $currentIndex to ${currentIndex + 1}") updateState { it.copy(currentPageIndex = currentIndex + 1) } } } @@ -392,11 +379,10 @@ class CraftsmanSetupViewModel( private fun validateCurrentPage() { val canProceed = when (state.value.currentStep) { - RegistrationStep.USER_TYPE -> state.value.userType != null RegistrationStep.SERVICE_SELECTION -> state.value.selectedCategoryIds.isNotEmpty() RegistrationStep.PERSONAL_INFO -> validatePersonalInfo(state.value.personalInfo) RegistrationStep.PORTFOLIO_UPLOAD -> state.value.portfolioImages.isNotEmpty() - RegistrationStep.IDENTITY_VERIFICATION -> true // Optional step + RegistrationStep.IDENTITY_VERIFICATION -> true } updateState { it.copy(canNavigateNext = canProceed) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/CustomerSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/CustomerSetupScreen.kt new file mode 100644 index 0000000..a3e9134 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/CustomerSetupScreen.kt @@ -0,0 +1,12 @@ +package org.example.project.presentation.screens.setup.customersetup + +import androidx.compose.runtime.Composable + + +@Composable +fun CustomerSetupScreen( + onComplete: () -> Unit={}, + onClose: () -> Unit={} +) { + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/init b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/init deleted file mode 100644 index 6a8de1b..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/init +++ /dev/null @@ -1 +0,0 @@ -TODO() \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eee6826..11934f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -adaptive = "1.2.0-alpha07" +adaptive = "1.2.0" agp = "8.13.0" android-compileSdk = "36" android-minSdk = "24" @@ -8,19 +8,19 @@ androidx-activity = "1.11.0" androidx-appcompat = "1.7.1" androidx-core = "1.17.0" androidx-espresso = "3.7.0" -androidx-lifecycle = "2.9.4" +androidx-lifecycle = "2.9.6" androidx-testExt = "1.3.0" -calfFilePicker = "0.8.0" +calfFilePicker = "0.9.0" coilComposeVersion = "3.3.0" -composeMultiplatform = "1.9.0" +composeMultiplatform = "1.9.3" datastorePreferences = "1.1.7" junit = "4.13.2" -kotlin = "2.2.20" +kotlin = "2.2.21" ksp = "2.2.20-2.0.4" koinTest = "4.1.1" koin = "4.1.1" -koinAnnotations = "2.2.0" -ktor = "3.3.1" +koinAnnotations = "2.3.1" +ktor = "3.3.2" coil = "3.3.0" koinComposeMultiplatform = "4.1.1" kotlinxCoroutinesTest = "1.10.2" @@ -31,8 +31,9 @@ google-services = "4.4.4" #newer version of firebase-bom causes gradle errors firebase-bom = "33.16.0" firebaseCrashlytics = "3.0.6" -ktorClientCio = "3.3.1" -ktorClientMock = "3.3.1" +ktorClientCio = "3.3.2" +ktorClientMock = "3.3.2" +navigationCompose = "2.9.1" [libraries] @@ -85,6 +86,7 @@ coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } +navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }