diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 0897082..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,6 +4,7 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0547ef6..0bc22a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,6 +100,9 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") + // DataStore + implementation(libs.datastore.preferences) + // Testing testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31b6170..0765467 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/Theme.NewPlanet"> by preferencesDataStore(name = "auth_preferences") + +@Singleton +class TokenManager @Inject constructor( + @ApplicationContext private val context: Context +) { + private val accessTokenKey = stringPreferencesKey("access_token") + private val refreshTokenKey = stringPreferencesKey("refresh_token") + + val accessToken: Flow = context.dataStore.data.map { preferences -> + preferences[accessTokenKey] + } + + val refreshToken: Flow = context.dataStore.data.map { preferences -> + preferences[refreshTokenKey] + } + + suspend fun getAccessToken(): String? { + return accessToken.first() + } + + suspend fun getRefreshToken(): String? { + return refreshToken.first() + } + + suspend fun saveTokens(accessToken: String, refreshToken: String) { + context.dataStore.edit { preferences -> + preferences[accessTokenKey] = accessToken + preferences[refreshTokenKey] = refreshToken + } + } + + suspend fun clearTokens() { + context.dataStore.edit { preferences -> + preferences.remove(accessTokenKey) + preferences.remove(refreshTokenKey) + } + } + + suspend fun isAuthenticated(): Boolean { + return getAccessToken() != null + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/data/local/entity/UserEntity.kt b/app/src/main/java/com/novayaplaneta/data/local/entity/UserEntity.kt index 9302d66..42baf0d 100644 --- a/app/src/main/java/com/novayaplaneta/data/local/entity/UserEntity.kt +++ b/app/src/main/java/com/novayaplaneta/data/local/entity/UserEntity.kt @@ -7,9 +7,10 @@ import androidx.room.PrimaryKey data class UserEntity( @PrimaryKey val id: String, - val name: String, + val fullName: String, val email: String, val role: String, - val token: String? + val createdAt: String?, + val updatedAt: String? ) diff --git a/app/src/main/java/com/novayaplaneta/data/local/mapper/UserMapper.kt b/app/src/main/java/com/novayaplaneta/data/local/mapper/UserMapper.kt index 3362d66..2ad514b 100644 --- a/app/src/main/java/com/novayaplaneta/data/local/mapper/UserMapper.kt +++ b/app/src/main/java/com/novayaplaneta/data/local/mapper/UserMapper.kt @@ -7,20 +7,22 @@ import com.novayaplaneta.domain.model.UserRole fun UserEntity.toDomain(): User { return User( id = id, - name = name, + fullName = fullName, email = email, role = UserRole.valueOf(role), - token = token + createdAt = createdAt, + updatedAt = updatedAt ) } fun User.toEntity(): UserEntity { return UserEntity( id = id, - name = name, + fullName = fullName, email = email, role = role.name, - token = token + createdAt = createdAt, + updatedAt = updatedAt ) } diff --git a/app/src/main/java/com/novayaplaneta/data/remote/AuthApi.kt b/app/src/main/java/com/novayaplaneta/data/remote/AuthApi.kt new file mode 100644 index 0000000..90dfef6 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/AuthApi.kt @@ -0,0 +1,42 @@ +package com.novayaplaneta.data.remote + +import com.novayaplaneta.data.remote.dto.MeResponse +import com.novayaplaneta.data.remote.dto.RefreshRequest +import com.novayaplaneta.data.remote.dto.RefreshResponse +import com.novayaplaneta.data.remote.dto.RegisterRequest +import com.novayaplaneta.data.remote.dto.RegisterResponse +import com.novayaplaneta.data.remote.dto.TokenResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST + +interface AuthApi { + @POST("api/v1/auth/register") + suspend fun register( + @Body request: RegisterRequest + ): Response + + @FormUrlEncoded + @POST("api/v1/auth/login") + suspend fun login( + @Field("grant_type") grantType: String = "password", + @Field("username") username: String, + @Field("password") password: String, + @Field("scope") scope: String = "", + @Field("client_id") clientId: String? = null, + @Field("client_secret") clientSecret: String? = null + ): Response + + @POST("api/v1/auth/refresh") + suspend fun refresh( + @Body request: RefreshRequest + ): Response + + @GET("api/v1/auth/me") + suspend fun getMe(): Response +} + + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/AuthInterceptor.kt b/app/src/main/java/com/novayaplaneta/data/remote/AuthInterceptor.kt new file mode 100644 index 0000000..54c8693 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/AuthInterceptor.kt @@ -0,0 +1,110 @@ +package com.novayaplaneta.data.remote + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.novayaplaneta.data.local.TokenManager +import com.novayaplaneta.data.remote.dto.RefreshRequest +import com.novayaplaneta.data.remote.dto.RefreshResponse +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Response +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AuthInterceptor @Inject constructor( + private val tokenManager: TokenManager +) : Interceptor { + + // Создаем отдельный Retrofit для refresh запросов без interceptor + private val refreshRetrofit: Retrofit by lazy { + val json = Json { + ignoreUnknownKeys = true + isLenient = true + encodeDefaults = false + } + val contentType = "application/json".toMediaType() + val okHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + Retrofit.Builder() + .baseUrl("http://localhost:8000/") + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory(contentType)) + .build() + } + + private val refreshAuthApi: AuthApi by lazy { + refreshRetrofit.create(AuthApi::class.java) + } + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + // Не добавляем токен к auth эндпоинтам + val isAuthEndpoint = originalRequest.url.encodedPath.contains("/auth/login") || + originalRequest.url.encodedPath.contains("/auth/register") || + originalRequest.url.encodedPath.contains("/auth/refresh") + + // Добавляем токен к запросу, если он есть и это не auth эндпоинт + val accessToken = runBlocking { tokenManager.getAccessToken() } + val requestWithToken = if (accessToken != null && !isAuthEndpoint) { + originalRequest.newBuilder() + .header("Authorization", "Bearer $accessToken") + .build() + } else { + originalRequest + } + + var response = chain.proceed(requestWithToken) + + // Если получили 401, пытаемся обновить токен + if (response.code == 401 && !isAuthEndpoint && !originalRequest.url.encodedPath.contains("/auth/refresh")) { + val refreshToken = runBlocking { tokenManager.getRefreshToken() } + + if (refreshToken != null) { + try { + val refreshResponse = runBlocking { + refreshAuthApi.refresh(RefreshRequest(refreshToken)) + } + + if (refreshResponse.isSuccessful && refreshResponse.body() != null) { + val tokenResponse = refreshResponse.body()!! + runBlocking { + tokenManager.saveTokens( + tokenResponse.access_token, + tokenResponse.refresh_token + ) + } + + // Повторяем исходный запрос с новым токеном + val newRequest = originalRequest.newBuilder() + .header("Authorization", "Bearer ${tokenResponse.access_token}") + .build() + response.close() + response = chain.proceed(newRequest) + } else { + // Refresh не удался, очищаем токены + runBlocking { tokenManager.clearTokens() } + } + } catch (e: Exception) { + // Ошибка при refresh, очищаем токены + runBlocking { tokenManager.clearTokens() } + } + } else { + // Нет refresh токена, очищаем + runBlocking { tokenManager.clearTokens() } + } + } + + return response + } +} + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/MeResponse.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/MeResponse.kt new file mode 100644 index 0000000..86a1d33 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/MeResponse.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class MeResponse( + val email: String, + val full_name: String, + val role: String, + val id: String, + val created_at: String, + val updated_at: String +) + + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/RefreshRequest.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/RefreshRequest.kt new file mode 100644 index 0000000..7883a33 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/RefreshRequest.kt @@ -0,0 +1,10 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class RefreshRequest( + val refresh_token: String +) + + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/RefreshResponse.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/RefreshResponse.kt new file mode 100644 index 0000000..fd0a7ca --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/RefreshResponse.kt @@ -0,0 +1,12 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class RefreshResponse( + val access_token: String, + val token_type: String, + val refresh_token: String +) + + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/RegisterRequest.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/RegisterRequest.kt new file mode 100644 index 0000000..3c879e2 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/RegisterRequest.kt @@ -0,0 +1,14 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.Serializable + +@Serializable +data class RegisterRequest( + val email: String, + val full_name: String, + @EncodeDefault + val role: String = "CHILD", + val password: String, +) + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/RegisterResponse.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/RegisterResponse.kt new file mode 100644 index 0000000..d22bf4a --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/RegisterResponse.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class RegisterResponse( + val email: String, + val full_name: String, + val role: String, + val id: String, + val created_at: String, + val updated_at: String +) + + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/TokenResponse.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/TokenResponse.kt new file mode 100644 index 0000000..be17850 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/TokenResponse.kt @@ -0,0 +1,12 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class TokenResponse( + val access_token: String, + val token_type: String, + val refresh_token: String +) + + diff --git a/app/src/main/java/com/novayaplaneta/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/novayaplaneta/data/repository/AuthRepositoryImpl.kt index f3a8f0f..d896356 100644 --- a/app/src/main/java/com/novayaplaneta/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/novayaplaneta/data/repository/AuthRepositoryImpl.kt @@ -1,10 +1,12 @@ package com.novayaplaneta.data.repository +import com.novayaplaneta.data.local.TokenManager import com.novayaplaneta.data.local.dao.UserDao import com.novayaplaneta.data.local.mapper.toDomain import com.novayaplaneta.data.local.mapper.toEntity -import com.novayaplaneta.data.remote.BackendApi -import com.novayaplaneta.data.remote.dto.LoginRequest +import com.novayaplaneta.data.remote.AuthApi +import com.novayaplaneta.data.remote.dto.RefreshRequest +import com.novayaplaneta.data.remote.dto.RegisterRequest import com.novayaplaneta.domain.model.User import com.novayaplaneta.domain.model.UserRole import com.novayaplaneta.domain.repository.AuthRepository @@ -14,25 +16,113 @@ import javax.inject.Inject class AuthRepositoryImpl @Inject constructor( private val userDao: UserDao, - private val api: BackendApi + private val authApi: AuthApi, + private val tokenManager: TokenManager ) : AuthRepository { + override suspend fun register(email: String, fullName: String, password: String, role: String): Result { + return try { + val response = authApi.register(RegisterRequest(email, fullName, role, password)) + if (response.isSuccessful && response.body() != null) { + val registerResponse = response.body()!! + val user = User( + id = registerResponse.id, + fullName = registerResponse.full_name, + email = registerResponse.email, + role = UserRole.valueOf(registerResponse.role), + createdAt = registerResponse.created_at, + updatedAt = registerResponse.updated_at + ) + // После регистрации нужно залогиниться, чтобы получить токены + val loginResult = login(email, password) + if (loginResult.isSuccess) { + saveUser(user) + Result.success(user) + } else { + Result.failure(Exception("Registration successful but login failed")) + } + } else { + val errorBody = response.errorBody()?.string() ?: "Registration failed" + Result.failure(Exception(errorBody)) + } + } catch (e: Exception) { + Result.failure(e) + } + } + override suspend fun login(email: String, password: String): Result { return try { - val response = api.login(LoginRequest(email, password)) + val response = authApi.login(username = email, password = password) if (response.isSuccessful && response.body() != null) { - val loginResponse = response.body()!! + val tokenResponse = response.body()!! + // Сохраняем токены + tokenManager.saveTokens(tokenResponse.access_token, tokenResponse.refresh_token) + + // Получаем информацию о пользователе + val meResponse = authApi.getMe() + if (meResponse.isSuccessful && meResponse.body() != null) { + val me = meResponse.body()!! + val user = User( + id = me.id, + fullName = me.full_name, + email = me.email, + role = UserRole.valueOf(me.role), + createdAt = me.created_at, + updatedAt = me.updated_at + ) + saveUser(user) + Result.success(user) + } else { + Result.failure(Exception("Failed to get user info")) + } + } else { + val errorBody = response.errorBody()?.string() ?: "Login failed" + Result.failure(Exception(errorBody)) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun refresh(): Result { + return try { + val refreshToken = tokenManager.getRefreshToken() + if (refreshToken == null) { + return Result.failure(Exception("No refresh token")) + } + + val response = authApi.refresh(RefreshRequest(refreshToken)) + if (response.isSuccessful && response.body() != null) { + val tokenResponse = response.body()!! + tokenManager.saveTokens(tokenResponse.access_token, tokenResponse.refresh_token) + Result.success(Unit) + } else { + tokenManager.clearTokens() + Result.failure(Exception("Refresh failed")) + } + } catch (e: Exception) { + tokenManager.clearTokens() + Result.failure(e) + } + } + + override suspend fun getMe(): Result { + return try { + val response = authApi.getMe() + if (response.isSuccessful && response.body() != null) { + val me = response.body()!! val user = User( - id = loginResponse.user.id, - name = loginResponse.user.name, - email = loginResponse.user.email, - role = UserRole.valueOf(loginResponse.user.role), - token = loginResponse.token + id = me.id, + fullName = me.full_name, + email = me.email, + role = UserRole.valueOf(me.role), + createdAt = me.created_at, + updatedAt = me.updated_at ) saveUser(user) Result.success(user) } else { - Result.failure(Exception("Login failed")) + Result.failure(Exception("Failed to get user info")) } } catch (e: Exception) { Result.failure(e) @@ -41,6 +131,7 @@ class AuthRepositoryImpl @Inject constructor( override suspend fun logout() { userDao.deleteAllUsers() + tokenManager.clearTokens() } override fun getCurrentUser(): Flow { diff --git a/app/src/main/java/com/novayaplaneta/di/NetworkModule.kt b/app/src/main/java/com/novayaplaneta/di/NetworkModule.kt index 82d8e7b..00a1852 100644 --- a/app/src/main/java/com/novayaplaneta/di/NetworkModule.kt +++ b/app/src/main/java/com/novayaplaneta/di/NetworkModule.kt @@ -1,6 +1,8 @@ package com.novayaplaneta.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.novayaplaneta.data.remote.AuthApi +import com.novayaplaneta.data.remote.AuthInterceptor import com.novayaplaneta.data.remote.BackendApi import dagger.Module import dagger.Provides @@ -17,43 +19,57 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object NetworkModule { - + private val json = Json { ignoreUnknownKeys = true isLenient = true encodeDefaults = false } - + @Provides @Singleton - fun provideOkHttpClient(): OkHttpClient { + fun provideOkHttpClient( + authInterceptor: AuthInterceptor + ): OkHttpClient { val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } - + return OkHttpClient.Builder() + .addInterceptor(authInterceptor) .addInterceptor(loggingInterceptor) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() } - @Provides @Singleton - fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { + fun provideRetrofit( + okHttpClient: OkHttpClient + ): Retrofit { val contentType = "application/json".toMediaType() - + return Retrofit.Builder() - .baseUrl("https://api.novayaplaneta.ru/") + .baseUrl("http://10.0.2.2:8000/") .client(okHttpClient) .addConverterFactory(json.asConverterFactory(contentType)) .build() } - + @Provides @Singleton - fun provideBackendApi(retrofit: Retrofit): BackendApi { + fun provideAuthApi( + retrofit: Retrofit + ): AuthApi { + return retrofit.create(AuthApi::class.java) + } + + @Provides + @Singleton + fun provideBackendApi( + retrofit: Retrofit + ): BackendApi { return retrofit.create(BackendApi::class.java) } } diff --git a/app/src/main/java/com/novayaplaneta/domain/model/User.kt b/app/src/main/java/com/novayaplaneta/domain/model/User.kt index afa7235..621a364 100644 --- a/app/src/main/java/com/novayaplaneta/domain/model/User.kt +++ b/app/src/main/java/com/novayaplaneta/domain/model/User.kt @@ -2,10 +2,11 @@ package com.novayaplaneta.domain.model data class User( val id: String, - val name: String, + val fullName: String, val email: String, val role: UserRole, - val token: String? = null + val createdAt: String? = null, + val updatedAt: String? = null ) enum class UserRole { diff --git a/app/src/main/java/com/novayaplaneta/domain/repository/AuthRepository.kt b/app/src/main/java/com/novayaplaneta/domain/repository/AuthRepository.kt index 76c2709..7c2db1a 100644 --- a/app/src/main/java/com/novayaplaneta/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/novayaplaneta/domain/repository/AuthRepository.kt @@ -4,7 +4,10 @@ import com.novayaplaneta.domain.model.User import kotlinx.coroutines.flow.Flow interface AuthRepository { + suspend fun register(email: String, fullName: String, password: String, role: String = "CHILD"): Result suspend fun login(email: String, password: String): Result + suspend fun refresh(): Result + suspend fun getMe(): Result suspend fun logout() fun getCurrentUser(): Flow suspend fun saveUser(user: User) diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/GetMeUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/GetMeUseCase.kt new file mode 100644 index 0000000..c51000d --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/GetMeUseCase.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.model.User +import com.novayaplaneta.domain.repository.AuthRepository +import javax.inject.Inject + +class GetMeUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(): Result { + return authRepository.getMe() + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/LoginUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/LoginUseCase.kt new file mode 100644 index 0000000..0c93988 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/LoginUseCase.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.model.User +import com.novayaplaneta.domain.repository.AuthRepository +import javax.inject.Inject + +class LoginUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke(email: String, password: String): Result { + return authRepository.login(email, password) + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/LogoutUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/LogoutUseCase.kt new file mode 100644 index 0000000..9440a02 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/LogoutUseCase.kt @@ -0,0 +1,14 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.repository.AuthRepository +import javax.inject.Inject + +class LogoutUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke() { + authRepository.logout() + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/RegisterUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/RegisterUseCase.kt new file mode 100644 index 0000000..d947b0a --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/RegisterUseCase.kt @@ -0,0 +1,20 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.model.User +import com.novayaplaneta.domain.repository.AuthRepository +import javax.inject.Inject + +class RegisterUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke( + email: String, + fullName: String, + password: String, + role: String = "CHILD" + ): Result { + return authRepository.register(email, fullName, password, role) + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt index a3018b1..6d88cff 100644 --- a/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt +++ b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt @@ -9,6 +9,7 @@ import com.novayaplaneta.ui.screens.ai.AIScreen import com.novayaplaneta.ui.screens.auth.ForgotPasswordScreen import com.novayaplaneta.ui.screens.auth.LoginScreen import com.novayaplaneta.ui.screens.auth.RegistrationScreen +import com.novayaplaneta.ui.screens.auth.SplashScreen import com.novayaplaneta.ui.screens.rewards.RewardsScreen import com.novayaplaneta.ui.screens.schedule.ScheduleScreen import com.novayaplaneta.ui.screens.settings.SettingsScreen @@ -19,13 +20,16 @@ import com.novayaplaneta.ui.screens.timer.TimerScreen fun NewPlanetNavigation( navController: NavHostController, modifier: Modifier = Modifier, - startDestination: String = "login" + startDestination: String = "splash" ) { NavHost( navController = navController, startDestination = startDestination, modifier = modifier ) { + composable("splash") { + SplashScreen(navController = navController) + } composable("login") { LoginScreen(navController = navController) } diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt index 3ff07a7..1a54a7e 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginScreen.kt @@ -122,44 +122,64 @@ fun LoginScreen( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - // Поле логина - Box( - modifier = Modifier - .fillMaxWidth() - .height(height = inputHeight) - .background( - color = LoginInputLightBlue, - shape = RoundedCornerShape(20.dp) - ), - contentAlignment = Alignment.CenterStart + // Поле email + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - TextField( - value = uiState.login, - onValueChange = { viewModel.onLoginChange(it) }, - placeholder = { - Text( - text = "Введи логин", - fontSize = inputTextSize, - color = Color.Gray.copy(alpha = 0.7f) - ) - }, + Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedTextColor = Color.Black, - focusedTextColor = Color.Black - ), - textStyle = MaterialTheme.typography.bodyLarge.copy( - fontSize = inputTextSize - ), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) - ) + .height(height = inputHeight) + .background( + color = if (uiState.errorMessage != null && uiState.email.isNotBlank() && !EmailValidator.isValid(uiState.email)) { + Color(0xFFFFEBEE) + } else { + LoginInputLightBlue + }, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.email, + onValueChange = { viewModel.onEmailChange(it) }, + placeholder = { + Text( + text = "Введи email", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + isError = uiState.errorMessage != null && uiState.email.isNotBlank() && !EmailValidator.isValid(uiState.email) + ) + } + + // Сообщение об ошибке email + if (uiState.errorMessage != null && uiState.email.isNotBlank() && !EmailValidator.isValid(uiState.email)) { + Text( + text = "Введите корректный email", + fontSize = (inputTextSizeValue * 0.8f).toInt().sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } } // Поле пароля @@ -208,12 +228,12 @@ fun LoginScreen( Button( onClick = { if (uiState.isFormValid) { - // Переход на экран расписания при заполненных полях - navController.navigate("schedule") { - popUpTo(0) { inclusive = true } + viewModel.login { + // Переход на экран расписания после успешного входа + navController.navigate("schedule") { + popUpTo(0) { inclusive = true } + } } - // Также вызываем логин для проверки через API (в фоне) - viewModel.login { } } }, modifier = Modifier diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginViewModel.kt index 03ac640..576c855 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginViewModel.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/LoginViewModel.kt @@ -2,7 +2,7 @@ package com.novayaplaneta.ui.screens.auth import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.novayaplaneta.domain.repository.AuthRepository +import com.novayaplaneta.domain.usecase.LoginUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -12,23 +12,25 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( - private val authRepository: AuthRepository + private val loginUseCase: LoginUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(LoginUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun onLoginChange(login: String) { + fun onEmailChange(email: String) { _uiState.value = _uiState.value.copy( - login = login, - isFormValid = isFormValid(login, _uiState.value.password) + email = email, + isFormValid = isFormValid(email, _uiState.value.password), + errorMessage = null ) } fun onPasswordChange(password: String) { _uiState.value = _uiState.value.copy( password = password, - isFormValid = isFormValid(_uiState.value.login, password) + isFormValid = isFormValid(_uiState.value.email, password), + errorMessage = null ) } @@ -38,7 +40,7 @@ class LoginViewModel @Inject constructor( viewModelScope.launch { _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) - val result = authRepository.login(_uiState.value.login, _uiState.value.password) + val result = loginUseCase(_uiState.value.email, _uiState.value.password) result.onSuccess { user -> _uiState.value = _uiState.value.copy(isLoading = false, isLoggedIn = true) @@ -52,13 +54,13 @@ class LoginViewModel @Inject constructor( } } - private fun isFormValid(login: String, password: String): Boolean { - return login.isNotBlank() && password.isNotBlank() + private fun isFormValid(email: String, password: String): Boolean { + return email.isNotBlank() && password.isNotBlank() && EmailValidator.isValid(email) } } data class LoginUiState( - val login: String = "", + val email: String = "", val password: String = "", val isLoading: Boolean = false, val isFormValid: Boolean = false, diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationScreen.kt index 8204bcf..a125d16 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationScreen.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationScreen.kt @@ -127,7 +127,7 @@ fun RegistrationScreen( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - // Поле логина + // Поле полного имени Box( modifier = Modifier .fillMaxWidth() @@ -139,11 +139,11 @@ fun RegistrationScreen( contentAlignment = Alignment.CenterStart ) { TextField( - value = uiState.login, - onValueChange = { viewModel.onLoginChange(it) }, + value = uiState.fullName, + onValueChange = { viewModel.onFullNameChange(it) }, placeholder = { Text( - text = "Введи логин", + text = "Введи полное имя", fontSize = inputTextSize, color = Color.Gray.copy(alpha = 0.7f) ) @@ -168,85 +168,114 @@ fun RegistrationScreen( } // Поле пароля - Box( - modifier = Modifier - .fillMaxWidth() - .height(height = inputHeight) - .background( - color = LoginInputLightBlue, - shape = RoundedCornerShape(20.dp) - ), - contentAlignment = Alignment.CenterStart + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - TextField( - value = uiState.password, - onValueChange = { viewModel.onPasswordChange(it) }, - placeholder = { - Text( - text = "Введи пароль", - fontSize = inputTextSize, - color = Color.Gray.copy(alpha = 0.7f) - ) - }, + Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - visualTransformation = PasswordVisualTransformation(), - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedTextColor = Color.Black, - focusedTextColor = Color.Black - ), - textStyle = MaterialTheme.typography.bodyLarge.copy( - fontSize = inputTextSize - ), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) - ) + .height(height = inputHeight) + .background( + color = if (uiState.passwordError != null) { + Color(0xFFFFEBEE) + } else { + LoginInputLightBlue + }, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.password, + onValueChange = { viewModel.onPasswordChange(it) }, + placeholder = { + Text( + text = "Введи пароль", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + visualTransformation = PasswordVisualTransformation(), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + isError = uiState.passwordError != null + ) + } + + if (uiState.passwordError != null) { + Text( + text = uiState.passwordError!!, + fontSize = (inputTextSizeValue * 0.8f).toInt().sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } } // Поле повторения пароля - Box( - modifier = Modifier - .fillMaxWidth() - .height(height = inputHeight) - .background( - color = LoginInputLightBlue, - shape = RoundedCornerShape(20.dp) - ), - contentAlignment = Alignment.CenterStart + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - TextField( - value = uiState.confirmPassword, - onValueChange = { viewModel.onConfirmPasswordChange(it) }, - placeholder = { - Text( - text = "Повтори пароль", - fontSize = inputTextSize, - color = Color.Gray.copy(alpha = 0.7f) - ) - }, + Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - visualTransformation = PasswordVisualTransformation(), - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedTextColor = Color.Black, - focusedTextColor = Color.Black - ), - textStyle = MaterialTheme.typography.bodyLarge.copy( - fontSize = inputTextSize - ), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) - ) + .height(height = inputHeight) + .background( + color = if (uiState.passwordError != null) { + Color(0xFFFFEBEE) + } else { + LoginInputLightBlue + }, + shape = RoundedCornerShape(20.dp) + ), + contentAlignment = Alignment.CenterStart + ) { + TextField( + value = uiState.confirmPassword, + onValueChange = { viewModel.onConfirmPasswordChange(it) }, + placeholder = { + Text( + text = "Повтори пароль", + fontSize = inputTextSize, + color = Color.Gray.copy(alpha = 0.7f) + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + visualTransformation = PasswordVisualTransformation(), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedTextColor = Color.Black, + focusedTextColor = Color.Black + ), + textStyle = MaterialTheme.typography.bodyLarge.copy( + fontSize = inputTextSize + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + isError = uiState.passwordError != null + ) + } } // Поле email @@ -309,76 +338,17 @@ fun RegistrationScreen( } } - // Поле кода (показывается после нажатия "Готово!" когда все 4 поля заполнены) - if (uiState.showCodeField) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(height = inputHeight) - .background( - color = LoginInputLightBlue, - shape = RoundedCornerShape(20.dp) - ), - contentAlignment = Alignment.CenterStart - ) { - TextField( - value = uiState.code, - onValueChange = { viewModel.onCodeChange(it) }, - placeholder = { - Text( - text = "Введи код из письма на почте", - fontSize = inputTextSize, - color = Color.Gray.copy(alpha = 0.7f) - ) - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedTextColor = Color.Black, - focusedTextColor = Color.Black - ), - textStyle = MaterialTheme.typography.bodyLarge.copy( - fontSize = inputTextSize - ), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) - ) - } - } - } - - // Кнопка "Готово!" - адаптивный размер - // Активна когда заполнены все 4 поля (до показа кода) или когда заполнен код - val isButtonEnabled = if (!uiState.showCodeField) { - uiState.isBasicFormValid && !uiState.isLoading - } else { - uiState.isFormValid && !uiState.isLoading - } - - val buttonColor = if (isButtonEnabled) { - ButtonDefaults.buttonColors( - containerColor = LoginGreenAccent, - contentColor = Color.White - ) - } else { - ButtonDefaults.buttonColors( - containerColor = LoginInputLightBlue, - contentColor = Color.Gray, - disabledContainerColor = LoginInputLightBlue, - disabledContentColor = Color.Gray - ) } + // Кнопка "Зарегистрироваться" - адаптивный размер Button( onClick = { - viewModel.onReadyClick { - navController.navigate("login") { - popUpTo("login") { inclusive = false } + if (uiState.isFormValid) { + viewModel.register { + // Переход на экран расписания после успешной регистрации + navController.navigate("schedule") { + popUpTo(0) { inclusive = true } + } } } }, @@ -386,8 +356,20 @@ fun RegistrationScreen( .fillMaxWidth() .height(height = inputHeight), shape = RoundedCornerShape(20.dp), - enabled = isButtonEnabled, - colors = buttonColor + enabled = uiState.isFormValid && !uiState.isLoading, + colors = if (uiState.isFormValid) { + ButtonDefaults.buttonColors( + containerColor = LoginGreenAccent, + contentColor = Color.White + ) + } else { + ButtonDefaults.buttonColors( + containerColor = LoginInputLightBlue, + contentColor = Color.Gray, + disabledContainerColor = LoginInputLightBlue, + disabledContentColor = Color.Gray + ) + } ) { if (uiState.isLoading) { CircularProgressIndicator( @@ -397,7 +379,7 @@ fun RegistrationScreen( } else { val buttonTextSize = (screenHeightDp * 0.027f).toInt().coerceIn(22, 34).sp Text( - text = "Готово!", + text = "Зарегистрироваться", fontSize = buttonTextSize, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationViewModel.kt index 32f1135..84bab80 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationViewModel.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/RegistrationViewModel.kt @@ -2,8 +2,8 @@ package com.novayaplaneta.ui.screens.auth import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.usecase.RegisterUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -11,64 +11,74 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class RegistrationViewModel @Inject constructor() : ViewModel() { +class RegistrationViewModel @Inject constructor( + private val registerUseCase: RegisterUseCase +) : ViewModel() { private val _uiState = MutableStateFlow(RegistrationUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun onLoginChange(login: String) { - _uiState.value = _uiState.value.copy(login = login) + fun onFullNameChange(fullName: String) { + _uiState.value = _uiState.value.copy( + fullName = fullName, + errorMessage = null + ) } fun onPasswordChange(password: String) { - _uiState.value = _uiState.value.copy(password = password) + _uiState.value = _uiState.value.copy( + password = password, + errorMessage = null + ) } fun onConfirmPasswordChange(password: String) { - _uiState.value = _uiState.value.copy(confirmPassword = password) + _uiState.value = _uiState.value.copy( + confirmPassword = password, + errorMessage = null + ) } fun onEmailChange(email: String) { - _uiState.value = _uiState.value.copy(email = email) + _uiState.value = _uiState.value.copy( + email = email, + errorMessage = null + ) } - fun onCodeChange(code: String) { - _uiState.value = _uiState.value.copy(code = code) - } - - fun onReadyClick(onSuccess: () -> Unit) { - val currentState = _uiState.value + fun register(onSuccess: () -> Unit) { + if (!_uiState.value.isFormValid) return - // Если поле кода еще не показано, но все 4 поля заполнены - показываем поле кода - if (!currentState.showCodeField && currentState.isBasicFormValid) { - _uiState.value = currentState.copy(showCodeField = true) - return - } - - // Если форма полностью валидна (включая код), выполняем регистрацию - if (currentState.isFormValid) { - viewModelScope.launch { - _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) - - // TODO: Реализовать вызов API для регистрации - // Пока что просто эмулируем успешную регистрацию - delay(1000) - - _uiState.value = _uiState.value.copy(isLoading = false) + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null) + + val result = registerUseCase( + email = _uiState.value.email, + fullName = _uiState.value.fullName, + password = _uiState.value.password, + role = "CHILD" + ) + + result.onSuccess { user -> + _uiState.value = _uiState.value.copy(isLoading = false, isRegistered = true) onSuccess() + }.onFailure { exception -> + _uiState.value = _uiState.value.copy( + isLoading = false, + errorMessage = exception.message ?: "Ошибка регистрации" + ) } } } } data class RegistrationUiState( - val login: String = "", + val fullName: String = "", val password: String = "", val confirmPassword: String = "", val email: String = "", - val code: String = "", - val showCodeField: Boolean = false, val isLoading: Boolean = false, + val isRegistered: Boolean = false, val errorMessage: String? = null ) { // Валидация email @@ -81,17 +91,19 @@ data class RegistrationUiState( "Введите корректный email" } else null - // Валидация первых 4 полей (без кода) - val isBasicFormValid: Boolean - get() = login.isNotBlank() && + // Валидация паролей + val passwordError: String? + get() = if (confirmPassword.isNotBlank() && password != confirmPassword) { + "Пароли не совпадают" + } else null + + // Валидация формы + val isFormValid: Boolean + get() = fullName.isNotBlank() && password.isNotBlank() && confirmPassword.isNotBlank() && isEmailValid && - password == confirmPassword - - // Валидация полной формы (включая код) - val isFormValid: Boolean - get() = isBasicFormValid && - code.isNotBlank() + password == confirmPassword && + password.length >= 6 } diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/SplashScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/SplashScreen.kt new file mode 100644 index 0000000..a32c982 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/SplashScreen.kt @@ -0,0 +1,45 @@ +package com.novayaplaneta.ui.screens.auth + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +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.Color +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.novayaplaneta.ui.theme.LoginBackgroundTurquoise + +@Composable +fun SplashScreen( + navController: NavController, + viewModel: SplashViewModel = hiltViewModel() +) { + LaunchedEffect(Unit) { + viewModel.checkAuth { isAuthenticated -> + if (isAuthenticated) { + navController.navigate("schedule") { + popUpTo(0) { inclusive = true } + } + } else { + navController.navigate("login") { + popUpTo(0) { inclusive = true } + } + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(color = LoginBackgroundTurquoise), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = Color.White) + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/auth/SplashViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/auth/SplashViewModel.kt new file mode 100644 index 0000000..87dbfe7 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/auth/SplashViewModel.kt @@ -0,0 +1,38 @@ +package com.novayaplaneta.ui.screens.auth + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.novayaplaneta.data.local.TokenManager +import com.novayaplaneta.domain.usecase.GetMeUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SplashViewModel @Inject constructor( + private val tokenManager: TokenManager, + private val getMeUseCase: GetMeUseCase +) : ViewModel() { + + fun checkAuth(onResult: (Boolean) -> Unit) { + viewModelScope.launch { + val hasToken = tokenManager.isAuthenticated() + + if (hasToken) { + // Пытаемся получить информацию о пользователе + val result = getMeUseCase() + if (result.isSuccess) { + onResult(true) + } else { + // Токен невалиден, очищаем и переходим на логин + tokenManager.clearTokens() + onResult(false) + } + } else { + onResult(false) + } + } + } +} + + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt index 76f94ba..82f3f0e 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt @@ -210,12 +210,12 @@ fun SettingsScreen( val textSize = (screenHeightDp * 0.025f).toInt().coerceIn(20, 28).sp UserInfoRow( label = "Имя:", - value = user.name, + value = user.fullName, textSize = textSize ) UserInfoRow( label = "Логин:", - value = "${user.name}12", // Используем имя + число как логин + value = "${user.fullName}12", // Используем имя + число как логин textSize = textSize ) UserInfoRow( diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt index 78c36c9..3f5df3e 100644 --- a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt @@ -48,10 +48,9 @@ class SettingsViewModel @Inject constructor( // Заглушка с тестовыми данными return User( id = "user_123", - name = "Коля", + fullName = "Коля", email = "kolya12@mail.ru", - role = UserRole.CHILD, - token = null + role = UserRole.CHILD ) } diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..fe0064a --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + 10.0.2.2 + localhost + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 232f61d..a112f20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ coil = "2.7.0" lottie = "6.1.0" coroutines = "1.9.0" ksp = "2.1.0-1.0.28" +datastore = "1.1.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -71,6 +72,9 @@ lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", versio kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +# DataStore +datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }