From 07a5e8ebc1e5a2ef4125df5aeb6f123d89147784 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:30:31 +0300 Subject: [PATCH 01/19] move splash screen to new package --- .../presentation/screens/{onboarding => splash}/SplashScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{onboarding => splash}/SplashScreen.kt (97%) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/SplashScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/splash/SplashScreen.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/SplashScreen.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/splash/SplashScreen.kt index b2bc47c..cb3a85b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/SplashScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/splash/SplashScreen.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.onboarding +package org.example.project.presentation.screens.splash import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement From b8ae9487ff8830d8ab367447aa86594619cd7737 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:31:50 +0300 Subject: [PATCH 02/19] move bottomSheet to to new package --- .../screen/register/RegisterScreen.kt | 184 ------------------ .../register/component/ErrorBottomSheet.kt | 2 +- .../PoliciesAndConditionsBottomSheet.kt | 2 +- 3 files changed, 2 insertions(+), 186 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/RegisterScreen.kt rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{screen => screens}/register/component/ErrorBottomSheet.kt (97%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{screen => screens}/register/component/PoliciesAndConditionsBottomSheet.kt (99%) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/RegisterScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/RegisterScreen.kt deleted file mode 100644 index 0b0251f..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/RegisterScreen.kt +++ /dev/null @@ -1,184 +0,0 @@ -package org.example.project.presentation.screen.register - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -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.style.TextAlign -import androidx.compose.ui.unit.dp -import crafto.composeapp.generated.resources.Res -import crafto.composeapp.generated.resources.and_text -import crafto.composeapp.generated.resources.continue_button -import crafto.composeapp.generated.resources.egypt_flag -import crafto.composeapp.generated.resources.enter_phone -import crafto.composeapp.generated.resources.logo -import crafto.composeapp.generated.resources.logo_icon -import crafto.composeapp.generated.resources.privacy_agreement -import crafto.composeapp.generated.resources.privacy_policy -import crafto.composeapp.generated.resources.terms_and_conditions -import crafto.composeapp.generated.resources.welcome_title -import org.example.project.presentation.designsystem.components.ButtonState -import org.example.project.presentation.designsystem.components.PrimaryButton -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, -) { - RegisterContent( - modifier = modifier, - onPrivacyPolicyClick = {}, - onTermsClick = {}, - onButtonClick = {} - ) -} - -@Composable -private fun RegisterContent( - modifier: Modifier, - onPrivacyPolicyClick: () -> Unit, - onTermsClick: () -> Unit, - onButtonClick: () -> Unit -) { - Box( - modifier = modifier.fillMaxSize().background(AppTheme.craftoColors.brand.primary) - ) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier.padding(top = 75.dp).background( - AppTheme.craftoColors.background.card, shape = RoundedCornerShape( - AppTheme.craftoRadius.full - ) - ).size(64.dp) - - ) { - Icon( - painter = painterResource(Res.drawable.logo), - contentDescription =stringResource(Res.string.logo_icon), - modifier = Modifier.align(Alignment.Center).offset(x = (-5).dp, y = (5).dp), - tint = AppTheme.craftoColors.brand.primary - ) - } - - Text( - text = stringResource(Res.string.welcome_title), - style = AppTheme.textStyle.title.large, - textAlign = TextAlign.Center, - color = AppTheme.craftoColors.background.card, - modifier = Modifier.padding(top = 16.dp, start = 24.dp, bottom = 67.dp) - ) - Box( - modifier = Modifier.fillMaxSize().background( - color = AppTheme.craftoColors.background.card, - shape = RoundedCornerShape( - topStart = AppTheme.craftoRadius.x5l, - topEnd = AppTheme.craftoRadius.x5l - ) - ).padding(horizontal = 24.dp) - ) { - Column { - Text( - text = stringResource(Res.string.enter_phone), - style = AppTheme.textStyle.title.medium, - color = AppTheme.craftoColors.shade.primary, - modifier = Modifier.padding(top = 40.dp, bottom = 24.dp) - ) - - TextField( - hint = "+20 000 - 000 - 0000", - startIcon = { - Image( - painter = painterResource(Res.drawable.egypt_flag), - contentDescription = stringResource(Res.string.egypt_flag) - ) - }, - showDividerLine = true, - maxLines = 1, - minLines = 1, - text = "", - onTextChange = {} - ) - - PrivacyAndTextSection( - normalText = stringResource(Res.string.privacy_agreement), - specialText = stringResource(Res.string.terms_and_conditions), - onClick = onTermsClick, - modifier = Modifier.padding(top = 12.dp).fillMaxWidth() - - ) - - PrivacyAndTextSection( - normalText = stringResource(Res.string.and_text), - specialText =stringResource(Res.string.privacy_policy), - onClick = onPrivacyPolicyClick, - modifier = Modifier.padding(bottom = 24.dp).fillMaxWidth() - ) - - PrimaryButton( - text = stringResource(Res.string.continue_button), - enabled = true, - onClick = onButtonClick, - buttonState = ButtonState.Enable, - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp) - ) - } - } - } - } -} - -@Composable -private fun PrivacyAndTextSection( - modifier: Modifier = Modifier, - normalText: String, - specialText: String, - onClick: () -> Unit = {} -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.Center - ) { - Text( - text = normalText, - style = AppTheme.textStyle.body.smallRegular, - color = AppTheme.craftoColors.shade.secondary - ) - Text( - text = specialText, - style = AppTheme.textStyle.body.smallRegular, - color = AppTheme.craftoColors.brand.primary, - modifier = Modifier.clickable { onClick } - ) - } -} - -@Preview -@Composable -private fun RegisterScreenPreview() { - AppTheme { - RegisterScreen() - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/component/ErrorBottomSheet.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/component/ErrorBottomSheet.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/component/ErrorBottomSheet.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/component/ErrorBottomSheet.kt index 49d59ff..e9a0023 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/component/ErrorBottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/component/ErrorBottomSheet.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screen.register.component +package org.example.project.presentation.screens.register.component import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/component/PoliciesAndConditionsBottomSheet.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/component/PoliciesAndConditionsBottomSheet.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/component/PoliciesAndConditionsBottomSheet.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/component/PoliciesAndConditionsBottomSheet.kt index ca890bc..717c046 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screen/register/component/PoliciesAndConditionsBottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/register/component/PoliciesAndConditionsBottomSheet.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screen.register.component +package org.example.project.presentation.screens.register.component import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues From d5a8afc094c8e0b050ed4a315b747cc080f4aaa3 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:32:21 +0300 Subject: [PATCH 03/19] add dependencies for coil and ktor --- composeApp/build.gradle.kts | 19 +++++++++++++++++++ gradle/libs.versions.toml | 38 ++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 33e72e3..2707e31 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -34,6 +34,9 @@ kotlin { implementation(libs.koin.android) implementation(libs.koin.androidx.compose) + + implementation(libs.ktor.client.okhttp) + } commonMain.dependencies { implementation(compose.runtime) @@ -46,15 +49,31 @@ kotlin { implementation(libs.androidx.lifecycle.runtimeCompose) api(libs.koin.core) + api(libs.koin.annotations) + implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) implementation(libs.lifecycle.viewmodel) implementation(libs.navigation.compose) + + 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) + + } commonTest.dependencies { implementation(libs.kotlin.test) } + + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } } + } android { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c51c471..8920498 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,17 +9,26 @@ androidx-core = "1.17.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.9.3" androidx-testExt = "1.3.0" +coilComposeVersion = "3.3.0" composeMultiplatform = "1.8.2" junit = "4.13.2" kotlin = "2.2.10" +coil = "3.3.0" + koin = "3.6.0-Beta4" koinComposeMultiplatform = "1.2.0-Beta4" navigationCompose = "2.8.0-alpha02" lifecycleViewModel = "2.8.2" +ktor = "3.2.3" +kotlinx-serialization = "1.6.3" +koin-annotations = "2.1.0" + + [libraries] +coil3-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilComposeVersion" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { module = "junit:junit", version.ref = "junit" } @@ -36,12 +45,39 @@ koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" } +koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations" } + koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" } navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +# ktor +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } + +# Coil +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" } + + + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } + +[bundles] +coil = [ + "coil", + "coil-compose-core", + "coil-compose", + "coil-network-ktor3", +] \ No newline at end of file From 8e1d30d753bd66595f21ccaad9e51105cf4bdc7f Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:35:41 +0300 Subject: [PATCH 04/19] add onboarding repo and entity --- .../org/example/project/domain/entity/OnboardingItem.kt | 7 +++++++ .../project/domain/repository/OnboardingRepository.kt | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/entity/OnboardingItem.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/repository/OnboardingRepository.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/OnboardingItem.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/OnboardingItem.kt new file mode 100644 index 0000000..8bdd5b9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/OnboardingItem.kt @@ -0,0 +1,7 @@ +package org.example.project.domain.entity + +data class OnboardingItem( + val imageRes : String, + val title : String, + val description : String +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/OnboardingRepository.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/OnboardingRepository.kt new file mode 100644 index 0000000..ee2dc6b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/OnboardingRepository.kt @@ -0,0 +1,7 @@ +package org.example.project.domain.repository + +import org.example.project.domain.entity.OnboardingItem + +interface OnboardingRepository { + suspend fun getOnboardingData(): List +} \ No newline at end of file From 0cf480fdeddadea13b078505e3f0b7807a291f13 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:36:34 +0300 Subject: [PATCH 05/19] add onboarding repo implementation , dto and response --- .../example/project/data/dto/OnBoardingDto.kt | 16 ++++++++++++ .../project/data/mapper/OnboardingMapper.kt | 11 ++++++++ .../repository/OnboardingRepositoryImp.kt | 25 +++++++++++++++++++ .../data/response/OnboardingResponse.kt | 11 ++++++++ 4 files changed, 63 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnBoardingDto.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnBoardingDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnBoardingDto.kt new file mode 100644 index 0000000..e662c34 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnBoardingDto.kt @@ -0,0 +1,16 @@ +package org.example.project.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class OnBoardingDto( + @SerialName("id") + val id : String, + @SerialName("title") + val title : String, + @SerialName("imageRes") + val imageUrl : String, + @SerialName("description") + val description : String +) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt new file mode 100644 index 0000000..7deece6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt @@ -0,0 +1,11 @@ +package org.example.project.data.mapper + +import org.example.project.data.dto.OnBoardingDto +import org.example.project.domain.entity.OnboardingItem + +fun OnBoardingDto.toEntity() : OnboardingItem = + OnboardingItem( + imageRes = imageUrl, + title = title, + description = description + ) \ 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 new file mode 100644 index 0000000..301105e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt @@ -0,0 +1,25 @@ +package org.example.project.data.repository + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import org.example.project.data.mapper.toEntity +import org.example.project.data.response.OnboardingResponse +import org.example.project.data.utils.NetworkConstants.ONBOARDING_END_POINT +import org.example.project.data.utils.safeApiCall +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 +) : OnboardingRepository { + + override suspend fun getOnboardingData(): List { + return safeApiCall { + httpClient.get("/$ONBOARDING_END_POINT") + }.onboardingData.map { it.toEntity() } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt new file mode 100644 index 0000000..c15bffe --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt @@ -0,0 +1,11 @@ +package org.example.project.data.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.example.project.data.dto.OnBoardingDto + +@Serializable +data class OnboardingResponse( + @SerialName("onboarding_data") + val onboardingData : List +) From c996d1b8b6f79343d84adcc122a7c4022a97ea35 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:37:21 +0300 Subject: [PATCH 06/19] add safe call function , constants object and ktor setup --- .../project/data/utils/NetworkConstants.kt | 5 + .../example/project/data/utils/safeApiCall.kt | 92 +++++++++++++++++++ .../org/example/project/di/NetworkModule.kt | 52 +++++++++++ 3 files changed, 149 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt new file mode 100644 index 0000000..8e4be7f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt @@ -0,0 +1,5 @@ +package org.example.project.data.utils + +object NetworkConstants { + const val ONBOARDING_END_POINT = "onboarding" //will be edited +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt new file mode 100644 index 0000000..f0a034f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt @@ -0,0 +1,92 @@ +package org.example.project.data.utils + +import io.ktor.client.call.body +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpStatusCode +import io.ktor.util.network.UnresolvedAddressException +import kotlinx.io.IOException + +internal suspend inline fun safeApiCall( + execute: suspend () -> HttpResponse +): T { + val result = try { + execute() + } catch (exception: IOException) { + logError(SAFE_API_CALL_TAG, "IOException", exception.message.toString()) + + } catch (exception: UnresolvedAddressException) { + logError(SAFE_API_CALL_TAG, "UnresolvedAddressException", exception.message.toString()) + } catch (exception: Exception) { + logError(SAFE_API_CALL_TAG, "Unknown exception", exception.message.toString()) + } + + return handleResponseStatusCode(result as HttpResponse) +} + +private suspend inline fun handleResponseStatusCode(result: HttpResponse): T { + return when (result.status.value) { + in 200..299 -> { + result.body() + } + + in 400..499 -> { + when (result.status) { + HttpStatusCode.Unauthorized -> { + logError( + HANDLE_ERROR_STATUS_TAG, + "Unauthorized", + "Not authorized to do this action" + ) + throw Exception() + } + + HttpStatusCode.NotFound -> { + logError( + HANDLE_ERROR_STATUS_TAG, + "Not found", + "the resource you requested could not be found" + ) + throw Exception() + } + + else -> { + logError( + HANDLE_ERROR_STATUS_TAG, + "Unknown 400s status code ${result.status.value}", + "An error with status code ${result.status.value} happened" + ) + throw Exception() + } + } + } + + in 500..599 -> { + logError( + HANDLE_ERROR_STATUS_TAG, + "Server error", + "An error occurred on the server side" + ) + throw Exception() + } + + else -> { + logError( + HANDLE_ERROR_STATUS_TAG, + "Unknown status code ${result.status.value}", + "An error with status code ${result.status.value} happened" + ) + throw Exception() + } + } +} + +private fun logError( + tag: String, + type: String, + message: String +) { + println("$tag----------- : $type $message") +} + +private const val SAFE_API_CALL_TAG = "safeApiCall" +private const val HANDLE_ERROR_STATUS_TAG = "handleErrorStatus" \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt new file mode 100644 index 0000000..6e7b787 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt @@ -0,0 +1,52 @@ +package org.example.project.di + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json +import io.ktor.client.plugins.logging.Logger +import kotlinx.serialization.json.Json +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +class NetworkModule { + @Single + fun provideHttpClient(): HttpClient { + return HttpClient { + defaultRequest { + // url("") // TODO: add base url + } + + install(Logging) { + level = LogLevel.ALL + logger = object : Logger { + override fun log(message: String) { + println(message) + } + } + } + + install(HttpTimeout) { + connectTimeoutMillis = TIME_OUT_INTERVAL_MILLI + requestTimeoutMillis = TIME_OUT_INTERVAL_MILLI + } + + install(ContentNegotiation) { + json( + Json { + isLenient = true + ignoreUnknownKeys = true + } + ) + } + } + } + + private companion object { + const val TIME_OUT_INTERVAL_MILLI = 15_000L + } +} \ No newline at end of file From 1daa939c1f7eefdc54863fb1d31a5259a5530f5e Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:38:22 +0300 Subject: [PATCH 07/19] add state management for onboarding screen --- .../onboarding/OnboardingScreenEffect.kt | 7 +++ .../OnboardingScreenInteractionListener.kt | 7 +++ .../onboarding/OnboardingScreenState.kt | 10 +++ .../onboarding/model/OnboardingUiState.kt | 16 +++++ .../viewmodel/OnboardingViewModel.kt | 62 +++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenEffect.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenInteractionListener.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/model/OnboardingUiState.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenEffect.kt new file mode 100644 index 0000000..a3760f3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenEffect.kt @@ -0,0 +1,7 @@ +package org.example.project.presentation.screens.onboarding + +interface OnboardingScreenEffect { + object NavigateToNext : OnboardingScreenEffect + object NavigateToGetStartedScreen : OnboardingScreenEffect + object NavigateToRegisterScreen : OnboardingScreenEffect +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenInteractionListener.kt new file mode 100644 index 0000000..2b3a48d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenInteractionListener.kt @@ -0,0 +1,7 @@ +package org.example.project.presentation.screens.onboarding + +interface OnboardingScreenInteractionListener { + fun onSkipClick() + fun onNextClick() + fun onGetStartedClick() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt new file mode 100644 index 0000000..5d13118 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt @@ -0,0 +1,10 @@ +package org.example.project.presentation.screens.onboarding + +import org.example.project.presentation.screens.onboarding.model.OnboardingUiState +import org.example.project.presentation.viewmodel.base.ErrorUiState + +data class OnboardingScreenState( + val onboardingData: List = emptyList(), + val loading: Boolean = false, + val errorMessage : ErrorUiState = ErrorUiState("") +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/model/OnboardingUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/model/OnboardingUiState.kt new file mode 100644 index 0000000..a725b93 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/model/OnboardingUiState.kt @@ -0,0 +1,16 @@ +package org.example.project.presentation.screens.onboarding.model + +import org.example.project.domain.entity.OnboardingItem + +data class OnboardingUiState( + val imageRes: String, + val title: String, + val description: String +) + +fun OnboardingItem.toUiState() : OnboardingUiState = + OnboardingUiState( + imageRes = imageRes, + title = title, + description = description + ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt new file mode 100644 index 0000000..6134650 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt @@ -0,0 +1,62 @@ +package org.example.project.presentation.viewmodel + +import kotlinx.coroutines.CoroutineDispatcher +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.presentation.screens.onboarding.OnboardingScreenEffect +import org.example.project.presentation.screens.onboarding.OnboardingScreenInteractionListener +import org.example.project.presentation.screens.onboarding.OnboardingScreenState +import org.example.project.presentation.screens.onboarding.model.toUiState +import org.example.project.presentation.viewmodel.base.BaseViewModel +import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.Provided + + +@KoinViewModel +class OnboardingViewModel( + @Provided private val repository: OnboardingRepository, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseViewModel(initialState = OnboardingScreenState()), + OnboardingScreenInteractionListener { + + init { + loadData() + } + + + private fun loadData() { + tryToCall( + call = { + updateState { it.copy(loading = true) } + repository.getOnboardingData() + }, + onSuccess = { ::onLoadDataSuccess }, + onError = { errorState -> updateState { it.copy(errorMessage = errorState) } }, + dispatcher = ioDispatcher + ) + } + + private fun onLoadDataSuccess(data: List) { + updateState { + it.copy( + onboardingData = data.map { item -> item.toUiState() }, + loading = false + ) + } + } + + override fun onSkipClick() { + sendNewEffect(OnboardingScreenEffect.NavigateToGetStartedScreen) + } + + override fun onNextClick() { + sendNewEffect(OnboardingScreenEffect.NavigateToNext) + } + + override fun onGetStartedClick() { + sendNewEffect(OnboardingScreenEffect.NavigateToRegisterScreen) + } +} \ No newline at end of file From dcbf8aa9d0bd94b3b0030fa99eae16e948864273 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 29 Sep 2025 15:38:56 +0300 Subject: [PATCH 08/19] update screen and OnBoardingItem with view model --- .../screens/onboarding/OnBoardingScreen.kt | 68 ++++++++----------- .../onboarding/composable/OnBoardingItem.kt | 19 ++---- 2 files changed, 34 insertions(+), 53 deletions(-) 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 0b17fd7..366dc18 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 @@ -17,55 +17,54 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope 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.get_started import crafto.composeapp.generated.resources.next -import crafto.composeapp.generated.resources.onboarding1 -import crafto.composeapp.generated.resources.onboarding2 -import crafto.composeapp.generated.resources.onboarding3 import crafto.composeapp.generated.resources.skip import kotlinx.coroutines.launch -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.composable.OnBoardingPage import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.PrimaryButton 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.viewmodel.OnboardingViewModel import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel @Composable fun OnBoardingScreen( - onBoardingPagesContent: List + viewModel: OnboardingViewModel = koinViewModel() + ) { + val state by viewModel.state.collectAsStateWithLifecycle() OnBoardingContent( - onSkipButtonClick = {}, - onGetStartButtonClick = {}, - onBoardingPagesContent = onBoardingPagesContent + state = state, + interactions = viewModel ) } @Composable fun OnBoardingContent( - onBoardingPagesContent: List, - onSkipButtonClick: () -> Unit, - onGetStartButtonClick: () -> Unit, - modifier: Modifier = Modifier + state : OnboardingScreenState, + interactions: OnboardingScreenInteractionListener, ) { val pagerState = - rememberPagerState(initialPage = 0, pageCount = { onBoardingPagesContent.size }) + rememberPagerState(initialPage = 0, pageCount = { state.onboardingData.size }) val coroutineScope = rememberCoroutineScope() val buttonText = - if (pagerState.currentPage == onBoardingPagesContent.size - 1) Res.string.get_started else Res.string.next + if (pagerState.currentPage == state.onboardingData.size - 1) Res.string.get_started else Res.string.next Column( - modifier = modifier.fillMaxSize() + modifier = Modifier.fillMaxSize() .background(AppTheme.craftoColors.background.screen) .padding(horizontal = 16.dp) .systemBarsPadding() @@ -88,7 +87,7 @@ fun OnBoardingContent( ) ) } - onSkipButtonClick() + interactions::onSkipClick }, buttonState = ButtonState.Enable, containerColor = AppTheme.craftoColors.button.secondary, @@ -100,7 +99,7 @@ fun OnBoardingContent( state = pagerState, modifier = Modifier.padding(vertical = 32.dp) ) { page -> - OnBoardingItem(onBoardingPagesContent[page]) + OnBoardingItem(state.onboardingData[page]) } Row( @@ -120,7 +119,7 @@ fun OnBoardingContent( text = stringResource(buttonText), enabled = true, onClick = { - if (pagerState.currentPage < onBoardingPagesContent.size - 1) { + if (pagerState.currentPage < state.onboardingData.size - 1) { val nextPage = pagerState.currentPage + 1 coroutineScope.launch { pagerState.animateScrollToPage( @@ -132,7 +131,7 @@ fun OnBoardingContent( ) } } else { - onGetStartButtonClick() + interactions::onGetStartedClick } }, buttonState = ButtonState.Enable, @@ -155,25 +154,14 @@ fun OnBoardingContent( private fun OnBoardingScreenPreview() { AppTheme(isDarkTheme = false) { OnBoardingContent( - onSkipButtonClick = {}, - onGetStartButtonClick = {}, - onBoardingPagesContent = listOf( - OnBoardingPage( - imageRes = Res.drawable.onboarding1, - title = "Relax, We’ve Got It Covered", - description = "From the comfort of your couch, post your request and let trusted professionals come to you, no calls, no stress." - ), - OnBoardingPage( - imageRes = Res.drawable.onboarding2, - title = "Find What You Need in Seconds", - description = "Browse dozens of home services — from quick fixes to big projects. Just tap a category and get started instantly." - ), - OnBoardingPage( - imageRes = Res.drawable.onboarding3, - title = "Post, Compare Offers and Choose!", - description = "Receive multiple offers from nearby professionals, check their prices and ratings, then pick the one that suits you best." - ), - ) + state = OnboardingScreenState(), + interactions = object : OnboardingScreenInteractionListener { + override fun onSkipClick() {} + + override fun onNextClick() {} + + override fun onGetStartedClick() {} + } ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt index 0428cb0..8446ea2 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt @@ -1,6 +1,5 @@ package org.example.project.presentation.screens.onboarding.composable -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,20 +12,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.painterResource - -data class OnBoardingPage( - val imageRes: DrawableResource, - val title: String, - val description: String -) +import org.example.project.presentation.screens.onboarding.model.OnboardingUiState @Composable fun OnBoardingItem( - page: OnBoardingPage, + page: OnboardingUiState, modifier: Modifier = Modifier ) { Column( @@ -41,10 +34,10 @@ fun OnBoardingItem( ).padding(bottom = 32.dp).height(335.dp) ) { - Image( - painter = painterResource(page.imageRes), + AsyncImage( + model = page.imageRes, contentDescription = "OnBoarding Image", - contentScale = ContentScale.FillBounds + contentScale = ContentScale.FillBounds ) } From a992b37f97da4e44662930fd5b8be4b53a0ba545 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Tue, 30 Sep 2025 00:12:28 +0300 Subject: [PATCH 09/19] Fix: Add base URL and remove comment in NetworkConstants --- .../kotlin/org/example/project/data/utils/NetworkConstants.kt | 2 +- .../commonMain/kotlin/org/example/project/di/NetworkModule.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt index 8e4be7f..7a4e04c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt @@ -1,5 +1,5 @@ package org.example.project.data.utils object NetworkConstants { - const val ONBOARDING_END_POINT = "onboarding" //will be edited + const val ONBOARDING_END_POINT = "onboarding" } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt index 6e7b787..c965ae4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt @@ -5,9 +5,9 @@ import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.serialization.kotlinx.json.json -import io.ktor.client.plugins.logging.Logger import kotlinx.serialization.json.Json import org.koin.core.annotation.Module import org.koin.core.annotation.Single @@ -18,7 +18,7 @@ class NetworkModule { fun provideHttpClient(): HttpClient { return HttpClient { defaultRequest { - // url("") // TODO: add base url + url("http://localhost:8085/") } install(Logging) { From 49159d87c3a13b9a8cfc6be48eba320579f621b4 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Wed, 1 Oct 2025 12:24:05 +0300 Subject: [PATCH 10/19] Refactor: Remove unused dependencies and versions --- composeApp/build.gradle.kts | 2 -- gradle/libs.versions.toml | 5 ----- 2 files changed, 7 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index aed6de8..6cc101a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -59,8 +59,6 @@ kotlin { implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) - implementation(libs.lifecycle.viewmodel) - implementation(libs.navigation.compose) implementation(libs.ktor.client.core) implementation(libs.ktor.serialization.kotlinx.json) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f9e3ef..57f5223 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,11 +26,6 @@ kotlinx-serialization = "1.6.3" koin-annotations = "2.1.0" -kotlin = "2.2.20" -koin = "4.1.1" -koinComposeMultiplatform = "4.1.1" - - [libraries] adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "adaptive" } adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "adaptive" } From 1a4e5628ce0ba6b6a166fd5a5b9f978e9cdd96b5 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Fri, 3 Oct 2025 21:12:11 +0300 Subject: [PATCH 11/19] feat: Integrate NetworkModule and dependencies --- .../src/commonMain/kotlin/org/example/project/di/initKoin.kt | 2 +- gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 efae486..a4aba07 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt @@ -7,6 +7,6 @@ import org.koin.ksp.generated.module fun initKoin(config: KoinAppDeclaration? = null) { startKoin { config?.invoke(this) - modules(CraftoModule().module) + modules(CraftoModule().module, NetworkModule().module) } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d23f55..70b3f00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,8 @@ coil = "3.3.0" koinComposeMultiplatform = "4.1.1" ksp = "2.2.10-2.0.2" koin-annotations = "2.1.0" +ktor = "2.3.5" +kotlinx-serialization = "1.5.1" From 2e4af5f15179f2f021f8e8bdfd594e78038ddf93 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 6 Oct 2025 22:48:02 +0300 Subject: [PATCH 12/19] feat: Update Ktor and add new dependencies --- composeApp/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 6e7fb88..bf0c799 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.google.services) alias(libs.plugins.firebase.crashlytics) alias(libs.plugins.ksp) + alias(libs.plugins.kotlinx.serialization) } kotlin { @@ -42,6 +43,8 @@ kotlin { implementation(libs.firebase.crashlytics.ktx) implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.android) } commonMain.dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70b3f00..46e4f10 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ coil = "3.3.0" koinComposeMultiplatform = "4.1.1" ksp = "2.2.10-2.0.2" koin-annotations = "2.1.0" -ktor = "2.3.5" +ktor = "3.2.3" kotlinx-serialization = "1.5.1" @@ -28,6 +28,7 @@ google-services = "4.4.3" #newer version of firebase-bom causes gradle errors firebase-bom = "33.16.0" firebaseCrashlytics = "3.0.6" +ktorClientCio = "3.3.0" [libraries] @@ -57,6 +58,7 @@ koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } # ktor +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCio" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } @@ -64,6 +66,9 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } + + # Coil coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } @@ -82,6 +87,8 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +kotlinx-serialization = {id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } + [bundles] coil = [ From 8a893c6e9017ce078c6c1f28200dad2695516583 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 6 Oct 2025 22:48:18 +0300 Subject: [PATCH 13/19] feat: Implement platform-specific Ktor HTTP engines --- .../kotlin/org/example/project/di/getHttpEngine.android.kt | 6 ++++++ .../kotlin/org/example/project/di/getHttpEngine.kt | 5 +++++ .../kotlin/org/example/project/di/getHttpEngine.ios.kt | 6 ++++++ 3 files changed, 17 insertions(+) create mode 100644 composeApp/src/androidMain/kotlin/org/example/project/di/getHttpEngine.android.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/getHttpEngine.kt create mode 100644 composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt diff --git a/composeApp/src/androidMain/kotlin/org/example/project/di/getHttpEngine.android.kt b/composeApp/src/androidMain/kotlin/org/example/project/di/getHttpEngine.android.kt new file mode 100644 index 0000000..596c727 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/org/example/project/di/getHttpEngine.android.kt @@ -0,0 +1,6 @@ +package org.example.project.di + +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.cio.CIO + +actual fun getHttpEngine(): HttpClientEngine = CIO.create() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/getHttpEngine.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/getHttpEngine.kt new file mode 100644 index 0000000..f0e2eeb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/getHttpEngine.kt @@ -0,0 +1,5 @@ +package org.example.project.di + +import io.ktor.client.engine.HttpClientEngine + +expect fun getHttpEngine(): HttpClientEngine \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt b/composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt new file mode 100644 index 0000000..fed8979 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt @@ -0,0 +1,6 @@ +package org.example.project.di + +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.darwin.Darwin + +actual fun getHttpEngine(): HttpClientEngine = Darwin.create() \ No newline at end of file From 0d1871d5fe2ff039fa51ae9c7a257ad6b5d89ed4 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 6 Oct 2025 22:48:30 +0300 Subject: [PATCH 14/19] Chore: Update base URL and HttpClient engine --- .../commonMain/kotlin/org/example/project/di/NetworkModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt index c965ae4..f3af83a 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt @@ -16,9 +16,9 @@ import org.koin.core.annotation.Single class NetworkModule { @Single fun provideHttpClient(): HttpClient { - return HttpClient { + return HttpClient(engine = getHttpEngine()) { defaultRequest { - url("http://localhost:8085/") + url("http://10.0.2.2:8085/") } install(Logging) { From 1060f357910e8092a30b204628c8464b7e7a6a16 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 6 Oct 2025 22:48:49 +0300 Subject: [PATCH 15/19] Refactor: Remove OnboardingResponse wrapper --- .../repository/OnboardingRepositoryImp.kt | 6 +- .../data/response/OnboardingResponse.kt | 11 -- .../screens/onboarding/OnBoardingScreen.kt | 150 ++++++++++-------- .../onboarding/composable/OnBoardingItem.kt | 2 + .../viewmodel/OnboardingViewModel.kt | 2 +- 5 files changed, 86 insertions(+), 85 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt 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 301105e..76f8411 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 @@ -2,8 +2,8 @@ package org.example.project.data.repository import io.ktor.client.HttpClient import io.ktor.client.request.get +import org.example.project.data.dto.OnBoardingDto import org.example.project.data.mapper.toEntity -import org.example.project.data.response.OnboardingResponse import org.example.project.data.utils.NetworkConstants.ONBOARDING_END_POINT import org.example.project.data.utils.safeApiCall import org.example.project.domain.entity.OnboardingItem @@ -18,8 +18,8 @@ class OnboardingRepositoryImp( ) : OnboardingRepository { override suspend fun getOnboardingData(): List { - return safeApiCall { + return safeApiCall> { httpClient.get("/$ONBOARDING_END_POINT") - }.onboardingData.map { it.toEntity() } + }.map { it.toEntity() } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt deleted file mode 100644 index c15bffe..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/response/OnboardingResponse.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.project.data.response - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.example.project.data.dto.OnBoardingDto - -@Serializable -data class OnboardingResponse( - @SerialName("onboarding_data") - val onboardingData : List -) 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 366dc18..926fc40 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 @@ -10,19 +10,21 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope 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.get_started import crafto.composeapp.generated.resources.next @@ -44,7 +46,7 @@ fun OnBoardingScreen( viewModel: OnboardingViewModel = koinViewModel() ) { - val state by viewModel.state.collectAsStateWithLifecycle() + val state by viewModel.state.collectAsState() OnBoardingContent( state = state, interactions = viewModel @@ -53,7 +55,7 @@ fun OnBoardingScreen( @Composable fun OnBoardingContent( - state : OnboardingScreenState, + state: OnboardingScreenState, interactions: OnboardingScreenInteractionListener, ) { val pagerState = @@ -62,7 +64,6 @@ fun OnBoardingContent( val buttonText = if (pagerState.currentPage == state.onboardingData.size - 1) Res.string.get_started else Res.string.next - Column( modifier = Modifier.fillMaxSize() .background(AppTheme.craftoColors.background.screen) @@ -70,80 +71,89 @@ fun OnBoardingContent( .systemBarsPadding() .verticalScroll(rememberScrollState()) ) { - Box( - modifier = Modifier.padding(top = 24.dp).align(Alignment.End), - ) { - SecondaryButton( - text = stringResource(Res.string.skip), - enabled = true, - onClick = { - val skipPage = pagerState.pageCount - 1 - coroutineScope.launch { - pagerState.animateScrollToPage( - page = skipPage, - animationSpec = spring( - dampingRatio = 0.85f, - stiffness = 44f - ) - ) - } - interactions::onSkipClick - }, - buttonState = ButtonState.Enable, - containerColor = AppTheme.craftoColors.button.secondary, - contentPadding = PaddingValues(vertical = 14.dp, horizontal = 24.dp) - ) - } - - HorizontalPager( - state = pagerState, - modifier = Modifier.padding(vertical = 32.dp) - ) { page -> - OnBoardingItem(state.onboardingData[page]) - } - - Row( - modifier = Modifier, - verticalAlignment = Alignment.CenterVertically - ) { - OnBoardingIndicator( - currentPage = pagerState.currentPage, - totalPage = pagerState.pageCount, - progressColor = AppTheme.craftoColors.brand.primary, - trackColor = AppTheme.craftoColors.shade.quaternary, - modifier = Modifier.width(100.dp) - ) - Spacer(modifier = Modifier.weight(1f)) - - PrimaryButton( - text = stringResource(buttonText), - enabled = true, - onClick = { - if (pagerState.currentPage < state.onboardingData.size - 1) { - val nextPage = pagerState.currentPage + 1 + if (state.loading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp).padding(top=40.dp), + color = AppTheme.craftoColors.brand.primary + ) + } + } else { + Box( + modifier = Modifier.padding(top = 24.dp).align(Alignment.End), + ) { + SecondaryButton( + text = stringResource(Res.string.skip), + enabled = true, + onClick = { + val skipPage = pagerState.pageCount - 1 coroutineScope.launch { pagerState.animateScrollToPage( - page = nextPage, + page = skipPage, animationSpec = spring( dampingRatio = 0.85f, - stiffness = 440f + stiffness = 44f ) ) } - } else { - interactions::onGetStartedClick - } - }, - buttonState = ButtonState.Enable, - modifier = Modifier.animateContentSize( - animationSpec = spring( - dampingRatio = 0.85f, - stiffness = 44f, + interactions::onSkipClick + }, + buttonState = ButtonState.Enable, + containerColor = AppTheme.craftoColors.button.secondary, + contentPadding = PaddingValues(vertical = 14.dp, horizontal = 24.dp) + ) + } + + HorizontalPager( + state = pagerState, + modifier = Modifier.padding(vertical = 32.dp) + ) { page -> + OnBoardingItem(state.onboardingData[page]) + } + + Row( + modifier = Modifier, + verticalAlignment = Alignment.CenterVertically + ) { + OnBoardingIndicator( + currentPage = pagerState.currentPage, + totalPage = pagerState.pageCount, + progressColor = AppTheme.craftoColors.brand.primary, + trackColor = AppTheme.craftoColors.shade.quaternary, + modifier = Modifier.width(100.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + + PrimaryButton( + text = stringResource(buttonText), + enabled = true, + onClick = { + if (pagerState.currentPage < state.onboardingData.size - 1) { + val nextPage = pagerState.currentPage + 1 + coroutineScope.launch { + pagerState.animateScrollToPage( + page = nextPage, + animationSpec = spring( + dampingRatio = 0.85f, + stiffness = 440f + ) + ) + } + } else { + interactions::onGetStartedClick + } + }, + buttonState = ButtonState.Enable, + modifier = Modifier.animateContentSize( + animationSpec = spring( + dampingRatio = 0.85f, + stiffness = 44f, + ), + alignment = Alignment.BottomEnd ), - alignment = Alignment.BottomEnd - ), - contentPadding = PaddingValues(vertical = 14.dp, horizontal = 24.dp) - ) + contentPadding = PaddingValues(vertical = 14.dp, horizontal = 24.dp) + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt index 7e6e4f0..86d677c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt @@ -3,6 +3,7 @@ package org.example.project.presentation.screens.onboarding.composable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -37,6 +38,7 @@ fun OnBoardingItem( AsyncImage( model = page.imageRes, contentDescription = "OnBoarding Image", + modifier= Modifier.fillMaxWidth(), contentScale = ContentScale.FillBounds ) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt index 6134650..b6b3c6c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt @@ -33,7 +33,7 @@ class OnboardingViewModel( updateState { it.copy(loading = true) } repository.getOnboardingData() }, - onSuccess = { ::onLoadDataSuccess }, + onSuccess = ::onLoadDataSuccess , onError = { errorState -> updateState { it.copy(errorMessage = errorState) } }, dispatcher = ioDispatcher ) From 51367f11d07524d1d0a0b11c7adb4c8619d3687f Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Mon, 6 Oct 2025 22:51:34 +0300 Subject: [PATCH 16/19] feat: Replace AccountSetupCategoryScreen with OnBoardingScreen --- composeApp/src/commonMain/kotlin/org/example/project/App.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index bda642c..7c5685b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -2,13 +2,13 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.ui.screens.setupScreens.AccountSetupCategoryScreen +import org.example.project.presentation.screens.onboarding.OnBoardingScreen import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @Preview fun App() { AppTheme { - AccountSetupCategoryScreen() + OnBoardingScreen() } } \ No newline at end of file From 0a24af53bbaeda39b0c7627f0690209b84b6dc6e Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Wed, 8 Oct 2025 15:41:10 +0300 Subject: [PATCH 17/19] Refactor: Improve Onboarding screen UI and structure --- .../screens/onboarding/OnBoardingScreen.kt | 123 +++++++++++------- .../onboarding/composable/OnBoardingItem.kt | 39 +++--- 2 files changed, 94 insertions(+), 68 deletions(-) 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 926fc40..7e5e56f 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 @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -29,6 +30,7 @@ import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.get_started import crafto.composeapp.generated.resources.next import crafto.composeapp.generated.resources.skip +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.PrimaryButton @@ -37,6 +39,7 @@ 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.viewmodel.OnboardingViewModel +import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel @@ -54,27 +57,26 @@ fun OnBoardingScreen( } @Composable -fun OnBoardingContent( +private fun OnBoardingContent( state: OnboardingScreenState, interactions: OnboardingScreenInteractionListener, ) { - val pagerState = - rememberPagerState(initialPage = 0, pageCount = { state.onboardingData.size }) + val pagerState = rememberPagerState(initialPage = 0, pageCount = { state.onboardingData.size }) val coroutineScope = rememberCoroutineScope() - val buttonText = - if (pagerState.currentPage == state.onboardingData.size - 1) Res.string.get_started else Res.string.next + val buttonText = if (pagerState.currentPage == state.onboardingData.size - 1) Res.string.get_started else Res.string.next Column( modifier = Modifier.fillMaxSize() .background(AppTheme.craftoColors.background.screen) .padding(horizontal = 16.dp) .systemBarsPadding() - .verticalScroll(rememberScrollState()) - ) { + .verticalScroll(rememberScrollState()), + + ) { if (state.loading) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator( - modifier = Modifier.size(24.dp).padding(top=40.dp), + modifier = Modifier.size(24.dp).padding(top = 40.dp), color = AppTheme.craftoColors.brand.primary ) } @@ -111,53 +113,74 @@ fun OnBoardingContent( OnBoardingItem(state.onboardingData[page]) } - Row( - modifier = Modifier, - verticalAlignment = Alignment.CenterVertically - ) { - OnBoardingIndicator( - currentPage = pagerState.currentPage, - totalPage = pagerState.pageCount, - progressColor = AppTheme.craftoColors.brand.primary, - trackColor = AppTheme.craftoColors.shade.quaternary, - modifier = Modifier.width(100.dp) - ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - PrimaryButton( - text = stringResource(buttonText), - enabled = true, - onClick = { - if (pagerState.currentPage < state.onboardingData.size - 1) { - val nextPage = pagerState.currentPage + 1 - coroutineScope.launch { - pagerState.animateScrollToPage( - page = nextPage, - animationSpec = spring( - dampingRatio = 0.85f, - stiffness = 440f - ) - ) - } - } else { - interactions::onGetStartedClick - } - }, - buttonState = ButtonState.Enable, - modifier = Modifier.animateContentSize( - animationSpec = spring( - dampingRatio = 0.85f, - stiffness = 44f, - ), - alignment = Alignment.BottomEnd - ), - contentPadding = PaddingValues(vertical = 14.dp, horizontal = 24.dp) - ) - } + OnboardingActionsRow( + pagerState = pagerState, + coroutineScope = coroutineScope, + state = state, + interactions = interactions, + buttonText = buttonText, + modifier = Modifier.padding(bottom = 24.dp) + ) } } } +@Composable +private fun OnboardingActionsRow( + modifier: Modifier, + pagerState: PagerState, + coroutineScope: CoroutineScope, + state: OnboardingScreenState, + interactions: OnboardingScreenInteractionListener, + buttonText: StringResource +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + OnBoardingIndicator( + currentPage = pagerState.currentPage, + totalPage = pagerState.pageCount, + progressColor = AppTheme.craftoColors.brand.primary, + trackColor = AppTheme.craftoColors.shade.quaternary, + modifier = Modifier.width(100.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + + PrimaryButton( + text = stringResource(buttonText), + enabled = true, + onClick = { + if (pagerState.currentPage < state.onboardingData.size - 1) { + val nextPage = pagerState.currentPage + 1 + coroutineScope.launch { + pagerState.animateScrollToPage( + page = nextPage, + animationSpec = spring( + dampingRatio = 0.85f, + stiffness = 440f + ) + ) + } + } else { + interactions::onGetStartedClick + } + }, + buttonState = ButtonState.Enable, + modifier = Modifier.animateContentSize( + animationSpec = spring( + dampingRatio = 0.85f, + stiffness = 44f, + ), + alignment = Alignment.BottomEnd + ), + contentPadding = PaddingValues(vertical = 14.dp, horizontal = 24.dp) + ) + } +} + @Preview @Composable diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt index 86d677c..f43c2d4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt @@ -3,7 +3,7 @@ package org.example.project.presentation.screens.onboarding.composable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -26,35 +26,38 @@ fun OnBoardingItem( Column( modifier = modifier.background(AppTheme.craftoColors.background.screen) ) { - Box( modifier = Modifier .background( shape = RoundedCornerShape(AppTheme.craftoRadius.x5l), color = Color.Transparent - ).padding(bottom = 32.dp).height(335.dp) - + ).height(335.dp) ) { AsyncImage( model = page.imageRes, contentDescription = "OnBoarding Image", - modifier= Modifier.fillMaxWidth(), - contentScale = ContentScale.FillBounds + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillBounds, + onError = { println("Image Load : ${it.result.throwable}") }, ) } - Text( - text = page.title, - style = AppTheme.textStyle.display, - color = AppTheme.craftoColors.shade.primary, - modifier = Modifier.padding(bottom = 16.dp) - ) + Column( + modifier = Modifier.padding(top = 32.dp).height(185.dp), + ) { + Text( + text = page.title, + style = AppTheme.textStyle.display, + color = AppTheme.craftoColors.shade.primary, + modifier = Modifier.padding(bottom = 16.dp, top = 19.dp), + ) - Text( - text = page.description, - style = AppTheme.textStyle.body.largeRegular, - color = AppTheme.craftoColors.shade.secondary, - modifier = Modifier.padding(bottom = 32.dp) - ) + Text( + text = page.description, + style = AppTheme.textStyle.body.largeRegular, + color = AppTheme.craftoColors.shade.secondary, + modifier = Modifier.padding(bottom = 19.dp) + ) + } } } \ No newline at end of file From eb306ffc3549e1041fa579e6bd4bdd971c9c3c9c Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Wed, 8 Oct 2025 18:31:55 +0300 Subject: [PATCH 18/19] Apply consistent naming and updates to Onboarding feature --- .../dto/{OnBoardingDto.kt => OnboardingDto.kt} | 2 +- .../project/data/mapper/OnboardingMapper.kt | 4 ++-- .../data/repository/OnboardingRepositoryImp.kt | 6 +++--- .../project/data/utils/NetworkConstants.kt | 2 +- .../org/example/project/di/NetworkModule.kt | 3 ++- .../{OnBoardingScreen.kt => OnboardingScreen.kt} | 16 ++++++++-------- .../screens/onboarding/OnboardingScreenState.kt | 2 +- ...ardingIndicator.kt => OnboardingIndicator.kt} | 2 +- .../{OnBoardingItem.kt => OnboardingItem.kt} | 2 +- 9 files changed, 20 insertions(+), 19 deletions(-) rename composeApp/src/commonMain/kotlin/org/example/project/data/dto/{OnBoardingDto.kt => OnboardingDto.kt} (92%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/{OnBoardingScreen.kt => OnboardingScreen.kt} (96%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/{OnBoardingIndicator.kt => OnboardingIndicator.kt} (98%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/{OnBoardingItem.kt => OnboardingItem.kt} (99%) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnBoardingDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnboardingDto.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnBoardingDto.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnboardingDto.kt index e662c34..3b355fa 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnBoardingDto.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnboardingDto.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class OnBoardingDto( +data class OnboardingDto( @SerialName("id") val id : String, @SerialName("title") diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt index 7deece6..ce1da95 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt @@ -1,9 +1,9 @@ package org.example.project.data.mapper -import org.example.project.data.dto.OnBoardingDto +import org.example.project.data.dto.OnboardingDto import org.example.project.domain.entity.OnboardingItem -fun OnBoardingDto.toEntity() : OnboardingItem = +fun OnboardingDto.toEntity() : OnboardingItem = OnboardingItem( imageRes = imageUrl, title = title, 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 76f8411..84365bf 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 @@ -2,7 +2,7 @@ package org.example.project.data.repository import io.ktor.client.HttpClient import io.ktor.client.request.get -import org.example.project.data.dto.OnBoardingDto +import org.example.project.data.dto.OnboardingDto import org.example.project.data.mapper.toEntity import org.example.project.data.utils.NetworkConstants.ONBOARDING_END_POINT import org.example.project.data.utils.safeApiCall @@ -18,8 +18,8 @@ class OnboardingRepositoryImp( ) : OnboardingRepository { override suspend fun getOnboardingData(): List { - return safeApiCall> { - httpClient.get("/$ONBOARDING_END_POINT") + return safeApiCall> { + httpClient.get(ONBOARDING_END_POINT) }.map { it.toEntity() } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt index 7a4e04c..9c7d8c5 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt @@ -1,5 +1,5 @@ package org.example.project.data.utils object NetworkConstants { - const val ONBOARDING_END_POINT = "onboarding" + const val ONBOARDING_END_POINT = "/onboarding" } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt index f3af83a..110d1ba 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt @@ -12,6 +12,7 @@ import kotlinx.serialization.json.Json import org.koin.core.annotation.Module import org.koin.core.annotation.Single +//192.168.1.15 @Module class NetworkModule { @Single @@ -47,6 +48,6 @@ class NetworkModule { } private companion object { - const val TIME_OUT_INTERVAL_MILLI = 15_000L + const val TIME_OUT_INTERVAL_MILLI = 10_000L } } \ 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 similarity index 96% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnBoardingScreen.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt index 7e5e56f..ef38cc8 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 @@ -36,8 +36,8 @@ import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.PrimaryButton 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.composable.OnboardingIndicator +import org.example.project.presentation.screens.onboarding.composable.OnboardingItem import org.example.project.presentation.viewmodel.OnboardingViewModel import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource @@ -45,19 +45,19 @@ import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel @Composable -fun OnBoardingScreen( +fun OnboardingScreen( viewModel: OnboardingViewModel = koinViewModel() ) { val state by viewModel.state.collectAsState() - OnBoardingContent( + OnboardingContent( state = state, interactions = viewModel ) } @Composable -private fun OnBoardingContent( +private fun OnboardingContent( state: OnboardingScreenState, interactions: OnboardingScreenInteractionListener, ) { @@ -110,7 +110,7 @@ private fun OnBoardingContent( state = pagerState, modifier = Modifier.padding(vertical = 32.dp) ) { page -> - OnBoardingItem(state.onboardingData[page]) + OnboardingItem(state.onboardingData[page]) } Spacer(modifier = Modifier.weight(1f)) @@ -140,7 +140,7 @@ private fun OnboardingActionsRow( modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { - OnBoardingIndicator( + OnboardingIndicator( currentPage = pagerState.currentPage, totalPage = pagerState.pageCount, progressColor = AppTheme.craftoColors.brand.primary, @@ -186,7 +186,7 @@ private fun OnboardingActionsRow( @Composable private fun OnBoardingScreenPreview() { AppTheme(isDarkTheme = false) { - OnBoardingContent( + OnboardingContent( state = OnboardingScreenState(), interactions = object : OnboardingScreenInteractionListener { override fun onSkipClick() {} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt index 5d13118..6a3a706 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt @@ -6,5 +6,5 @@ import org.example.project.presentation.viewmodel.base.ErrorUiState data class OnboardingScreenState( val onboardingData: List = emptyList(), val loading: Boolean = false, - val errorMessage : ErrorUiState = ErrorUiState("") + val errorMessage : ErrorUiState?= null ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingIndicator.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnboardingIndicator.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingIndicator.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnboardingIndicator.kt index d2f192a..b3fdf26 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingIndicator.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnboardingIndicator.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp import org.example.project.presentation.designsystem.textstyle.AppTheme @Composable -fun OnBoardingIndicator( +fun OnboardingIndicator( currentPage: Int, totalPage: Int, modifier: Modifier = Modifier, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnboardingItem.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnboardingItem.kt index f43c2d4..7a7d44f 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnBoardingItem.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/composable/OnboardingItem.kt @@ -19,7 +19,7 @@ import org.example.project.presentation.screens.onboarding.model.OnboardingUiSta @Composable -fun OnBoardingItem( +fun OnboardingItem( page: OnboardingUiState, modifier: Modifier = Modifier ) { From 31d12d05ebeba4eb92921525f31b7bbcec4e2937 Mon Sep 17 00:00:00 2001 From: Hend Sayed Date: Wed, 8 Oct 2025 19:13:53 +0300 Subject: [PATCH 19/19] Fix: Rename OnBoardingScreen to OnboardingScreen --- composeApp/src/commonMain/kotlin/org/example/project/App.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index 7c5685b..1236764 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -2,13 +2,13 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.screens.onboarding.OnBoardingScreen +import org.example.project.presentation.screens.onboarding.OnboardingScreen import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @Preview fun App() { AppTheme { - OnBoardingScreen() + OnboardingScreen() } } \ No newline at end of file