commit c11ed4394b21ad469db9730e6d95ca93e69c2634 Author: 1billy17 Date: Thu May 15 03:55:35 2025 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f0ef7ca --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.example.mymobilka" + compileSdk = 35 + + defaultConfig { + applicationId = "com.example.mymobilka" + minSdk = 24 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.foundation.layout.android) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") + implementation("androidx.navigation:navigation-compose:2.8.9") + implementation("androidx.compose.runtime:runtime:1.5.4") + implementation("androidx.compose.animation:animation:1.5.4") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + + implementation("com.auth0.android:jwtdecode:2.0.2") + + implementation("io.insert-koin:koin-core:3.5.0") + implementation("io.insert-koin:koin-android:3.5.0") + implementation("io.insert-koin:koin-androidx-compose:3.5.0") + + implementation("androidx.datastore:datastore-preferences:1.0.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/mymobilka/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/mymobilka/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..bf79943 --- /dev/null +++ b/app/src/androidTest/java/com/example/mymobilka/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.mymobilka + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.mymobilka", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8f04ced --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/MainActivity.kt b/app/src/main/java/com/example/mymobilka/MainActivity.kt new file mode 100644 index 0000000..8cf8225 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/MainActivity.kt @@ -0,0 +1,39 @@ +package com.example.mymobilka + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import com.example.mymobilka.navigation.AppNavigation +import com.example.mymobilka.screens.StartScreen.StartSc +import com.example.mymobilka.storage.AppPreferences +import com.example.mymobilka.ui.theme.MatuleTheme +import kotlinx.coroutines.launch +import org.koin.compose.koinInject + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MatuleTheme { + val appPreferences: AppPreferences = koinInject() + val isOnboardingCompleted by appPreferences.isOnboardingCompleted.collectAsState(initial = false) + val scope = rememberCoroutineScope() + + if (isOnboardingCompleted) { + AppNavigation() + } else { + StartSc { + scope.launch { + appPreferences.setOnboardingCompleted(true) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/MyApp.kt b/app/src/main/java/com/example/mymobilka/MyApp.kt new file mode 100644 index 0000000..0599c4f --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/MyApp.kt @@ -0,0 +1,17 @@ +package com.example.mymobilka + +import android.app.Application +import com.example.mymobilka.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class MyApp : Application() { + override fun onCreate() { + super.onCreate() + + startKoin { + androidContext(this@MyApp) + modules(appModule) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/auth/AuthManager.kt b/app/src/main/java/com/example/mymobilka/auth/AuthManager.kt new file mode 100644 index 0000000..fd1f236 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/auth/AuthManager.kt @@ -0,0 +1,35 @@ +package com.example.mymobilka.auth + +import android.content.Context +import androidx.core.content.edit +import com.auth0.android.jwt.JWT + +class AuthManager(private val context: Context) { + private val sharedPrefs = context.getSharedPreferences("auth_prefs", Context.MODE_PRIVATE) + + fun saveAuthToken(token: String) { + sharedPrefs.edit { + putString("auth_token", "Bearer $token") + } + } + + fun getAuthToken(): String? { + return sharedPrefs.getString("auth_token", null) + } + + fun clearAuth() { + sharedPrefs.edit { + remove("auth_token") + } + } + + fun isUserAuthenticated(): Boolean { + val token = getAuthToken() ?: return false + return try { + val jwt = JWT(token) + !jwt.isExpired(10) + } catch (e: Exception) { + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Requests/ForgotPasswordRequest.kt b/app/src/main/java/com/example/mymobilka/client/Models/Requests/ForgotPasswordRequest.kt new file mode 100644 index 0000000..4774388 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Requests/ForgotPasswordRequest.kt @@ -0,0 +1,5 @@ +package com.example.mymobilka.client.Models.Requests + +data class ForgotPasswordRequest( + val email: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Requests/SignInRequest.kt b/app/src/main/java/com/example/mymobilka/client/Models/Requests/SignInRequest.kt new file mode 100644 index 0000000..85e93cd --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Requests/SignInRequest.kt @@ -0,0 +1,6 @@ +package com.example.mymobilka.client.Models.Requests + +data class SignInRequest( + val email: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Requests/SingUpRequest.kt b/app/src/main/java/com/example/mymobilka/client/Models/Requests/SingUpRequest.kt new file mode 100644 index 0000000..fd127dd --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Requests/SingUpRequest.kt @@ -0,0 +1,7 @@ +package com.example.mymobilka.client.Models.Requests + +data class SignUpRequest( + val email: String, + val password: String, + val name: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Requests/VerifyCodeRequest.kt b/app/src/main/java/com/example/mymobilka/client/Models/Requests/VerifyCodeRequest.kt new file mode 100644 index 0000000..131fb27 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Requests/VerifyCodeRequest.kt @@ -0,0 +1,6 @@ +package com.example.mymobilka.client.Models.Requests + +data class VerifyCodeRequest( + val email: String, + val code: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Responses/AuthResponse.kt b/app/src/main/java/com/example/mymobilka/client/Models/Responses/AuthResponse.kt new file mode 100644 index 0000000..7e91ba7 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Responses/AuthResponse.kt @@ -0,0 +1,5 @@ +package com.example.mymobilka.client.Models.Responses + +data class AuthResponse( + val token: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Responses/ForgotPasswordResponse.kt b/app/src/main/java/com/example/mymobilka/client/Models/Responses/ForgotPasswordResponse.kt new file mode 100644 index 0000000..7e1b44d --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Responses/ForgotPasswordResponse.kt @@ -0,0 +1,6 @@ +package com.example.mymobilka.client.Models.Responses + +data class ForgotPasswordResponse( + val message: String, + val code: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Responses/PasswordResponse.kt b/app/src/main/java/com/example/mymobilka/client/Models/Responses/PasswordResponse.kt new file mode 100644 index 0000000..ed69efe --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Responses/PasswordResponse.kt @@ -0,0 +1,5 @@ +package com.example.mymobilka.client.Models.Responses + +data class PasswordResponse( + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/Models/Responses/UserResponse.kt b/app/src/main/java/com/example/mymobilka/client/Models/Responses/UserResponse.kt new file mode 100644 index 0000000..b54adf7 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/Models/Responses/UserResponse.kt @@ -0,0 +1,7 @@ +package com.example.mymobilka.client.Models.Responses + +data class UserResponse( + val id: Int, + val email: String, + val name: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/client/api/AuthApi.kt b/app/src/main/java/com/example/mymobilka/client/api/AuthApi.kt new file mode 100644 index 0000000..730efab --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/client/api/AuthApi.kt @@ -0,0 +1,31 @@ +package com.example.mymobilka.client.api + +import com.example.mymobilka.client.Models.Requests.ForgotPasswordRequest +import com.example.mymobilka.client.Models.Requests.SignInRequest +import com.example.mymobilka.client.Models.Requests.SignUpRequest +import com.example.mymobilka.client.Models.Requests.VerifyCodeRequest +import com.example.mymobilka.client.Models.Responses.AuthResponse +import com.example.mymobilka.client.Models.Responses.ForgotPasswordResponse +import com.example.mymobilka.client.Models.Responses.PasswordResponse + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface AuthApi { + @POST("api/sign-up") + suspend fun signUp(@Body request: SignUpRequest): Response + + @POST("api/sign-in") + suspend fun signIn(@Body request: SignInRequest): Response + + @POST("api/forgot-password") + suspend fun forgotPassword( + @Body request: ForgotPasswordRequest + ): Response + + @POST("api/verify-reset-code") + suspend fun verifyResetCode( + @Body request: VerifyCodeRequest + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/di/AppModule.kt b/app/src/main/java/com/example/mymobilka/di/AppModule.kt new file mode 100644 index 0000000..4e7681b --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/di/AppModule.kt @@ -0,0 +1,43 @@ +package com.example.mymobilka.di + +import com.example.mymobilka.auth.AuthManager +import com.example.mymobilka.client.api.AuthApi +import com.example.mymobilka.repository.AuthRepository +import com.example.mymobilka.storage.AppPreferences +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +val appModule = module { + single { GsonBuilder().create() } + + single { + val interceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + } + + single { + Retrofit.Builder() + .baseUrl("http://10.0.2.2:8080/") + .client(get()) + .addConverterFactory(GsonConverterFactory.create(get())) + .build() + } + + single { get().create(AuthApi::class.java) } + + single { AuthRepository(get()) } + + single { AuthManager(androidContext()) } + + single { AppPreferences(androidContext()) } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/navigation/AppNavigation.kt b/app/src/main/java/com/example/mymobilka/navigation/AppNavigation.kt new file mode 100644 index 0000000..75d8cbf --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/navigation/AppNavigation.kt @@ -0,0 +1,92 @@ +package com.example.mymobilka.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.example.mymobilka.screens.SignInScreen.SignInSc +import com.example.mymobilka.screens.StartScreen.SlideSc +import com.example.mymobilka.screens.SingUpScreen.SignUpSc +import com.example.mymobilka.screens.ForgotPasswordScreen.ForgotPasswordSc +import com.example.mymobilka.screens.CodeForPasswordScreen.CodeForPasswordSc + +sealed class Screen(val route: String) { + object SlideSc : Screen("slide_screen") + object SignInSc : Screen("sign_in_screen") + object SignUpSc : Screen("sign_up_screen") + object ForgotPasswordSc : Screen("forgot_password_screen") + object CodeForPasswordSc : Screen("code_for_password_screen/{email}") { + fun createRoute(email: String) = "code_for_password_screen/$email" + } +} + +@Composable +fun AppNavigation() { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = Screen.SlideSc.route + ) { + composable(Screen.SlideSc.route) { + SlideSc( + onNavigateToSignIn = { + navController.navigate(Screen.SignInSc.route) { + popUpTo(Screen.SlideSc.route) { inclusive = true } + } + } + ) + } + + composable(Screen.SignInSc.route) { + SignInSc( + onSignInSuccess = { }, + onNavigateToSignUp = { + navController.navigate(Screen.SignUpSc.route) + }, + onNavigateToForgotPassword = { + navController.navigate(Screen.ForgotPasswordSc.route) + }, + onNavigateToStart = { + navController.navigate(Screen.SlideSc.route) { + popUpTo(Screen.SignInSc.route) { inclusive = true } + } + } + ) + } + + composable(Screen.SignUpSc.route) { + SignUpSc( + onSignUpSuccess = { }, + onNavigateToSignIn = { + navController.popBackStack() + } + ) + } + + composable(Screen.ForgotPasswordSc.route) { + ForgotPasswordSc( + onBack = { navController.popBackStack() }, + onNavigateToCodeInput = { email -> + navController.navigate(Screen.CodeForPasswordSc.createRoute(email)) + } + ) + } + + composable( + route = Screen.CodeForPasswordSc.route, + arguments = listOf(navArgument("email") { type = NavType.StringType }) + ) { backStackEntry -> + val email = backStackEntry.arguments?.getString("email") ?: "" + CodeForPasswordSc( + email = email, + onBack = { navController.popBackStack() }, + onPasswordVerified = { password -> + navController.popBackStack(Screen.SignInSc.route, inclusive = false) + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/repository/AuthRepository.kt b/app/src/main/java/com/example/mymobilka/repository/AuthRepository.kt new file mode 100644 index 0000000..f4fcafc --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/repository/AuthRepository.kt @@ -0,0 +1,70 @@ +package com.example.mymobilka.repository + +import com.example.mymobilka.client.Models.Requests.ForgotPasswordRequest +import com.example.mymobilka.client.api.AuthApi +import com.example.mymobilka.client.Models.Requests.SignInRequest +import com.example.mymobilka.client.Models.Requests.SignUpRequest +import com.example.mymobilka.client.Models.Requests.VerifyCodeRequest +import com.example.mymobilka.client.Models.Responses.AuthResponse +import com.example.mymobilka.client.Models.Responses.ForgotPasswordResponse +import com.example.mymobilka.client.Models.Responses.PasswordResponse +import com.example.mymobilka.utils.Result + +class AuthRepository ( + private val authApi: AuthApi +) { + suspend fun signIn(email: String, password: String): Result { + return try { + val response = authApi.signIn(SignInRequest(email, password)) + if (response.isSuccessful && response.body() != null) { + Result.Success(response.body()!!) + } else { + Result.Error(Exception("Invalid credentials")) + } + } catch (e: Exception) { + Result.Error(e) + } + } + + suspend fun signUp(name: String, email: String, password: String): Result { + return try { + val response = authApi.signUp(SignUpRequest(email, password, name)) + if (response.isSuccessful) { + Result.Success(response.body()!!) + } else { + Result.Error(Exception("Registration failed")) + } + } catch (e: Exception) { + Result.Error(e) + } + } + + suspend fun sendResetCode(email: String): Result { + return try { + val request = ForgotPasswordRequest(email = email) + val response = authApi.forgotPassword(request) + + if (response.isSuccessful && response.body() != null) { + Result.Success(response.body()!!) + } else { + val errorMsg = response.errorBody()?.string() ?: "Unknown error" + Result.Error(Exception("Failed to send code: $errorMsg")) + } + } catch (e: Exception) { + Result.Error(e) + } + } + + suspend fun verifyResetCode(email: String, code: String): Result { + return try { + val response = authApi.verifyResetCode(VerifyCodeRequest(email, code)) + if (response.isSuccessful && response.body() != null) { + Result.Success(response.body()!!) + } else { + Result.Error(Exception(response.message())) + } + } catch (e: Exception) { + Result.Error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/screens/CodeForPasswordScreen/CodeForPasswordSc.kt b/app/src/main/java/com/example/mymobilka/screens/CodeForPasswordScreen/CodeForPasswordSc.kt new file mode 100644 index 0000000..295ac5a --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/screens/CodeForPasswordScreen/CodeForPasswordSc.kt @@ -0,0 +1,255 @@ +package com.example.mymobilka.screens.CodeForPasswordScreen + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.mymobilka.R +import com.example.mymobilka.client.Models.Responses.PasswordResponse +import com.example.mymobilka.repository.AuthRepository +import com.example.mymobilka.ui.theme.MatuleTheme +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay +import org.koin.compose.koinInject +import com.example.mymobilka.utils.Result.Success +import com.example.mymobilka.utils.Result.Error +import com.example.mymobilka.utils.Result.Loading + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CodeForPasswordSc( + email: String, + onBack: () -> Unit, + onPasswordVerified: (password: String) -> Unit +) { + var otp by remember { mutableStateOf("") } + var countdown by remember { mutableStateOf(30) } + var isResendEnabled by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + val authRepository: AuthRepository = koinInject() + val focusManager = LocalFocusManager.current + + LaunchedEffect(Unit) { + while (countdown > 0) { + delay(1000) + countdown-- + } + isResendEnabled = true + } + + Scaffold( + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(id = R.drawable.back_arrow), + contentDescription = "Назад", + tint = MatuleTheme.colors.text + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent + ) + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = "OTP Проверка", + style = MatuleTheme.typography.headingBold32, + color = MatuleTheme.colors.text, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Пожалуйста, Проверьте Свою Электронную Почту,\nЧтобы Увидеть Код Подтверждения", + style = MatuleTheme.typography.bodyRegular16, + color = MatuleTheme.colors.subTextDark, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = email, + style = MatuleTheme.typography.bodyRegular16, + color = MatuleTheme.colors.accent, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = "OTP Код", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.text, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + repeat(6) { index -> + OutlinedTextField( + value = otp.getOrNull(index)?.toString() ?: "", + onValueChange = { + if (it.length <= 1) { + otp = otp.padEnd(index, '0').take(index) + it + + otp.drop(index + 1) + if (it.length == 1 && index < 5) { + focusManager.moveFocus(FocusDirection.Next) + } + } + }, + modifier = Modifier + .width(48.dp) + .height(56.dp), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = if (index == 5) ImeAction.Done else ImeAction.Next + ), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MatuleTheme.colors.accent, + unfocusedBorderColor = Color.LightGray + ), + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Center, + fontSize = 20.sp + ), + singleLine = true, + maxLines = 1 + ) + } + } + + errorMessage?.let { + Text( + text = it, + color = Color.Red, + modifier = Modifier.padding(top = 8.dp) + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { + if (isResendEnabled) { + coroutineScope.launch { + isLoading = true + errorMessage = null + when (val result = authRepository.sendResetCode(email)) { + is Success -> { + countdown = 30 + isResendEnabled = false + } + is Error -> { + errorMessage = (result as Error).exception.message + } + Loading -> { + + } + } + isLoading = false + } + } + }, + enabled = isResendEnabled && !isLoading + ) { + Text( + text = "Отправить заново", + color = if (isResendEnabled) MatuleTheme.colors.accent + else MatuleTheme.colors.subTextDark + ) + } + + Text( + text = "0:$countdown", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.subTextDark + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = { + if (otp.length == 6) { + coroutineScope.launch { + isLoading = true + errorMessage = null + when (val result = authRepository.verifyResetCode(email, otp)) { + is Success -> { + onPasswordVerified((result as Success).data.password) + } + is Error -> { + errorMessage = (result as Error).exception.message + } + Loading -> { + } + } + isLoading = false + } + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MatuleTheme.colors.accent + ), + enabled = otp.length == 6 && !isLoading + ) { + if (isLoading) { + CircularProgressIndicator(color = Color.White) + } else { + Text( + text = "Подтвердить", + style = MatuleTheme.typography.bodyRegular16.copy( + color = Color.White + ) + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/screens/ForgotPasswordScreen/ForgotPasswordSc.kt b/app/src/main/java/com/example/mymobilka/screens/ForgotPasswordScreen/ForgotPasswordSc.kt new file mode 100644 index 0000000..5ce3b8b --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/screens/ForgotPasswordScreen/ForgotPasswordSc.kt @@ -0,0 +1,215 @@ +package com.example.mymobilka.screens.ForgotPasswordScreen + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.mymobilka.R +import com.example.mymobilka.repository.AuthRepository +import com.example.mymobilka.ui.theme.MatuleTheme +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.koin.compose.koinInject + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ForgotPasswordSc( + onBack: () -> Unit, + onNavigateToCodeInput: (email: String) -> Unit +) { + var email by remember { mutableStateOf("") } + var showMessage by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + val authRepository: AuthRepository = koinInject() + + val alpha by animateFloatAsState( + targetValue = if (showMessage) 1f else 0f, + animationSpec = tween(durationMillis = 300) + ) + + Scaffold( + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(id = R.drawable.back_arrow), + contentDescription = "Назад", + tint = MatuleTheme.colors.text + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent + ) + ) + } + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = "Забыл Пароль", + style = MatuleTheme.typography.headingBold32, + color = MatuleTheme.colors.text, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Введите Свою Учетную Запись\nДля Сброса", + style = MatuleTheme.typography.bodyRegular16, + color = MatuleTheme.colors.subTextDark, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(40.dp)) + + OutlinedTextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + text = "xyz@gmail.com", + color = MatuleTheme.colors.hint + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MatuleTheme.colors.accent, + unfocusedBorderColor = Color.LightGray + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) + ) + + errorMessage?.let { + Text( + text = it, + color = Color.Red, + modifier = Modifier.padding(top = 8.dp) + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = { + if (email.isNotBlank()) { + coroutineScope.launch { + isLoading = true + errorMessage = null + when (val result = authRepository.sendResetCode(email)) { + is com.example.mymobilka.utils.Result.Success -> { + showMessage = true + delay(3000) + showMessage = false + onNavigateToCodeInput(email) + } + is com.example.mymobilka.utils.Result.Error -> { + errorMessage = result.exception.message + } + com.example.mymobilka.utils.Result.Loading -> {} + } + isLoading = false + } + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MatuleTheme.colors.accent + ), + enabled = !isLoading + ) { + if (isLoading) { + CircularProgressIndicator(color = Color.White) + } else { + Text( + text = "Отправить", + style = MatuleTheme.typography.bodyRegular16.copy( + color = Color.White + ) + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + } + + if (showMessage) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + .alpha(alpha), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .background( + color = MatuleTheme.colors.block, + shape = MaterialTheme.shapes.medium + ) + .padding(24.dp) + .width(IntrinsicSize.Max) + ) { + Text( + text = "Проверьте Ваш Email", + style = MatuleTheme.typography.headingBold32, + color = MatuleTheme.colors.text, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Мы Отправили Код Восстановления\nПароля На Вашу Электронную Почту", + style = MatuleTheme.typography.bodyRegular16, + color = MatuleTheme.colors.subTextDark, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = email, + style = MatuleTheme.typography.bodyRegular16, + color = MatuleTheme.colors.accent, + textAlign = TextAlign.Center + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/screens/SignInScreen/SignInSc.kt b/app/src/main/java/com/example/mymobilka/screens/SignInScreen/SignInSc.kt new file mode 100644 index 0000000..85f1c4c --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/screens/SignInScreen/SignInSc.kt @@ -0,0 +1,235 @@ +package com.example.mymobilka.screens.SignInScreen + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.mymobilka.R +import com.example.mymobilka.auth.AuthManager +import com.example.mymobilka.client.Models.Responses.AuthResponse +import com.example.mymobilka.repository.AuthRepository +import com.example.mymobilka.ui.theme.MatuleTheme +import com.example.mymobilka.utils.Result +import kotlinx.coroutines.launch +import org.koin.androidx.compose.get + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SignInSc( + onSignInSuccess: () -> Unit, + onNavigateToSignUp: () -> Unit, + onNavigateToForgotPassword: () -> Unit, + onNavigateToStart: () -> Unit +) { + val authRepository: AuthRepository = get() + val authManager: AuthManager = get() + + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + + Scaffold( + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onNavigateToStart) { + Icon( + painter = painterResource(id = R.drawable.back_arrow), + contentDescription = "Назад", + tint = MatuleTheme.colors.text + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + actionIconContentColor = MatuleTheme.colors.text + ) + ) + }, + bottomBar = { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.Transparent + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextButton( + onClick = onNavigateToSignUp, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Вы впервые? Создать пользователя", + color = MatuleTheme.colors.accent, + style = MatuleTheme.typography.bodyRegular14 + ) + } + } + } + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Привет!", + style = MatuleTheme.typography.headingBold32, + color = MatuleTheme.colors.text, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Заполните Свои Данные Или\nПродолжите Через Социальные Медиа", + style = MatuleTheme.typography.bodyRegular16, + color = MatuleTheme.colors.subTextDark, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = "Email", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.text, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + OutlinedTextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + text = "xyz@gmail.com", + color = MatuleTheme.colors.hint + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MatuleTheme.colors.accent, + unfocusedBorderColor = Color.LightGray + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Пароль", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.text, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation(), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MatuleTheme.colors.accent, + unfocusedBorderColor = Color.LightGray + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onNavigateToForgotPassword, + modifier = Modifier.align(Alignment.Start) + ) { + Text( + text = "Восстановить", + color = MatuleTheme.colors.accent, + style = MatuleTheme.typography.bodyRegular14 + ) + } + + Spacer(modifier = Modifier.height(40.dp)) + + Button( + onClick = { + if (email.isBlank() || password.isBlank()) { + errorMessage = "Пожалуйста, заполните все поля" + return@Button + } + + isLoading = true + errorMessage = null + + coroutineScope.launch { + val result = authRepository.signIn(email, password) + when (result) { + is Result.Success -> { + val authResponse = result.data as? AuthResponse + authResponse?.token?.let { token -> + authManager.saveAuthToken(token) + onSignInSuccess() + } ?: run { + errorMessage = "Ошибка сервера" + } + } + is Result.Error -> { + errorMessage = result.exception.message ?: "Ошибка входа" + } + Result.Loading -> { + // Обработка состояния загрузки + } + } + isLoading = false + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MatuleTheme.colors.accent + ), + enabled = !isLoading + ) { + if (isLoading) { + CircularProgressIndicator(color = Color.White) + } else { + Text("Войти", style = MatuleTheme.typography.bodyRegular16.copy(color = Color.White)) + } + } + + errorMessage?.let { message -> + Text( + text = message, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + } +} diff --git a/app/src/main/java/com/example/mymobilka/screens/SingUpScreen/SingUpSc.kt b/app/src/main/java/com/example/mymobilka/screens/SingUpScreen/SingUpSc.kt new file mode 100644 index 0000000..55ed3d4 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/screens/SingUpScreen/SingUpSc.kt @@ -0,0 +1,261 @@ +package com.example.mymobilka.screens.SingUpScreen + +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.mymobilka.R +import com.example.mymobilka.auth.AuthManager +import com.example.mymobilka.repository.AuthRepository +import com.example.mymobilka.ui.theme.MatuleTheme +import com.example.mymobilka.utils.Result +import kotlinx.coroutines.launch +import org.koin.compose.koinInject + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SignUpSc( + onSignUpSuccess: () -> Unit, + onNavigateToSignIn: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val authRepository: AuthRepository = koinInject() + val authManager: AuthManager = koinInject() + + var name by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var agreeToTerms by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onNavigateToSignIn) { + Icon( + painter = painterResource(id = R.drawable.back_arrow), + contentDescription = "Назад", + tint = MatuleTheme.colors.text + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + actionIconContentColor = MatuleTheme.colors.text + ) + ) + }, + bottomBar = { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color.Transparent + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextButton( + onClick = onNavigateToSignIn, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Есть аккаунт? Войти", + color = MatuleTheme.colors.accent, + style = MatuleTheme.typography.bodyRegular14 + ) + } + } + } + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Регистрация", + style = MatuleTheme.typography.headingBold32, + color = MatuleTheme.colors.text, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Заполните Свои Данные Или Продолжите Через Социальные Медиа", + style = MatuleTheme.typography.bodyRegular16, + color = MatuleTheme.colors.subTextDark, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(40.dp)) + + // Имя + Text( + text = "Ваше имя", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.text, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + OutlinedTextField( + value = name, + onValueChange = { name = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text("xxxxxxxx", color = MatuleTheme.colors.hint) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MatuleTheme.colors.accent, + unfocusedBorderColor = Color.LightGray + ), + singleLine = true + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Email", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.text, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + OutlinedTextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text("xyz@gmail.com", color = MatuleTheme.colors.hint) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MatuleTheme.colors.accent, + unfocusedBorderColor = Color.LightGray + ), + singleLine = true + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Пароль", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.text, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text("••••••••", color = MatuleTheme.colors.hint) + }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MatuleTheme.colors.accent, + unfocusedBorderColor = Color.LightGray + ), + singleLine = true + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Checkbox( + checked = agreeToTerms, + onCheckedChange = { agreeToTerms = it }, + colors = CheckboxDefaults.colors( + checkedColor = MatuleTheme.colors.accent, + uncheckedColor = Color.LightGray + ) + ) + Text( + text = "Даю согласие на обработку персональных данных", + style = MatuleTheme.typography.bodyRegular14, + color = MatuleTheme.colors.text, + modifier = Modifier.padding(start = 8.dp) + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { + if (name.isBlank() || email.isBlank() || password.isBlank()) { + Toast.makeText(context, "Все поля должны быть заполнены", Toast.LENGTH_SHORT).show() + return@Button + } + + scope.launch { + isLoading = true + when (val result = authRepository.signUp(name, email, password)) { + is Result.Success -> { + authManager.saveAuthToken(result.data.token) + Toast.makeText(context, "Регистрация прошла успешно", Toast.LENGTH_SHORT).show() + onSignUpSuccess() + } + + is Result.Error -> { + Toast.makeText(context, "Ошибка регистрации: ${result.exception.message}", Toast.LENGTH_LONG).show() + } + + Result.Loading -> { + } + } + isLoading = false + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MatuleTheme.colors.accent + ), + enabled = agreeToTerms && !isLoading, + shape = MaterialTheme.shapes.medium + ) { + if (isLoading) { + CircularProgressIndicator( + color = Color.White, + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp + ) + } else { + Text( + text = "Зарегистрироваться", + style = MatuleTheme.typography.bodyRegular16.copy(color = Color.White) + ) + } + } + } + } +} diff --git a/app/src/main/java/com/example/mymobilka/screens/StartScreen/SlideSc.kt b/app/src/main/java/com/example/mymobilka/screens/StartScreen/SlideSc.kt new file mode 100644 index 0000000..9805e0a --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/screens/StartScreen/SlideSc.kt @@ -0,0 +1,185 @@ +package com.example.mymobilka.screens.StartScreen + + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.runtime.getValue +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.mymobilka.R +import kotlinx.coroutines.launch + +@Composable +fun SlideSc( + onNavigateToSignIn: () -> Unit, +) { + val pagerState = rememberPagerState(pageCount = { 3 }) + val scope = rememberCoroutineScope() + + val pages = listOf( + OnboardingPage( + title = "ДОБРО ПОЖАЛОВАТЬ", + description = "Умная, великолепная и модная коллекция\nИзучите сейчас", + image = R.drawable.sneakers1 + ), + OnboardingPage( + title = "Начнем путешествие", + description = "В вашей комнате много красивых\nи привлекательных растений", + image = R.drawable.sneakers2 + ), + OnboardingPage( + title = "У Вас Есть Сила,\nЧтобы", + description = "Создать свой уникальный стиль", + image = R.drawable.sneakers3 + ) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf(Color(0xFF48B2E7), Color(0xFF0076B1)) + ) + ) + ) { + HorizontalPager( + state = pagerState, + modifier = Modifier.weight(1f) + ) { page -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp) + ) { + Image( + painter = painterResource(id = pages[page].image), + contentDescription = null, + modifier = Modifier + .fillMaxWidth(0.9f) + .height(300.dp) + .padding(bottom = 40.dp) + ) + + Text( + text = pages[page].title, + color = Color.White, + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + lineHeight = 40.sp, + modifier = Modifier.padding(horizontal = 24.dp) + ) + + Text( + text = pages[page].description, + color = Color.White.copy(alpha = 0.9f), + fontSize = 20.sp, + textAlign = TextAlign.Center, + lineHeight = 28.sp, + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(top = 24.dp) + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(pages.size) { index -> + val isSelected = pagerState.currentPage == index + + val width by animateDpAsState( + targetValue = if (isSelected) 40.dp else 16.dp, + label = "indicator_width" + ) + + val color by animateColorAsState( + targetValue = if (isSelected) Color.White else Color.White.copy(alpha = 0.4f), + label = "indicator_color" + ) + + Box( + modifier = Modifier + .height(6.dp) + .width(width) + .background( + color = color, + shape = RoundedCornerShape(50) + ) + ) + + if (index != pages.lastIndex) { + Spacer(modifier = Modifier.width(8.dp)) + } + } + } + + Spacer(modifier = Modifier.height(40.dp)) + + Button( + onClick = { + scope.launch { + if (pagerState.currentPage < pages.lastIndex) { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } else { + onNavigateToSignIn() + } + } + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + shape = RoundedCornerShape(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .height(60.dp) + ) { + Text( + text = when (pagerState.currentPage) { + 0 -> "Начать" + 1 -> "Далее" + 2 -> "Начать пользоваться" + else -> "" + }, + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(48.dp)) + } +} + +data class OnboardingPage( + val title: String, + val description: String, + val image: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/screens/StartScreen/StartSc.kt b/app/src/main/java/com/example/mymobilka/screens/StartScreen/StartSc.kt new file mode 100644 index 0000000..e99c0ce --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/screens/StartScreen/StartSc.kt @@ -0,0 +1,35 @@ +package com.example.mymobilka.screens.StartScreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay + +@Composable +fun StartSc(onNavigate: () -> Unit) { + LaunchedEffect(Unit) { + delay(1500) + onNavigate() + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Brush.horizontalGradient(listOf(Color(0xFF48B2E7), Color(0xFF0076B1)))) + ) { + Text( + text = "Matule", + fontSize = 32.sp, + color = Color.White, + modifier = Modifier.align(Alignment.Center) + ) + } +} diff --git a/app/src/main/java/com/example/mymobilka/storage/AppPreferences.kt b/app/src/main/java/com/example/mymobilka/storage/AppPreferences.kt new file mode 100644 index 0000000..af864b5 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/storage/AppPreferences.kt @@ -0,0 +1,27 @@ +package com.example.mymobilka.storage + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + +class AppPreferences(private val context: Context) { + companion object { + private val ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed") + } + + val isOnboardingCompleted: Flow = context.dataStore.data + .map { preferences -> preferences[ONBOARDING_COMPLETED] ?: false } + + suspend fun setOnboardingCompleted(completed: Boolean) { + context.dataStore.edit { preferences -> + preferences[ONBOARDING_COMPLETED] = completed + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/ui/theme/Color.kt b/app/src/main/java/com/example/mymobilka/ui/theme/Color.kt new file mode 100644 index 0000000..bc15c8c --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/ui/theme/Color.kt @@ -0,0 +1,3 @@ +package com.example.mymobilka.ui.theme + +import androidx.compose.ui.graphics.Color \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/ui/theme/Theme.kt b/app/src/main/java/com/example/mymobilka/ui/theme/Theme.kt new file mode 100644 index 0000000..f279d50 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/ui/theme/Theme.kt @@ -0,0 +1,113 @@ +package com.example.mymobilka.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.example.mymobilka.R + +@Immutable +data class MatuleColors( + val block: Color, + val text: Color, + val subTextDark: Color, + val background: Color, + val hint: Color, + val accent: Color +) + +@Immutable +data class MatuleTextStyle( + val headingBold32: TextStyle, + val subTitleRegular16: TextStyle, + val bodyRegular16: TextStyle, + val bodyRegular14: TextStyle, + val bodyRegular12: TextStyle, + + ) + +val LocalMatuleTypography = staticCompositionLocalOf { + MatuleTextStyle( + headingBold32 = TextStyle.Default, + subTitleRegular16 = TextStyle.Default, + bodyRegular16 = TextStyle.Default, + bodyRegular14 = TextStyle.Default, + bodyRegular12 = TextStyle.Default + ) +} + +val LocalMatuleColors = staticCompositionLocalOf { + MatuleColors( + block = Color.Unspecified, + text = Color.Unspecified, + subTextDark = Color.Unspecified, + background = Color.Unspecified, + hint = Color.Unspecified, + accent = Color.Unspecified + + ) +} + + + +val matuleFontFamily = FontFamily( + Font(R.font.roboto_serif, FontWeight.Normal), + Font(R.font.roboto_serif_bold, FontWeight.Bold), + Font(R.font.roboto_serif_black, FontWeight.Black), + Font(R.font.roboto_serif_medium, FontWeight.Medium), + Font(R.font.roboto_serif_extrabold, FontWeight.ExtraBold), + Font(R.font.roboto_mono_semibold, FontWeight.SemiBold) +) + + + +@Composable +fun MatuleTheme( content: (@Composable () -> Unit)){ + val matuleColors = MatuleColors( + block = Color(0xFFFFFFFF), + text = Color(0xFF2B2B2B), + subTextDark = Color(0xFF707B81), + background = Color(0xFFF7F7F9), + hint = Color(0xFF6A6A6A), + accent = Color(0xFF48B2E7) + + + ) + val matuleTypography = MatuleTextStyle( + headingBold32 = TextStyle(fontFamily = matuleFontFamily, fontWeight = FontWeight.Bold, fontSize = 32.sp), + subTitleRegular16 = TextStyle(fontFamily = matuleFontFamily, fontWeight = FontWeight.Normal, fontSize = 16.sp), + bodyRegular16 = TextStyle(fontFamily = matuleFontFamily, fontWeight = FontWeight.Normal, fontSize = 16.sp), + bodyRegular14 = TextStyle(fontFamily = matuleFontFamily, fontWeight = FontWeight.Normal, fontSize = 14.sp), + bodyRegular12 = TextStyle(fontFamily = matuleFontFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp) + ) + + CompositionLocalProvider( + LocalMatuleColors provides matuleColors, + LocalMatuleTypography provides matuleTypography, + content = content + ) +} + +object MatuleTheme{ + val colors: MatuleColors + @Composable + get() = LocalMatuleColors.current + val typography + @Composable + get() = LocalMatuleTypography.current +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mymobilka/ui/theme/Type.kt b/app/src/main/java/com/example/mymobilka/ui/theme/Type.kt new file mode 100644 index 0000000..907e709 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/ui/theme/Type.kt @@ -0,0 +1,2 @@ +package com.example.mymobilka.ui.theme + diff --git a/app/src/main/java/com/example/mymobilka/utils/Result.kt b/app/src/main/java/com/example/mymobilka/utils/Result.kt new file mode 100644 index 0000000..86a8587 --- /dev/null +++ b/app/src/main/java/com/example/mymobilka/utils/Result.kt @@ -0,0 +1,7 @@ +package com.example.mymobilka.utils + +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val exception: Exception) : Result() + object Loading : Result() +} \ No newline at end of file diff --git a/app/src/main/res/drawable/back_arrow.xml b/app/src/main/res/drawable/back_arrow.xml new file mode 100644 index 0000000..b240169 --- /dev/null +++ b/app/src/main/res/drawable/back_arrow.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/sneakers1.png b/app/src/main/res/drawable/sneakers1.png new file mode 100644 index 0000000..e748c16 Binary files /dev/null and b/app/src/main/res/drawable/sneakers1.png differ diff --git a/app/src/main/res/drawable/sneakers2.png b/app/src/main/res/drawable/sneakers2.png new file mode 100644 index 0000000..0f954f8 Binary files /dev/null and b/app/src/main/res/drawable/sneakers2.png differ diff --git a/app/src/main/res/drawable/sneakers3.png b/app/src/main/res/drawable/sneakers3.png new file mode 100644 index 0000000..e268b9a Binary files /dev/null and b/app/src/main/res/drawable/sneakers3.png differ diff --git a/app/src/main/res/font/roboto_mono_semibold.ttf b/app/src/main/res/font/roboto_mono_semibold.ttf new file mode 100644 index 0000000..c37e9bc Binary files /dev/null and b/app/src/main/res/font/roboto_mono_semibold.ttf differ diff --git a/app/src/main/res/font/roboto_serif.ttf b/app/src/main/res/font/roboto_serif.ttf new file mode 100644 index 0000000..27e3380 Binary files /dev/null and b/app/src/main/res/font/roboto_serif.ttf differ diff --git a/app/src/main/res/font/roboto_serif_black.ttf b/app/src/main/res/font/roboto_serif_black.ttf new file mode 100644 index 0000000..d3c565d Binary files /dev/null and b/app/src/main/res/font/roboto_serif_black.ttf differ diff --git a/app/src/main/res/font/roboto_serif_bold.ttf b/app/src/main/res/font/roboto_serif_bold.ttf new file mode 100644 index 0000000..3c0a7ef Binary files /dev/null and b/app/src/main/res/font/roboto_serif_bold.ttf differ diff --git a/app/src/main/res/font/roboto_serif_extrabold.ttf b/app/src/main/res/font/roboto_serif_extrabold.ttf new file mode 100644 index 0000000..e3d3f50 Binary files /dev/null and b/app/src/main/res/font/roboto_serif_extrabold.ttf differ diff --git a/app/src/main/res/font/roboto_serif_medium.ttf b/app/src/main/res/font/roboto_serif_medium.ttf new file mode 100644 index 0000000..a0699c3 Binary files /dev/null and b/app/src/main/res/font/roboto_serif_medium.ttf differ diff --git a/app/src/main/res/font/roboto_serif_semibold.ttf b/app/src/main/res/font/roboto_serif_semibold.ttf new file mode 100644 index 0000000..4077d52 Binary files /dev/null and b/app/src/main/res/font/roboto_serif_semibold.ttf differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3de1a39 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + My Application + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..e48770a --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +