export literal strings to resources
This commit is contained in:
parent
e6d05e5d6d
commit
dfd8414be6
@ -13,7 +13,7 @@ abstract class BaseRepository {
|
|||||||
return if (response.isSuccessful) {
|
return if (response.isSuccessful) {
|
||||||
response.body()?.let {
|
response.body()?.let {
|
||||||
ApiResult.Success(it)
|
ApiResult.Success(it)
|
||||||
} ?: ApiResult.Error(-999, "Empty response body")
|
} ?: ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Empty response body")
|
||||||
} else {
|
} else {
|
||||||
response.errorBody().toApiError()
|
response.errorBody().toApiError()
|
||||||
}
|
}
|
||||||
@ -28,7 +28,7 @@ abstract class BaseRepository {
|
|||||||
return if (response.isSuccessful) {
|
return if (response.isSuccessful) {
|
||||||
response.body()?.let {
|
response.body()?.let {
|
||||||
ApiResult.Success(transform(it))
|
ApiResult.Success(transform(it))
|
||||||
} ?: ApiResult.Error(-999, "Empty response body")
|
} ?: ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Empty response body")
|
||||||
} else {
|
} else {
|
||||||
response.errorBody().toApiError()
|
response.errorBody().toApiError()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,12 @@ import okhttp3.ResponseBody
|
|||||||
|
|
||||||
fun ResponseBody?.toApiError(): ApiResult.Error {
|
fun ResponseBody?.toApiError(): ApiResult.Error {
|
||||||
if (this == null) {
|
if (this == null) {
|
||||||
return ApiResult.Error(-999, "Unknown error")
|
return ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Unknown error")
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
val error = json.decodeFromString<ErrorResponse>(string())
|
val error = json.decodeFromString<ErrorResponse>(string())
|
||||||
ApiResult.Error(error.error, error.message)
|
ApiResult.Error(error.error, error.message)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
ApiResult.Error(-999, "Unknown error")
|
ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,5 +2,10 @@ package com.longnh15.heimdall.domain.model
|
|||||||
|
|
||||||
sealed class ApiResult<out T> {
|
sealed class ApiResult<out T> {
|
||||||
data class Success<T>(val data: T): ApiResult<T>()
|
data class Success<T>(val data: T): ApiResult<T>()
|
||||||
data class Error(val code: Int, val message: String): ApiResult<Nothing>()
|
data class Error(val code: Int, val message: String): ApiResult<Nothing>() {
|
||||||
|
companion object {
|
||||||
|
/** Sentinel code for client-side failures (no/garbled response). [message] is an internal default, not shown to users. */
|
||||||
|
const val CODE_UNKNOWN = -999
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -38,6 +38,7 @@ import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -61,7 +62,7 @@ fun AuthFormTextField(
|
|||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
label: String,
|
label: String,
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
error: String? = null,
|
error: UiText? = null,
|
||||||
isHideContent: Boolean = false,
|
isHideContent: Boolean = false,
|
||||||
isLastField: Boolean = false,
|
isLastField: Boolean = false,
|
||||||
autoCorrectEnabled: Boolean = true,
|
autoCorrectEnabled: Boolean = true,
|
||||||
@ -121,7 +122,7 @@ fun AuthFormTextField(
|
|||||||
}
|
}
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
Text(
|
Text(
|
||||||
text = error,
|
text = error.asString(),
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package com.longnh15.heimdall.ui.auth.login
|
package com.longnh15.heimdall.ui.auth.login
|
||||||
|
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
|
|
||||||
sealed class LoginEvent {
|
sealed class LoginEvent {
|
||||||
data object LoginSuccess: LoginEvent()
|
data object LoginSuccess: LoginEvent()
|
||||||
data class LoginError(val message: String): LoginEvent()
|
data class LoginError(val message: UiText): LoginEvent()
|
||||||
}
|
}
|
||||||
@ -38,12 +38,15 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -55,12 +58,13 @@ fun LoginScreen(
|
|||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val state = viewModel.state
|
val state = viewModel.state
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.uiEvent.collect { event ->
|
viewModel.uiEvent.collect { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is LoginEvent.LoginSuccess -> navigateToVault()
|
is LoginEvent.LoginSuccess -> navigateToVault()
|
||||||
is LoginEvent.LoginError -> snackbarHostState.showSnackbar(event.message)
|
is LoginEvent.LoginError -> snackbarHostState.showSnackbar(event.message.asString(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +91,7 @@ fun LoginScreen(
|
|||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Welcome back",
|
text = stringResource(R.string.login_title),
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
)
|
)
|
||||||
@ -95,7 +99,7 @@ fun LoginScreen(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Access your secure vault credentials",
|
text = stringResource(R.string.login_subtitle),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
@ -107,8 +111,8 @@ fun LoginScreen(
|
|||||||
AuthFormTextField(
|
AuthFormTextField(
|
||||||
value = state.email,
|
value = state.email,
|
||||||
onValueChange = viewModel::onEmailChanged,
|
onValueChange = viewModel::onEmailChanged,
|
||||||
label = "Email",
|
label = stringResource(R.string.field_email_label),
|
||||||
placeholder = "you@email.com",
|
placeholder = stringResource(R.string.login_email_placeholder),
|
||||||
error = state.emailError,
|
error = state.emailError,
|
||||||
keyboardType = KeyboardType.Email
|
keyboardType = KeyboardType.Email
|
||||||
)
|
)
|
||||||
@ -116,8 +120,8 @@ fun LoginScreen(
|
|||||||
AuthFormTextField(
|
AuthFormTextField(
|
||||||
value = state.password,
|
value = state.password,
|
||||||
onValueChange = viewModel::onPasswordChanged,
|
onValueChange = viewModel::onPasswordChanged,
|
||||||
label = "Password",
|
label = stringResource(R.string.field_password_label),
|
||||||
placeholder = "••••••••",
|
placeholder = stringResource(R.string.field_password_placeholder),
|
||||||
error = state.passwordError,
|
error = state.passwordError,
|
||||||
isHideContent = !state.isShowPassword,
|
isHideContent = !state.isShowPassword,
|
||||||
autoCorrectEnabled = false,
|
autoCorrectEnabled = false,
|
||||||
@ -143,7 +147,7 @@ fun LoginScreen(
|
|||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.widthIn(max = 480.dp)
|
.widthIn(max = 480.dp)
|
||||||
) {
|
) {
|
||||||
Text("Forgot password?")
|
Text(stringResource(R.string.login_forgot_password))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@ -161,7 +165,7 @@ fun LoginScreen(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Login",
|
text = stringResource(R.string.login_button),
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -173,13 +177,13 @@ fun LoginScreen(
|
|||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Don't have an account?",
|
text = stringResource(R.string.login_no_account),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
TextButton(onClick = navigateToRegister) {
|
TextButton(onClick = navigateToRegister) {
|
||||||
Text(
|
Text(
|
||||||
text = "Create one",
|
text = stringResource(R.string.login_create_one),
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package com.longnh15.heimdall.ui.auth.login
|
package com.longnh15.heimdall.ui.auth.login
|
||||||
|
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
|
|
||||||
data class LoginState(
|
data class LoginState(
|
||||||
val email: String = "",
|
val email: String = "",
|
||||||
val password: String = "",
|
val password: String = "",
|
||||||
|
|
||||||
val emailError: String? = null,
|
val emailError: UiText? = null,
|
||||||
val passwordError: String? = null,
|
val passwordError: UiText? = null,
|
||||||
|
|
||||||
val isShowPassword: Boolean = false,
|
val isShowPassword: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,9 +5,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
import com.longnh15.heimdall.common.ValidationHelper
|
import com.longnh15.heimdall.common.ValidationHelper
|
||||||
import com.longnh15.heimdall.domain.model.ApiResult
|
import com.longnh15.heimdall.domain.model.ApiResult
|
||||||
import com.longnh15.heimdall.domain.repository.AuthRepository
|
import com.longnh15.heimdall.domain.repository.AuthRepository
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
|
import com.longnh15.heimdall.ui.common.toUiText
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -38,18 +41,18 @@ class LoginViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = authRepository.login(state.email, state.password)) {
|
when (val result = authRepository.login(state.email, state.password)) {
|
||||||
is ApiResult.Success -> _uiEvent.send(LoginEvent.LoginSuccess)
|
is ApiResult.Success -> _uiEvent.send(LoginEvent.LoginSuccess)
|
||||||
is ApiResult.Error -> _uiEvent.send(LoginEvent.LoginError(result.message))
|
is ApiResult.Error -> _uiEvent.send(LoginEvent.LoginError(result.toUiText()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validate(): Boolean {
|
private fun validate(): Boolean {
|
||||||
val emailError = when {
|
val emailError = when {
|
||||||
state.email.isBlank() -> "Email is required"
|
state.email.isBlank() -> UiText.Res(R.string.error_email_required)
|
||||||
!ValidationHelper.isEmail(state.email) -> "Enter a valid email address"
|
!ValidationHelper.isEmail(state.email) -> UiText.Res(R.string.error_email_invalid)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
val passwordError = if (state.password.isBlank()) "Password is required" else null
|
val passwordError = if (state.password.isBlank()) UiText.Res(R.string.error_password_required) else null
|
||||||
|
|
||||||
state = state.copy(emailError = emailError, passwordError = passwordError)
|
state = state.copy(emailError = emailError, passwordError = passwordError)
|
||||||
return emailError == null && passwordError == null
|
return emailError == null && passwordError == null
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package com.longnh15.heimdall.ui.auth.register
|
package com.longnh15.heimdall.ui.auth.register
|
||||||
|
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
|
|
||||||
sealed class RegisterEvent {
|
sealed class RegisterEvent {
|
||||||
data object RegisterSuccess: RegisterEvent()
|
data object RegisterSuccess: RegisterEvent()
|
||||||
data class RegisterError(val message: String): RegisterEvent()
|
data class RegisterError(val message: UiText): RegisterEvent()
|
||||||
}
|
}
|
||||||
@ -27,19 +27,25 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
import com.longnh15.heimdall.ui.auth.login.AuthDivider
|
import com.longnh15.heimdall.ui.auth.login.AuthDivider
|
||||||
import com.longnh15.heimdall.ui.auth.login.AuthForm
|
import com.longnh15.heimdall.ui.auth.login.AuthForm
|
||||||
import com.longnh15.heimdall.ui.auth.login.AuthFormTextField
|
import com.longnh15.heimdall.ui.auth.login.AuthFormTextField
|
||||||
@ -51,7 +57,18 @@ fun RegisterScreen(
|
|||||||
viewModel: RegisterViewModel = viewModel(),
|
viewModel: RegisterViewModel = viewModel(),
|
||||||
navigateToLogin: () -> Unit = {},
|
navigateToLogin: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val state = viewModel.state
|
val state = viewModel.state
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.uiEvent.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is RegisterEvent.RegisterSuccess -> navigateToLogin()
|
||||||
|
is RegisterEvent.RegisterError -> snackbarHostState.showSnackbar(event.message.asString(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HeimdallTheme {
|
HeimdallTheme {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -72,7 +89,7 @@ fun RegisterScreen(
|
|||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Create account",
|
text = stringResource(R.string.register_title),
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
)
|
)
|
||||||
@ -80,7 +97,7 @@ fun RegisterScreen(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Secure your digital life with enterprise-grade encryption.",
|
text = stringResource(R.string.register_subtitle),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
@ -92,24 +109,26 @@ fun RegisterScreen(
|
|||||||
AuthFormTextField(
|
AuthFormTextField(
|
||||||
value = state.name,
|
value = state.name,
|
||||||
onValueChange = viewModel::onNameChanged,
|
onValueChange = viewModel::onNameChanged,
|
||||||
label = "Full name",
|
label = stringResource(R.string.register_name_label),
|
||||||
placeholder = "John Doe",
|
placeholder = stringResource(R.string.register_name_placeholder),
|
||||||
keyboardType = KeyboardType.Text
|
keyboardType = KeyboardType.Text
|
||||||
)
|
)
|
||||||
AuthDivider()
|
AuthDivider()
|
||||||
AuthFormTextField(
|
AuthFormTextField(
|
||||||
value = state.email,
|
value = state.email,
|
||||||
onValueChange = viewModel::onEmailChanged,
|
onValueChange = viewModel::onEmailChanged,
|
||||||
label = "Email",
|
label = stringResource(R.string.field_email_label),
|
||||||
placeholder = "name@vault.com",
|
placeholder = stringResource(R.string.register_email_placeholder),
|
||||||
|
error = state.emailError,
|
||||||
keyboardType = KeyboardType.Email,
|
keyboardType = KeyboardType.Email,
|
||||||
)
|
)
|
||||||
AuthDivider()
|
AuthDivider()
|
||||||
AuthFormTextField(
|
AuthFormTextField(
|
||||||
value = state.password,
|
value = state.password,
|
||||||
onValueChange = viewModel::onPasswordChanged,
|
onValueChange = viewModel::onPasswordChanged,
|
||||||
label = "Password",
|
label = stringResource(R.string.field_password_label),
|
||||||
placeholder = "••••••••",
|
placeholder = stringResource(R.string.field_password_placeholder),
|
||||||
|
error = state.passwordError,
|
||||||
isHideContent = !state.isShowPassword,
|
isHideContent = !state.isShowPassword,
|
||||||
autoCorrectEnabled = false,
|
autoCorrectEnabled = false,
|
||||||
isLastField = true,
|
isLastField = true,
|
||||||
@ -143,7 +162,7 @@ fun RegisterScreen(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Create account",
|
text = stringResource(R.string.register_button),
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -151,7 +170,7 @@ fun RegisterScreen(
|
|||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "By creating an account you agree to our Terms of Service and Privacy Policy.",
|
text = stringResource(R.string.register_terms),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
@ -165,13 +184,13 @@ fun RegisterScreen(
|
|||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Already have an account?",
|
text = stringResource(R.string.register_have_account),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
TextButton(onClick = navigateToLogin) {
|
TextButton(onClick = navigateToLogin) {
|
||||||
Text(
|
Text(
|
||||||
text = "Sign in",
|
text = stringResource(R.string.register_sign_in),
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
package com.longnh15.heimdall.ui.auth.register
|
package com.longnh15.heimdall.ui.auth.register
|
||||||
|
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
|
|
||||||
data class RegisterState(
|
data class RegisterState(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val email: String = "",
|
val email: String = "",
|
||||||
val password: String = "",
|
val password: String = "",
|
||||||
|
|
||||||
val emailError: String? = null,
|
val emailError: UiText? = null,
|
||||||
val passwordError: String? = null,
|
val passwordError: UiText? = null,
|
||||||
|
|
||||||
val isShowPassword: Boolean = false
|
val isShowPassword: Boolean = false
|
||||||
)
|
)
|
||||||
@ -5,9 +5,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
import com.longnh15.heimdall.common.ValidationHelper
|
import com.longnh15.heimdall.common.ValidationHelper
|
||||||
import com.longnh15.heimdall.domain.model.ApiResult
|
import com.longnh15.heimdall.domain.model.ApiResult
|
||||||
import com.longnh15.heimdall.domain.repository.AuthRepository
|
import com.longnh15.heimdall.domain.repository.AuthRepository
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
|
import com.longnh15.heimdall.ui.common.toUiText
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -42,20 +45,21 @@ class RegisterViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = authRepository.register(state.email, state.password, state.name)) {
|
when (val result = authRepository.register(state.email, state.password, state.name)) {
|
||||||
is ApiResult.Success -> _uiEvent.send(RegisterEvent.RegisterSuccess)
|
is ApiResult.Success -> _uiEvent.send(RegisterEvent.RegisterSuccess)
|
||||||
is ApiResult.Error -> _uiEvent.send(RegisterEvent.RegisterError(result.message))
|
is ApiResult.Error -> _uiEvent.send(RegisterEvent.RegisterError(result.toUiText()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validate(): Boolean {
|
private fun validate(): Boolean {
|
||||||
val emailError = when {
|
val emailError = when {
|
||||||
state.email.isBlank() -> "Email is required"
|
state.email.isBlank() -> UiText.Res(R.string.error_email_required)
|
||||||
!ValidationHelper.isEmail(state.email) -> "Enter a valid email address"
|
!ValidationHelper.isEmail(state.email) -> UiText.Res(R.string.error_email_invalid)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
val passwordError = when {
|
val passwordError = when {
|
||||||
state.password.isBlank() -> "Password is required"
|
state.password.isBlank() -> UiText.Res(R.string.error_password_required)
|
||||||
state.password.length < 12 -> "Password must be at least 12 characters"
|
state.password.length < MIN_PASSWORD_LENGTH ->
|
||||||
|
UiText.Res(R.string.error_password_too_short, MIN_PASSWORD_LENGTH)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,4 +67,7 @@ class RegisterViewModel(
|
|||||||
return emailError == null && passwordError == null
|
return emailError == null && passwordError == null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val MIN_PASSWORD_LENGTH = 12
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,8 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AutofillNotEnabledDialog(
|
fun AutofillNotEnabledDialog(
|
||||||
@ -18,15 +20,15 @@ fun AutofillNotEnabledDialog(
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(Icons.Outlined.Warning, contentDescription = null)
|
Icon(Icons.Outlined.Warning, contentDescription = null)
|
||||||
},
|
},
|
||||||
title = { Text("Autofill not enabled") },
|
title = { Text(stringResource(R.string.autofill_dialog_title)) },
|
||||||
text = {
|
text = {
|
||||||
Text("Heimdall wasn't selected as your autofill provider. Open settings and tap Heimdall to enable it.")
|
Text(stringResource(R.string.autofill_dialog_body))
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onRetry) { Text("Open settings") }
|
TextButton(onClick = onRetry) { Text(stringResource(R.string.open_settings)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) { Text("Skip for now") }
|
TextButton(onClick = onDismiss) { Text(stringResource(R.string.autofill_dialog_skip)) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.longnh15.heimdall.R
|
import com.longnh15.heimdall.R
|
||||||
@ -33,7 +34,7 @@ fun OnboardingFinalPage(
|
|||||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
) {
|
) {
|
||||||
OnboardingBadge {
|
OnboardingBadge {
|
||||||
Text("You're all set")
|
Text(stringResource(R.string.onboarding_final_badge))
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -43,20 +44,20 @@ fun OnboardingFinalPage(
|
|||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painterResource(R.drawable.tick_icon),
|
painterResource(R.drawable.tick_icon),
|
||||||
"Page icon",
|
stringResource(R.string.onboarding_page_icon_desc),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Heimdall is watching", style = MaterialTheme.typography.headlineMedium)
|
Text(stringResource(R.string.onboarding_final_title), style = MaterialTheme.typography.headlineMedium)
|
||||||
Text(
|
Text(
|
||||||
text = "Your vault is ready. Start adding passwords or import from your browser to get going.",
|
text = stringResource(R.string.onboarding_final_body),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
Button(onClick = navigateToVault) {
|
Button(onClick = navigateToVault) {
|
||||||
Text("Enter my vault ↗")
|
Text(stringResource(R.string.onboarding_final_enter_vault))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -34,11 +35,11 @@ fun OnboardingIntroducePage(modifier: Modifier = Modifier) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
) {
|
) {
|
||||||
OnboardingBadge {
|
OnboardingBadge {
|
||||||
Text("What you get")
|
Text(stringResource(R.string.onboarding_introduce_badge))
|
||||||
}
|
}
|
||||||
Text("Everything in one vault", style = MaterialTheme.typography.headlineMedium)
|
Text(stringResource(R.string.onboarding_introduce_title), style = MaterialTheme.typography.headlineMedium)
|
||||||
Text(
|
Text(
|
||||||
text = "Heimdall keeps your passwords safe and fills them in automatically",
|
text = stringResource(R.string.onboarding_introduce_body),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
@ -47,18 +48,18 @@ fun OnboardingIntroducePage(modifier: Modifier = Modifier) {
|
|||||||
) {
|
) {
|
||||||
FeatureItem(
|
FeatureItem(
|
||||||
iconDrawableRes = R.drawable.lock_icon,
|
iconDrawableRes = R.drawable.lock_icon,
|
||||||
text = "End-to-end encrypted",
|
text = stringResource(R.string.onboarding_feature_encrypted_title),
|
||||||
sub = "Only you can see your data"
|
sub = stringResource(R.string.onboarding_feature_encrypted_sub)
|
||||||
)
|
)
|
||||||
FeatureItem(
|
FeatureItem(
|
||||||
iconDrawableRes = R.drawable.crosshair_icon,
|
iconDrawableRes = R.drawable.crosshair_icon,
|
||||||
text = "Autofill anywhere",
|
text = stringResource(R.string.onboarding_feature_autofill_title),
|
||||||
sub = "Works across all your apps"
|
sub = stringResource(R.string.onboarding_feature_autofill_sub)
|
||||||
)
|
)
|
||||||
FeatureItem(
|
FeatureItem(
|
||||||
iconDrawableRes = R.drawable.person_icon,
|
iconDrawableRes = R.drawable.person_icon,
|
||||||
text = "Biometric unlock",
|
text = stringResource(R.string.onboarding_feature_biometric_title),
|
||||||
sub = "Fingerprint or face to open"
|
sub = stringResource(R.string.onboarding_feature_biometric_sub)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,7 +90,7 @@ fun FeatureItem(
|
|||||||
.background(Color(0xFFEEEDFE), RoundedCornerShape(8.dp)),
|
.background(Color(0xFFEEEDFE), RoundedCornerShape(8.dp)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Image(painterResource(iconDrawableRes), "Icon")
|
Image(painterResource(iconDrawableRes), stringResource(R.string.onboarding_feature_icon_desc))
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
Text(text = text, fontSize = 13.sp, fontWeight = FontWeight(500), color = MaterialTheme.colorScheme.onPrimary)
|
Text(text = text, fontSize = 13.sp, fontWeight = FontWeight(500), color = MaterialTheme.colorScheme.onPrimary)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.longnh15.heimdall.R
|
import com.longnh15.heimdall.R
|
||||||
@ -34,7 +35,7 @@ fun OnboardingRequestEnablePage(
|
|||||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
) {
|
) {
|
||||||
OnboardingBadge {
|
OnboardingBadge {
|
||||||
Text("Setup · 1 of 2")
|
Text(stringResource(R.string.onboarding_enable_badge))
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -44,20 +45,20 @@ fun OnboardingRequestEnablePage(
|
|||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painterResource(R.drawable.page_icon),
|
painterResource(R.drawable.page_icon),
|
||||||
"Page icon",
|
stringResource(R.string.onboarding_page_icon_desc),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Enable autofill", style = MaterialTheme.typography.headlineMedium)
|
Text(stringResource(R.string.onboarding_enable_title), style = MaterialTheme.typography.headlineMedium)
|
||||||
Text(
|
Text(
|
||||||
text = "Let Heimdall fill your passwords automatically. You'll be taken to Android settings — tap Heimdall and you're done.",
|
text = stringResource(R.string.onboarding_enable_body),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
Button(onClick = onOpenAutofillSettings) {
|
Button(onClick = onOpenAutofillSettings) {
|
||||||
Text("Open settings")
|
Text(stringResource(R.string.open_settings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -36,12 +37,12 @@ fun OnboardingWelcomePage(modifier: Modifier = Modifier) {
|
|||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painterResource(R.drawable.shield_icon),
|
painterResource(R.drawable.shield_icon),
|
||||||
"Heimdall Logo",
|
stringResource(R.string.onboarding_welcome_logo_desc),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text("Welcome to Heimdall", style = MaterialTheme.typography.headlineMedium)
|
Text(stringResource(R.string.onboarding_welcome_title), style = MaterialTheme.typography.headlineMedium)
|
||||||
Text(
|
Text(
|
||||||
text = "Your all-seeing guardian for passwords. Secure, private, and always watching over your credentials",
|
text = stringResource(R.string.onboarding_welcome_body),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,9 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LockOverlay(
|
fun LockOverlay(
|
||||||
@ -43,12 +45,12 @@ fun LockOverlay(
|
|||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier.size(48.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Vault is locked",
|
text = stringResource(R.string.vault_locked_title),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Authenticate to access your credentials",
|
text = stringResource(R.string.vault_locked_subtitle),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@ -60,7 +62,7 @@ fun LockOverlay(
|
|||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text("Unlock with biometrics")
|
Text(stringResource(R.string.vault_unlock_biometrics))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,14 +17,16 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VaultHeader() {
|
fun VaultHeader() {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text("My vault")
|
Text(stringResource(R.string.vault_title))
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors().copy(
|
colors = TopAppBarDefaults.topAppBarColors().copy(
|
||||||
containerColor = MaterialTheme.colorScheme.background
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
|
|||||||
@ -13,8 +13,10 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VaultScreen(
|
fun VaultScreen(
|
||||||
@ -33,12 +35,14 @@ fun VaultScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val requestBiometric = remember { {
|
val unlockTitle = stringResource(R.string.vault_unlock_title)
|
||||||
|
val unlockSubtitle = stringResource(R.string.vault_unlock_subtitle)
|
||||||
|
val requestBiometric = remember(unlockTitle, unlockSubtitle) { {
|
||||||
val request = biometricRequest(
|
val request = biometricRequest(
|
||||||
title = "Unlock Heimdall",
|
title = unlockTitle,
|
||||||
AuthenticationRequest.Biometric.Fallback.DeviceCredential,
|
AuthenticationRequest.Biometric.Fallback.DeviceCredential,
|
||||||
) {
|
) {
|
||||||
setSubtitle("Confirm your identity to access your vault")
|
setSubtitle(unlockSubtitle)
|
||||||
}
|
}
|
||||||
launcher.launch(request)
|
launcher.launch(request)
|
||||||
} }
|
} }
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package com.longnh15.heimdall.ui.vault.edit
|
package com.longnh15.heimdall.ui.vault.edit
|
||||||
|
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
|
|
||||||
sealed class AddEditEvent {
|
sealed class AddEditEvent {
|
||||||
data object SaveSuccess: AddEditEvent()
|
data object SaveSuccess: AddEditEvent()
|
||||||
data class SaveError(val message: String): AddEditEvent()
|
data class SaveError(val message: UiText): AddEditEvent()
|
||||||
}
|
}
|
||||||
@ -20,9 +20,11 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -34,7 +36,7 @@ fun AddEditHeader(
|
|||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text("New item")
|
Text(stringResource(R.string.add_edit_title))
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
colors = TopAppBarDefaults.topAppBarColors().copy(
|
colors = TopAppBarDefaults.topAppBarColors().copy(
|
||||||
@ -64,7 +66,7 @@ fun AddEditHeader(
|
|||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (isSaving) "Saving…" else "Save",
|
text = if (isSaving) stringResource(R.string.add_edit_saving) else stringResource(R.string.add_edit_save),
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
fontWeight = FontWeight(500),
|
fontWeight = FontWeight(500),
|
||||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
|||||||
@ -24,10 +24,13 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -37,12 +40,13 @@ fun AddEditVaultItemScreen(
|
|||||||
popScreen: () -> Unit,
|
popScreen: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.uiEvent.collect { event ->
|
viewModel.uiEvent.collect { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
AddEditEvent.SaveSuccess -> popScreen()
|
AddEditEvent.SaveSuccess -> popScreen()
|
||||||
is AddEditEvent.SaveError -> snackbarHostState.showSnackbar(event.message)
|
is AddEditEvent.SaveError -> snackbarHostState.showSnackbar(event.message.asString(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,20 +70,20 @@ fun AddEditVaultItemScreen(
|
|||||||
modifier.padding(paddingValues).padding(horizontal = 16.dp),
|
modifier.padding(paddingValues).padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
AddEditFormHeader("details")
|
AddEditFormHeader(stringResource(R.string.add_edit_section_details))
|
||||||
AddEditForm {
|
AddEditForm {
|
||||||
AddEditFormTextField(
|
AddEditFormTextField(
|
||||||
value = viewModel.state.name,
|
value = viewModel.state.name,
|
||||||
onValueChange = viewModel::onNameChanged,
|
onValueChange = viewModel::onNameChanged,
|
||||||
label = "Name",
|
label = stringResource(R.string.add_edit_name_label),
|
||||||
placeholder = "e.g. Google"
|
placeholder = stringResource(R.string.add_edit_name_placeholder)
|
||||||
)
|
)
|
||||||
AddEditDivider()
|
AddEditDivider()
|
||||||
AddEditFormTextField(
|
AddEditFormTextField(
|
||||||
value = viewModel.state.url,
|
value = viewModel.state.url,
|
||||||
onValueChange = viewModel::onUrlChanged,
|
onValueChange = viewModel::onUrlChanged,
|
||||||
label = "URL",
|
label = stringResource(R.string.add_edit_url_label),
|
||||||
placeholder = "google.com",
|
placeholder = stringResource(R.string.add_edit_url_placeholder),
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {},
|
onClick = {},
|
||||||
@ -90,20 +94,20 @@ fun AddEditVaultItemScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AddEditFormHeader("credentials")
|
AddEditFormHeader(stringResource(R.string.add_edit_section_credentials))
|
||||||
AddEditForm {
|
AddEditForm {
|
||||||
AddEditFormTextField(
|
AddEditFormTextField(
|
||||||
viewModel.state.username,
|
viewModel.state.username,
|
||||||
onValueChange = viewModel::onUsernameChanged,
|
onValueChange = viewModel::onUsernameChanged,
|
||||||
label = "Username",
|
label = stringResource(R.string.add_edit_username_label),
|
||||||
placeholder = "you@email.com"
|
placeholder = stringResource(R.string.add_edit_username_placeholder)
|
||||||
)
|
)
|
||||||
AddEditDivider()
|
AddEditDivider()
|
||||||
AddEditFormTextField(
|
AddEditFormTextField(
|
||||||
value = viewModel.state.password,
|
value = viewModel.state.password,
|
||||||
onValueChange = viewModel::onPasswordChanged,
|
onValueChange = viewModel::onPasswordChanged,
|
||||||
label = "Password",
|
label = stringResource(R.string.field_password_label),
|
||||||
placeholder = "••••••••",
|
placeholder = stringResource(R.string.field_password_placeholder),
|
||||||
isHideContent = viewModel.state.isShowPassword.not(),
|
isHideContent = viewModel.state.isShowPassword.not(),
|
||||||
isLastField = true,
|
isLastField = true,
|
||||||
autoCorrectEnabled = false,
|
autoCorrectEnabled = false,
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.longnh15.heimdall.R
|
||||||
import com.longnh15.heimdall.domain.repository.CredentialRepository
|
import com.longnh15.heimdall.domain.repository.CredentialRepository
|
||||||
|
import com.longnh15.heimdall.ui.common.UiText
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -52,7 +54,8 @@ class AddEditVaultViewModel(
|
|||||||
_uiEvent.send(AddEditEvent.SaveSuccess)
|
_uiEvent.send(AddEditEvent.SaveSuccess)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
state = state.copy(isSaving = false)
|
state = state.copy(isSaving = false)
|
||||||
_uiEvent.send(AddEditEvent.SaveError(e.message ?: "Something went wrong."))
|
val message = e.message?.let { UiText.Dynamic(it) } ?: UiText.Res(R.string.error_generic)
|
||||||
|
_uiEvent.send(AddEditEvent.SaveError(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,91 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Heimdall</string>
|
<string name="app_name">Heimdall</string>
|
||||||
|
|
||||||
|
<!-- Errors / validation -->
|
||||||
|
<string name="error_email_required">Email is required</string>
|
||||||
|
<string name="error_email_invalid">Enter a valid email address</string>
|
||||||
|
<string name="error_password_required">Password is required</string>
|
||||||
|
<string name="error_password_too_short">Password must be at least %1$d characters</string>
|
||||||
|
<string name="error_generic">Something went wrong.</string>
|
||||||
|
|
||||||
|
<!-- Shared form fields -->
|
||||||
|
<string name="field_email_label">Email</string>
|
||||||
|
<string name="field_password_label">Password</string>
|
||||||
|
<string name="field_password_placeholder">••••••••</string>
|
||||||
|
<string name="open_settings">Open settings</string>
|
||||||
|
|
||||||
|
<!-- Login -->
|
||||||
|
<string name="login_title">Welcome back</string>
|
||||||
|
<string name="login_subtitle">Access your secure vault credentials</string>
|
||||||
|
<string name="login_email_placeholder">you@email.com</string>
|
||||||
|
<string name="login_forgot_password">Forgot password?</string>
|
||||||
|
<string name="login_button">Login</string>
|
||||||
|
<string name="login_no_account">Don\'t have an account?</string>
|
||||||
|
<string name="login_create_one">Create one</string>
|
||||||
|
|
||||||
|
<!-- Register -->
|
||||||
|
<string name="register_title">Create account</string>
|
||||||
|
<string name="register_subtitle">Secure your digital life with enterprise-grade encryption.</string>
|
||||||
|
<string name="register_name_label">Full name</string>
|
||||||
|
<string name="register_name_placeholder">John Doe</string>
|
||||||
|
<string name="register_email_placeholder">name@vault.com</string>
|
||||||
|
<string name="register_button">Create account</string>
|
||||||
|
<string name="register_terms">By creating an account you agree to our Terms of Service and Privacy Policy.</string>
|
||||||
|
<string name="register_have_account">Already have an account?</string>
|
||||||
|
<string name="register_sign_in">Sign in</string>
|
||||||
|
|
||||||
|
<!-- Onboarding: welcome -->
|
||||||
|
<string name="onboarding_welcome_logo_desc">Heimdall Logo</string>
|
||||||
|
<string name="onboarding_welcome_title">Welcome to Heimdall</string>
|
||||||
|
<string name="onboarding_welcome_body">Your all-seeing guardian for passwords. Secure, private, and always watching over your credentials</string>
|
||||||
|
|
||||||
|
<!-- Onboarding: introduce -->
|
||||||
|
<string name="onboarding_introduce_badge">What you get</string>
|
||||||
|
<string name="onboarding_introduce_title">Everything in one vault</string>
|
||||||
|
<string name="onboarding_introduce_body">Heimdall keeps your passwords safe and fills them in automatically</string>
|
||||||
|
<string name="onboarding_feature_icon_desc">Icon</string>
|
||||||
|
<string name="onboarding_feature_encrypted_title">End-to-end encrypted</string>
|
||||||
|
<string name="onboarding_feature_encrypted_sub">Only you can see your data</string>
|
||||||
|
<string name="onboarding_feature_autofill_title">Autofill anywhere</string>
|
||||||
|
<string name="onboarding_feature_autofill_sub">Works across all your apps</string>
|
||||||
|
<string name="onboarding_feature_biometric_title">Biometric unlock</string>
|
||||||
|
<string name="onboarding_feature_biometric_sub">Fingerprint or face to open</string>
|
||||||
|
|
||||||
|
<!-- Onboarding: request enable -->
|
||||||
|
<string name="onboarding_page_icon_desc">Page icon</string>
|
||||||
|
<string name="onboarding_enable_badge">Setup · 1 of 2</string>
|
||||||
|
<string name="onboarding_enable_title">Enable autofill</string>
|
||||||
|
<string name="onboarding_enable_body">Let Heimdall fill your passwords automatically. You\'ll be taken to Android settings — tap Heimdall and you\'re done.</string>
|
||||||
|
|
||||||
|
<!-- Onboarding: final -->
|
||||||
|
<string name="onboarding_final_badge">You\'re all set</string>
|
||||||
|
<string name="onboarding_final_title">Heimdall is watching</string>
|
||||||
|
<string name="onboarding_final_body">Your vault is ready. Start adding passwords or import from your browser to get going.</string>
|
||||||
|
<string name="onboarding_final_enter_vault">Enter my vault ↗</string>
|
||||||
|
|
||||||
|
<!-- Autofill not enabled dialog -->
|
||||||
|
<string name="autofill_dialog_title">Autofill not enabled</string>
|
||||||
|
<string name="autofill_dialog_body">Heimdall wasn\'t selected as your autofill provider. Open settings and tap Heimdall to enable it.</string>
|
||||||
|
<string name="autofill_dialog_skip">Skip for now</string>
|
||||||
|
|
||||||
|
<!-- Vault -->
|
||||||
|
<string name="vault_title">My vault</string>
|
||||||
|
<string name="vault_unlock_title">Unlock Heimdall</string>
|
||||||
|
<string name="vault_unlock_subtitle">Confirm your identity to access your vault</string>
|
||||||
|
<string name="vault_locked_title">Vault is locked</string>
|
||||||
|
<string name="vault_locked_subtitle">Authenticate to access your credentials</string>
|
||||||
|
<string name="vault_unlock_biometrics">Unlock with biometrics</string>
|
||||||
|
|
||||||
|
<!-- Add / edit vault item -->
|
||||||
|
<string name="add_edit_title">New item</string>
|
||||||
|
<string name="add_edit_saving">Saving…</string>
|
||||||
|
<string name="add_edit_save">Save</string>
|
||||||
|
<string name="add_edit_section_details">details</string>
|
||||||
|
<string name="add_edit_section_credentials">credentials</string>
|
||||||
|
<string name="add_edit_name_label">Name</string>
|
||||||
|
<string name="add_edit_name_placeholder">e.g. Google</string>
|
||||||
|
<string name="add_edit_url_label">URL</string>
|
||||||
|
<string name="add_edit_url_placeholder">google.com</string>
|
||||||
|
<string name="add_edit_username_label">Username</string>
|
||||||
|
<string name="add_edit_username_placeholder">you@email.com</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
x
Reference in New Issue
Block a user