diff --git a/app/src/main/java/com/longnh15/heimdall/data/repository/BaseRepository.kt b/app/src/main/java/com/longnh15/heimdall/data/repository/BaseRepository.kt index 42e0f08..4c8f3e4 100644 --- a/app/src/main/java/com/longnh15/heimdall/data/repository/BaseRepository.kt +++ b/app/src/main/java/com/longnh15/heimdall/data/repository/BaseRepository.kt @@ -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() } diff --git a/app/src/main/java/com/longnh15/heimdall/data/source/remote/ErrorBodyParser.kt b/app/src/main/java/com/longnh15/heimdall/data/source/remote/ErrorBodyParser.kt index 4b1d8d6..627d07d 100644 --- a/app/src/main/java/com/longnh15/heimdall/data/source/remote/ErrorBodyParser.kt +++ b/app/src/main/java/com/longnh15/heimdall/data/source/remote/ErrorBodyParser.kt @@ -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(string()) ApiResult.Error(error.error, error.message) } catch (_: Exception) { - ApiResult.Error(-999, "Unknown error") + ApiResult.Error(ApiResult.Error.CODE_UNKNOWN, "Unknown error") } } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/domain/model/ApiResult.kt b/app/src/main/java/com/longnh15/heimdall/domain/model/ApiResult.kt index f482e91..6efc7db 100644 --- a/app/src/main/java/com/longnh15/heimdall/domain/model/ApiResult.kt +++ b/app/src/main/java/com/longnh15/heimdall/domain/model/ApiResult.kt @@ -2,5 +2,10 @@ package com.longnh15.heimdall.domain.model sealed class ApiResult { data class Success(val data: T): ApiResult() - data class Error(val code: Int, val message: String): ApiResult() + data class Error(val code: Int, val message: String): ApiResult() { + 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/AuthForm.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/AuthForm.kt index cea6708..afe412b 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/AuthForm.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/AuthForm.kt @@ -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 diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginEvent.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginEvent.kt index 450fb24..92e1a09 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginEvent.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginEvent.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginScreen.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginScreen.kt index 449ce3a..792ec78 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginScreen.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginScreen.kt @@ -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 ) } diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginState.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginState.kt index 8278f8a..df57a11 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginState.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginState.kt @@ -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, ) diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginViewModel.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginViewModel.kt index b4ad292..fa6bcb6 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginViewModel.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/login/LoginViewModel.kt @@ -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 diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterEvent.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterEvent.kt index 6d8dde4..71d4813 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterEvent.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterEvent.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterScreen.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterScreen.kt index dd694dc..157347a 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterScreen.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterScreen.kt @@ -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 ) } diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterState.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterState.kt index 0840181..069379d 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterState.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterState.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterViewModel.kt b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterViewModel.kt index 63cef89..ce1f9c1 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterViewModel.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/auth/register/RegisterViewModel.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt b/app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt index a69f107..335da56 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt @@ -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)) } } ) } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/common/UiText.kt b/app/src/main/java/com/longnh15/heimdall/ui/common/UiText.kt new file mode 100644 index 0000000..1857c55 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/common/UiText.kt @@ -0,0 +1,39 @@ +package com.longnh15.heimdall.ui.common + +import android.content.Context +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.longnh15.heimdall.R +import com.longnh15.heimdall.domain.model.ApiResult + +/** + * A string that may be defined either as a runtime value (e.g. a message from the + * backend) or as a string resource. Lets layers without a [Context] (ViewModels, + * domain/data) describe user-facing text without resolving it, deferring resolution + * to the UI where a [Context] / composition is available. + */ +sealed interface UiText { + data class Dynamic(val value: String) : UiText + + class Res(@StringRes val id: Int, vararg val args: Any) : UiText + + @Composable + fun asString(): String = when (this) { + is Dynamic -> value + is Res -> stringResource(id, *args) + } + + fun asString(context: Context): String = when (this) { + is Dynamic -> value + is Res -> context.getString(id, *args) + } +} + +/** + * Maps an API error to user-facing text: client-side failures ([ApiResult.Error.CODE_UNKNOWN]) + * become a localized generic message, while real server messages are shown as-is. + */ +fun ApiResult.Error.toUiText(): UiText = + if (code == ApiResult.Error.CODE_UNKNOWN) UiText.Res(R.string.error_generic) + else UiText.Dynamic(message) diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt index f743b92..aa80a99 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt @@ -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)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt index 33170af..fb534f3 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt @@ -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) diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt index 8e95612..d146781 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt @@ -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)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt index 16e7100..afee0d2 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt @@ -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 ) } diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt index d8f6d60..6628ffa 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt @@ -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)) } } } diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt index 769be45..1da50c2 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt @@ -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 diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt index c6826a2..b98c16f 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt @@ -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) } } diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt index 9ac9efe..120ec2f 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt index 30e890c..62cb259 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt @@ -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 diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt index b2139f7..a1fe0fd 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt @@ -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, diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt index 588e489..5697aee 100644 --- a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt @@ -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)) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b3f407..938e984 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,91 @@ Heimdall - \ No newline at end of file + + + Email is required + Enter a valid email address + Password is required + Password must be at least %1$d characters + Something went wrong. + + + Email + Password + •••••••• + Open settings + + + Welcome back + Access your secure vault credentials + you@email.com + Forgot password? + Login + Don\'t have an account? + Create one + + + Create account + Secure your digital life with enterprise-grade encryption. + Full name + John Doe + name@vault.com + Create account + By creating an account you agree to our Terms of Service and Privacy Policy. + Already have an account? + Sign in + + + Heimdall Logo + Welcome to Heimdall + Your all-seeing guardian for passwords. Secure, private, and always watching over your credentials + + + What you get + Everything in one vault + Heimdall keeps your passwords safe and fills them in automatically + Icon + End-to-end encrypted + Only you can see your data + Autofill anywhere + Works across all your apps + Biometric unlock + Fingerprint or face to open + + + Page icon + Setup · 1 of 2 + Enable autofill + Let Heimdall fill your passwords automatically. You\'ll be taken to Android settings — tap Heimdall and you\'re done. + + + You\'re all set + Heimdall is watching + Your vault is ready. Start adding passwords or import from your browser to get going. + Enter my vault ↗ + + + Autofill not enabled + Heimdall wasn\'t selected as your autofill provider. Open settings and tap Heimdall to enable it. + Skip for now + + + My vault + Unlock Heimdall + Confirm your identity to access your vault + Vault is locked + Authenticate to access your credentials + Unlock with biometrics + + + New item + Saving… + Save + details + credentials + Name + e.g. Google + URL + google.com + Username + you@email.com +