feat: Implement setup screen and login logic

- Add SetupScreen with UI for server URL, username, and password input.
- Make SetupScreen the initial screen in the navigation graph.
- Implement secure credential storage using EncryptedSharedPreferences.
- Create CredentialsRepository and AuthRepository to manage credentials and auth tokens.
- Add LoginUseCase to handle the business logic for logging in.
- Implement a temporary Retrofit client in ItemRepository to handle login against a user-provided URL.
- Integrate login logic into SetupViewModel.
- Update all relevant project documentation and DI modules.
This commit is contained in:
2025-08-08 20:17:50 +03:00
parent 01e9b7bb00
commit 2853b5a47e
23 changed files with 602 additions and 23 deletions

View File

@@ -82,6 +82,9 @@ dependencies {
// [DEPENDENCY] Logging // [DEPENDENCY] Logging
implementation(Libs.timber) implementation(Libs.timber)
// [DEPENDENCY] Security
implementation(Libs.securityCrypto)
// [DEPENDENCY] Testing // [DEPENDENCY] Testing
testImplementation(Libs.junit) testImplementation(Libs.junit)
androidTestImplementation(Libs.extJunit) androidTestImplementation(Libs.extJunit)

View File

@@ -8,6 +8,13 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.homebox.lens.ui.screen.dashboard.DashboardScreen import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen
import com.homebox.lens.ui.screen.setup.SetupScreen
// [CORE-LOGIC] // [CORE-LOGIC]
/** /**
@@ -19,12 +26,36 @@ fun NavGraph() {
val navController = rememberNavController() val navController = rememberNavController()
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Dashboard.route startDestination = Screen.Setup.route
) { ) {
composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Setup.route) { inclusive = true }
}
})
}
composable(route = Screen.Dashboard.route) { composable(route = Screen.Dashboard.route) {
DashboardScreen() DashboardScreen()
} }
// TODO: Добавить остальные экраны в граф навигации composable(route = Screen.InventoryList.route) {
InventoryListScreen()
}
composable(route = Screen.ItemDetails.route) {
ItemDetailsScreen()
}
composable(route = Screen.ItemEdit.route) {
ItemEditScreen()
}
composable(route = Screen.LabelsList.route) {
LabelsListScreen()
}
composable(route = Screen.LocationsList.route) {
LocationsListScreen()
}
composable(route = Screen.Search.route) {
SearchScreen()
}
} }
} }
// [END_FILE_NavGraph.kt] // [END_FILE_NavGraph.kt]

View File

@@ -11,14 +11,17 @@ package com.homebox.lens.navigation
* @property route Строковый идентификатор маршрута. * @property route Строковый идентификатор маршрута.
*/ */
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
/** data object Setup : Screen("setup_screen")
* [CONTRACT]
* Представляет экран "Дэшборд".
*/
data object Dashboard : Screen("dashboard_screen") data object Dashboard : Screen("dashboard_screen")
data object InventoryList : Screen("inventory_list_screen")
// TODO: Добавить объекты для остальных экранов: data object ItemDetails : Screen("item_details_screen/{itemId}") {
// data object ItemDetails : Screen("item_details_screen") fun createRoute(itemId: String) = "item_details_screen/$itemId"
// data object Search : Screen("search_screen") }
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
}
data object LabelsList : Screen("labels_list_screen")
data object LocationsList : Screen("locations_list_screen")
data object Search : Screen("search_screen")
} }
// [END_FILE_Screen.kt] // [END_FILE_Screen.kt]

View File

@@ -0,0 +1,113 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt
package com.homebox.lens.ui.screen.setup
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
// [ENTRYPOINT]
@Composable
fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(),
onSetupComplete: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.isSetupComplete) {
onSetupComplete()
}
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onUsernameChange = viewModel::onUsernameChange,
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect
)
}
// [CONTENT]
@Composable
private fun SetupScreenContent(
uiState: SetupUiState,
onServerUrlChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("Server Setup") })
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text("Server URL") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text("Username") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onConnectClick,
enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Text("Connect")
}
}
uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
SetupScreenContent(
uiState = SetupUiState(error = "Failed to connect"),
onServerUrlChange = {},
onUsernameChange = {},
onPasswordChange = {},
onConnectClick = {}
)
}
// [END_FILE_SetupScreen.kt]

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupUiState.kt
package com.homebox.lens.ui.screen.setup
// [STATE]
data class SetupUiState(
val serverUrl: String = "",
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val isSetupComplete: Boolean = false
)
// [END_FILE_SetupUiState.kt]

View File

@@ -0,0 +1,88 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupViewModel.kt
package com.homebox.lens.ui.screen.setup
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.model.Result
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.usecase.LoginUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
// [VIEWMODEL]
@HiltViewModel
class SetupViewModel @Inject constructor(
private val credentialsRepository: CredentialsRepository,
private val loginUseCase: LoginUseCase
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow(SetupUiState())
val uiState = _uiState.asStateFlow()
init {
loadCredentials()
}
private fun loadCredentials() {
viewModelScope.launch {
credentialsRepository.getCredentials().collect { credentials ->
if (credentials != null) {
_uiState.update {
it.copy(
serverUrl = credentials.serverUrl,
username = credentials.username,
password = credentials.password
)
}
}
}
}
}
// [ACTION]
fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) }
}
// [ACTION]
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
// [ACTION]
fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) }
}
// [ACTION]
fun connect() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
val credentials = Credentials(
serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password
)
credentialsRepository.saveCredentials(credentials)
when (val result = loginUseCase(credentials)) {
is Result.Success -> {
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
}
is Result.Error -> {
_uiState.update { it.copy(isLoading = false, error = result.exception.message ?: "Login failed") }
}
}
}
}
}
// [END_FILE_SetupViewModel.kt]

View File

@@ -94,6 +94,9 @@ object Libs {
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4" const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling" const val composeUiTooling = "androidx.compose.ui:ui-tooling"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest" const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
// Security
const val securityCrypto = "androidx.security:security-crypto:${Versions.securityCrypto}"
} }
// [END_FILE_Dependencies.kt] // [END_FILE_Dependencies.kt]

View File

@@ -10,7 +10,9 @@ import com.homebox.lens.data.api.dto.ItemSummaryDto
import com.homebox.lens.data.api.dto.ItemUpdateDto import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.data.api.dto.LabelOutDto import com.homebox.lens.data.api.dto.LabelOutDto
import com.homebox.lens.data.api.dto.LocationOutCountDto import com.homebox.lens.data.api.dto.LocationOutCountDto
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.dto.PaginationResultDto import com.homebox.lens.data.api.dto.PaginationResultDto
import com.homebox.lens.data.api.dto.TokenResponseDto
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
@@ -27,6 +29,10 @@ import retrofit2.http.Query
*/ */
interface HomeboxApiService { interface HomeboxApiService {
// [ENDPOINT] Auth
@POST("v1/users/login")
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
// [ENDPOINT] Items // [ENDPOINT] Items
@GET("v1/items") @GET("v1/items")
suspend fun getItems( suspend fun getItems(

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LoginFormDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LoginFormDto(
@Json(name = "username") val username: String,
@Json(name = "password") val password: String,
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
)
// [END_FILE_LoginFormDto.kt]

View File

@@ -0,0 +1,15 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] TokenResponseDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TokenResponseDto(
@Json(name = "token") val token: String,
@Json(name = "attachmentToken") val attachmentToken: String,
@Json(name = "expiresAt") val expiresAt: String
)
// [END_FILE_TokenResponseDto.kt]

View File

@@ -56,12 +56,19 @@ object ApiModule {
// [PROVIDER] // [PROVIDER]
@Provides @Provides
@Singleton @Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit { fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
return MoshiConverterFactory.create(moshi)
}
// [PROVIDER]
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
// [ACTION] Build Retrofit instance // [ACTION] Build Retrofit instance
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(BASE_URL) .baseUrl(BASE_URL)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi)) .addConverterFactory(moshiConverterFactory)
.build() .build()
} }

View File

@@ -3,11 +3,13 @@
package com.homebox.lens.data.di package com.homebox.lens.data.di
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.repository.AuthRepositoryImpl
import com.homebox.lens.data.repository.ItemRepositoryImpl import com.homebox.lens.data.repository.CredentialsRepositoryImpl
import com.homebox.lens.domain.repository.AuthRepository
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.repository.ItemRepository import com.homebox.lens.domain.repository.ItemRepository
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
@@ -15,18 +17,23 @@ import javax.inject.Singleton
// [CONTRACT] // [CONTRACT]
/** /**
* [MODULE: DaggerHilt('RepositoryModule')] * [MODULE: DaggerHilt('RepositoryModule')]
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository. * [PURPOSE] Предоставляет реализации для интерфейсов репозиториев.
*/ */
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object RepositoryModule { abstract class RepositoryModule {
// [PROVIDER] @Binds
@Provides
@Singleton @Singleton
fun provideItemRepository(apiService: HomeboxApiService): ItemRepository { abstract fun bindItemRepository(itemRepositoryImpl: com.homebox.lens.data.repository.ItemRepositoryImpl): ItemRepository
return ItemRepositoryImpl(apiService)
} @Binds
@Singleton
abstract fun bindCredentialsRepository(credentialsRepositoryImpl: CredentialsRepositoryImpl): CredentialsRepository
@Binds
@Singleton
abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
} }
// [END_FILE_RepositoryModule.kt] // [END_FILE_RepositoryModule.kt]

View File

@@ -0,0 +1,37 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] StorageModule.kt
package com.homebox.lens.data.di
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object StorageModule {
@Provides
@Singleton
fun provideEncryptedSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
// [END_FILE_StorageModule.kt]

View File

@@ -0,0 +1,30 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] AuthRepositoryImpl.kt
package com.homebox.lens.data.repository
import android.content.SharedPreferences
import com.homebox.lens.domain.repository.AuthRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
class AuthRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences
) : AuthRepository {
companion object {
private const val KEY_AUTH_TOKEN = "key_auth_token"
}
override suspend fun saveToken(token: String) {
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
}
override fun getToken(): Flow<String?> = flow {
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
}.flowOn(Dispatchers.IO)
}
// [END_FILE_AuthRepositoryImpl.kt]

View File

@@ -0,0 +1,46 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] CredentialsRepositoryImpl.kt
package com.homebox.lens.data.repository
import android.content.SharedPreferences
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.CredentialsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
// [REPOSITORY_IMPL]
class CredentialsRepositoryImpl @Inject constructor(
private val encryptedPrefs: SharedPreferences
) : CredentialsRepository {
companion object {
private const val KEY_SERVER_URL = "key_server_url"
private const val KEY_USERNAME = "key_username"
private const val KEY_PASSWORD = "key_password"
}
override suspend fun saveCredentials(credentials: Credentials) {
encryptedPrefs.edit()
.putString(KEY_SERVER_URL, credentials.serverUrl)
.putString(KEY_USERNAME, credentials.username)
.putString(KEY_PASSWORD, credentials.password)
.apply()
}
override fun getCredentials(): Flow<Credentials?> = flow {
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
val username = encryptedPrefs.getString(KEY_USERNAME, null)
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
if (serverUrl != null && username != null && password != null) {
emit(Credentials(serverUrl, username, password))
} else {
emit(null)
}
}.flowOn(Dispatchers.IO)
}
// [END_FILE_CredentialsRepositoryImpl.kt]

View File

@@ -4,10 +4,15 @@
package com.homebox.lens.data.repository package com.homebox.lens.data.repository
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.dto.toDomain import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.data.api.dto.toDto import com.homebox.lens.data.api.dto.toDto
import com.homebox.lens.domain.model.* import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.repository.AuthRepository
import com.homebox.lens.domain.repository.ItemRepository import com.homebox.lens.domain.repository.ItemRepository
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -19,9 +24,29 @@ import javax.inject.Singleton
*/ */
@Singleton @Singleton
class ItemRepositoryImpl @Inject constructor( class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService private val apiService: HomeboxApiService,
private val authRepository: AuthRepository,
private val okHttpClient: OkHttpClient,
private val moshiConverterFactory: MoshiConverterFactory
) : ItemRepository { ) : ItemRepository {
override suspend fun login(credentials: Credentials): Result<Unit> {
return try {
val tempApiService = Retrofit.Builder()
.baseUrl(credentials.serverUrl)
.client(okHttpClient)
.addConverterFactory(moshiConverterFactory)
.build()
.create(HomeboxApiService::class.java)
val loginForm = LoginFormDto(credentials.username, credentials.password)
val tokenResponse = tempApiService.login(loginForm)
authRepository.saveToken(tokenResponse.token)
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/** /**
* [CONTRACT] @see ItemRepository.createItem * [CONTRACT] @see ItemRepository.createItem
*/ */

View File

@@ -0,0 +1,18 @@
// [PACKAGE] com.homebox.lens.domain.model
// [FILE] Credentials.kt
package com.homebox.lens.domain.model
/**
* [CONTRACT]
* Data class to hold server credentials.
* @property serverUrl The URL of the Homebox server.
* @property username The username for authentication.
* @property password The password for authentication.
*/
data class Credentials(
val serverUrl: String,
val username: String,
val password: String
)
// [END_FILE_Credentials.kt]

View File

@@ -0,0 +1,27 @@
// [PACKAGE] com.homebox.lens.domain.repository
// [FILE] AuthRepository.kt
package com.homebox.lens.domain.repository
import kotlinx.coroutines.flow.Flow
/**
* [CONTRACT]
* Repository for managing authentication tokens.
*/
interface AuthRepository {
/**
* [CONTRACT]
* Saves the authentication token.
* @param token The token to save.
*/
suspend fun saveToken(token: String)
/**
* [CONTRACT]
* Retrieves the authentication token.
* @return A Flow emitting the token, or null if not found.
*/
fun getToken(): Flow<String?>
}
// [END_FILE_AuthRepository.kt]

View File

@@ -0,0 +1,28 @@
// [PACKAGE] com.homebox.lens.domain.repository
// [FILE] CredentialsRepository.kt
package com.homebox.lens.domain.repository
import com.homebox.lens.domain.model.Credentials
import kotlinx.coroutines.flow.Flow
/**
* [CONTRACT]
* Repository for managing user credentials.
*/
interface CredentialsRepository {
/**
* [CONTRACT]
* Saves the user credentials securely.
* @param credentials The credentials to save.
*/
suspend fun saveCredentials(credentials: Credentials)
/**
* [CONTRACT]
* Retrieves the saved user credentials.
* @return A Flow emitting the saved [Credentials], or null if none are saved.
*/
fun getCredentials(): Flow<Credentials?>
}
// [END_FILE_CredentialsRepository.kt]

View File

@@ -12,6 +12,7 @@ import com.homebox.lens.domain.model.*
* Определяет контракт, которому должен следовать слой данных. * Определяет контракт, которому должен следовать слой данных.
*/ */
interface ItemRepository { interface ItemRepository {
suspend fun login(credentials: Credentials): Result<Unit>
suspend fun createItem(newItemData: ItemCreate): ItemSummary suspend fun createItem(newItemData: ItemCreate): ItemSummary
suspend fun getItemDetails(itemId: String): ItemOut suspend fun getItemDetails(itemId: String): ItemOut
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.domain.usecase
// [FILE] LoginUseCase.kt
package com.homebox.lens.domain.usecase
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
import com.homebox.lens.domain.model.Result
/**
* [CONTRACT]
* Use case for user login.
* @param itemRepository The repository to handle item and auth operations.
*/
class LoginUseCase @Inject constructor(
private val itemRepository: ItemRepository
) {
/**
* [CONTRACT]
* Executes the login process.
* @param credentials The user's credentials.
* @return A [Result] object indicating success or failure.
*/
suspend operator fun invoke(credentials: Credentials): Result<Unit> {
return itemRepository.login(credentials)
}
}
// [END_FILE_LoginUseCase.kt]

View File

@@ -59,6 +59,15 @@
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="stub" spec_ref_id="screen_search"> <file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="stub" spec_ref_id="screen_search">
<purpose_summary>ViewModel for the Search screen.</purpose_summary> <purpose_summary>ViewModel for the Search screen.</purpose_summary>
</file> </file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" status="stub" spec_ref_id="screen_setup">
<purpose_summary>UI for the Setup screen.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt" status="stub" spec_ref_id="screen_setup">
<purpose_summary>ViewModel for the Setup screen.</purpose_summary>
</file>
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupUiState.kt" status="implemented" spec_ref_id="screen_setup">
<purpose_summary>UI state for the Setup screen.</purpose_summary>
</file>
</module> </module>
<module name="data" type="android_library"> <module name="data" type="android_library">
<purpose_summary>Data layer, responsible for data sources (network, local DB) and repository implementations.</purpose_summary> <purpose_summary>Data layer, responsible for data sources (network, local DB) and repository implementations.</purpose_summary>
@@ -80,12 +89,33 @@
<file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="implemented" ref_id="di_repo"> <file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="implemented" ref_id="di_repo">
<purpose_summary>Hilt module for binding repository interfaces to their implementations.</purpose_summary> <purpose_summary>Hilt module for binding repository interfaces to their implementations.</purpose_summary>
</file> </file>
<file name="data/src/main/java/com/homebox/lens/data/di/StorageModule.kt" status="implemented" ref_id="di_storage">
<purpose_summary>Hilt module for providing storage-related dependencies (EncryptedSharedPreferences).</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/repository/CredentialsRepositoryImpl.kt" status="implemented" ref_id="repo_credentials_impl">
<purpose_summary>Implementation of the CredentialsRepository.</purpose_summary>
</file>
<file name="data/src/main/java/com/homebox/lens/data/repository/AuthRepositoryImpl.kt" status="implemented" ref_id="repo_auth_impl">
<purpose_summary>Implementation of the AuthRepository.</purpose_summary>
</file>
</module> </module>
<module name="domain" type="kotlin_jvm_library"> <module name="domain" type="kotlin_jvm_library">
<purpose_summary>Domain layer, contains business logic, use cases, and repository interfaces. Pure Kotlin module.</purpose_summary> <purpose_summary>Domain layer, contains business logic, use cases, and repository interfaces. Pure Kotlin module.</purpose_summary>
<file name="domain/src/main/java/com/homebox/lens/domain/model/Credentials.kt" status="implemented" ref_id="model_credentials">
<purpose_summary>Data class for holding user credentials.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/repository/AuthRepository.kt" status="implemented" ref_id="repo_auth_interface">
<purpose_summary>Interface for the auth repository.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/repository/CredentialsRepository.kt" status="implemented" ref_id="repo_credentials_interface">
<purpose_summary>Interface for the credentials repository.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="implemented" ref_id="repo_interface"> <file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="implemented" ref_id="repo_interface">
<purpose_summary>Interface defining the contract for data operations related to items.</purpose_summary> <purpose_summary>Interface defining the contract for data operations related to items.</purpose_summary>
</file> </file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" status="implemented" spec_ref_id="uc_login">
<purpose_summary>Use case for user login.</purpose_summary>
</file>
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="implemented" spec_ref_id="uc_create_item"> <file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="implemented" spec_ref_id="uc_create_item">
<purpose_summary>Use case for creating a new item.</purpose_summary> <purpose_summary>Use case for creating a new item.</purpose_summary>
</file> </file>

View File

@@ -117,6 +117,7 @@
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" /> <USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" /> <USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" /> <USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
<!-- UI Screens --> <!-- UI Screens -->
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" /> <UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
@@ -126,5 +127,6 @@
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" /> <UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" /> <UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" /> <UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
</IMPLEMENTATION_MAP> </IMPLEMENTATION_MAP>
</PROJECT_SPECIFICATION> </PROJECT_SPECIFICATION>