├── .gitignore ├── LICENSE.txt ├── README.md ├── androidApp ├── build.gradle.kts └── src │ └── androidMain │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ └── com │ │ └── myapplication │ │ ├── App.kt │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ └── values │ ├── ic_launcher_background.xml │ └── strings.xml ├── app_preview.gif ├── build.gradle.kts ├── cleanup.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Configuration │ └── Config.xcconfig ├── Podfile ├── Podfile.swift ├── iosApp.xcodeproj │ └── project.pbxproj └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── readme_images ├── android_app_running.png ├── banner.png ├── edit_run_config.png ├── hello_world_ios.png ├── open_project_view.png ├── run_on_android.png ├── target_device.png └── text_field_added.png ├── settings.gradle.kts └── shared ├── build.gradle.kts └── src ├── androidMain ├── AndroidManifest.xml └── kotlin │ ├── core │ ├── Context.kt │ ├── DataStore.kt │ └── platformModule.kt │ ├── di │ └── Utils.kt │ └── main.android.kt ├── commonMain ├── kotlin │ ├── App.kt │ ├── core │ │ ├── Context.kt │ │ ├── DataStore.kt │ │ ├── platformModule.kt │ │ └── viewModelDefinition.kt │ ├── data │ │ ├── core │ │ │ └── AppDataStoreManager.kt │ │ ├── model │ │ │ ├── Category.kt │ │ │ ├── Product.kt │ │ │ ├── Rating.kt │ │ │ ├── TextFieldState.kt │ │ │ ├── request │ │ │ │ ├── LoginRequest.kt │ │ │ │ └── RegisterModel.kt │ │ │ └── response │ │ │ │ ├── BaseResponse.kt │ │ │ │ ├── LoginResponse.kt │ │ │ │ └── RegisterResponse.kt │ │ ├── network │ │ │ ├── Resource.kt │ │ │ └── Urls.kt │ │ └── repository │ │ │ ├── AppPreferences.kt │ │ │ ├── AuthRepositoryImp.kt │ │ │ ├── CreateDatastore.kt │ │ │ └── HomeRepositoryImp.kt │ ├── di │ │ ├── AppModule.kt │ │ └── Utils.kt │ ├── domain │ │ ├── core │ │ │ └── AppDataStore.kt │ │ ├── repository │ │ │ ├── AuthRepository.kt │ │ │ └── HomeRepository.kt │ │ └── usecase │ │ │ ├── CategoryUseCase.kt │ │ │ ├── GetProfileUseCase.kt │ │ │ ├── LoginUseCase.kt │ │ │ ├── ProductUseCase.kt │ │ │ └── RegisterUseCase.kt │ ├── presentation │ │ ├── base │ │ │ ├── BaseViewModel.kt │ │ │ └── DataStoreKeys.kt │ │ ├── components │ │ │ ├── AppBar.kt │ │ │ ├── AppButtons.kt │ │ │ ├── AppSlider.kt │ │ │ ├── AppText.kt │ │ │ ├── CategoryCardTag.kt │ │ │ ├── CustomDialog.kt │ │ │ ├── Gap.kt │ │ │ ├── LoadingIndicator.kt │ │ │ ├── ModalBottomSheet.kt │ │ │ ├── ProductCard.kt │ │ │ ├── ProfileSectionCard.kt │ │ │ └── TabNavigationItem.kt │ │ ├── screens │ │ │ ├── auth │ │ │ │ ├── login │ │ │ │ │ ├── LoginScreen.kt │ │ │ │ │ └── LoginViewModel.kt │ │ │ │ ├── register │ │ │ │ │ ├── RegisterScreen.kt │ │ │ │ │ └── RegisterViewModel.kt │ │ │ │ └── updateProfile │ │ │ │ │ ├── UpdateProfileScreen.kt │ │ │ │ │ └── UpdateProfileViewModel.kt │ │ │ ├── category │ │ │ │ ├── SelectedCategoryScreen.kt │ │ │ │ └── SelectedCategoryViewModel.kt │ │ │ ├── main │ │ │ │ ├── MainScreen.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ └── taps │ │ │ │ │ ├── category │ │ │ │ │ ├── CategoriesViewModel.kt │ │ │ │ │ ├── CategoryScreen.kt │ │ │ │ │ └── CategoryTab.kt │ │ │ │ │ ├── home │ │ │ │ │ ├── HomeScreen.kt │ │ │ │ │ ├── HomeTab.kt │ │ │ │ │ └── HomeViewModel.kt │ │ │ │ │ ├── profile │ │ │ │ │ ├── ProfileScreen.kt │ │ │ │ │ ├── ProfileTab.kt │ │ │ │ │ └── ProfileViewModel.kt │ │ │ │ │ └── search │ │ │ │ │ ├── SearchScreen.kt │ │ │ │ │ ├── SearchTab.kt │ │ │ │ │ └── SearchViewModel.kt │ │ │ ├── product │ │ │ │ └── DetailScreen.kt │ │ │ ├── settings │ │ │ │ └── SettingsScreen.kt │ │ │ └── splash │ │ │ │ ├── SplashScreen.kt │ │ │ │ └── SplashViewModel.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Dimens.kt │ │ │ └── Type.kt │ └── utils │ │ ├── AppStrings.kt │ │ └── CommonUtil.kt └── resources │ ├── arrow_right.xml │ ├── banner1.png │ ├── banner2.png │ ├── banner3.png │ ├── compose-multiplatform.xml │ ├── flag.xml │ ├── ic_arrow_down.xml │ ├── not_found.png │ ├── visibility.xml │ └── visibility_off.xml └── iosMain └── kotlin ├── core ├── Context.kt ├── DataStore.kt ├── platformModule.kt └── viewModelDefinition.kt ├── di └── Utils.kt └── main.ios.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | build/ 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | iosApp/Podfile.lock 11 | iosApp/Pods/* 12 | iosApp/iosApp.xcworkspace/* 13 | iosApp/iosApp.xcodeproj/* 14 | !iosApp/iosApp.xcodeproj/project.pbxproj 15 | shared/shared.podspec 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Store KMP 2 | Compose Multiplatform Project ( android , ios ) 3 | 4 | ![](app_preview.gif) 5 | 6 | 🔍 Features Snapshot: 7 | - Login, Sign Up ✅ 8 | - Profile and update Profile✅ 9 | - Home ✅ 10 | - Products And ProductDetails✅ 11 | - Categories And Category Details ✅ 12 | - Search, Cart ,Setting ,Logout ✅ 13 | 14 | 15 | 💻 Tech Stack Highlights: 16 | - Clean Archetecture with MVI 17 | - Kotlin Multiplatform 18 | - Kotlin Coroutines 19 | - Compose Multiplatform 20 | - Material3 21 | - Ktor 22 | - Datastore 23 | - Precompose 24 | - Koin 25 | - Voyager 26 | - Moko 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /androidApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.application") 4 | id("org.jetbrains.compose") 5 | } 6 | 7 | kotlin { 8 | android() 9 | sourceSets { 10 | val androidMain by getting { 11 | dependencies { 12 | implementation(project(":shared")) 13 | } 14 | } 15 | } 16 | } 17 | 18 | android { 19 | compileSdk = (findProperty("android.compileSdk") as String).toInt() 20 | namespace = "com.myapplication" 21 | 22 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 23 | 24 | defaultConfig { 25 | applicationId = "com.myapplication.MyApplication" 26 | minSdk = (findProperty("android.minSdk") as String).toInt() 27 | targetSdk = (findProperty("android.targetSdk") as String).toInt() 28 | versionCode = 1 29 | versionName = "1.0" 30 | } 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_17 33 | targetCompatibility = JavaVersion.VERSION_17 34 | } 35 | kotlin { 36 | jvmToolchain(17) 37 | } 38 | } 39 | dependencies { 40 | implementation("io.insert-koin:koin-android:3.4.0") 41 | } 42 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/ic_launcher-playstore.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/kotlin/com/myapplication/App.kt: -------------------------------------------------------------------------------- 1 | package com.myapplication 2 | 3 | 4 | import android.app.Application 5 | import org.koin.core.component.KoinComponent 6 | class App : Application(), KoinComponent { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | // startKoin { 11 | // androidContext(this@App) 12 | // modules(appModule(this@App)) 13 | // } 14 | } 15 | } -------------------------------------------------------------------------------- /androidApp/src/androidMain/kotlin/com/myapplication/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.myapplication 2 | 3 | import MainView 4 | import android.os.Bundle 5 | import androidx.activity.compose.setContent 6 | import androidx.appcompat.app.AppCompatActivity 7 | 8 | class MainActivity : AppCompatActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | setContent { 13 | MainView(application) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4285F4 4 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | KMP Store 3 | -------------------------------------------------------------------------------- /app_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/app_preview.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | kotlin("multiplatform").apply(false) 5 | id("com.android.application").apply(false) 6 | id("com.android.library").apply(false) 7 | id("org.jetbrains.compose").apply(false) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf .idea 3 | ./gradlew clean 4 | rm -rf .gradle 5 | rm -rf build 6 | rm -rf */build 7 | rm -rf iosApp/iosApp.xcworkspace 8 | rm -rf iosApp/Pods 9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace 10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | 4 | #Kotlin 5 | kotlin.code.style=official 6 | 7 | #MPP 8 | kotlin.mpp.stability.nowarn=true 9 | kotlin.mpp.enableCInteropCommonization=true 10 | kotlin.mpp.androidSourceSetLayoutVersion=2 11 | 12 | #Compose 13 | org.jetbrains.compose.experimental.uikit.enabled=true 14 | kotlin.native.cacheKind=none 15 | 16 | #Android 17 | android.useAndroidX=true 18 | android.enableJetifier=true 19 | android.compileSdk=34 20 | android.targetSdk=34 21 | android.minSdk=24 22 | 23 | #Versions 24 | kotlin.version=1.9.10 25 | agp.version=8.0.2 26 | compose.version=1.5.3 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 05 01:48:10 PST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.myapplication.MyApplication 3 | APP_NAME=StoreAppKMP 4 | -------------------------------------------------------------------------------- /iosApp/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'iosApp' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for iosApp 9 | pod 'shared', :path => '../shared' 10 | 11 | 12 | end 13 | -------------------------------------------------------------------------------- /iosApp/Podfile.swift: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'iosApp' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for iosApp 9 | pod 'shared', :path => '../shared' 10 | 11 | 12 | end 13 | 14 | 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import shared 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | Main_iosKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.all, edges: .bottom) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleLocalizations 16 | 17 | en 18 | ar 19 | 20 | CFBundleName 21 | $(PRODUCT_NAME) 22 | CFBundlePackageType 23 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 24 | CFBundleShortVersionString 25 | 1.0 26 | CFBundleVersion 27 | 1 28 | LSRequiresIPhoneOS 29 | 30 | UIApplicationSceneManifest 31 | 32 | UIApplicationSupportsMultipleScenes 33 | 34 | 35 | UILaunchScreen 36 | 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import shared 3 | @main 4 | struct iOSApp: App { 5 | // init(){ 6 | // AppModuleKt.doInitKoin() 7 | // } 8 | var body: some Scene { 9 | WindowGroup { 10 | ZStack { 11 | Color.white.ignoresSafeArea(.all) // status bar color 12 | ContentView() 13 | }.preferredColorScheme(.light) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /readme_images/android_app_running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/android_app_running.png -------------------------------------------------------------------------------- /readme_images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/banner.png -------------------------------------------------------------------------------- /readme_images/edit_run_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/edit_run_config.png -------------------------------------------------------------------------------- /readme_images/hello_world_ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/hello_world_ios.png -------------------------------------------------------------------------------- /readme_images/open_project_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/open_project_view.png -------------------------------------------------------------------------------- /readme_images/run_on_android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/run_on_android.png -------------------------------------------------------------------------------- /readme_images/target_device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/target_device.png -------------------------------------------------------------------------------- /readme_images/text_field_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/text_field_added.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "MyApplication" 2 | 3 | include(":androidApp") 4 | include(":shared") 5 | 6 | pluginManagement { 7 | repositories { 8 | gradlePluginPortal() 9 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 10 | google() 11 | } 12 | 13 | plugins { 14 | val kotlinVersion = extra["kotlin.version"] as String 15 | val agpVersion = extra["agp.version"] as String 16 | val composeVersion = extra["compose.version"] as String 17 | 18 | kotlin("jvm").version(kotlinVersion) 19 | kotlin("multiplatform").version(kotlinVersion) 20 | kotlin("android").version(kotlinVersion) 21 | 22 | id("com.android.application").version(agpVersion) 23 | id("com.android.library").version(agpVersion) 24 | 25 | id("org.jetbrains.compose").version(composeVersion) 26 | id("dev.icerock.moko").version("0.23.0") 27 | } 28 | } 29 | 30 | dependencyResolutionManagement { 31 | repositories { 32 | google() 33 | mavenCentral() 34 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | kotlin("native.cocoapods") 4 | id("com.android.library") 5 | id("org.jetbrains.compose") 6 | // kotlin("plugin.serialization") version "1.8.21" 7 | id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0-Beta1" 8 | 9 | // id("dev.icerock.mobile.multiplatform-resources") version "0.23.0" 10 | 11 | } 12 | 13 | kotlin { 14 | androidTarget() 15 | 16 | iosX64() 17 | iosArm64() 18 | iosSimulatorArm64() 19 | 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | isStatic = true 27 | baseName = "shared" 28 | // export("dev.icerock.moko:resources:0.23.0") 29 | // export("dev.icerock.moko:resources-compose:0.23.0") 30 | 31 | } 32 | } 33 | 34 | val myAttribute = Attribute.of("myOwnAttribute", String::class.java) 35 | 36 | 37 | configurations.all { 38 | if (name == "podDebugFrameworkIosFat") { 39 | attributes { 40 | // put a unique attribute 41 | attribute(myAttribute, "pod-debug") 42 | } 43 | } 44 | if (name == "podReleaseFrameworkIosFat") { 45 | attributes { 46 | // put a unique attribute 47 | attribute(myAttribute, "pod-release") 48 | } 49 | } 50 | } 51 | 52 | 53 | cocoapods { 54 | version = "1.0.0" 55 | summary = "Some description for the Shared Module" 56 | homepage = "Link to the Shared Module homepage" 57 | ios.deploymentTarget = "14.1" 58 | podfile = project.file("../iosApp/Podfile") 59 | framework { 60 | baseName = "shared" 61 | isStatic = true 62 | } 63 | // extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" 64 | } 65 | 66 | sourceSets { 67 | 68 | val voyagerVersion = "1.0.0-rc07" 69 | 70 | val commonMain by getting { 71 | dependencies { 72 | implementation(compose.runtime) 73 | implementation(compose.foundation) 74 | implementation(compose.material) 75 | 76 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) 77 | implementation(compose.components.resources) 78 | implementation("media.kamel:kamel-image:0.6.0") 79 | implementation("io.ktor:ktor-client-core:2.3.1") 80 | implementation("io.ktor:ktor-client-content-negotiation:2.3.1") 81 | implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.1") 82 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") 83 | implementation("io.ktor:ktor-client-json:2.1.3") 84 | implementation("io.ktor:ktor-client-logging:2.3.1") 85 | api("dev.icerock.moko:mvvm-core:0.16.1") // only ViewModel, EventsDispatcher, Dispatchers.UI 86 | api("dev.icerock.moko:mvvm-compose:0.16.1") // api mvvm-core, getViewModel for Compose Multiplatfrom 87 | api("io.insert-koin:koin-core:3.4.0") 88 | api("io.insert-koin:koin-test:3.4.0") 89 | implementation("io.insert-koin:koin-compose:1.0.4") 90 | 91 | // Voyager 92 | // implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion") 93 | implementation("cafe.adriel.voyager:voyager-navigator:$voyagerVersion") 94 | implementation("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion") 95 | implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion") 96 | 97 | // implementation("dev.icerock.moko:resources:0.23.0") 98 | // implementation("dev.icerock.moko:resources-compose:0.23.0") // for compose multiplatform 99 | // implementation("dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.3.1") 100 | 101 | implementation("androidx.datastore:datastore-preferences-core:1.1.0-alpha03") 102 | implementation("co.touchlab:kermit:2.0.0-RC4") 103 | 104 | 105 | } 106 | 107 | } 108 | val androidMain by getting { 109 | dependencies { 110 | api("androidx.activity:activity-compose:1.6.1") 111 | api("androidx.appcompat:appcompat:1.6.1") 112 | api("androidx.core:core-ktx:1.9.0") 113 | implementation("io.ktor:ktor-client-android:2.3.1") 114 | implementation("io.insert-koin:koin-core:3.4.0") 115 | implementation("io.insert-koin:koin-android:3.4.0") 116 | implementation("androidx.compose.ui:ui-tooling-preview-android:1.5.4") 117 | implementation("androidx.datastore:datastore-preferences:1.0.0") 118 | 119 | } 120 | } 121 | val iosX64Main by getting 122 | val iosArm64Main by getting 123 | val iosSimulatorArm64Main by getting 124 | val iosMain by creating { 125 | dependsOn(commonMain) 126 | iosX64Main.dependsOn(this) 127 | iosArm64Main.dependsOn(this) 128 | iosSimulatorArm64Main.dependsOn(this) 129 | 130 | dependencies { 131 | implementation("io.ktor:ktor-client-darwin:2.3.1") 132 | } 133 | } 134 | } 135 | 0 136 | 137 | 138 | } 139 | //multiplatformResources { 140 | // multiplatformResourcesPackage = "com.bn.store.kmp" 141 | // disableStaticFrameworkWarning = true 142 | //} 143 | 144 | android { 145 | compileSdk = (findProperty("android.compileSdk") as String).toInt() 146 | namespace = "com.bn.store.kmp" 147 | 148 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 149 | sourceSets["main"].res.srcDirs("src/androidMain/res") 150 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 151 | 152 | defaultConfig { 153 | minSdk = (findProperty("android.minSdk") as String).toInt() 154 | } 155 | compileOptions { 156 | sourceCompatibility = JavaVersion.VERSION_17 157 | targetCompatibility = JavaVersion.VERSION_17 158 | } 159 | kotlin { 160 | jvmToolchain(17) 161 | } 162 | } 163 | 164 | -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/core/Context.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | 4 | import android.app.Application 5 | import java.lang.ref.WeakReference 6 | 7 | actual typealias Context = Application -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/core/DataStore.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import androidx.datastore.preferences.core.stringPreferencesKey 7 | import androidx.datastore.preferences.preferencesDataStore 8 | import data.core.APP_DATASTORE 9 | import kotlinx.coroutines.flow.first 10 | 11 | 12 | val Context.dataStore: DataStore by preferencesDataStore(APP_DATASTORE) 13 | 14 | actual suspend fun Context.getData(key: String): String? { 15 | return dataStore.data.first()[stringPreferencesKey(key)] ?: "" 16 | } 17 | 18 | actual suspend fun Context.putData(key: String, `object`: String) { 19 | dataStore.edit { 20 | it[stringPreferencesKey(key)] = `object` 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/core/platformModule.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import io.ktor.client.engine.android.Android 4 | import org.koin.dsl.module 5 | 6 | actual fun platformModule() = module { 7 | single { 8 | Android.create() 9 | } 10 | } -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/di/Utils.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel 4 | import org.koin.androidx.viewmodel.dsl.viewModel 5 | import org.koin.core.definition.Definition 6 | import org.koin.core.definition.KoinDefinition 7 | import org.koin.core.module.Module 8 | import org.koin.core.qualifier.Qualifier 9 | 10 | actual inline fun Module.viewModelDefinition( 11 | qualifier: Qualifier?, 12 | noinline definition: Definition, 13 | ): KoinDefinition = viewModel(qualifier = qualifier, definition = definition) -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/main.android.kt: -------------------------------------------------------------------------------- 1 | import android.app.Application 2 | import androidx.compose.runtime.Composable 3 | import core.Context 4 | 5 | actual fun getPlatformName(): String = "Android" 6 | 7 | @Composable 8 | fun MainView(application: Application) = App(application) 9 | 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | 2 | import androidx.compose.foundation.shape.AbsoluteCutCornerShape 3 | 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.unit.dp 9 | import cafe.adriel.voyager.navigator.Navigator 10 | import core.Context 11 | import di.appModule 12 | import org.koin.compose.KoinApplication 13 | import presentation.screens.splash.SplashScreen 14 | 15 | @Composable 16 | fun StoreAppTheme( 17 | content: @Composable () -> Unit 18 | ) { 19 | MaterialTheme( 20 | colors = MaterialTheme.colors.copy(primary = Color.Black), 21 | shapes = MaterialTheme.shapes.copy( 22 | small = AbsoluteCutCornerShape(0.dp), 23 | medium = AbsoluteCutCornerShape(0.dp), 24 | large = AbsoluteCutCornerShape(0.dp) 25 | ) 26 | ) { 27 | content() 28 | } 29 | } 30 | 31 | @Composable 32 | fun App(context:Context) { 33 | KoinApplication(application = { 34 | modules(appModule(context)) 35 | }) { 36 | StoreAppTheme { 37 | Navigator(SplashScreen()) 38 | } 39 | } 40 | 41 | } 42 | 43 | 44 | expect fun getPlatformName(): String 45 | 46 | 47 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/core/Context.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | expect class Context -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/core/DataStore.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | expect suspend fun Context.putData(key: String, `object`: String) 6 | 7 | expect suspend fun Context.getData(key: String): String? 8 | 9 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/core/platformModule.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import org.koin.core.module.Module 4 | 5 | expect fun platformModule(): Module -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/core/viewModelDefinition.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/core/AppDataStoreManager.kt: -------------------------------------------------------------------------------- 1 | package data.core 2 | 3 | import core.Context 4 | import core.getData 5 | import core.putData 6 | import domain.core.AppDataStore 7 | 8 | const val APP_DATASTORE = "com.bn.store.kmp" 9 | 10 | class AppDataStoreManager(val context: Context) : AppDataStore { 11 | 12 | override suspend fun setValue( 13 | key: String, 14 | value: String 15 | ) { 16 | context.putData(key, value) 17 | } 18 | 19 | override suspend fun readValue( 20 | key: String, 21 | ): String? { 22 | return context.getData(key) 23 | } 24 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/Category.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | @Serializable 7 | data class Category( 8 | val creationAt: String?=null, 9 | val id: Int?=null, 10 | val image: String?=null, 11 | val name: String?=null, 12 | val updatedAt: String?=null 13 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/Product.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | 4 | import data.model.response.BaseResponse 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Product( 9 | val category: Category, 10 | val creationAt: String, 11 | val description: String, 12 | val id: Int, 13 | val images: List, 14 | val price: Float, 15 | val title: String, 16 | val updatedAt: String, 17 | val rate: Double? = 4.5, 18 | val count: Int? = 49, 19 | ):BaseResponse() -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/Rating.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | 8 | data class Rating( 9 | val count: Int, 10 | val rate: Double 11 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/TextFieldState.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | data class TextFieldState( 4 | val text: String = "", 5 | val hint: String = "", 6 | val isHintVisible: Boolean = true 7 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/request/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package data.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | @Serializable 7 | data class LoginRequest( 8 | val email: String, 9 | val password: String, 10 | ) 11 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/request/RegisterModel.kt: -------------------------------------------------------------------------------- 1 | package data.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RegisterModel( 7 | val email: String, 8 | val password: String, 9 | val name: String, 10 | val avatar: String, 11 | val id: Int?=null, 12 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/response/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | package data.model.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | open class BaseResponse( 7 | val statusCode: Int?=null, 8 | val message: String?=null 9 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/response/LoginResponse.kt: -------------------------------------------------------------------------------- 1 | package data.model.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class LoginResponse( 7 | val access_token: String?=null, 8 | val refresh_token: String?=null, 9 | ): BaseResponse() -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/model/response/RegisterResponse.kt: -------------------------------------------------------------------------------- 1 | package data.model.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RegisterResponse( 7 | val avatar: String? = null, 8 | val creationAt: String? = null, 9 | val email: String? = null, 10 | val id: Int? = null, 11 | val name: String? = null, 12 | val password: String? = null, 13 | val role: String? = null, 14 | val updatedAt: String? = null, 15 | val error: String? = null, 16 | val message: List? = null, 17 | val statusCode: Int? = null 18 | ) 19 | 20 | 21 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/network/Resource.kt: -------------------------------------------------------------------------------- 1 | package data.network 2 | 3 | 4 | sealed class Resource { 5 | data class Success(val result: R): Resource() 6 | data class Failure(val exception: Exception): Resource() 7 | object Loading: Resource() 8 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/network/Urls.kt: -------------------------------------------------------------------------------- 1 | package data.network 2 | 3 | object Urls { 4 | const val BASE_URL = "https://api.escuelajs.co/api/v1/" 5 | const val LOGIN = "${BASE_URL}auth/login" 6 | const val REGISTER = "${BASE_URL}users" 7 | const val GET_PROFILE_DATA = "${BASE_URL}auth/profile" 8 | const val UPDATE_PROFILE = "${BASE_URL}users/" 9 | const val GET_ALL_PRODUCTS = "${BASE_URL}products" 10 | const val PRODUCT_DETAILS = "${BASE_URL}products" 11 | const val CATEGORIES = "${BASE_URL}categories" 12 | const val GET_CATEGORY_PRODUCTS = "products" 13 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/repository/AppPreferences.kt: -------------------------------------------------------------------------------- 1 | package data.repository 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.core.IOException 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.emptyPreferences 9 | import androidx.datastore.preferences.core.intPreferencesKey 10 | import androidx.datastore.preferences.core.stringPreferencesKey 11 | import co.touchlab.kermit.Logger 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.catch 14 | import kotlinx.coroutines.flow.first 15 | import kotlinx.coroutines.flow.map 16 | import org.koin.core.component.KoinComponent 17 | 18 | 19 | data class AppPreferences( 20 | val token: String = "", 21 | ) 22 | 23 | class AppPreferencesRepository( 24 | private val dataStore: DataStore 25 | ) : KoinComponent { 26 | 27 | private val logger = Logger.withTag("UserPreferencesManager") 28 | 29 | private object PreferencesKeys { 30 | val USER_TOKEN = stringPreferencesKey("USER_TOKEN") 31 | } 32 | 33 | suspend fun clear() { 34 | dataStore.edit { 35 | it.clear() 36 | } 37 | } 38 | 39 | /** 40 | * Use this if you don't want to observe a flow. 41 | */ 42 | suspend fun fetchInitialPreferences() = 43 | mapAppPreferences(dataStore.data.first().toPreferences()) 44 | 45 | /** 46 | * Get the user preferences flow. When it's collected, keys are mapped to the 47 | * [UserPreferences] data class. 48 | */ 49 | val userPreferencesFlow: Flow = dataStore.data 50 | .catch { exception -> 51 | // dataStore.data throws an IOException when an error is encountered when reading data 52 | if (exception is IOException) { 53 | logger.d { "Error reading preferences: $exception" } 54 | emit(emptyPreferences()) 55 | } else { 56 | throw exception 57 | } 58 | }.map { preferences -> 59 | mapAppPreferences(preferences) 60 | } 61 | 62 | 63 | /** 64 | * Sets the userId that we get from the Ktor API (on button click). 65 | */ 66 | suspend fun setUserToken(userToken: String) { 67 | dataStore.edit { preferences -> 68 | preferences[PreferencesKeys.USER_TOKEN] = userToken 69 | } 70 | } 71 | 72 | 73 | /** 74 | * Get the preferences key, then map it to the data class. 75 | */ 76 | private fun mapAppPreferences(preferences: Preferences): AppPreferences { 77 | val userToken = preferences[PreferencesKeys.USER_TOKEN] ?: "" 78 | Logger.d { "lastScreen: $userToken" } 79 | return AppPreferences(userToken) 80 | } 81 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/repository/AuthRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package data.repository 2 | 3 | import data.core.AppDataStoreManager 4 | import data.model.request.LoginRequest 5 | import data.model.response.LoginResponse 6 | import data.model.request.RegisterModel 7 | import data.model.response.RegisterResponse 8 | import data.network.Resource 9 | import data.network.Urls 10 | import domain.core.AppDataStore 11 | import domain.repository.AuthRepository 12 | import io.ktor.client.HttpClient 13 | import io.ktor.client.call.body 14 | import io.ktor.client.request.get 15 | import io.ktor.client.request.header 16 | import io.ktor.client.request.post 17 | import io.ktor.client.request.put 18 | import io.ktor.client.request.setBody 19 | import presentation.base.DataStoreKeys.TOKEN 20 | 21 | class AuthRepositoryImp(private val httpClient: HttpClient, private val appDataStoreManager: AppDataStore, 22 | ) : AuthRepository { 23 | override suspend fun login(email: String, password: String): Resource { 24 | val response = httpClient.post(Urls.LOGIN) { 25 | setBody(LoginRequest(email,password)) 26 | }.body() 27 | val isFailed = response.message != null 28 | return if(!isFailed){ 29 | try { 30 | Resource.Success( 31 | response 32 | ) 33 | } catch (e: Exception) { 34 | e.printStackTrace() 35 | Resource.Failure(e) 36 | } 37 | }else{ 38 | Resource.Failure(Exception(response.message)) 39 | } 40 | 41 | } 42 | 43 | override suspend fun register(registerModel: RegisterModel): Resource { 44 | val response = httpClient.post(Urls.REGISTER) { 45 | setBody(registerModel) 46 | }.body() 47 | return try { 48 | if(response.message.isNullOrEmpty()){ 49 | Resource.Success( 50 | response 51 | ) 52 | }else{ 53 | Resource.Failure(Exception(response.message[0])) 54 | } 55 | } catch (e: Exception) { 56 | e.printStackTrace() 57 | Resource.Failure(e) 58 | } 59 | } 60 | 61 | override suspend fun getProfile() :Resource { 62 | val token = appDataStoreManager.readValue(TOKEN) 63 | val response = httpClient.get(Urls.GET_PROFILE_DATA) { 64 | header("Authorization","Bearer $token") 65 | }.body() 66 | return try { 67 | if(response.message.isNullOrEmpty()){ 68 | Resource.Success( 69 | response 70 | ) 71 | }else{ 72 | Resource.Failure(Exception(response.message[0])) 73 | } 74 | } catch (e: Exception) { 75 | e.printStackTrace() 76 | Resource.Failure(e) 77 | } 78 | } 79 | 80 | 81 | override suspend fun updateProfile(registerModel: RegisterModel):Resource { 82 | val response = httpClient.put(Urls.UPDATE_PROFILE+registerModel.id) { 83 | }.body() 84 | return try { 85 | if(response.message.isNullOrEmpty()){ 86 | Resource.Success( 87 | response 88 | ) 89 | }else{ 90 | Resource.Failure(Exception(response.message[0])) 91 | } 92 | } catch (e: Exception) { 93 | e.printStackTrace() 94 | Resource.Failure(e) 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/repository/CreateDatastore.kt: -------------------------------------------------------------------------------- 1 | package data.repository 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 5 | import androidx.datastore.preferences.core.Preferences 6 | import kotlinx.atomicfu.locks.SynchronizedObject 7 | import kotlinx.atomicfu.locks.synchronized 8 | import okio.Path.Companion.toPath 9 | 10 | private lateinit var dataStore: DataStore 11 | 12 | private val lock = SynchronizedObject() 13 | 14 | /** 15 | * Gets the singleton DataStore instance, creating it if necessary. 16 | */ 17 | fun getDataStore(producePath: () -> String): DataStore = 18 | synchronized(lock) { 19 | if (::dataStore.isInitialized) { 20 | dataStore 21 | } else { 22 | PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() }) 23 | .also { dataStore = it } 24 | } 25 | } 26 | 27 | const val dataStoreFileName = "com.bn.store.kmp.preferences" 28 | 29 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/repository/HomeRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package data.repository 2 | 3 | import data.model.Category 4 | import data.model.Product 5 | import data.model.request.LoginRequest 6 | import data.model.response.LoginResponse 7 | import data.model.request.RegisterModel 8 | import data.model.response.RegisterResponse 9 | import data.network.Resource 10 | import data.network.Urls 11 | import data.network.Urls.CATEGORIES 12 | import data.network.Urls.GET_CATEGORY_PRODUCTS 13 | import domain.repository.AuthRepository 14 | import domain.repository.HomeRepository 15 | import io.ktor.client.HttpClient 16 | import io.ktor.client.call.body 17 | import io.ktor.client.request.get 18 | import io.ktor.client.request.post 19 | import io.ktor.client.request.setBody 20 | 21 | class HomeRepositoryImp(private val httpClient: HttpClient) : HomeRepository { 22 | override suspend fun getAllProducts(): Resource> { 23 | val response = httpClient.get(Urls.GET_ALL_PRODUCTS) { 24 | }.body>() 25 | return try { 26 | if(!response.isEmpty()){ 27 | Resource.Success( 28 | response 29 | ) 30 | }else{ 31 | Resource.Failure(Exception("Empty Products")) 32 | } 33 | } catch (e: Exception) { 34 | e.printStackTrace() 35 | Resource.Failure(e) 36 | } 37 | } 38 | 39 | override suspend fun getProductDetails(productId:Int): Resource { 40 | val response = httpClient.get(Urls.PRODUCT_DETAILS+"/$productId") { 41 | }.body() 42 | return try { 43 | if(response.message.isNullOrEmpty()){ 44 | Resource.Success( 45 | response 46 | ) 47 | }else{ 48 | Resource.Failure(Exception("Empty Products")) 49 | } 50 | } catch (e: Exception) { 51 | e.printStackTrace() 52 | Resource.Failure(e) 53 | } 54 | } 55 | 56 | override suspend fun getCategories(): Resource> { 57 | val response = httpClient.get(CATEGORIES) { 58 | 59 | }.body>() 60 | return try { 61 | if(!response.isEmpty()){ 62 | Resource.Success( 63 | response 64 | ) 65 | }else{ 66 | Resource.Failure(Exception("Empty Categories")) 67 | } 68 | } catch (e: Exception) { 69 | e.printStackTrace() 70 | Resource.Failure(e) 71 | } 72 | 73 | } 74 | 75 | override suspend fun getCategoryProducts(catId: Int): Resource> { 76 | val response = httpClient.get("$CATEGORIES/$catId/$GET_CATEGORY_PRODUCTS") { 77 | 78 | }.body>() 79 | return try { 80 | if(!response.isEmpty()){ 81 | Resource.Success( 82 | response 83 | ) 84 | }else{ 85 | Resource.Failure(Exception("Empty Products")) 86 | } 87 | } catch (e: Exception) { 88 | e.printStackTrace() 89 | Resource.Failure(e) 90 | } 91 | 92 | } 93 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import core.Context 4 | import core.platformModule 5 | import data.core.AppDataStoreManager 6 | import data.repository.AppPreferencesRepository 7 | import data.repository.AuthRepositoryImp 8 | import data.repository.HomeRepositoryImp 9 | import domain.core.AppDataStore 10 | import domain.repository.AuthRepository 11 | import domain.repository.HomeRepository 12 | import domain.usecase.CategoryUseCase 13 | import domain.usecase.GetProfileUseCase 14 | import domain.usecase.LoginUseCase 15 | import domain.usecase.ProductUseCase 16 | import domain.usecase.RegisterUseCase 17 | import io.ktor.client.HttpClient 18 | import io.ktor.client.plugins.DefaultRequest 19 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 20 | import io.ktor.client.plugins.logging.LogLevel 21 | import io.ktor.client.plugins.logging.Logger 22 | import io.ktor.client.plugins.logging.Logging 23 | import io.ktor.client.plugins.observer.ResponseObserver 24 | import io.ktor.client.request.header 25 | import io.ktor.http.ContentType 26 | import io.ktor.http.HttpHeaders 27 | import io.ktor.serialization.kotlinx.json.json 28 | import org.koin.core.context.startKoin 29 | import org.koin.dsl.KoinAppDeclaration 30 | import org.koin.dsl.module 31 | import presentation.screens.auth.login.LoginViewModel 32 | import presentation.screens.auth.register.RegisterViewModel 33 | import presentation.screens.auth.updateProfile.UpdateProfileViewModel 34 | import presentation.screens.category.SelectedCategoryViewModel 35 | import presentation.screens.main.MainViewModel 36 | import presentation.screens.main.taps.category.CategoriesViewModel 37 | import presentation.screens.main.taps.home.HomeViewModel 38 | import presentation.screens.main.taps.profile.ProfileViewModel 39 | import presentation.screens.main.taps.search.SearchViewModel 40 | import presentation.screens.splash.SplashViewModel 41 | 42 | 43 | fun initKoin(context: Context, appDeclaration: KoinAppDeclaration = {}) = startKoin { 44 | appDeclaration() 45 | modules(platformModule(), appModule(context)) 46 | } 47 | 48 | fun initKoin(context: Context) = initKoin(context) {} 49 | 50 | 51 | fun appModule(context: Context) = module { 52 | single { createKtorClient() } 53 | single { AuthRepositoryImp(get(),get()) } 54 | single { HomeRepositoryImp(get()) } 55 | 56 | single { LoginUseCase(get()) } 57 | single { RegisterUseCase(get()) } 58 | single { CategoryUseCase(get()) } 59 | single { ProductUseCase(get()) } 60 | single { GetProfileUseCase(get()) } 61 | 62 | viewModelDefinition { LoginViewModel(get(), get()) } 63 | viewModelDefinition { RegisterViewModel(get()) } 64 | viewModelDefinition { MainViewModel(get()) } 65 | viewModelDefinition { SplashViewModel(get()) } 66 | viewModelDefinition { ProfileViewModel(get(),get()) } 67 | viewModelDefinition { CategoriesViewModel(get()) } 68 | viewModelDefinition { SelectedCategoryViewModel(get()) } 69 | viewModelDefinition { SearchViewModel(get()) } 70 | viewModelDefinition { UpdateProfileViewModel(get(),get()) } 71 | viewModelDefinition { HomeViewModel(get(),get(),get()) } 72 | 73 | 74 | single { AppDataStoreManager(context) } 75 | single { AppPreferencesRepository(get()) } 76 | 77 | } 78 | 79 | 80 | private const val TIME_OUT = 60_000 81 | 82 | 83 | fun createKtorClient(): HttpClient { 84 | return HttpClient() { 85 | // Configure your Ktor client here 86 | install(ContentNegotiation) { 87 | json(kotlinx.serialization.json.Json { 88 | ignoreUnknownKeys = true 89 | explicitNulls = false 90 | }) 91 | } 92 | install(Logging) { 93 | logger = object : Logger { 94 | override fun log(message: String) { 95 | println("HTTP:Logger=>$message") 96 | } 97 | 98 | } 99 | level = LogLevel.ALL 100 | } 101 | 102 | install(ResponseObserver) { 103 | onResponse { response -> 104 | println("HTTP:status:=>${response.status.value}") 105 | } 106 | } 107 | install(DefaultRequest) { 108 | header(HttpHeaders.ContentType, ContentType.Application.Json) 109 | } 110 | } 111 | } 112 | 113 | 114 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/di/Utils.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel 4 | import org.koin.core.definition.Definition 5 | import org.koin.core.definition.KoinDefinition 6 | import org.koin.core.module.Module 7 | import org.koin.core.qualifier.Qualifier 8 | 9 | expect inline fun Module.viewModelDefinition( 10 | qualifier: Qualifier? = null, 11 | noinline definition: Definition 12 | ): KoinDefinition -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/core/AppDataStore.kt: -------------------------------------------------------------------------------- 1 | package domain.core 2 | 3 | 4 | interface AppDataStore { 5 | 6 | suspend fun setValue( 7 | key: String, 8 | value: String 9 | ) 10 | 11 | suspend fun readValue( 12 | key: String, 13 | ): String? 14 | 15 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/repository/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package domain.repository 2 | 3 | import data.model.response.LoginResponse 4 | import data.model.request.RegisterModel 5 | import data.model.response.RegisterResponse 6 | import data.network.Resource 7 | 8 | interface AuthRepository { 9 | suspend fun login(userName: String, password: String): Resource 10 | suspend fun register(registerModel: RegisterModel): Resource 11 | suspend fun getProfile(): Resource 12 | suspend fun updateProfile(registerModel: RegisterModel): Resource 13 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/repository/HomeRepository.kt: -------------------------------------------------------------------------------- 1 | package domain.repository 2 | 3 | import data.model.Category 4 | import data.model.Product 5 | import data.model.response.LoginResponse 6 | import data.model.request.RegisterModel 7 | import data.model.response.RegisterResponse 8 | import data.network.Resource 9 | 10 | interface HomeRepository { 11 | suspend fun getAllProducts(): Resource> 12 | suspend fun getProductDetails(productId:Int): Resource 13 | suspend fun getCategories(): Resource> 14 | suspend fun getCategoryProducts(catId:Int): Resource> 15 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/usecase/CategoryUseCase.kt: -------------------------------------------------------------------------------- 1 | package domain.usecase 2 | 3 | import domain.repository.HomeRepository 4 | 5 | 6 | class CategoryUseCase 7 | constructor( 8 | private val repo: HomeRepository, 9 | ) { 10 | suspend fun invoke() = repo.getCategories() 11 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/usecase/GetProfileUseCase.kt: -------------------------------------------------------------------------------- 1 | package domain.usecase 2 | 3 | import data.model.request.RegisterModel 4 | import domain.repository.AuthRepository 5 | 6 | 7 | class GetProfileUseCase 8 | constructor( 9 | private val repo: AuthRepository, 10 | ) { 11 | suspend fun invoke() = repo.getProfile() 12 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/usecase/LoginUseCase.kt: -------------------------------------------------------------------------------- 1 | package domain.usecase 2 | 3 | import domain.repository.AuthRepository 4 | 5 | 6 | class LoginUseCase 7 | constructor( 8 | private val repo: AuthRepository, 9 | ) { 10 | suspend fun invoke(username:String,password:String) = repo.login(username,password) 11 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/usecase/ProductUseCase.kt: -------------------------------------------------------------------------------- 1 | package domain.usecase 2 | 3 | import data.model.request.RegisterModel 4 | import domain.repository.AuthRepository 5 | import domain.repository.HomeRepository 6 | 7 | 8 | class ProductUseCase 9 | constructor( 10 | private val repo: HomeRepository, 11 | ) { 12 | suspend fun getProductDetailsUseCase(productId:Int) = repo.getProductDetails(productId) 13 | suspend fun getAllProducts() = repo.getAllProducts() 14 | suspend fun getCategoryProducts(categoryId:Int) = repo.getCategoryProducts(categoryId) 15 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/usecase/RegisterUseCase.kt: -------------------------------------------------------------------------------- 1 | package domain.usecase 2 | 3 | import data.model.request.RegisterModel 4 | import domain.repository.AuthRepository 5 | 6 | 7 | class RegisterUseCase 8 | constructor( 9 | private val repo: AuthRepository, 10 | ) { 11 | suspend fun invoke(registerModel: RegisterModel) = repo.register(registerModel) 12 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.base 2 | 3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel 4 | import kotlinx.coroutines.CoroutineExceptionHandler 5 | import kotlinx.coroutines.Job 6 | import kotlinx.coroutines.launch 7 | 8 | abstract class BaseViewModel : ViewModel() { 9 | protected lateinit var launchIn: Job 10 | protected lateinit var secondIn: Job 11 | 12 | 13 | val handlerException by lazy { 14 | CoroutineExceptionHandler { _, ex -> 15 | viewModelScope.launch { 16 | 17 | } 18 | } 19 | } 20 | 21 | abstract fun setStateEvent(state: AllStateEvent) 22 | abstract fun setUiEvent(state: AllStateEvent) 23 | open class AllStateEvent { 24 | } 25 | 26 | 27 | override fun onCleared() { 28 | super.onCleared() 29 | if (::launchIn.isInitialized) launchIn.cancel() 30 | if (::secondIn.isInitialized) secondIn.cancel() 31 | } 32 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/base/DataStoreKeys.kt: -------------------------------------------------------------------------------- 1 | package presentation.base 2 | 3 | object DataStoreKeys { 4 | val TOKEN = "com.bn.store.kmp.TOKEN" 5 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/AppBar.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material.Icon 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Text 13 | import androidx.compose.material.TopAppBar 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import org.jetbrains.compose.resources.ExperimentalResourceApi 21 | import org.jetbrains.compose.resources.painterResource 22 | import presentation.theme.keyLine2 23 | 24 | 25 | @OptIn(ExperimentalResourceApi::class) 26 | @Composable 27 | fun AppBar( 28 | appBarBackground: Color = MaterialTheme.colors.primary, 29 | appBarContentColor: Color = MaterialTheme.colors.onPrimary, 30 | barTitle: String, 31 | leadingIcon: String? = null, 32 | trailingIcon: String? = null, 33 | onLeadingIconClicked: (() -> Unit)? = null, 34 | onTrailingIconClicked: (() -> Unit)? = null, 35 | ) { 36 | TopAppBar( 37 | backgroundColor = appBarBackground, 38 | contentColor = appBarContentColor, 39 | elevation = 0.dp, 40 | modifier = Modifier 41 | .background(color = appBarBackground) 42 | .padding(start = 16.dp, end = 16.dp) 43 | ) { 44 | leadingIcon?.let { 45 | Icon( 46 | painter = painterResource(it), 47 | contentDescription = "leading icon of $barTitle", 48 | modifier = Modifier.clickable { onLeadingIconClicked?.let { clicked -> clicked() } } 49 | 50 | ) 51 | Spacer(modifier = Modifier.size(keyLine2)) 52 | } 53 | Text( 54 | modifier = Modifier, 55 | text = barTitle, 56 | style = MaterialTheme.typography.body1.copy( 57 | color = appBarContentColor, 58 | fontWeight = FontWeight.SemiBold 59 | ) 60 | ) 61 | 62 | trailingIcon?.let { 63 | Column(modifier = Modifier.fillMaxWidth()) { 64 | Icon( 65 | painter = painterResource(trailingIcon), 66 | contentDescription = it.toString(), 67 | modifier = Modifier 68 | .size(20.dp) 69 | .align(Alignment.End) 70 | .clickable { onTrailingIconClicked?.let { clicked -> clicked() } } 71 | ) 72 | } 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/AppButtons.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.Button 11 | import androidx.compose.material.ButtonColors 12 | import androidx.compose.material.ButtonDefaults 13 | import androidx.compose.material.CircularProgressIndicator 14 | import androidx.compose.material.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.drawBehind 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.drawscope.Stroke 21 | import androidx.compose.ui.text.TextStyle 22 | import androidx.compose.ui.text.font.Font 23 | import androidx.compose.ui.text.font.FontFamily 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.text.style.TextAlign 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import presentation.theme.DarkPurple 29 | import presentation.theme.yellow 30 | 31 | @Composable 32 | fun AppPrimaryButtonPreview() { 33 | // AppPrimaryButton( 34 | // modifier = Modifier 35 | // .padding(horizontal = 40.dp, vertical = 12.dp) 36 | // .width(400.dp) 37 | // .height(90.dp), 38 | // buttonText = stringResource(id = R.string.sign_in), 39 | // isLoading = false, 40 | // 41 | // shape = RoundedCornerShape(16.dp), 42 | // colors = ButtonDefaults.buttonColors( 43 | // contentColor = yellow, 44 | // containerColor = DarkPurple, 45 | // disabledContainerColor = grayTextColor 46 | // ), 47 | // textColor = yellow, 48 | // style = TextStyle( 49 | // fontFamily = semiBoldFont, 50 | // fontSize = 18.sp, 51 | // fontWeight = FontWeight.SemiBold, 52 | // textAlign = TextAlign.Center 53 | // ) 54 | // ) { 55 | // // navController?.navigate(AuthScreens.RegisterStep1.route) 56 | // } 57 | 58 | } 59 | 60 | @Composable 61 | fun AppPrimaryButton( 62 | modifier: Modifier = Modifier, 63 | buttonText: String = "", 64 | isLoading: Boolean = false, 65 | colors: ButtonColors = ButtonDefaults.buttonColors( 66 | contentColor = yellow, 67 | backgroundColor = DarkPurple 68 | ), 69 | shape: RoundedCornerShape = 70 | RoundedCornerShape(16.dp), 71 | style: TextStyle = TextStyle( 72 | // fontFamily = fontFamilyResource(MR.fonts.somar_bold), 73 | fontWeight = FontWeight.Normal, 74 | fontSize = 14.sp, 75 | ), 76 | textColor: Color = yellow, 77 | onClicked: () -> Unit, 78 | ) { 79 | Button(modifier = modifier 80 | .fillMaxWidth() 81 | .height(61.dp) 82 | .padding(top = 34.dp), 83 | shape = shape, 84 | colors = colors, 85 | enabled = !isLoading, 86 | onClick = { 87 | onClicked() 88 | }) { 89 | val strokeWidth = 2.dp 90 | Row(verticalAlignment = Alignment.CenterVertically) { 91 | AnimatedVisibility(visible = isLoading) { 92 | CircularProgressIndicator( 93 | modifier = Modifier.drawBehind { 94 | drawCircle( 95 | Color.White, 96 | radius = size.width / 2 - strokeWidth.toPx() / 2, 97 | style = Stroke(strokeWidth.toPx()) 98 | ) 99 | }, 100 | color = Color.LightGray, 101 | strokeWidth = strokeWidth 102 | ) 103 | } 104 | 105 | Text( 106 | text = buttonText, 107 | modifier = Modifier.weight(1f), 108 | style = style, 109 | color = textColor, 110 | textAlign = TextAlign.Center, 111 | ) 112 | } 113 | 114 | } 115 | } 116 | 117 | 118 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/AppSlider.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.animation.core.animateDpAsState 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.foundation.layout.wrapContentHeight 17 | import androidx.compose.foundation.layout.wrapContentSize 18 | import androidx.compose.foundation.pager.HorizontalPager 19 | import androidx.compose.foundation.pager.rememberPagerState 20 | import androidx.compose.foundation.shape.CircleShape 21 | import androidx.compose.material.Card 22 | import androidx.compose.material.Icon 23 | import androidx.compose.material.IconButton 24 | import androidx.compose.material.icons.Icons 25 | import androidx.compose.material.icons.filled.KeyboardArrowLeft 26 | import androidx.compose.material.icons.filled.KeyboardArrowRight 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.LaunchedEffect 29 | import androidx.compose.runtime.rememberCoroutineScope 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.draw.clip 33 | import androidx.compose.ui.graphics.Color 34 | import androidx.compose.ui.unit.dp 35 | import kotlinx.coroutines.delay 36 | import kotlinx.coroutines.launch 37 | import org.jetbrains.compose.resources.ExperimentalResourceApi 38 | import org.jetbrains.compose.resources.painterResource 39 | 40 | 41 | @OptIn(ExperimentalFoundationApi::class, ExperimentalResourceApi::class) 42 | @Composable 43 | fun AppSlider() { 44 | val images = listOf( 45 | "banner1.png", 46 | "banner2.png", 47 | "banner3.png", 48 | ) 49 | 50 | val pagerState = rememberPagerState { images.size } 51 | 52 | LaunchedEffect(Unit) { 53 | while (true) { 54 | delay(4000) 55 | val nextPage = (pagerState.currentPage + 1) % pagerState.pageCount 56 | pagerState.scrollToPage(nextPage) 57 | } 58 | } 59 | val scope = rememberCoroutineScope() 60 | 61 | Column( 62 | modifier = Modifier.wrapContentHeight().fillMaxWidth(), 63 | horizontalAlignment = Alignment.CenterHorizontally 64 | ) { 65 | Box(modifier = Modifier.wrapContentSize()) { 66 | HorizontalPager( 67 | state = pagerState, 68 | Modifier 69 | .wrapContentSize() 70 | 71 | ) { currentPage -> 72 | Card( 73 | Modifier 74 | .wrapContentSize() 75 | .padding(26.dp), 76 | elevation = 8.dp 77 | ) { 78 | Image( 79 | modifier = Modifier.fillMaxWidth().height(200.dp), 80 | painter = painterResource(images[currentPage]), 81 | contentDescription = "" 82 | ) 83 | } 84 | } 85 | IconButton( 86 | onClick = { 87 | val nextPage = pagerState.currentPage + 1 88 | if (nextPage < images.size) { 89 | scope.launch { 90 | pagerState.scrollToPage(nextPage) 91 | } 92 | } 93 | }, 94 | Modifier 95 | .padding(30.dp) 96 | .size(48.dp) 97 | .align(Alignment.CenterEnd) 98 | .clip(CircleShape), 99 | // colors = IconButtonDefaults.iconButtonColors( 100 | // containerColor = Color(0x52373737) 101 | // ) 102 | ) { 103 | Icon( 104 | imageVector = Icons.Filled.KeyboardArrowRight, contentDescription = "", 105 | Modifier.fillMaxSize(), 106 | tint = Color.LightGray 107 | ) 108 | } 109 | IconButton( 110 | onClick = { 111 | val prevPage = pagerState.currentPage - 1 112 | if (prevPage >= 0) { 113 | scope.launch { 114 | pagerState.scrollToPage(prevPage) 115 | } 116 | } 117 | }, 118 | Modifier 119 | .padding(30.dp) 120 | .size(48.dp) 121 | .align(Alignment.CenterStart) 122 | .clip(CircleShape), 123 | // colors = IconButtonDefaults.iconButtonColors( 124 | // containerColor = Color(0x52373737) 125 | // ) 126 | ) { 127 | Icon( 128 | imageVector = Icons.Filled.KeyboardArrowLeft, contentDescription = "", 129 | Modifier.fillMaxSize(), 130 | tint = Color.LightGray 131 | ) 132 | } 133 | } 134 | 135 | PageIndicator( 136 | pageCount = images.size, 137 | currentPage = pagerState.currentPage, 138 | modifier = Modifier 139 | ) 140 | 141 | } 142 | 143 | } 144 | 145 | @Composable 146 | fun PageIndicator(pageCount: Int, currentPage: Int, modifier: Modifier) { 147 | Row( 148 | horizontalArrangement = Arrangement.SpaceBetween, 149 | verticalAlignment = Alignment.CenterVertically, 150 | modifier = modifier 151 | ) { 152 | repeat(pageCount) { 153 | IndicatorDots(isSelected = it == currentPage, modifier = modifier) 154 | } 155 | } 156 | } 157 | 158 | @Composable 159 | fun IndicatorDots(isSelected: Boolean, modifier: Modifier) { 160 | val size = animateDpAsState(targetValue = if (isSelected) 12.dp else 10.dp, label = "") 161 | Box( 162 | modifier = modifier.padding(2.dp) 163 | .size(size.value) 164 | .clip(CircleShape) 165 | .background(if (isSelected) Color(0xff373737) else Color(0xA8373737)) 166 | ) 167 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/AppText.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.layout.wrapContentWidth 10 | import androidx.compose.foundation.text.ClickableText 11 | import androidx.compose.foundation.text.KeyboardOptions 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.IconButton 14 | import androidx.compose.material.MaterialTheme 15 | import androidx.compose.material.Text 16 | import androidx.compose.material.TextField 17 | import androidx.compose.material.TextFieldDefaults 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.saveable.rememberSaveable 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.text.SpanStyle 27 | import androidx.compose.ui.text.TextStyle 28 | import androidx.compose.ui.text.buildAnnotatedString 29 | import androidx.compose.ui.text.font.FontWeight 30 | import androidx.compose.ui.text.input.PasswordVisualTransformation 31 | import androidx.compose.ui.text.input.VisualTransformation 32 | import androidx.compose.ui.text.withStyle 33 | import androidx.compose.ui.unit.dp 34 | import androidx.compose.ui.unit.sp 35 | import org.jetbrains.compose.resources.ExperimentalResourceApi 36 | import presentation.theme.DarkPurple 37 | import presentation.theme.blackTextColor 38 | import presentation.theme.gray2 39 | import presentation.theme.grayTextColor 40 | import org.jetbrains.compose.resources.painterResource 41 | 42 | 43 | @Composable 44 | fun AppText( 45 | text: String = "Text Here", 46 | modifier: Modifier = Modifier, 47 | style: TextStyle = TextStyle( 48 | // fontFamily = boldFont, 49 | fontWeight = FontWeight.Normal, 50 | fontSize = 12.sp, 51 | ), 52 | color: Color = blackTextColor 53 | ) { 54 | Text( 55 | text = text, 56 | modifier = modifier, 57 | style = style, 58 | color = color, 59 | ) 60 | } 61 | 62 | 63 | @Composable 64 | fun AnnotatedClickableText( 65 | modifier: Modifier, 66 | baseText: String? = "first ", 67 | baseTextColor: Color = blackTextColor, 68 | annotatedTextTag: String = "Annotated", 69 | annotatedText: String = "annotated", 70 | annotatedTextStyle: SpanStyle = SpanStyle( 71 | color = DarkPurple, 72 | fontSize = 14.sp, 73 | fontWeight = FontWeight.Bold, 74 | // fontFamily = boldFont 75 | ), onClicked: () -> Unit = {} 76 | ) { 77 | val annotatedText = buildAnnotatedString { 78 | //append your initial text 79 | withStyle( 80 | style = SpanStyle( 81 | color = baseTextColor, fontSize = 13.sp, 82 | // fontFamily = semiBoldFont 83 | ) 84 | ) { 85 | append(baseText) 86 | } 87 | //Start of the pushing annotation which you want to color and make them clickable later 88 | pushStringAnnotation( 89 | tag = annotatedTextTag,// provide tag which will then be provided when you click the text 90 | annotation = annotatedText 91 | ) 92 | //add text with your different color/style 93 | withStyle( 94 | style = annotatedTextStyle 95 | ) { 96 | append(annotatedText) 97 | } 98 | // when pop is called it means the end of annotation with current tag 99 | pop() 100 | } 101 | 102 | ClickableText( 103 | modifier = modifier, 104 | text = annotatedText, 105 | onClick = { offset -> 106 | if (annotatedText.getStringAnnotations( 107 | tag = annotatedTextTag,// tag which you used in the buildAnnotatedString 108 | start = offset, 109 | end = offset 110 | ).isNotEmpty() 111 | ) { 112 | annotatedText.getStringAnnotations( 113 | tag = annotatedTextTag,// tag which you used in the buildAnnotatedString 114 | start = offset, 115 | end = offset 116 | )[0].let { annotation -> 117 | //do your stuff when it gets clicked 118 | onClicked() 119 | // Log.d("Clicked", annotation.item) 120 | } 121 | } 122 | 123 | 124 | } 125 | ) 126 | } 127 | 128 | 129 | 130 | @OptIn(ExperimentalResourceApi::class) 131 | @Composable 132 | fun AppTextField( 133 | modifier: Modifier, 134 | value: String, 135 | error: String? = null, 136 | hintLabel: String, 137 | placeHolder: String = "", 138 | visualTransformation: VisualTransformation = VisualTransformation.None, 139 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 140 | hintColor: Color = grayTextColor, 141 | singleLine: Boolean = true, 142 | maxLines: Int = 1, 143 | showingIndicator: Boolean = true, 144 | trailingIconAction: @Composable (() -> Unit)? = null, 145 | onValueChanged: (text: String) -> Unit = {} 146 | ) { 147 | 148 | var passwordVisible by rememberSaveable { mutableStateOf(visualTransformation == PasswordVisualTransformation()) } 149 | 150 | Column { 151 | TextField( 152 | value = value, 153 | keyboardOptions = keyboardOptions, 154 | visualTransformation = if (visualTransformation == PasswordVisualTransformation() && passwordVisible) PasswordVisualTransformation() else VisualTransformation.None, 155 | onValueChange = { 156 | onValueChanged.invoke(it) 157 | }, 158 | label = { 159 | Text(hintLabel, color = hintColor) 160 | }, 161 | maxLines = maxLines, 162 | placeholder = { 163 | Text(placeHolder, color = grayTextColor) 164 | }, 165 | colors = TextFieldDefaults.textFieldColors( 166 | backgroundColor = Color.White, 167 | unfocusedIndicatorColor = if (showingIndicator) grayTextColor else Color.Transparent, 168 | focusedIndicatorColor = if (showingIndicator) gray2 else Color.Transparent, 169 | ), 170 | singleLine = singleLine, 171 | textStyle = TextStyle( 172 | color = blackTextColor, 173 | fontWeight = FontWeight.Bold, 174 | // fontFamily = regularFont, 175 | fontSize = 16.sp 176 | ), 177 | modifier = modifier, 178 | trailingIcon = { 179 | if (visualTransformation == PasswordVisualTransformation()) { 180 | val image = if (passwordVisible) "visibility.xml" else "visibility_off.xml" 181 | val description = if (passwordVisible) "Hide password" else "Show password" 182 | IconButton(onClick = { passwordVisible = !passwordVisible }) { 183 | Icon(painter = painterResource("flag.xml") , description) 184 | } 185 | } else if (trailingIconAction !== null) { 186 | trailingIconAction() 187 | } 188 | 189 | }) 190 | 191 | if (error != null) { 192 | Text( 193 | text = error, 194 | modifier = modifier, 195 | color = MaterialTheme.colors.error 196 | ) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/CategoryCardTag.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.CircleShape 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import data.model.Category 21 | import presentation.theme.textColorSemiBlack 22 | 23 | 24 | @Composable 25 | fun CategoryCardTag( 26 | category: Category, 27 | onTap: (category: Category) -> Unit, 28 | ) { 29 | Box( 30 | modifier = Modifier 31 | .padding(8.dp) 32 | .padding(horizontal = 4.dp) 33 | .clip(CircleShape) 34 | .width(100.dp) 35 | .height(30.dp) 36 | .clickable { 37 | onTap(category) 38 | } 39 | .background(color = textColorSemiBlack), 40 | contentAlignment = Alignment.Center, 41 | content = { 42 | Text( 43 | textAlign = TextAlign.Center, 44 | text = category.name ?: "", 45 | style = TextStyle( 46 | color = Color.White, 47 | fontSize = 12.sp 48 | ) 49 | ) 50 | }) 51 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/CustomDialog.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | import androidx.compose.foundation.background 3 | import androidx.compose.foundation.border 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material.Button 15 | import androidx.compose.material.ButtonDefaults 16 | import androidx.compose.material.Card 17 | import androidx.compose.material.ExperimentalMaterialApi 18 | import androidx.compose.material.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.graphics.Color.Companion.DarkGray 24 | import androidx.compose.ui.graphics.Color.Companion.Red 25 | import androidx.compose.ui.graphics.Color.Companion.White 26 | import androidx.compose.ui.text.TextStyle 27 | import androidx.compose.ui.text.font.FontWeight 28 | import androidx.compose.ui.text.style.TextAlign 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.sp 31 | import presentation.theme.DarkPurple 32 | import presentation.theme.Gold 33 | import presentation.theme.yellow 34 | import utils.AppStrings 35 | 36 | @OptIn(ExperimentalMaterialApi::class) 37 | @Composable 38 | fun CustomDialogSheet( 39 | title: String, 40 | buttonText: String, 41 | message: String, 42 | onAccept: () -> Unit, 43 | onReject: () -> Unit 44 | ) { 45 | ModalBottomSheet( 46 | onDismissRequest = { onReject() }, 47 | ) { 48 | CustomDialog(title, buttonText, message, onAccept, onReject) 49 | } 50 | 51 | } 52 | 53 | @Composable 54 | fun CustomDialog( 55 | title: String, 56 | buttonText: String, 57 | message: String, 58 | onAccept: () -> Unit, 59 | onReject: () -> Unit 60 | ) { 61 | 62 | Card( 63 | modifier = Modifier 64 | .fillMaxWidth() 65 | .height(250.dp), 66 | shape = RoundedCornerShape(30.dp) 67 | ) { 68 | Column( 69 | modifier = Modifier 70 | .fillMaxSize() 71 | .background(White) 72 | .padding(horizontal = 24.dp), 73 | horizontalAlignment = Alignment.Start 74 | ) { 75 | 76 | Spacer(modifier = Modifier.height(21.dp)) 77 | Box( 78 | modifier = Modifier 79 | .width(134.dp) 80 | .height(5.dp) 81 | .background( 82 | color = Color(0xFFD0D0DA), 83 | shape = RoundedCornerShape(size = 100.dp) 84 | ) 85 | .align(Alignment.CenterHorizontally) 86 | ) 87 | Spacer(modifier = Modifier.height(28.dp)) 88 | 89 | Text( 90 | text = title, 91 | style = TextStyle( 92 | fontSize = 24.sp, 93 | fontWeight = FontWeight(700), 94 | color = Color.Red, 95 | ) 96 | ) 97 | 98 | Spacer(modifier = Modifier.height(20.dp)) 99 | 100 | Text( 101 | text = message, 102 | style = TextStyle( 103 | fontSize = 16.sp, 104 | fontWeight = FontWeight(500), 105 | color = Color.Black, 106 | ) 107 | ) 108 | 109 | Spacer(modifier = Modifier.height(30.dp)) 110 | 111 | Row { 112 | 113 | Button( 114 | onClick = { onReject() }, 115 | modifier = Modifier 116 | .fillMaxWidth(0.5f) 117 | .border( 118 | width = 1.dp, 119 | color = DarkPurple, 120 | shape = RoundedCornerShape(size = 12.dp) 121 | ).height(60.dp) 122 | , 123 | 124 | shape = RoundedCornerShape(size = 12.dp), 125 | colors = ButtonDefaults.buttonColors( 126 | contentColor = DarkGray, 127 | backgroundColor = Red, 128 | ), 129 | ) { 130 | Text(text = AppStrings.cancel.stringValue, style = TextStyle(color = Color.White)) 131 | } 132 | Spacer( 133 | modifier = Modifier.width(15.dp), 134 | ) 135 | 136 | Button( 137 | onClick = { onAccept() }, 138 | shape = RoundedCornerShape(size = 12.dp), 139 | colors = ButtonDefaults.buttonColors( 140 | contentColor = Gold 141 | ), 142 | modifier = Modifier 143 | .fillMaxWidth() 144 | .height(60.dp) 145 | ) { 146 | Text( 147 | text = AppStrings.yes.stringValue + "! $buttonText", 148 | textAlign = TextAlign.Center 149 | ) 150 | } 151 | } 152 | } 153 | } 154 | 155 | } 156 | 157 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/Gap.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.layout.onGloballyPositioned 17 | import androidx.compose.ui.layout.positionInRoot 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.dp 20 | 21 | 22 | /** 23 | * You can use it to add space in Flex layout 24 | * Ex. Gap(20) , to add space with 20 dp in [Column] or [Row] 25 | */ 26 | @Composable 27 | fun Gap( 28 | size: Dp, 29 | ) { 30 | var positionInParent by remember { mutableStateOf(Offset.Zero) } 31 | Box(modifier = Modifier 32 | .size(size) 33 | .onGloballyPositioned { coordinates -> 34 | positionInParent = coordinates.positionInRoot() 35 | } 36 | .then( 37 | Modifier 38 | .width( 39 | if (positionInParent.x == 0f) { 40 | 0.dp 41 | } else { 42 | size 43 | } 44 | ) 45 | .height( 46 | if (positionInParent.y == 0f) { 47 | 0.dp 48 | } else { 49 | size 50 | } 51 | ) 52 | )) 53 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/LoadingIndicator.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.infiniteRepeatable 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.Spacer 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.width 16 | import androidx.compose.foundation.shape.CircleShape 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.unit.Dp 25 | import androidx.compose.ui.unit.dp 26 | import kotlinx.coroutines.delay 27 | 28 | 29 | @Composable 30 | fun imageLoadingIndicator(){ 31 | Column( 32 | modifier = Modifier 33 | .fillMaxSize(1.0f), 34 | verticalArrangement = Arrangement.Center, 35 | horizontalAlignment = Alignment.CenterHorizontally 36 | ) { 37 | LoadingAnimation3() 38 | } 39 | } 40 | 41 | @Composable 42 | fun LoadingAnimation3( 43 | circleColor: Color = Color(0xFF35898F), 44 | circleSize: Dp = 36.dp, 45 | animationDelay: Int = 400, 46 | initialAlpha: Float = 0.3f 47 | ) { 48 | 49 | // 3 circles 50 | val circles = listOf( 51 | remember { 52 | Animatable(initialValue = initialAlpha) 53 | }, 54 | remember { 55 | Animatable(initialValue = initialAlpha) 56 | }, 57 | remember { 58 | Animatable(initialValue = initialAlpha) 59 | } 60 | ) 61 | 62 | circles.forEachIndexed { index, animatable -> 63 | 64 | LaunchedEffect(Unit) { 65 | 66 | // Use coroutine delay to sync animations 67 | delay(timeMillis = (animationDelay / circles.size).toLong() * index) 68 | 69 | animatable.animateTo( 70 | targetValue = 1f, 71 | animationSpec = infiniteRepeatable( 72 | animation = tween( 73 | durationMillis = animationDelay 74 | ), 75 | repeatMode = RepeatMode.Reverse 76 | ) 77 | ) 78 | } 79 | } 80 | 81 | // container for circles 82 | Row( 83 | modifier = Modifier 84 | //.border(width = 2.dp, color = Color.Magenta) 85 | ) { 86 | 87 | // adding each circle 88 | circles.forEachIndexed { index, animatable -> 89 | 90 | // gap between the circles 91 | if (index != 0) { 92 | Spacer(modifier = Modifier.width(width = 6.dp)) 93 | } 94 | 95 | Box( 96 | modifier = Modifier 97 | .size(size = circleSize) 98 | .clip(shape = CircleShape) 99 | .background( 100 | color = circleColor 101 | .copy(alpha = animatable.value) 102 | ) 103 | ) { 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/ProductCard.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.offset 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.width 16 | import androidx.compose.foundation.layout.wrapContentHeight 17 | import androidx.compose.foundation.shape.CircleShape 18 | import androidx.compose.foundation.shape.RoundedCornerShape 19 | import androidx.compose.material.Card 20 | import androidx.compose.material.CircularProgressIndicator 21 | import androidx.compose.material.ExperimentalMaterialApi 22 | import androidx.compose.material.Icon 23 | import androidx.compose.material.Text 24 | import androidx.compose.material.icons.Icons 25 | import androidx.compose.material.icons.outlined.Favorite 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.draw.clip 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.graphics.ColorFilter 32 | import androidx.compose.ui.layout.ContentScale 33 | import androidx.compose.ui.text.TextStyle 34 | import androidx.compose.ui.text.font.FontWeight 35 | import androidx.compose.ui.unit.dp 36 | import androidx.compose.ui.unit.sp 37 | import data.model.Product 38 | import io.kamel.image.KamelImage 39 | import io.kamel.image.asyncPainterResource 40 | import org.jetbrains.compose.resources.ExperimentalResourceApi 41 | import org.jetbrains.compose.resources.painterResource 42 | import presentation.theme.PrimaryColor 43 | 44 | 45 | @OptIn(ExperimentalMaterialApi::class, ExperimentalResourceApi::class) 46 | @Composable 47 | fun ProductCard( 48 | productModel: Product, 49 | onTap: () -> Unit, 50 | ) { 51 | Card( 52 | onClick = { 53 | onTap() 54 | }, 55 | elevation = 0.dp, 56 | modifier = Modifier.padding(8.dp) 57 | ) { 58 | Column(modifier = Modifier.width(170.dp).wrapContentHeight()) { 59 | Box { 60 | KamelImage( 61 | asyncPainterResource(productModel.images[0]), 62 | productModel.description, 63 | contentScale = ContentScale.Fit, 64 | modifier = Modifier.fillMaxWidth().height(200.dp), 65 | onFailure = { 66 | KamelImage( 67 | asyncPainterResource(productModel.images[0]), 68 | productModel.description, 69 | contentScale = ContentScale.Fit, 70 | onLoading = { progress -> CircularProgressIndicator(progress) }, 71 | modifier = Modifier.fillMaxWidth().height(200.dp), 72 | onFailure = { 73 | Image( 74 | modifier = Modifier, 75 | contentScale = ContentScale.Fit, 76 | painter = painterResource("not_found.png"), 77 | contentDescription = null 78 | ) 79 | } 80 | ) 81 | } 82 | ) 83 | Box(modifier = Modifier.padding(8.dp)) { 84 | Text( 85 | "20%", 86 | modifier = Modifier.background(Color.Red, shape = RoundedCornerShape(30.dp)) 87 | .padding(vertical = 8.dp, horizontal = 12.dp), 88 | style = TextStyle( 89 | color = Color.White, 90 | ) 91 | ) 92 | } 93 | } 94 | Box( 95 | modifier = Modifier 96 | .align(Alignment.End) 97 | .offset(y = -24.dp) 98 | .padding(8.dp) 99 | .size(36.dp) 100 | .clip(CircleShape) 101 | .clickable { 102 | } 103 | .background(color = Color.White), 104 | content = { 105 | Icon( 106 | modifier = Modifier 107 | .padding(8.dp) 108 | .size(40.dp), 109 | imageVector = Icons.Outlined.Favorite, 110 | tint = Color.Red, 111 | contentDescription = "avatar", 112 | ) 113 | }) 114 | 115 | 116 | Column( 117 | modifier = Modifier.offset(y = -36.dp) 118 | .padding(8.dp) 119 | ) { 120 | Text( 121 | productModel.category.name?:"", style = TextStyle( 122 | fontSize = 11.sp, 123 | fontWeight = FontWeight(400), 124 | color = Color(0xFF9B9B9B), 125 | 126 | ) 127 | ) 128 | Gap(5.dp) 129 | Text( 130 | productModel.title, style = TextStyle( 131 | fontSize = 16.sp, 132 | fontWeight = FontWeight(400), 133 | color = Color(0xFF222222), 134 | ), 135 | maxLines = 2 136 | ) 137 | Gap(5.dp) 138 | 139 | Text( 140 | "${productModel.price} $", 141 | style = TextStyle( 142 | fontSize = 14.sp, 143 | lineHeight = 20.sp, 144 | fontWeight = FontWeight(500), 145 | color = Color(0xFF222222), 146 | ) 147 | ) 148 | } 149 | } 150 | 151 | } 152 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/ProfileSectionCard.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.layout.ContentScale 21 | import androidx.compose.ui.platform.testTag 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.text.style.TextAlign 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import org.jetbrains.compose.resources.ExperimentalResourceApi 28 | import org.jetbrains.compose.resources.painterResource 29 | import presentation.theme.BorderColor 30 | 31 | @OptIn(ExperimentalResourceApi::class) 32 | @Composable 33 | fun ProfileSectionCard( 34 | icon: @Composable () -> Unit, 35 | title: String, 36 | color: Color = Color.Black, 37 | withLine: Boolean = true, 38 | onClicked: () -> Unit 39 | ) { 40 | Column { 41 | Row( 42 | modifier = Modifier 43 | .clickable { 44 | onClicked() 45 | } 46 | .testTag(title) 47 | .padding(vertical = 20.dp), 48 | verticalAlignment = Alignment.CenterVertically 49 | ) { 50 | icon() 51 | Spacer(modifier = Modifier.width(15.dp)) 52 | 53 | Text( 54 | text = title, 55 | style = TextStyle( 56 | fontSize = 16.sp, 57 | fontWeight = FontWeight(500), 58 | color = color, 59 | textAlign = TextAlign.Center, 60 | ) 61 | ) 62 | Spacer(modifier = Modifier.weight(1f)) 63 | Image( 64 | modifier = Modifier.size(16.dp), 65 | painter = painterResource("arrow_right.xml"), 66 | contentDescription = title, 67 | contentScale = ContentScale.None 68 | ) 69 | 70 | } 71 | if (withLine) { 72 | Box( 73 | modifier = Modifier 74 | .padding(0.dp) 75 | .width(345.dp) 76 | .height(1.dp) 77 | .background(color = BorderColor) 78 | ) 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/components/TabNavigationItem.kt: -------------------------------------------------------------------------------- 1 | package presentation.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.material.BottomNavigationItem 6 | import androidx.compose.material.Icon 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import cafe.adriel.voyager.navigator.tab.LocalTabNavigator 11 | import cafe.adriel.voyager.navigator.tab.Tab 12 | import presentation.theme.PrimaryColor 13 | 14 | @Composable 15 | fun RowScope.TabNavigationItem(tab: Tab) { 16 | val tabNavigator = LocalTabNavigator.current 17 | BottomNavigationItem( 18 | selected = tabNavigator.current.key == tab.key, 19 | onClick = { tabNavigator.current = tab }, 20 | icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) }, 21 | selectedContentColor = PrimaryColor, 22 | unselectedContentColor = Color.Black, 23 | modifier = Modifier.background(color = Color.White) 24 | ) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/auth/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.auth.login 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import data.model.response.LoginResponse 6 | import data.model.response.RegisterResponse 7 | import data.model.TextFieldState 8 | import data.network.Resource 9 | import domain.core.AppDataStore 10 | import domain.usecase.LoginUseCase 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | import kotlinx.coroutines.flow.asStateFlow 14 | import kotlinx.coroutines.launch 15 | import presentation.base.BaseViewModel 16 | import presentation.base.BaseViewModel.AllStateEvent 17 | import presentation.base.DataStoreKeys 18 | import utils.AppStrings 19 | 20 | 21 | class LoginViewModel( 22 | private val loginUseCase: LoginUseCase, 23 | private val appDataStoreManager: AppDataStore, 24 | ) : BaseViewModel() { 25 | 26 | private val _userNameError: MutableStateFlow = MutableStateFlow(null) 27 | val nameError = _userNameError.asStateFlow() 28 | 29 | private val _passwordError: MutableStateFlow = MutableStateFlow(null) 30 | val passwordError = _passwordError.asStateFlow() 31 | 32 | private val _userNameState = mutableStateOf( 33 | TextFieldState( 34 | text = "john@mail.com", 35 | hint = "Enter your Email", 36 | isHintVisible = false, 37 | ) 38 | ) 39 | val userName: State = _userNameState 40 | 41 | private val _passwordState = mutableStateOf( 42 | TextFieldState( 43 | text = "changeme", 44 | hint = "Enter your password", 45 | isHintVisible = false, 46 | ) 47 | ) 48 | val password: State = _passwordState 49 | 50 | private val _login = MutableStateFlow?>(null) 51 | val login: StateFlow?> = _login 52 | 53 | private val _register = MutableStateFlow?>(null) 54 | val register: StateFlow?> = _register 55 | 56 | 57 | override fun setStateEvent(state: AllStateEvent) { 58 | when (state) { 59 | is LoginStateIntent.Login -> { 60 | viewModelScope.launch { 61 | if (userName.value.text.length >= 5 && password.value.text.length >= 6) { 62 | _login.value = Resource.Loading 63 | _login.value = loginUseCase.invoke(userName.value.text, password.value.text) 64 | } 65 | } 66 | } 67 | is LoginStateIntent.SaveToken -> { 68 | viewModelScope.launch { 69 | appDataStoreManager.setValue(DataStoreKeys.TOKEN,state.token) 70 | } 71 | } 72 | } 73 | 74 | } 75 | 76 | override fun setUiEvent(state: AllStateEvent) { 77 | when (state) { 78 | is LoginUIStateEvent.EnteredUserName -> { 79 | _userNameState.value = userName.value.copy( 80 | text = state.value 81 | ) 82 | viewModelScope.launch { 83 | if (userName.value.text.isEmpty()) { 84 | _userNameError.emit(AppStrings.user_name_validation.stringValue) 85 | } else { 86 | _userNameError.emit(null) 87 | } 88 | } 89 | } 90 | 91 | is LoginUIStateEvent.EnteredPassword -> { 92 | _passwordState.value = password.value.copy( 93 | text = state.value 94 | ) 95 | viewModelScope.launch { 96 | if (password.value.text.length < 6) { 97 | _passwordError.emit(AppStrings.PasswordValidation.stringValue) 98 | } else { 99 | _passwordError.emit(null) 100 | } 101 | } 102 | 103 | } 104 | } 105 | } 106 | 107 | fun clearLoginState() { 108 | viewModelScope.launch { 109 | _login.emit(null) 110 | } 111 | } 112 | } 113 | 114 | 115 | sealed class LoginStateIntent : AllStateEvent() { 116 | object Login : LoginStateIntent() 117 | data class SaveToken(val token: String) : LoginStateIntent() 118 | // data class Register(val registerModel: RegisterModel) : AuthStateIntent() 119 | } 120 | 121 | sealed class LoginUIStateEvent : AllStateEvent() { 122 | data class EnteredUserName(val value: String) : LoginUIStateEvent() 123 | data class EnteredPassword(val value: String) : LoginUIStateEvent() 124 | } 125 | 126 | 127 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/auth/register/RegisterViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.auth.register 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import data.model.request.RegisterModel 6 | import data.model.response.RegisterResponse 7 | import data.model.TextFieldState 8 | import data.network.Resource 9 | import domain.usecase.RegisterUseCase 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.flow.asStateFlow 13 | import kotlinx.coroutines.launch 14 | import presentation.base.BaseViewModel 15 | import presentation.base.BaseViewModel.AllStateEvent 16 | import utils.AppStrings 17 | 18 | 19 | class RegisterViewModel( 20 | private val registerUseCase: RegisterUseCase, 21 | ) : BaseViewModel() { 22 | 23 | private val _nameError: MutableStateFlow = MutableStateFlow(null) 24 | val nameError = _nameError.asStateFlow() 25 | 26 | private val _emailError: MutableStateFlow = MutableStateFlow(null) 27 | val emailError = _emailError.asStateFlow() 28 | 29 | private val _passwordError: MutableStateFlow = MutableStateFlow(null) 30 | val passwordError = _passwordError.asStateFlow() 31 | 32 | private val _nameState = mutableStateOf( 33 | TextFieldState( 34 | text = "", 35 | hint = "Enter your Name", 36 | isHintVisible = false, 37 | ) 38 | ) 39 | val userName: State = _nameState 40 | 41 | private val _emailState = mutableStateOf( 42 | TextFieldState( 43 | text = "", 44 | hint = "Enter your valid email", 45 | isHintVisible = false, 46 | ) 47 | ) 48 | val email: State = _emailState 49 | 50 | private val _passwordState = mutableStateOf( 51 | TextFieldState( 52 | text = "", 53 | hint = "Enter your valid password", 54 | isHintVisible = false, 55 | ) 56 | ) 57 | val password: State = _passwordState 58 | 59 | 60 | private val _register = MutableStateFlow?>(null) 61 | val register: StateFlow?> = _register 62 | 63 | 64 | override fun setStateEvent(state: AllStateEvent) { 65 | when (state) { 66 | is RegisterStateIntent.Register -> { 67 | viewModelScope.launch { 68 | if (userName.value.text.length >= 5 && password.value.text.length >= 6 && email.value.text.length >= 6) { 69 | _register.value = Resource.Loading 70 | _register.value = registerUseCase.invoke( 71 | RegisterModel( 72 | email=email.value.text, 73 | name = userName.value.text, 74 | password = password.value.text, 75 | avatar = "https://api.lorem.space/image/face?w=640&h=480", 76 | ) 77 | ) 78 | } 79 | } 80 | } 81 | 82 | } 83 | 84 | } 85 | 86 | override fun setUiEvent(state: AllStateEvent) { 87 | when (state) { 88 | is RegisterUIEvent.EnteredName -> { 89 | _nameState.value = userName.value.copy( 90 | text = state.value 91 | ) 92 | viewModelScope.launch { 93 | if (userName.value.text.isEmpty()) { 94 | _nameError.emit(AppStrings.user_name_validation.stringValue) 95 | } else { 96 | _nameError.emit(null) 97 | } 98 | } 99 | } 100 | 101 | is RegisterUIEvent.EnteredPassword -> { 102 | _passwordState.value = password.value.copy( 103 | text = state.value 104 | ) 105 | viewModelScope.launch { 106 | if (password.value.text.length < 6) { 107 | _passwordError.emit(AppStrings.PasswordValidation.stringValue) 108 | } else { 109 | _passwordError.emit(null) 110 | } 111 | } 112 | } 113 | is RegisterUIEvent.EnteredEmail -> { 114 | _emailState.value = email.value.copy( 115 | text = state.value 116 | ) 117 | viewModelScope.launch { 118 | if (email.value.text.length < 6) { 119 | _emailError.emit(AppStrings.email_validation.stringValue) 120 | } else { 121 | _emailError.emit(null) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | fun clearAllState() { 129 | viewModelScope.launch { 130 | _register.emit(null) 131 | } 132 | } 133 | } 134 | 135 | 136 | 137 | sealed class RegisterStateIntent : AllStateEvent() { 138 | object Register : RegisterStateIntent() 139 | } 140 | 141 | sealed class RegisterUIEvent : AllStateEvent() { 142 | data class EnteredName(val value: String) : RegisterUIEvent() 143 | data class EnteredEmail(val value: String) : RegisterUIEvent() 144 | data class EnteredPassword(val value: String) : RegisterUIEvent() 145 | } 146 | 147 | 148 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/auth/updateProfile/UpdateProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.auth.updateProfile 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import data.model.Category 6 | import data.model.request.RegisterModel 7 | import data.model.response.RegisterResponse 8 | import data.model.TextFieldState 9 | import data.network.Resource 10 | import domain.usecase.GetProfileUseCase 11 | import domain.usecase.RegisterUseCase 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.launch 16 | import presentation.base.BaseViewModel 17 | import presentation.base.BaseViewModel.AllStateEvent 18 | import utils.AppStrings 19 | 20 | 21 | class UpdateProfileViewModel( 22 | private val registerUseCase: RegisterUseCase, 23 | private val getProfileUseCase: GetProfileUseCase, 24 | ) : BaseViewModel() { 25 | 26 | private val _nameError: MutableStateFlow = MutableStateFlow(null) 27 | val nameError = _nameError.asStateFlow() 28 | 29 | private val _emailError: MutableStateFlow = MutableStateFlow(null) 30 | val emailError = _emailError.asStateFlow() 31 | 32 | private val _passwordError: MutableStateFlow = MutableStateFlow(null) 33 | val passwordError = _passwordError.asStateFlow() 34 | 35 | private val _nameState = mutableStateOf( 36 | TextFieldState( 37 | text = "", 38 | hint = "Enter your Name", 39 | isHintVisible = false, 40 | ) 41 | ) 42 | val userName: State = _nameState 43 | 44 | private val _emailState = mutableStateOf( 45 | TextFieldState( 46 | text = "", 47 | hint = "Enter your valid email", 48 | isHintVisible = false, 49 | ) 50 | ) 51 | val email: State = _emailState 52 | 53 | private val _passwordState = mutableStateOf( 54 | TextFieldState( 55 | text = "", 56 | hint = "Enter your valid password", 57 | isHintVisible = false, 58 | ) 59 | ) 60 | val password: State = _passwordState 61 | 62 | 63 | private var userProfile:RegisterResponse?=null 64 | 65 | 66 | private val _updateProfile = MutableStateFlow?>(null) 67 | val updateProfile: StateFlow?> = _updateProfile 68 | 69 | init { 70 | setStateEvent(UpdateProfileStateIntent.GettingProfile) 71 | } 72 | 73 | override fun setStateEvent(state: AllStateEvent) { 74 | when (state) { 75 | is UpdateProfileStateIntent.GettingProfile -> { 76 | viewModelScope.launch { 77 | val response = getProfileUseCase.invoke() 78 | when(getProfileUseCase.invoke()){ 79 | is Resource.Failure -> {} 80 | Resource.Loading -> {} 81 | is Resource.Success -> { 82 | userProfile = (response as Resource.Success).result 83 | userProfile?.let { 84 | setUiEvent(UpdateProfileUIEvent.EnteredName(userProfile?.name?:"")) 85 | setUiEvent(UpdateProfileUIEvent.EnteredEmail(userProfile?.email?:"")) 86 | setUiEvent(UpdateProfileUIEvent.EnteredPassword(userProfile?.password?:"")) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | is UpdateProfileStateIntent.UpdateProfile -> { 93 | viewModelScope.launch { 94 | if (userName.value.text.length >= 5 && password.value.text.length >= 6 && email.value.text.length >= 6) { 95 | _updateProfile.value = Resource.Loading 96 | _updateProfile.value = registerUseCase.invoke( 97 | RegisterModel( 98 | email=email.value.text, 99 | name = userName.value.text, 100 | password = password.value.text, 101 | avatar = "https://api.lorem.space/image/face?w=640&h=480", 102 | ) 103 | ) 104 | } 105 | } 106 | } 107 | 108 | } 109 | 110 | } 111 | 112 | override fun setUiEvent(state: AllStateEvent) { 113 | when (state) { 114 | is UpdateProfileUIEvent.EnteredName -> { 115 | _nameState.value = userName.value.copy( 116 | text = state.value 117 | ) 118 | viewModelScope.launch { 119 | if (userName.value.text.isEmpty()) { 120 | _nameError.emit(AppStrings.user_name_validation.stringValue) 121 | } else { 122 | _nameError.emit(null) 123 | } 124 | } 125 | } 126 | 127 | is UpdateProfileUIEvent.EnteredPassword -> { 128 | _passwordState.value = password.value.copy( 129 | text = state.value 130 | ) 131 | viewModelScope.launch { 132 | if (password.value.text.length < 6) { 133 | _passwordError.emit(AppStrings.PasswordValidation.stringValue) 134 | } else { 135 | _passwordError.emit(null) 136 | } 137 | } 138 | } 139 | is UpdateProfileUIEvent.EnteredEmail -> { 140 | _emailState.value = email.value.copy( 141 | text = state.value 142 | ) 143 | viewModelScope.launch { 144 | if (email.value.text.length < 6) { 145 | _emailError.emit(AppStrings.email_validation.stringValue) 146 | } else { 147 | _emailError.emit(null) 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | fun clearAllState() { 155 | viewModelScope.launch { 156 | _updateProfile.emit(null) 157 | } 158 | } 159 | } 160 | 161 | 162 | 163 | sealed class UpdateProfileStateIntent : AllStateEvent() { 164 | object GettingProfile : UpdateProfileStateIntent() 165 | object UpdateProfile : UpdateProfileStateIntent() 166 | } 167 | 168 | sealed class UpdateProfileUIEvent : AllStateEvent() { 169 | data class EnteredName(val value: String) : UpdateProfileUIEvent() 170 | data class EnteredEmail(val value: String) : UpdateProfileUIEvent() 171 | data class EnteredPassword(val value: String) : UpdateProfileUIEvent() 172 | } 173 | 174 | 175 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/category/SelectedCategoryScreen.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.category 2 | 3 | 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.lazy.grid.GridCells 11 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.Scaffold 14 | import androidx.compose.material.SnackbarHost 15 | import androidx.compose.material.SnackbarHostState 16 | import androidx.compose.material.Text 17 | import androidx.compose.material.TopAppBar 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.filled.ArrowBack 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.text.TextStyle 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.text.style.TextAlign 30 | import androidx.compose.ui.unit.dp 31 | import androidx.compose.ui.unit.sp 32 | import cafe.adriel.voyager.core.screen.Screen 33 | import cafe.adriel.voyager.navigator.LocalNavigator 34 | import cafe.adriel.voyager.navigator.Navigator 35 | import cafe.adriel.voyager.navigator.currentOrThrow 36 | import data.model.Category 37 | import data.model.Product 38 | import data.network.Resource 39 | import kotlinx.coroutines.launch 40 | import org.koin.compose.koinInject 41 | import presentation.components.AppSlider 42 | import presentation.components.LoadingAnimation3 43 | import presentation.components.ProductCard 44 | import presentation.screens.product.DetailTopBar 45 | import presentation.screens.product.ProductDetailsScreen 46 | 47 | class SelectedCategoryScreen(val category: Category) : Screen { 48 | 49 | @Composable 50 | override fun Content() { 51 | val navigator: Navigator = LocalNavigator.currentOrThrow 52 | val viewModel: SelectedCategoryViewModel = koinInject() 53 | 54 | 55 | val snackState = remember { SnackbarHostState() } 56 | val snackScope = rememberCoroutineScope() 57 | val products = viewModel.products.collectAsState() 58 | 59 | viewModel.setStateEvent(SelectedCategoryStateIntent.GetProducts(category.id ?: 0)) 60 | 61 | SnackbarHost(hostState = snackState, Modifier) 62 | 63 | fun launchSnackBar(message: String) { 64 | snackScope.launch { snackState.showSnackbar(message) } 65 | } 66 | 67 | Scaffold( 68 | modifier = Modifier 69 | .fillMaxSize() 70 | .padding(horizontal = 16.dp), 71 | topBar = { CategoryDetailTopBar(navigator = navigator,category) }, 72 | ) { paddingValues -> 73 | Column( 74 | Modifier.padding(paddingValues) 75 | ) { 76 | AppSlider() 77 | 78 | Text( 79 | modifier = Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp), 80 | text = "Products", style = TextStyle( 81 | fontSize = 14.sp, 82 | lineHeight = 20.sp, 83 | fontWeight = FontWeight(500), 84 | color = Color(0xFF9B9B9B), 85 | textAlign = TextAlign.Center, 86 | ) 87 | ) 88 | 89 | products.value.let { result -> 90 | when (result) { 91 | is Resource.Failure -> { 92 | launchSnackBar("some failures occurred") 93 | } 94 | 95 | Resource.Loading -> { 96 | Column( 97 | modifier = Modifier 98 | .fillMaxSize(1.0f), 99 | verticalArrangement = Arrangement.Center, 100 | horizontalAlignment = Alignment.CenterHorizontally 101 | ) { 102 | LoadingAnimation3() 103 | } 104 | } 105 | 106 | is Resource.Success -> { 107 | LazyVerticalGrid( 108 | modifier = Modifier, 109 | columns = GridCells.Adaptive(168.dp), 110 | contentPadding = PaddingValues( 111 | start = 12.dp, 112 | top = 2.dp, 113 | end = 12.dp, 114 | bottom = 16.dp 115 | ), 116 | content = { 117 | items(result.result.size) { index -> 118 | val product = result.result[index] 119 | ProductCard( 120 | product 121 | ) { 122 | navigator?.push(ProductDetailsScreen(product)) 123 | } 124 | } 125 | } 126 | ) 127 | } 128 | 129 | null -> {} 130 | } 131 | } 132 | } 133 | } 134 | 135 | 136 | } 137 | } 138 | 139 | @Composable 140 | fun CategoryDetailTopBar(navigator: Navigator?=null,cateogry: Category) { 141 | TopAppBar( 142 | title = { Text(text = "${cateogry.name}") }, 143 | navigationIcon = { 144 | Icon( 145 | modifier = Modifier.clickable { 146 | navigator?.pop() 147 | }, 148 | imageVector = Icons.Default.ArrowBack, 149 | contentDescription = "Back Button" 150 | ) 151 | }, 152 | backgroundColor = Color.Transparent, 153 | elevation = 0.dp, 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/category/SelectedCategoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.category 2 | 3 | import data.model.Category 4 | import data.model.Product 5 | import data.network.Resource 6 | import domain.core.AppDataStore 7 | import domain.usecase.CategoryUseCase 8 | import domain.usecase.ProductUseCase 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.launch 12 | import presentation.base.BaseViewModel 13 | import presentation.base.BaseViewModel.AllStateEvent 14 | 15 | 16 | class SelectedCategoryViewModel( 17 | private val productUseCase: ProductUseCase, 18 | ) : BaseViewModel() { 19 | 20 | 21 | private val _products = MutableStateFlow>?>(null) 22 | val products: StateFlow>?> = _products 23 | 24 | override fun setStateEvent(state: AllStateEvent) { 25 | when (state) { 26 | is SelectedCategoryStateIntent.GetProducts -> { 27 | viewModelScope.launch { 28 | _products.value = Resource.Loading 29 | _products.value = productUseCase.getCategoryProducts(state.categoryId) 30 | } 31 | } 32 | } 33 | 34 | } 35 | override fun setUiEvent(state: AllStateEvent) { 36 | } 37 | 38 | fun clearStates() { 39 | viewModelScope.launch { 40 | _products.emit(null) 41 | } 42 | } 43 | } 44 | 45 | 46 | sealed class SelectedCategoryStateIntent : AllStateEvent() { 47 | data class GetProducts(val categoryId: Int) : SelectedCategoryStateIntent() 48 | } 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main 2 | 3 | 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.animation.slideInVertically 6 | import androidx.compose.animation.slideOutVertically 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material.BottomNavigation 9 | import androidx.compose.material.Scaffold 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.saveable.rememberSaveable 13 | import androidx.compose.ui.Modifier 14 | import cafe.adriel.voyager.core.screen.Screen 15 | import cafe.adriel.voyager.navigator.LocalNavigator 16 | import cafe.adriel.voyager.navigator.Navigator 17 | import cafe.adriel.voyager.navigator.currentOrThrow 18 | import cafe.adriel.voyager.navigator.tab.CurrentTab 19 | import cafe.adriel.voyager.navigator.tab.TabNavigator 20 | import org.jetbrains.compose.resources.ExperimentalResourceApi 21 | import org.koin.compose.koinInject 22 | import presentation.components.TabNavigationItem 23 | import presentation.screens.main.taps.category.CategoryScreen 24 | import presentation.screens.main.taps.category.CategoryTab 25 | import presentation.screens.main.taps.home.HomeScreen 26 | import presentation.screens.main.taps.home.HomeTab 27 | import presentation.screens.main.taps.profile.ProfileTab 28 | import presentation.screens.main.taps.search.SearchTab 29 | 30 | object MainScreen : Screen { 31 | 32 | @Composable 33 | override fun Content() { 34 | val navigator = LocalNavigator.currentOrThrow 35 | MainScreen.mainContent(navigator) 36 | } 37 | 38 | @Composable 39 | private fun mainContent( 40 | navigator: Navigator? = null, 41 | viewModel: MainViewModel = koinInject() 42 | ) { 43 | 44 | val navBackStackEntry = navigator?.lastItem 45 | val bottomBarState = rememberSaveable { (mutableStateOf(true)) } 46 | when(navBackStackEntry){ 47 | is HomeScreen ->{ 48 | bottomBarState.value = true 49 | } 50 | is CategoryScreen ->{ 51 | bottomBarState.value = false 52 | } 53 | } 54 | 55 | TabNavigator( 56 | tab = HomeTab 57 | ) { 58 | Scaffold( 59 | modifier = Modifier.fillMaxSize(), 60 | bottomBar = { 61 | 62 | AnimatedVisibility( 63 | visible = bottomBarState.value, 64 | enter = slideInVertically(initialOffsetY = { it }), 65 | exit = slideOutVertically(targetOffsetY = { it }), 66 | ) { 67 | BottomNavigation { 68 | TabNavigationItem(HomeTab) 69 | TabNavigationItem(CategoryTab) 70 | TabNavigationItem(SearchTab) 71 | TabNavigationItem(ProfileTab) 72 | } 73 | } 74 | }, 75 | content = { CurrentTab() }, 76 | ) 77 | } 78 | 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import domain.core.AppDataStore 6 | import kotlinx.coroutines.launch 7 | import presentation.base.BaseViewModel 8 | import presentation.base.BaseViewModel.AllStateEvent 9 | import presentation.base.DataStoreKeys 10 | 11 | 12 | class MainViewModel( 13 | private val appDataStoreManager: AppDataStore, 14 | ) : BaseViewModel() { 15 | 16 | private val _userToken = mutableStateOf(String()) 17 | val userToken: State = _userToken 18 | 19 | override fun setStateEvent(state: AllStateEvent) { 20 | when (state) { 21 | is MainStateIntent.GettingToken -> { 22 | viewModelScope.launch { 23 | val token = appDataStoreManager.readValue(DataStoreKeys.TOKEN) 24 | _userToken.value = token?:"" 25 | } 26 | } 27 | } 28 | 29 | } 30 | 31 | override fun setUiEvent(state: AllStateEvent) { 32 | } 33 | 34 | } 35 | 36 | 37 | sealed class MainStateIntent : AllStateEvent() { 38 | object GettingToken : MainStateIntent() 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/category/CategoriesViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.category 2 | 3 | import data.model.Category 4 | import data.network.Resource 5 | import domain.usecase.CategoryUseCase 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | import kotlinx.coroutines.launch 9 | import presentation.base.BaseViewModel 10 | import presentation.base.BaseViewModel.AllStateEvent 11 | 12 | 13 | class CategoriesViewModel( 14 | private val categoryUseCase: CategoryUseCase, 15 | ) : BaseViewModel() { 16 | 17 | 18 | private val _categories = MutableStateFlow>?>(null) 19 | val categories: StateFlow>?> = _categories 20 | 21 | init { 22 | setStateEvent(CategoriesStateIntent.GetCategories) 23 | } 24 | 25 | override fun setStateEvent(state: AllStateEvent) { 26 | when (state) { 27 | is CategoriesStateIntent.GetCategories -> { 28 | viewModelScope.launch { 29 | _categories.value = Resource.Loading 30 | _categories.value = categoryUseCase.invoke() 31 | } 32 | } 33 | } 34 | 35 | } 36 | 37 | override fun setUiEvent(state: AllStateEvent) { 38 | } 39 | 40 | fun clearCategoriesStates() { 41 | viewModelScope.launch { 42 | _categories.emit(null) 43 | } 44 | } 45 | } 46 | 47 | 48 | sealed class CategoriesStateIntent : AllStateEvent() { 49 | object GetCategories : CategoriesStateIntent() 50 | } 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/category/CategoryScreen.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.category 2 | 3 | 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.lazy.LazyColumn 14 | import androidx.compose.foundation.lazy.LazyRow 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material.Card 17 | import androidx.compose.material.SnackbarHost 18 | import androidx.compose.material.SnackbarHostState 19 | import androidx.compose.material.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.collectAsState 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.clip 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.layout.ContentScale 29 | import androidx.compose.ui.text.TextStyle 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.text.style.TextAlign 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.unit.sp 34 | import cafe.adriel.voyager.core.screen.Screen 35 | import cafe.adriel.voyager.navigator.LocalNavigator 36 | import cafe.adriel.voyager.navigator.Navigator 37 | import cafe.adriel.voyager.navigator.currentOrThrow 38 | import data.model.Category 39 | import data.network.Resource 40 | import io.kamel.image.KamelImage 41 | import io.kamel.image.asyncPainterResource 42 | import kotlinx.coroutines.launch 43 | import org.jetbrains.compose.resources.ExperimentalResourceApi 44 | import org.koin.compose.koinInject 45 | import presentation.components.CategoryCardTag 46 | import presentation.components.Gap 47 | import presentation.components.LoadingAnimation3 48 | import presentation.screens.category.SelectedCategoryScreen 49 | import presentation.screens.main.taps.home.HomeStateIntent 50 | import presentation.screens.main.taps.home.HomeViewModel 51 | 52 | object CategoryScreen : Screen { 53 | 54 | @Composable 55 | override fun Content() { 56 | val navigator = LocalNavigator.currentOrThrow 57 | mainContent(navigator) 58 | } 59 | 60 | @OptIn(ExperimentalResourceApi::class) 61 | @Composable 62 | private fun mainContent( 63 | navigator: Navigator? = null, 64 | ) { 65 | val viewModel: CategoriesViewModel = koinInject() 66 | 67 | val categories = viewModel.categories.collectAsState() 68 | 69 | /*val userToken = viewModel?.userToken?.value 70 | viewModel.setStateEvent(MainStateIntent.GettingToken) 71 | Text("Hello Your Token is : $userToken")*/ 72 | 73 | val snackState = remember { SnackbarHostState() } 74 | val snackScope = rememberCoroutineScope() 75 | 76 | SnackbarHost(hostState = snackState, Modifier) 77 | 78 | fun launchSnackBar(message: String) { 79 | snackScope.launch { snackState.showSnackbar(message) } 80 | } 81 | 82 | categories.value.let { result -> 83 | when (result) { 84 | is Resource.Failure -> { 85 | launchSnackBar("some failures occurred") 86 | } 87 | Resource.Loading -> { 88 | Column( 89 | modifier = Modifier 90 | .fillMaxSize(1.0f), 91 | verticalArrangement = Arrangement.Center, 92 | horizontalAlignment = Alignment.CenterHorizontally 93 | ) { 94 | LoadingAnimation3() 95 | } 96 | } 97 | 98 | is Resource.Success -> { 99 | CategoryContent( 100 | navigator, 101 | result.result 102 | ) 103 | } 104 | 105 | null -> {} 106 | } 107 | } 108 | 109 | 110 | } 111 | 112 | 113 | @Composable 114 | private fun CategoryContent(navigator: Navigator?,categories: List) { 115 | Column( 116 | modifier = Modifier 117 | .background(color = Color(0xFFF9F9F9)) 118 | .padding(16.dp) 119 | ) { 120 | Text( 121 | "Choose category", style = TextStyle( 122 | fontSize = 14.sp, 123 | lineHeight = 20.sp, 124 | fontWeight = FontWeight(500), 125 | color = Color(0xFF9B9B9B), 126 | textAlign = TextAlign.Center, 127 | ) 128 | ) 129 | Gap(10.dp) 130 | LazyColumn { 131 | items(categories.size) { 132 | Card( 133 | elevation = 4.dp, 134 | modifier = Modifier.background( 135 | color = androidx.compose.ui.graphics.Color.White, 136 | shape = RoundedCornerShape(10.dp) 137 | ).clickable { 138 | navigator?.push(SelectedCategoryScreen(categories[it])) 139 | } 140 | .clip( 141 | shape = RoundedCornerShape(10.dp), 142 | ) 143 | ) { 144 | Row( 145 | verticalAlignment = Alignment.CenterVertically, 146 | modifier = Modifier.height(100.dp).clip( 147 | shape = RoundedCornerShape(10.dp), 148 | ).background(color = Color.White, shape = RoundedCornerShape(10.dp)) 149 | .clip( 150 | shape = RoundedCornerShape(10.dp), 151 | ) 152 | ) { 153 | Text( 154 | categories[it].name?:"", 155 | modifier = Modifier.weight(1f).padding(start = 16.dp), 156 | style = TextStyle( 157 | fontSize = 18.sp, 158 | lineHeight = 22.sp, 159 | fontWeight = FontWeight(400), 160 | color = Color(0xFF222222), 161 | ) 162 | ) 163 | KamelImage( 164 | asyncPainterResource(categories[it].image?:""), 165 | categories[it].name, 166 | contentScale = ContentScale.FillWidth, 167 | modifier = Modifier.weight(1f) 168 | ) 169 | } 170 | } 171 | Gap(10.dp) 172 | Spacer(modifier = Modifier.height(10.dp)) 173 | } 174 | } 175 | } 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/category/CategoryTab.kt: -------------------------------------------------------------------------------- 1 | 2 | package presentation.screens.main.taps.category 3 | 4 | import androidx.compose.animation.ExperimentalAnimationApi 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.Menu 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 10 | import cafe.adriel.voyager.navigator.Navigator 11 | import cafe.adriel.voyager.navigator.tab.Tab 12 | import cafe.adriel.voyager.navigator.tab.TabOptions 13 | import cafe.adriel.voyager.transitions.SlideTransition 14 | 15 | object CategoryTab : Tab { 16 | 17 | override val options: TabOptions 18 | @Composable 19 | get() { 20 | val title = "Cart" 21 | val icon = rememberVectorPainter(Icons.Default.Menu) 22 | 23 | return remember { 24 | TabOptions( 25 | index = 0u, 26 | title = title, 27 | icon = icon 28 | ) 29 | } 30 | } 31 | 32 | @Composable 33 | override fun Content() { 34 | Navigator(screen = CategoryScreen) { navigator -> 35 | SlideTransition(navigator = navigator) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/home/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.home 2 | 3 | 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.foundation.lazy.grid.GridCells 11 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 12 | import androidx.compose.material.SnackbarHost 13 | import androidx.compose.material.SnackbarHostState 14 | import androidx.compose.material.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.collectAsState 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.rememberCoroutineScope 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.text.TextStyle 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.text.style.TextAlign 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import cafe.adriel.voyager.core.screen.Screen 28 | import cafe.adriel.voyager.navigator.LocalNavigator 29 | import cafe.adriel.voyager.navigator.Navigator 30 | import cafe.adriel.voyager.navigator.currentOrThrow 31 | import data.network.Resource 32 | import kotlinx.coroutines.launch 33 | import org.koin.compose.koinInject 34 | import presentation.components.AppSlider 35 | import presentation.components.CategoryCardTag 36 | import presentation.components.LoadingAnimation3 37 | import presentation.components.ProductCard 38 | import presentation.screens.product.ProductDetailsScreen 39 | 40 | class HomeScreen : Screen { 41 | 42 | @Composable 43 | override fun Content() { 44 | val navigator: Navigator = LocalNavigator.currentOrThrow 45 | val viewModel: HomeViewModel = koinInject() 46 | 47 | val categories = viewModel.categories.collectAsState() 48 | val products = viewModel.products.collectAsState() 49 | 50 | 51 | val snackState = remember { SnackbarHostState() } 52 | val snackScope = rememberCoroutineScope() 53 | 54 | SnackbarHost(hostState = snackState, Modifier) 55 | 56 | fun launchSnackBar(message: String) { 57 | snackScope.launch { snackState.showSnackbar(message) } 58 | } 59 | 60 | Column { 61 | AppSlider() 62 | 63 | Text( 64 | modifier = Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp), 65 | text = "Choose category", style = TextStyle( 66 | fontSize = 14.sp, 67 | lineHeight = 20.sp, 68 | fontWeight = FontWeight(500), 69 | color = Color(0xFF9B9B9B), 70 | textAlign = TextAlign.Center, 71 | ) 72 | ) 73 | 74 | categories.value.let { result -> 75 | when (result) { 76 | is Resource.Failure -> { 77 | launchSnackBar("some failures occurred") 78 | } 79 | 80 | Resource.Loading -> { 81 | Column( 82 | modifier = Modifier 83 | .fillMaxSize(1.0f), 84 | verticalArrangement = Arrangement.Center, 85 | horizontalAlignment = Alignment.CenterHorizontally 86 | ) { 87 | LoadingAnimation3() 88 | } 89 | } 90 | 91 | is Resource.Success -> { 92 | LazyRow { 93 | items(result.result.size) { 94 | var categoryItem = result.result[it] 95 | CategoryCardTag(categoryItem) { 96 | viewModel.setStateEvent(HomeStateIntent.SelectCategory(it.id?:0)) 97 | } 98 | } 99 | } 100 | } 101 | 102 | null -> {} 103 | } 104 | } 105 | 106 | products.value.let { result -> 107 | when (result) { 108 | is Resource.Failure -> { 109 | launchSnackBar("some failures occurred") 110 | } 111 | 112 | Resource.Loading -> { 113 | Column( 114 | modifier = Modifier 115 | .fillMaxSize(1.0f), 116 | verticalArrangement = Arrangement.Center, 117 | horizontalAlignment = Alignment.CenterHorizontally 118 | ) { 119 | LoadingAnimation3() 120 | } 121 | } 122 | 123 | is Resource.Success -> { 124 | LazyVerticalGrid( 125 | modifier = Modifier, 126 | columns = GridCells.Adaptive(168.dp), 127 | contentPadding = PaddingValues( 128 | start = 12.dp, 129 | top = 2.dp, 130 | end = 12.dp, 131 | bottom = 16.dp 132 | ), 133 | content = { 134 | items(result.result.size) { index -> 135 | val product = result.result[index] 136 | ProductCard( 137 | product 138 | ) { 139 | navigator?.push(ProductDetailsScreen(product)) 140 | } 141 | } 142 | } 143 | ) 144 | } 145 | 146 | null -> {} 147 | } 148 | } 149 | } 150 | 151 | 152 | } 153 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/home/HomeTab.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalAnimationApi::class) 2 | 3 | package presentation.screens.main.taps.home 4 | 5 | import androidx.compose.animation.ExperimentalAnimationApi 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.Home 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 11 | import cafe.adriel.voyager.navigator.Navigator 12 | import cafe.adriel.voyager.navigator.tab.Tab 13 | import cafe.adriel.voyager.navigator.tab.TabOptions 14 | import cafe.adriel.voyager.transitions.SlideTransition 15 | 16 | object HomeTab : Tab { 17 | 18 | override val options: TabOptions 19 | @Composable 20 | get() { 21 | val title = "Home" 22 | val icon = rememberVectorPainter(Icons.Default.Home) 23 | 24 | return remember { 25 | TabOptions( 26 | index = 0u, 27 | title = title, 28 | icon = icon 29 | ) 30 | } 31 | } 32 | 33 | @Composable 34 | override fun Content() { 35 | Navigator(screen = HomeScreen()) { navigator -> 36 | SlideTransition(navigator = navigator) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.home 2 | 3 | import data.model.Category 4 | import data.model.Product 5 | import data.network.Resource 6 | import domain.core.AppDataStore 7 | import domain.usecase.CategoryUseCase 8 | import domain.usecase.ProductUseCase 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.launch 12 | import presentation.base.BaseViewModel 13 | import presentation.base.BaseViewModel.AllStateEvent 14 | 15 | 16 | class HomeViewModel( 17 | private val categoryUseCase: CategoryUseCase, 18 | private val productUseCase: ProductUseCase, 19 | private val appDataStoreManager: AppDataStore, 20 | ) : BaseViewModel() { 21 | 22 | 23 | private val _categories = MutableStateFlow>?>(null) 24 | val categories: StateFlow>?> = _categories 25 | 26 | private val _products = MutableStateFlow>?>(null) 27 | val products: StateFlow>?> = _products 28 | 29 | 30 | init { 31 | setStateEvent(HomeStateIntent.GetCategories) 32 | } 33 | 34 | override fun setStateEvent(state: AllStateEvent) { 35 | when (state) { 36 | is HomeStateIntent.GetCategories -> { 37 | viewModelScope.launch { 38 | _categories.value = Resource.Loading 39 | _categories.value = categoryUseCase.invoke() 40 | when(_categories.value){ 41 | is Resource.Failure -> {} 42 | Resource.Loading -> {} 43 | is Resource.Success -> { 44 | _products.value = productUseCase.getCategoryProducts( 45 | (_categories.value as Resource.Success>).result[0].id ?:0) 46 | } 47 | null -> {} 48 | } 49 | } 50 | } 51 | 52 | is HomeStateIntent.SelectCategory -> { 53 | viewModelScope.launch { 54 | _products.value = Resource.Loading 55 | _products.value = productUseCase.getCategoryProducts(state.categoryId) 56 | } 57 | } 58 | } 59 | 60 | } 61 | override fun setUiEvent(state: AllStateEvent) { 62 | } 63 | 64 | fun clearHomeStates() { 65 | viewModelScope.launch { 66 | _categories.emit(null) 67 | _products.emit(null) 68 | } 69 | } 70 | } 71 | 72 | 73 | sealed class HomeStateIntent : AllStateEvent() { 74 | object GetCategories : HomeStateIntent() 75 | data class SelectCategory(val categoryId: Int) : HomeStateIntent() 76 | } 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/profile/ProfileScreen.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.profile 2 | 3 | 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.PaddingValues 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.heightIn 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.layout.width 18 | import androidx.compose.foundation.lazy.LazyColumn 19 | import androidx.compose.foundation.lazy.LazyRow 20 | import androidx.compose.foundation.lazy.grid.GridCells 21 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 22 | import androidx.compose.foundation.rememberScrollState 23 | import androidx.compose.foundation.shape.CircleShape 24 | import androidx.compose.foundation.shape.RoundedCornerShape 25 | import androidx.compose.foundation.verticalScroll 26 | import androidx.compose.material.Card 27 | import androidx.compose.material.Icon 28 | import androidx.compose.material.SnackbarHost 29 | import androidx.compose.material.SnackbarHostState 30 | import androidx.compose.material.Text 31 | import androidx.compose.material.TextField 32 | import androidx.compose.material.TextFieldDefaults 33 | import androidx.compose.material.icons.Icons 34 | import androidx.compose.material.icons.filled.Edit 35 | import androidx.compose.material.icons.filled.Search 36 | import androidx.compose.runtime.Composable 37 | import androidx.compose.runtime.collectAsState 38 | import androidx.compose.runtime.getValue 39 | import androidx.compose.runtime.mutableStateOf 40 | import androidx.compose.runtime.remember 41 | import androidx.compose.runtime.rememberCoroutineScope 42 | import androidx.compose.runtime.setValue 43 | import androidx.compose.ui.Alignment 44 | import androidx.compose.ui.Modifier 45 | import androidx.compose.ui.graphics.Color 46 | import androidx.compose.ui.text.TextStyle 47 | import androidx.compose.ui.text.font.FontWeight 48 | import androidx.compose.ui.text.style.TextAlign 49 | import androidx.compose.ui.unit.dp 50 | import androidx.compose.ui.unit.sp 51 | import cafe.adriel.voyager.core.screen.Screen 52 | import cafe.adriel.voyager.navigator.LocalNavigator 53 | import cafe.adriel.voyager.navigator.Navigator 54 | import cafe.adriel.voyager.navigator.currentOrThrow 55 | import data.network.Resource 56 | import io.kamel.image.KamelImage 57 | import io.kamel.image.asyncPainterResource 58 | import kotlinx.coroutines.launch 59 | import org.koin.compose.koinInject 60 | import presentation.components.AppSlider 61 | import presentation.components.CategoryCardTag 62 | import presentation.components.CustomDialogSheet 63 | import presentation.components.LoadingAnimation3 64 | import presentation.components.ProductCard 65 | import presentation.components.ProfileSectionCard 66 | import presentation.screens.auth.updateProfile.UpdateProfileScreen 67 | import presentation.screens.product.ProductDetailsScreen 68 | import presentation.screens.settings.SettingsScreen 69 | import presentation.theme.gray2 70 | import utils.AppStrings 71 | 72 | class ProfileScreen : Screen { 73 | 74 | @Composable 75 | override fun Content() { 76 | val navigator :Navigator = LocalNavigator.currentOrThrow 77 | val viewModel: ProfileViewModel = koinInject() 78 | 79 | var showLogOutSheet by remember { mutableStateOf(false) } 80 | val logoutState = viewModel.logout.collectAsState() 81 | 82 | val nameState = viewModel.userName.value 83 | val emailState = viewModel.email.value 84 | 85 | Column(modifier = Modifier.padding(16.dp)) { 86 | 87 | Text( 88 | text = "Profile", 89 | style = TextStyle( 90 | fontSize = 24.sp, 91 | fontWeight = FontWeight(700), 92 | color = Color.Black, 93 | 94 | ), 95 | ) 96 | 97 | Spacer(modifier = Modifier.height(30.dp)) 98 | 99 | Row( 100 | modifier = Modifier 101 | .fillMaxWidth() 102 | .height(90.dp), 103 | horizontalArrangement = Arrangement.Center, 104 | verticalAlignment = Alignment.CenterVertically 105 | ) { 106 | 107 | Card(modifier = Modifier.size(80.dp), shape = CircleShape) { 108 | KamelImage( 109 | asyncPainterResource("https://i.ibb.co/cyP8x9m/my-passport-photo.jpg"), 110 | contentDescription = "profile_pic" 111 | ) 112 | } 113 | 114 | Spacer(modifier = Modifier.width(10.dp)) 115 | 116 | Column { 117 | Text( 118 | text = nameState?.text ?: "", 119 | style = TextStyle( 120 | fontSize = 16.sp, 121 | fontWeight = FontWeight(700), 122 | color = Color.Black, 123 | ) 124 | ) 125 | 126 | Spacer(modifier = Modifier.height(5.dp)) 127 | 128 | Text( 129 | text = emailState?.text ?: "", 130 | style = TextStyle( 131 | fontSize = 14.sp, 132 | fontWeight = FontWeight(600), 133 | color = gray2, 134 | ) 135 | ) 136 | 137 | Spacer(modifier = Modifier.height(10.dp)) 138 | 139 | Spacer(modifier = Modifier.weight(1f)) 140 | 141 | 142 | } 143 | 144 | 145 | 146 | Spacer(modifier = Modifier.weight(1f)) 147 | 148 | Icon( 149 | modifier = Modifier 150 | .clickable { 151 | navigator?.push(UpdateProfileScreen) 152 | } 153 | .size(24.dp), 154 | tint = Color.Black, 155 | imageVector = Icons.Default.Edit, 156 | contentDescription = "" 157 | ) 158 | } 159 | 160 | 161 | Spacer(modifier = Modifier.height(30.dp)) 162 | 163 | Column( 164 | Modifier.verticalScroll(rememberScrollState()) 165 | 166 | ) { 167 | ProfileSectionCard({ }, title = "My Orders") { 168 | } 169 | 170 | ProfileSectionCard({ }, title = "Cart") { 171 | 172 | } 173 | 174 | ProfileSectionCard( 175 | { }, 176 | title = "Settings" 177 | ) { 178 | navigator?.push(SettingsScreen) 179 | } 180 | ProfileSectionCard( 181 | { 182 | }, 183 | color = Color.Red, 184 | withLine = false, 185 | title = "Logout" 186 | ) { 187 | showLogOutSheet = true 188 | } 189 | 190 | } 191 | 192 | 193 | if (showLogOutSheet) { 194 | CustomDialogSheet( 195 | title = AppStrings.log_out.stringValue, 196 | buttonText = AppStrings.log_out.stringValue, 197 | message = AppStrings.are_you_sure_you_want_log_out.stringValue, 198 | onAccept = { 199 | showLogOutSheet = false 200 | viewModel.setStateEvent(ProfileStateIntent.LogoutUser) 201 | }, 202 | onReject = { 203 | showLogOutSheet = false 204 | }) 205 | } 206 | 207 | } 208 | 209 | when (logoutState?.value) { 210 | true -> { 211 | navigator?.popUntilRoot() 212 | } 213 | null -> {} 214 | else -> {} 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/profile/ProfileTab.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.profile 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.rememberScrollState 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.foundation.verticalScroll 16 | import androidx.compose.material.Card 17 | import androidx.compose.material.Icon 18 | import androidx.compose.material.Text 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.Edit 21 | import androidx.compose.material.icons.filled.Person 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.collectAsState 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableStateOf 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 32 | import androidx.compose.ui.text.TextStyle 33 | import androidx.compose.ui.text.font.FontWeight 34 | import androidx.compose.ui.unit.dp 35 | import androidx.compose.ui.unit.sp 36 | import cafe.adriel.voyager.navigator.LocalNavigator 37 | import cafe.adriel.voyager.navigator.Navigator 38 | import cafe.adriel.voyager.navigator.currentOrThrow 39 | import cafe.adriel.voyager.navigator.tab.Tab 40 | import cafe.adriel.voyager.navigator.tab.TabOptions 41 | import cafe.adriel.voyager.transitions.SlideTransition 42 | import io.kamel.image.KamelImage 43 | import io.kamel.image.asyncPainterResource 44 | import org.koin.compose.koinInject 45 | import presentation.components.CustomDialogSheet 46 | import presentation.components.ProfileSectionCard 47 | import presentation.screens.main.taps.search.SearchScreen 48 | import presentation.screens.settings.SettingsScreen 49 | import presentation.theme.gray2 50 | import utils.AppStrings 51 | 52 | 53 | object ProfileTab : Tab { 54 | 55 | override val options: TabOptions 56 | @Composable 57 | get() { 58 | val icon = rememberVectorPainter(Icons.Default.Person) 59 | 60 | return remember { 61 | TabOptions( 62 | index = 3u, 63 | title = "Profile", 64 | icon = icon 65 | ) 66 | } 67 | } 68 | 69 | @Composable 70 | override fun Content() { 71 | Navigator(screen = ProfileScreen()) { navigator -> 72 | SlideTransition(navigator = navigator) 73 | } 74 | } 75 | 76 | 77 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/profile/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.profile 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import data.model.TextFieldState 6 | import data.model.response.RegisterResponse 7 | import data.network.Resource 8 | import domain.core.AppDataStore 9 | import domain.usecase.GetProfileUseCase 10 | import kotlinx.coroutines.async 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | 14 | import kotlinx.coroutines.launch 15 | import presentation.base.BaseViewModel 16 | import presentation.base.BaseViewModel.AllStateEvent 17 | import presentation.base.DataStoreKeys 18 | import presentation.screens.auth.updateProfile.UpdateProfileStateIntent 19 | import presentation.screens.auth.updateProfile.UpdateProfileUIEvent 20 | 21 | 22 | class ProfileViewModel( 23 | private val appDataStoreManager: AppDataStore, 24 | private val getProfileUseCase: GetProfileUseCase, 25 | ) : BaseViewModel() { 26 | 27 | 28 | private val _logout = MutableStateFlow(null) 29 | val logout: StateFlow = _logout 30 | 31 | private var userProfile:RegisterResponse?=null 32 | 33 | private val _nameState = mutableStateOf( 34 | TextFieldState( 35 | text = "", 36 | hint = "Enter your Name", 37 | isHintVisible = false, 38 | ) 39 | ) 40 | val userName: State = _nameState 41 | 42 | private val _emailState = mutableStateOf( 43 | TextFieldState( 44 | text = "", 45 | hint = "Enter your valid email", 46 | isHintVisible = false, 47 | ) 48 | ) 49 | val email: State = _emailState 50 | 51 | 52 | init { 53 | setStateEvent(ProfileStateIntent.GettingProfile) 54 | } 55 | override fun setStateEvent(state: AllStateEvent) { 56 | when (state) { 57 | is ProfileStateIntent.LogoutUser -> { 58 | viewModelScope.launch { 59 | async { 60 | appDataStoreManager.setValue(DataStoreKeys.TOKEN,"") 61 | }.await() 62 | _logout.emit(true) 63 | 64 | 65 | } 66 | } 67 | is ProfileStateIntent.GettingProfile -> { 68 | viewModelScope.launch { 69 | val response = getProfileUseCase.invoke() 70 | when(getProfileUseCase.invoke()){ 71 | is Resource.Failure -> {} 72 | Resource.Loading -> {} 73 | is Resource.Success -> { 74 | userProfile = (response as Resource.Success).result 75 | userProfile?.let { 76 | _nameState.value = userName.value.copy( 77 | text = userProfile?.name?:"" 78 | ) 79 | _emailState.value = email.value.copy( 80 | text = userProfile?.email?:"" 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | } 90 | 91 | override fun setUiEvent(state: AllStateEvent) { 92 | } 93 | 94 | override fun onCleared() { 95 | super.onCleared() 96 | // viewModelScope.launch { 97 | // _login.emit(null) 98 | // } 99 | } 100 | } 101 | 102 | 103 | 104 | sealed class ProfileStateIntent : AllStateEvent() { 105 | object LogoutUser : ProfileStateIntent() 106 | object GettingProfile : ProfileStateIntent() 107 | 108 | 109 | } 110 | 111 | 112 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/search/SearchScreen.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.search 2 | 3 | 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.heightIn 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.lazy.LazyColumn 13 | import androidx.compose.foundation.lazy.LazyRow 14 | import androidx.compose.foundation.lazy.grid.GridCells 15 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material.Icon 18 | import androidx.compose.material.SnackbarHost 19 | import androidx.compose.material.SnackbarHostState 20 | import androidx.compose.material.Text 21 | import androidx.compose.material.TextField 22 | import androidx.compose.material.TextFieldDefaults 23 | import androidx.compose.material.icons.Icons 24 | import androidx.compose.material.icons.filled.Search 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.collectAsState 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.rememberCoroutineScope 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.ui.text.TextStyle 33 | import androidx.compose.ui.text.font.FontWeight 34 | import androidx.compose.ui.text.style.TextAlign 35 | import androidx.compose.ui.unit.dp 36 | import androidx.compose.ui.unit.sp 37 | import cafe.adriel.voyager.core.screen.Screen 38 | import cafe.adriel.voyager.navigator.LocalNavigator 39 | import cafe.adriel.voyager.navigator.Navigator 40 | import cafe.adriel.voyager.navigator.currentOrThrow 41 | import data.network.Resource 42 | import kotlinx.coroutines.launch 43 | import org.koin.compose.koinInject 44 | import presentation.components.AppSlider 45 | import presentation.components.CategoryCardTag 46 | import presentation.components.LoadingAnimation3 47 | import presentation.components.ProductCard 48 | import presentation.screens.product.ProductDetailsScreen 49 | 50 | class SearchScreen : Screen { 51 | 52 | @Composable 53 | override fun Content() { 54 | val viewModel: SearchViewModel = koinInject() 55 | val products = viewModel.products.collectAsState() 56 | val snackState = remember { SnackbarHostState() } 57 | val snackScope = rememberCoroutineScope() 58 | val navigator: Navigator = LocalNavigator.currentOrThrow 59 | 60 | SnackbarHost(hostState = snackState, Modifier) 61 | 62 | fun launchSnackBar(message: String) { 63 | snackScope.launch { snackState.showSnackbar(message) } 64 | } 65 | 66 | LazyColumn { 67 | item { 68 | TextField( 69 | leadingIcon = { 70 | Icon(Icons.Default.Search, "Search") 71 | }, 72 | label = { 73 | Text( 74 | text = "What Are you looking for ?", 75 | textAlign = TextAlign.Center, 76 | style = TextStyle( 77 | color = Color(0xFFADB3DA), 78 | ) 79 | ) 80 | }, 81 | value = "", onValueChange = {}, 82 | colors = TextFieldDefaults.outlinedTextFieldColors( 83 | textColor = Color(0xFFADB3DA), 84 | disabledTextColor = Color.Transparent, 85 | backgroundColor = Color(0xFFEFF1F8), 86 | focusedBorderColor = Color.Transparent, 87 | unfocusedBorderColor = Color.Transparent, 88 | disabledBorderColor = Color.Transparent, 89 | ), 90 | shape = RoundedCornerShape(10.dp), 91 | modifier = Modifier 92 | .padding(16.dp) 93 | .fillMaxWidth() 94 | .background(Color.White, shape = RoundedCornerShape(10.dp)), 95 | 96 | ) 97 | } 98 | 99 | item { 100 | products.value.let { result -> 101 | when (result) { 102 | is Resource.Failure -> { 103 | launchSnackBar("some failures occurred") 104 | } 105 | 106 | Resource.Loading -> { 107 | Column( 108 | modifier = Modifier 109 | .fillMaxSize(1.0f), 110 | verticalArrangement = Arrangement.Center, 111 | horizontalAlignment = Alignment.CenterHorizontally 112 | ) { 113 | LoadingAnimation3() 114 | } 115 | } 116 | 117 | is Resource.Success -> { 118 | LazyVerticalGrid( 119 | columns = GridCells.Adaptive(minSize = 150.dp), 120 | modifier = Modifier.heightIn(max = 800.dp) 121 | ) { 122 | items(result.result.size) { index -> 123 | val product = result.result[index] 124 | ProductCard( 125 | product 126 | ) { 127 | navigator.push(ProductDetailsScreen(product)) 128 | } 129 | } 130 | } 131 | } 132 | null -> {} 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/search/SearchTab.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.search 2 | 3 | 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Search 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 9 | import cafe.adriel.voyager.navigator.Navigator 10 | import cafe.adriel.voyager.navigator.tab.Tab 11 | import cafe.adriel.voyager.navigator.tab.TabOptions 12 | import cafe.adriel.voyager.transitions.SlideTransition 13 | 14 | 15 | object SearchTab : Tab { 16 | 17 | override val options: TabOptions 18 | @Composable 19 | get() { 20 | val icon = rememberVectorPainter(Icons.Default.Search) 21 | 22 | return remember { 23 | TabOptions( 24 | index = 2u, 25 | title = "Search", 26 | icon = icon 27 | ) 28 | } 29 | } 30 | 31 | @Composable 32 | override fun Content() { 33 | Navigator(screen = SearchScreen()) { navigator -> 34 | SlideTransition(navigator = navigator) 35 | } 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/main/taps/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.main.taps.search 2 | 3 | import data.model.Product 4 | import data.network.Resource 5 | import domain.usecase.ProductUseCase 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | import kotlinx.coroutines.launch 9 | import presentation.base.BaseViewModel 10 | import presentation.base.BaseViewModel.AllStateEvent 11 | 12 | 13 | class SearchViewModel( 14 | private val productUseCase: ProductUseCase, 15 | ) : BaseViewModel() { 16 | 17 | 18 | private val _products = MutableStateFlow>?>(null) 19 | val products: StateFlow>?> = _products 20 | 21 | 22 | init { 23 | setStateEvent(SearchStateIntent.GetAllProducts) 24 | } 25 | 26 | override fun setStateEvent(state: AllStateEvent) { 27 | when (state) { 28 | is SearchStateIntent.GetAllProducts -> { 29 | viewModelScope.launch { 30 | _products.value = Resource.Loading 31 | _products.value = productUseCase.getAllProducts() 32 | } 33 | } 34 | } 35 | 36 | } 37 | 38 | override fun setUiEvent(state: AllStateEvent) { 39 | } 40 | 41 | fun clearHomeStates() { 42 | viewModelScope.launch { 43 | _products.emit(null) 44 | } 45 | } 46 | } 47 | 48 | 49 | sealed class SearchStateIntent : AllStateEvent() { 50 | object GetAllProducts : SearchStateIntent() 51 | } 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/splash/SplashScreen.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.splash 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.width 11 | import androidx.compose.material.Surface 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.collectAsState 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.ColorFilter 18 | import androidx.compose.ui.graphics.drawscope.withTransform 19 | import androidx.compose.ui.layout.ContentScale 20 | import androidx.compose.ui.unit.dp 21 | import cafe.adriel.voyager.core.screen.Screen 22 | import cafe.adriel.voyager.navigator.LocalNavigator 23 | import cafe.adriel.voyager.navigator.Navigator 24 | import cafe.adriel.voyager.navigator.currentOrThrow 25 | import kotlinx.coroutines.delay 26 | import kotlinx.coroutines.launch 27 | import org.jetbrains.compose.resources.ExperimentalResourceApi 28 | import org.jetbrains.compose.resources.painterResource 29 | import org.koin.compose.koinInject 30 | import presentation.screens.auth.login.LoginScreen 31 | import presentation.screens.main.MainScreen 32 | import presentation.theme.BOLD_SILVER_BACKGROUND_COLOR 33 | import presentation.theme.PrimaryColor 34 | import presentation.theme.SPLASH_ANIMATED_BG_COLOR 35 | 36 | class SplashScreen : Screen { 37 | @Composable 38 | override fun Content() { 39 | val navigator = LocalNavigator.currentOrThrow 40 | SplashScreenContent(navigator) 41 | } 42 | } 43 | 44 | 45 | @Composable 46 | fun SplashScreenContent(navigator: Navigator? = null, 47 | viewModel:SplashViewModel = koinInject()) { 48 | 49 | 50 | val isLogin = viewModel?.isLogin?.collectAsState() 51 | 52 | val scale = remember { 53 | Animatable(0f) 54 | } 55 | 56 | LaunchedEffect(key1 = true, block = { 57 | scale.animateTo( 58 | targetValue = 0.9f, 59 | animationSpec = tween( 60 | durationMillis = 1500, 61 | ) 62 | ) 63 | 64 | delay(10L) 65 | when(isLogin?.value){ 66 | true->{ 67 | navigator?.push(MainScreen) 68 | } 69 | false->{ 70 | navigator?.push(LoginScreen) 71 | } 72 | else->{} 73 | } 74 | }) 75 | SplashAnimationWithContent() 76 | } 77 | 78 | 79 | @OptIn(ExperimentalResourceApi::class) 80 | @Composable 81 | fun SplashAnimationWithContent() { 82 | val logo = painterResource("compose-multiplatform.xml") 83 | val angle1Y = remember { 84 | Animatable(20f) 85 | } 86 | val angle2 = remember { 87 | Animatable(20f) 88 | } 89 | LaunchedEffect(angle1Y, angle2) { 90 | launch { 91 | angle1Y.animateTo(180f, animationSpec = tween(1500)) 92 | } 93 | launch { 94 | angle2.animateTo(180f, animationSpec = tween(1500)) 95 | } 96 | } 97 | 98 | Surface(Modifier.fillMaxSize()) { 99 | Canvas(modifier = Modifier 100 | .fillMaxSize() 101 | .background(SPLASH_ANIMATED_BG_COLOR), 102 | onDraw = { 103 | withTransform({ 104 | // translate(angle1Y.value) 105 | scale(scaleX = angle1Y.value, scaleY = angle2.value) 106 | 107 | }) { 108 | drawCircle(color = BOLD_SILVER_BACKGROUND_COLOR, radius = 8f) 109 | } 110 | } 111 | ) 112 | Image( 113 | modifier = Modifier 114 | .width(40.dp) 115 | .padding(horizontal = 96.dp), 116 | contentScale = ContentScale.Fit, 117 | painter = logo, 118 | colorFilter = ColorFilter.tint(PrimaryColor), 119 | contentDescription = null 120 | ) 121 | } 122 | 123 | } 124 | 125 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/screens/splash/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.screens.splash 2 | 3 | import domain.core.AppDataStore 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import kotlinx.coroutines.flow.StateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import kotlinx.coroutines.launch 8 | import presentation.base.BaseViewModel 9 | import presentation.base.BaseViewModel.AllStateEvent 10 | import presentation.base.DataStoreKeys 11 | 12 | 13 | class SplashViewModel( 14 | private val appDataStoreManager: AppDataStore, 15 | ) : BaseViewModel() { 16 | 17 | private val _isLogin = MutableStateFlow(null) 18 | val isLogin: StateFlow = _isLogin.asStateFlow() 19 | 20 | 21 | init { 22 | viewModelScope.launch { 23 | val token = appDataStoreManager.readValue(DataStoreKeys.TOKEN)?:"" 24 | _isLogin.value = token.isNotBlank() 25 | } 26 | } 27 | 28 | override fun setStateEvent(state: AllStateEvent) { 29 | } 30 | 31 | override fun setUiEvent(state: AllStateEvent) { 32 | } 33 | 34 | } 35 | 36 | 37 | sealed class MainStateIntent : AllStateEvent() { 38 | object GettingToken : MainStateIntent() 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | val BOLD_SILVER_BACKGROUND_COLOR = Color(0xFFF7F5EF) 13 | val SPLASH_ANIMATED_BG_COLOR = Color(0xFFEBEBEB) 14 | val DarkPurple = Color(0xFFC41406) 15 | val Purple200 = Color(0xFF93448C) 16 | val blackTextColor = Color(0xFF101010) 17 | val blackTextColorLight = Color(0xFF1E1E1E) 18 | val gray2 = Color(0xFF8E8EA9) 19 | val yellow = Color(0xFFFFBF51) 20 | val Gold = Color(0xFFFFBF51) 21 | val grayTextColor = Color(0xFF8E8EA9) 22 | val grayBackground = Color(0xFFF0F0F6) 23 | val BorderColor = Color(0xFFE9E8F8) 24 | val PrimaryColor = Color(0xFFDB3022) 25 | val textColorSemiBlack = Color(0xFF222222) 26 | 27 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/theme/Dimens.kt: -------------------------------------------------------------------------------- 1 | package presentation.theme 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | val keyLine0 = 2.dp 6 | val keyLine1 = 4.dp 7 | val keyLine2 = 8.dp 8 | val keyLine3 = 16.dp 9 | val keyLine4 = 18.dp 10 | val bottomAvoidFloat = 76.dp 11 | 12 | val AppBarSize = 50.dp -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package presentation.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.text.TextStyle 6 | import androidx.compose.ui.text.font.Font 7 | import androidx.compose.ui.text.font.FontFamily 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.unit.sp 11 | 12 | // Set of Material typography styles to start with 13 | 14 | 15 | //val fontFamily: FontFamily = fontFamilyResource(MR.fonts.) 16 | 17 | //val somarBoldFont = fontFamilyResource( 18 | // MR.fonts.som 19 | //) 20 | //val somarSemiBold = FontFamily( 21 | // Font(R.font.somar_semibold) 22 | //) 23 | //val somarRegular = FontFamily( 24 | // Font(R.font.somar_regular) 25 | //) 26 | 27 | val Typography = Typography( 28 | body1 = TextStyle( 29 | // fontFamily = somarRegular, 30 | fontWeight = FontWeight.Normal, 31 | fontSize = 20.sp, 32 | ), 33 | body2 = TextStyle( 34 | // fontFamily = somarRegular, 35 | fontWeight = FontWeight.Normal, 36 | fontSize = 20.sp, 37 | ), 38 | h5 = TextStyle( 39 | color = Color.Black, 40 | // fontFamily = somarBoldFont, 41 | fontWeight = FontWeight.ExtraBold, 42 | fontSize = 24.sp, 43 | ), 44 | h4 = TextStyle( 45 | color = textColorSemiBlack, 46 | // fontFamily = somarSemiBold, 47 | fontWeight = FontWeight.Bold, 48 | fontSize = 20.sp, 49 | textAlign = TextAlign.Center 50 | ), 51 | h3 = TextStyle( 52 | color = textColorSemiBlack, 53 | // fontFamily = somarRegular, 54 | fontWeight = FontWeight.Bold, 55 | fontSize = 14.sp, 56 | textAlign = TextAlign.Center 57 | ), 58 | h2 = TextStyle( 59 | color = textColorSemiBlack, 60 | // fontFamily = somarRegular, 61 | fontWeight = FontWeight.Bold, 62 | fontSize = 16.sp, 63 | textAlign = TextAlign.Center 64 | ) 65 | 66 | /* Other default text styles to override 67 | button = TextStyle( 68 | fontFamily = FontFamily.Default, 69 | fontWeight = FontWeight.W500, 70 | fontSize = 14.sp 71 | ), 72 | caption = TextStyle( 73 | fontFamily = FontFamily.Default, 74 | fontWeight = FontWeight.Normal, 75 | fontSize = 12.sp 76 | ) 77 | */ 78 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/utils/AppStrings.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | enum class AppStrings(val stringValue: String) { 4 | Login("Login"), 5 | register("Register"), 6 | update_profile("Update Profile"), 7 | PhoneNumber("Phone Number"), 8 | Password("Password"), 9 | Email("Email"), 10 | Username("User Name"), 11 | LoginDescription("login by email and password"), 12 | register_description("register your information to login"), 13 | update_profile_description("update your profile information"), 14 | DontHaveAccount("Don't Have an account ?"), 15 | RegitserNewAccount("Register New Account"), 16 | have_account("Have an Account?"), 17 | PasswordValidation("password must be upper than 6 char"), 18 | email_validation("please enter valid email to login"), 19 | user_name_validation("please enter valid user name"), 20 | yes("Yes"), 21 | cancel("Cancel"), 22 | log_out("Logout"), 23 | are_you_sure_you_want_log_out("Are you sure you want log out?") 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/utils/CommonUtil.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | object CommonUtil { 6 | fun isValidEmail(email: String): Boolean { 7 | val emailPattern = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") 8 | return emailPattern.matches(email) 9 | } 10 | 11 | inline fun jsonToObject(jsonString: String): T { 12 | return Json.decodeFromString(jsonString) 13 | } 14 | 15 | 16 | } -------------------------------------------------------------------------------- /shared/src/commonMain/resources/arrow_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /shared/src/commonMain/resources/banner1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/banner1.png -------------------------------------------------------------------------------- /shared/src/commonMain/resources/banner2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/banner2.png -------------------------------------------------------------------------------- /shared/src/commonMain/resources/banner3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/banner3.png -------------------------------------------------------------------------------- /shared/src/commonMain/resources/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/resources/flag.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 14 | 17 | 21 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 52 | 55 | 58 | 61 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /shared/src/commonMain/resources/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/resources/not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/not_found.png -------------------------------------------------------------------------------- /shared/src/commonMain/resources/visibility.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /shared/src/commonMain/resources/visibility_off.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/core/Context.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import platform.darwin.NSObject 4 | 5 | actual typealias Context = NSObject -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/core/DataStore.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import platform.Foundation.NSUserDefaults 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | 6 | 7 | actual suspend fun Context.putData(key: String, `object`: String) { 8 | val sharedFlow = MutableSharedFlow() 9 | NSUserDefaults.standardUserDefaults().setObject(`object`, key) 10 | sharedFlow.emit(`object`) 11 | } 12 | 13 | actual suspend inline fun Context.getData(key: String): String? { 14 | return NSUserDefaults.standardUserDefaults().stringForKey(key) 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/core/platformModule.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import io.ktor.client.engine.darwin.Darwin 4 | import org.koin.dsl.module 5 | 6 | actual fun platformModule() = module { 7 | single { Darwin.create() } 8 | 9 | } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/core/viewModelDefinition.kt: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel 4 | import org.koin.core.definition.Definition 5 | import org.koin.core.definition.KoinDefinition 6 | import org.koin.core.module.Module 7 | import org.koin.core.qualifier.Qualifier 8 | 9 | 10 | inline fun Module.viewModelDefinition( 11 | qualifier: Qualifier?, 12 | noinline definition: Definition, 13 | ): KoinDefinition = factory(qualifier = qualifier, definition = definition) -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/di/Utils.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel 4 | import org.koin.core.definition.Definition 5 | import org.koin.core.definition.KoinDefinition 6 | import org.koin.core.module.Module 7 | import org.koin.core.qualifier.Qualifier 8 | 9 | actual inline fun Module.viewModelDefinition( 10 | qualifier: Qualifier?, 11 | noinline definition: Definition, 12 | ): KoinDefinition = factory(qualifier = qualifier, definition = definition) -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/main.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import core.Context 3 | 4 | 5 | actual fun getPlatformName(): String = "iOS" 6 | 7 | fun MainViewController() = ComposeUIViewController { App(Context()) } --------------------------------------------------------------------------------