Добавил работу с сетью и сценарии авторизации
This commit is contained in:
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -4,6 +4,7 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
@@ -12,7 +13,6 @@
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -21,7 +21,7 @@ import com.novayaplaneta.data.local.entity.UserEntity
|
||||
RewardEntity::class,
|
||||
ChatMessageEntity::class
|
||||
],
|
||||
version = 1,
|
||||
version = 2,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class NewPlanetDatabase : RoomDatabase() {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.novayaplaneta.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> 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<String?> = context.dataStore.data.map { preferences ->
|
||||
preferences[accessTokenKey]
|
||||
}
|
||||
|
||||
val refreshToken: Flow<String?> = 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
42
app/src/main/java/com/novayaplaneta/data/remote/AuthApi.kt
Normal file
42
app/src/main/java/com/novayaplaneta/data/remote/AuthApi.kt
Normal file
@@ -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<RegisterResponse>
|
||||
|
||||
@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<TokenResponse>
|
||||
|
||||
@POST("api/v1/auth/refresh")
|
||||
suspend fun refresh(
|
||||
@Body request: RefreshRequest
|
||||
): Response<RefreshResponse>
|
||||
|
||||
@GET("api/v1/auth/me")
|
||||
suspend fun getMe(): Response<MeResponse>
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.novayaplaneta.data.remote.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RefreshRequest(
|
||||
val refresh_token: String
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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<User> {
|
||||
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<User> {
|
||||
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<Unit> {
|
||||
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<User> {
|
||||
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<User?> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<User>
|
||||
suspend fun login(email: String, password: String): Result<User>
|
||||
suspend fun refresh(): Result<Unit>
|
||||
suspend fun getMe(): Result<User>
|
||||
suspend fun logout()
|
||||
fun getCurrentUser(): Flow<User?>
|
||||
suspend fun saveUser(user: User)
|
||||
|
||||
@@ -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<User> {
|
||||
return authRepository.getMe()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<User> {
|
||||
return authRepository.login(email, password)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<User> {
|
||||
return authRepository.register(email, fullName, password, role)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<LoginUiState> = _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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<RegistrationUiState> = _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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
7
app/src/main/res/xml/network_security_config.xml
Normal file
7
app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user