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 @@
+
@@ -12,7 +13,6 @@
-
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" }