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) {
|
||||
response.body()?.let {
|
||||
ApiResult.Success(it)
|
||||
} ?: ApiResult.Error(-999, "Empty response body")
|
||||
} ?: ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Empty response body")
|
||||
} else {
|
||||
response.errorBody().toApiError()
|
||||
}
|
||||
@ -28,7 +28,7 @@ abstract class BaseRepository {
|
||||
return if (response.isSuccessful) {
|
||||
response.body()?.let {
|
||||
ApiResult.Success(transform(it))
|
||||
} ?: ApiResult.Error(-999, "Empty response body")
|
||||
} ?: ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Empty response body")
|
||||
} else {
|
||||
response.errorBody().toApiError()
|
||||
}
|
||||
|
||||
@ -7,12 +7,12 @@ import okhttp3.ResponseBody
|
||||
|
||||
fun ResponseBody?.toApiError(): ApiResult.Error {
|
||||
if (this == null) {
|
||||
return ApiResult.Error(-999, "Unknown error")
|
||||
return ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Unknown error")
|
||||
}
|
||||
return try {
|
||||
val error = json.decodeFromString<ErrorResponse>(string())
|
||||
ApiResult.Error(error.error, error.message)
|
||||
} 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> {
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.longnh15.heimdall.ui.common.UiText
|
||||
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
||||
|
||||
@Composable
|
||||
@ -61,7 +62,7 @@ fun AuthFormTextField(
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
placeholder: String,
|
||||
error: String? = null,
|
||||
error: UiText? = null,
|
||||
isHideContent: Boolean = false,
|
||||
isLastField: Boolean = false,
|
||||
autoCorrectEnabled: Boolean = true,
|
||||
@ -121,7 +122,7 @@ fun AuthFormTextField(
|
||||
}
|
||||
if (error != null) {
|
||||
Text(
|
||||
text = error,
|
||||
text = error.asString(),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontSize = 12.sp
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.longnh15.heimdall.ui.auth.login
|
||||
|
||||
import com.longnh15.heimdall.ui.common.UiText
|
||||
|
||||
sealed class 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.draw.clip
|
||||
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.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.longnh15.heimdall.R
|
||||
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
||||
|
||||
@Composable
|
||||
@ -55,12 +58,13 @@ fun LoginScreen(
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val state = viewModel.state
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.uiEvent.collect { event ->
|
||||
when (event) {
|
||||
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))
|
||||
|
||||
Text(
|
||||
text = "Welcome back",
|
||||
text = stringResource(R.string.login_title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
@ -95,7 +99,7 @@ fun LoginScreen(
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Access your secure vault credentials",
|
||||
text = stringResource(R.string.login_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
@ -107,8 +111,8 @@ fun LoginScreen(
|
||||
AuthFormTextField(
|
||||
value = state.email,
|
||||
onValueChange = viewModel::onEmailChanged,
|
||||
label = "Email",
|
||||
placeholder = "you@email.com",
|
||||
label = stringResource(R.string.field_email_label),
|
||||
placeholder = stringResource(R.string.login_email_placeholder),
|
||||
error = state.emailError,
|
||||
keyboardType = KeyboardType.Email
|
||||
)
|
||||
@ -116,8 +120,8 @@ fun LoginScreen(
|
||||
AuthFormTextField(
|
||||
value = state.password,
|
||||
onValueChange = viewModel::onPasswordChanged,
|
||||
label = "Password",
|
||||
placeholder = "••••••••",
|
||||
label = stringResource(R.string.field_password_label),
|
||||
placeholder = stringResource(R.string.field_password_placeholder),
|
||||
error = state.passwordError,
|
||||
isHideContent = !state.isShowPassword,
|
||||
autoCorrectEnabled = false,
|
||||
@ -143,7 +147,7 @@ fun LoginScreen(
|
||||
.align(Alignment.End)
|
||||
.widthIn(max = 480.dp)
|
||||
) {
|
||||
Text("Forgot password?")
|
||||
Text(stringResource(R.string.login_forgot_password))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@ -161,7 +165,7 @@ fun LoginScreen(
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Login",
|
||||
text = stringResource(R.string.login_button),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
@ -173,13 +177,13 @@ fun LoginScreen(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Don't have an account?",
|
||||
text = stringResource(R.string.login_no_account),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
TextButton(onClick = navigateToRegister) {
|
||||
Text(
|
||||
text = "Create one",
|
||||
text = stringResource(R.string.login_create_one),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package com.longnh15.heimdall.ui.auth.login
|
||||
|
||||
import com.longnh15.heimdall.ui.common.UiText
|
||||
|
||||
data class LoginState(
|
||||
val email: String = "",
|
||||
val password: String = "",
|
||||
|
||||
val emailError: String? = null,
|
||||
val passwordError: String? = null,
|
||||
val emailError: UiText? = null,
|
||||
val passwordError: UiText? = null,
|
||||
|
||||
val isShowPassword: Boolean = false,
|
||||
)
|
||||
|
||||
@ -5,9 +5,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.longnh15.heimdall.R
|
||||
import com.longnh15.heimdall.common.ValidationHelper
|
||||
import com.longnh15.heimdall.domain.model.ApiResult
|
||||
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.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@ -38,18 +41,18 @@ class LoginViewModel(
|
||||
viewModelScope.launch {
|
||||
when (val result = authRepository.login(state.email, state.password)) {
|
||||
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 {
|
||||
val emailError = when {
|
||||
state.email.isBlank() -> "Email is required"
|
||||
!ValidationHelper.isEmail(state.email) -> "Enter a valid email address"
|
||||
state.email.isBlank() -> UiText.Res(R.string.error_email_required)
|
||||
!ValidationHelper.isEmail(state.email) -> UiText.Res(R.string.error_email_invalid)
|
||||
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)
|
||||
return emailError == null && passwordError == null
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.longnh15.heimdall.ui.auth.register
|
||||
|
||||
import com.longnh15.heimdall.ui.common.UiText
|
||||
|
||||
sealed class 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.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.AuthForm
|
||||
import com.longnh15.heimdall.ui.auth.login.AuthFormTextField
|
||||
@ -51,7 +57,18 @@ fun RegisterScreen(
|
||||
viewModel: RegisterViewModel = viewModel(),
|
||||
navigateToLogin: () -> Unit = {},
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
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 {
|
||||
Scaffold(
|
||||
@ -72,7 +89,7 @@ fun RegisterScreen(
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Create account",
|
||||
text = stringResource(R.string.register_title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
@ -80,7 +97,7 @@ fun RegisterScreen(
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Secure your digital life with enterprise-grade encryption.",
|
||||
text = stringResource(R.string.register_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
@ -92,24 +109,26 @@ fun RegisterScreen(
|
||||
AuthFormTextField(
|
||||
value = state.name,
|
||||
onValueChange = viewModel::onNameChanged,
|
||||
label = "Full name",
|
||||
placeholder = "John Doe",
|
||||
label = stringResource(R.string.register_name_label),
|
||||
placeholder = stringResource(R.string.register_name_placeholder),
|
||||
keyboardType = KeyboardType.Text
|
||||
)
|
||||
AuthDivider()
|
||||
AuthFormTextField(
|
||||
value = state.email,
|
||||
onValueChange = viewModel::onEmailChanged,
|
||||
label = "Email",
|
||||
placeholder = "name@vault.com",
|
||||
label = stringResource(R.string.field_email_label),
|
||||
placeholder = stringResource(R.string.register_email_placeholder),
|
||||
error = state.emailError,
|
||||
keyboardType = KeyboardType.Email,
|
||||
)
|
||||
AuthDivider()
|
||||
AuthFormTextField(
|
||||
value = state.password,
|
||||
onValueChange = viewModel::onPasswordChanged,
|
||||
label = "Password",
|
||||
placeholder = "••••••••",
|
||||
label = stringResource(R.string.field_password_label),
|
||||
placeholder = stringResource(R.string.field_password_placeholder),
|
||||
error = state.passwordError,
|
||||
isHideContent = !state.isShowPassword,
|
||||
autoCorrectEnabled = false,
|
||||
isLastField = true,
|
||||
@ -143,7 +162,7 @@ fun RegisterScreen(
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Create account",
|
||||
text = stringResource(R.string.register_button),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
@ -151,7 +170,7 @@ fun RegisterScreen(
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
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,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
@ -165,13 +184,13 @@ fun RegisterScreen(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Already have an account?",
|
||||
text = stringResource(R.string.register_have_account),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
TextButton(onClick = navigateToLogin) {
|
||||
Text(
|
||||
text = "Sign in",
|
||||
text = stringResource(R.string.register_sign_in),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
package com.longnh15.heimdall.ui.auth.register
|
||||
|
||||
import com.longnh15.heimdall.ui.common.UiText
|
||||
|
||||
data class RegisterState(
|
||||
val name: String = "",
|
||||
val email: String = "",
|
||||
val password: String = "",
|
||||
|
||||
val emailError: String? = null,
|
||||
val passwordError: String? = null,
|
||||
val emailError: UiText? = null,
|
||||
val passwordError: UiText? = null,
|
||||
|
||||
val isShowPassword: Boolean = false
|
||||
)
|
||||
@ -5,9 +5,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.longnh15.heimdall.R
|
||||
import com.longnh15.heimdall.common.ValidationHelper
|
||||
import com.longnh15.heimdall.domain.model.ApiResult
|
||||
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.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@ -42,20 +45,21 @@ class RegisterViewModel(
|
||||
viewModelScope.launch {
|
||||
when (val result = authRepository.register(state.email, state.password, state.name)) {
|
||||
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 {
|
||||
val emailError = when {
|
||||
state.email.isBlank() -> "Email is required"
|
||||
!ValidationHelper.isEmail(state.email) -> "Enter a valid email address"
|
||||
state.email.isBlank() -> UiText.Res(R.string.error_email_required)
|
||||
!ValidationHelper.isEmail(state.email) -> UiText.Res(R.string.error_email_invalid)
|
||||
else -> null
|
||||
}
|
||||
val passwordError = when {
|
||||
state.password.isBlank() -> "Password is required"
|
||||
state.password.length < 12 -> "Password must be at least 12 characters"
|
||||
state.password.isBlank() -> UiText.Res(R.string.error_password_required)
|
||||
state.password.length < MIN_PASSWORD_LENGTH ->
|
||||
UiText.Res(R.string.error_password_too_short, MIN_PASSWORD_LENGTH)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@ -63,4 +67,7 @@ class RegisterViewModel(
|
||||
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.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.longnh15.heimdall.R
|
||||
|
||||
@Composable
|
||||
fun AutofillNotEnabledDialog(
|
||||
@ -18,15 +20,15 @@ fun AutofillNotEnabledDialog(
|
||||
icon = {
|
||||
Icon(Icons.Outlined.Warning, contentDescription = null)
|
||||
},
|
||||
title = { Text("Autofill not enabled") },
|
||||
title = { Text(stringResource(R.string.autofill_dialog_title)) },
|
||||
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 = {
|
||||
TextButton(onClick = onRetry) { Text("Open settings") }
|
||||
TextButton(onClick = onRetry) { Text(stringResource(R.string.open_settings)) }
|
||||
},
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.longnh15.heimdall.R
|
||||
@ -33,7 +34,7 @@ fun OnboardingFinalPage(
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
OnboardingBadge {
|
||||
Text("You're all set")
|
||||
Text(stringResource(R.string.onboarding_final_badge))
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -43,20 +44,20 @@ fun OnboardingFinalPage(
|
||||
) {
|
||||
Image(
|
||||
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 = "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
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -34,11 +35,11 @@ fun OnboardingIntroducePage(modifier: Modifier = Modifier) {
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
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 = "Heimdall keeps your passwords safe and fills them in automatically",
|
||||
text = stringResource(R.string.onboarding_introduce_body),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Column(
|
||||
@ -47,18 +48,18 @@ fun OnboardingIntroducePage(modifier: Modifier = Modifier) {
|
||||
) {
|
||||
FeatureItem(
|
||||
iconDrawableRes = R.drawable.lock_icon,
|
||||
text = "End-to-end encrypted",
|
||||
sub = "Only you can see your data"
|
||||
text = stringResource(R.string.onboarding_feature_encrypted_title),
|
||||
sub = stringResource(R.string.onboarding_feature_encrypted_sub)
|
||||
)
|
||||
FeatureItem(
|
||||
iconDrawableRes = R.drawable.crosshair_icon,
|
||||
text = "Autofill anywhere",
|
||||
sub = "Works across all your apps"
|
||||
text = stringResource(R.string.onboarding_feature_autofill_title),
|
||||
sub = stringResource(R.string.onboarding_feature_autofill_sub)
|
||||
)
|
||||
FeatureItem(
|
||||
iconDrawableRes = R.drawable.person_icon,
|
||||
text = "Biometric unlock",
|
||||
sub = "Fingerprint or face to open"
|
||||
text = stringResource(R.string.onboarding_feature_biometric_title),
|
||||
sub = stringResource(R.string.onboarding_feature_biometric_sub)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -89,7 +90,7 @@ fun FeatureItem(
|
||||
.background(Color(0xFFEEEDFE), RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(painterResource(iconDrawableRes), "Icon")
|
||||
Image(painterResource(iconDrawableRes), stringResource(R.string.onboarding_feature_icon_desc))
|
||||
}
|
||||
Column {
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.longnh15.heimdall.R
|
||||
@ -34,7 +35,7 @@ fun OnboardingRequestEnablePage(
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
OnboardingBadge {
|
||||
Text("Setup · 1 of 2")
|
||||
Text(stringResource(R.string.onboarding_enable_badge))
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -44,20 +45,20 @@ fun OnboardingRequestEnablePage(
|
||||
) {
|
||||
Image(
|
||||
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 = "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
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -36,12 +37,12 @@ fun OnboardingWelcomePage(modifier: Modifier = Modifier) {
|
||||
) {
|
||||
Image(
|
||||
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 = "Your all-seeing guardian for passwords. Secure, private, and always watching over your credentials",
|
||||
text = stringResource(R.string.onboarding_welcome_body),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
@ -19,7 +19,9 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.longnh15.heimdall.R
|
||||
|
||||
@Composable
|
||||
fun LockOverlay(
|
||||
@ -43,12 +45,12 @@ fun LockOverlay(
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Vault is locked",
|
||||
text = stringResource(R.string.vault_locked_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Text(
|
||||
text = "Authenticate to access your credentials",
|
||||
text = stringResource(R.string.vault_locked_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@ -60,7 +62,7 @@ fun LockOverlay(
|
||||
modifier = Modifier.size(18.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.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.longnh15.heimdall.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VaultHeader() {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("My vault")
|
||||
Text(stringResource(R.string.vault_title))
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors().copy(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
|
||||
@ -13,8 +13,10 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.longnh15.heimdall.R
|
||||
|
||||
@Composable
|
||||
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(
|
||||
title = "Unlock Heimdall",
|
||||
title = unlockTitle,
|
||||
AuthenticationRequest.Biometric.Fallback.DeviceCredential,
|
||||
) {
|
||||
setSubtitle("Confirm your identity to access your vault")
|
||||
setSubtitle(unlockSubtitle)
|
||||
}
|
||||
launcher.launch(request)
|
||||
} }
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.longnh15.heimdall.ui.vault.edit
|
||||
|
||||
import com.longnh15.heimdall.ui.common.UiText
|
||||
|
||||
sealed class 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.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.longnh15.heimdall.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -34,7 +36,7 @@ fun AddEditHeader(
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("New item")
|
||||
Text(stringResource(R.string.add_edit_title))
|
||||
},
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
colors = TopAppBarDefaults.topAppBarColors().copy(
|
||||
@ -64,7 +66,7 @@ fun AddEditHeader(
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
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,
|
||||
fontWeight = FontWeight(500),
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
|
||||
@ -24,10 +24,13 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.longnh15.heimdall.R
|
||||
import com.longnh15.heimdall.ui.theme.HeimdallTheme
|
||||
|
||||
@Composable
|
||||
@ -37,12 +40,13 @@ fun AddEditVaultItemScreen(
|
||||
popScreen: () -> Unit,
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.uiEvent.collect { event ->
|
||||
when (event) {
|
||||
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),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
AddEditFormHeader("details")
|
||||
AddEditFormHeader(stringResource(R.string.add_edit_section_details))
|
||||
AddEditForm {
|
||||
AddEditFormTextField(
|
||||
value = viewModel.state.name,
|
||||
onValueChange = viewModel::onNameChanged,
|
||||
label = "Name",
|
||||
placeholder = "e.g. Google"
|
||||
label = stringResource(R.string.add_edit_name_label),
|
||||
placeholder = stringResource(R.string.add_edit_name_placeholder)
|
||||
)
|
||||
AddEditDivider()
|
||||
AddEditFormTextField(
|
||||
value = viewModel.state.url,
|
||||
onValueChange = viewModel::onUrlChanged,
|
||||
label = "URL",
|
||||
placeholder = "google.com",
|
||||
label = stringResource(R.string.add_edit_url_label),
|
||||
placeholder = stringResource(R.string.add_edit_url_placeholder),
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {},
|
||||
@ -90,20 +94,20 @@ fun AddEditVaultItemScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
AddEditFormHeader("credentials")
|
||||
AddEditFormHeader(stringResource(R.string.add_edit_section_credentials))
|
||||
AddEditForm {
|
||||
AddEditFormTextField(
|
||||
viewModel.state.username,
|
||||
onValueChange = viewModel::onUsernameChanged,
|
||||
label = "Username",
|
||||
placeholder = "you@email.com"
|
||||
label = stringResource(R.string.add_edit_username_label),
|
||||
placeholder = stringResource(R.string.add_edit_username_placeholder)
|
||||
)
|
||||
AddEditDivider()
|
||||
AddEditFormTextField(
|
||||
value = viewModel.state.password,
|
||||
onValueChange = viewModel::onPasswordChanged,
|
||||
label = "Password",
|
||||
placeholder = "••••••••",
|
||||
label = stringResource(R.string.field_password_label),
|
||||
placeholder = stringResource(R.string.field_password_placeholder),
|
||||
isHideContent = viewModel.state.isShowPassword.not(),
|
||||
isLastField = true,
|
||||
autoCorrectEnabled = false,
|
||||
|
||||
@ -5,7 +5,9 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.longnh15.heimdall.R
|
||||
import com.longnh15.heimdall.domain.repository.CredentialRepository
|
||||
import com.longnh15.heimdall.ui.common.UiText
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@ -52,7 +54,8 @@ class AddEditVaultViewModel(
|
||||
_uiEvent.send(AddEditEvent.SaveSuccess)
|
||||
} catch (e: Exception) {
|
||||
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>
|
||||
<string name="app_name">Heimdall</string>
|
||||
</resources>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user