Add email registration screen with AuthService integration
Introduce EmailRegisterScreen (Compose) with contract, validation, and loading UX matching the email sign-in layout. Register submits via AuthService.registerWithEmail; success navigates to Manage and clears the login stack. Email login Register action opens the new route instead of handling registration on the same screen. Made-with: Cursor
This commit is contained in:
@@ -54,6 +54,7 @@ private val LegacyLoginGradientEnd = Color(0xFF5CE4BB)
|
|||||||
@Composable
|
@Composable
|
||||||
fun EmailLoginScreen(
|
fun EmailLoginScreen(
|
||||||
onLogInToManage: () -> Unit,
|
onLogInToManage: () -> Unit,
|
||||||
|
onOpenRegister: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
viewModel: EmailLoginViewModel = hiltViewModel()
|
viewModel: EmailLoginViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
@@ -64,6 +65,7 @@ fun EmailLoginScreen(
|
|||||||
viewModel.actions.collectLatest { action ->
|
viewModel.actions.collectLatest { action ->
|
||||||
when (action) {
|
when (action) {
|
||||||
Action.OpenManage -> onLogInToManage()
|
Action.OpenManage -> onLogInToManage()
|
||||||
|
Action.NavigateToRegister -> onOpenRegister()
|
||||||
is Action.ShowMessage -> {
|
is Action.ShowMessage -> {
|
||||||
Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object EmailLoginScreenContract {
|
|||||||
|
|
||||||
sealed interface Action {
|
sealed interface Action {
|
||||||
data object OpenManage : Action
|
data object OpenManage : Action
|
||||||
|
data object NavigateToRegister : Action
|
||||||
data class ShowMessage(val message: String) : Action
|
data class ShowMessage(val message: String) : Action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ class EmailLoginViewModel @Inject constructor(
|
|||||||
is Event.PasswordChanged -> {
|
is Event.PasswordChanged -> {
|
||||||
_uiState.value = _uiState.value.copy(password = event.value)
|
_uiState.value = _uiState.value.copy(password = event.value)
|
||||||
}
|
}
|
||||||
Event.SignInClicked -> submitEmailAuth(register = false)
|
Event.SignInClicked -> submitEmailAuth()
|
||||||
Event.RegisterClicked -> submitEmailAuth(register = true)
|
Event.RegisterClicked -> _actions.tryEmit(Action.NavigateToRegister)
|
||||||
Event.BackClicked -> Unit
|
Event.BackClicked -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submitEmailAuth(register: Boolean) {
|
private fun submitEmailAuth() {
|
||||||
val email = _uiState.value.email.trim()
|
val email = _uiState.value.email.trim()
|
||||||
val password = _uiState.value.password
|
val password = _uiState.value.password
|
||||||
if (email.isEmpty() || password.isEmpty()) {
|
if (email.isEmpty() || password.isEmpty()) {
|
||||||
@@ -64,11 +64,7 @@ class EmailLoginViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||||
try {
|
try {
|
||||||
val result = if (register) {
|
val result = authService.loginWithEmailPassword(email, password)
|
||||||
authService.registerWithEmail(email, password, name = "")
|
|
||||||
} else {
|
|
||||||
authService.loginWithEmailPassword(email, password)
|
|
||||||
}
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
_actions.tryEmit(Action.OpenManage)
|
_actions.tryEmit(Action.OpenManage)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
package org.db3.airmq.features.login
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import org.db3.airmq.R
|
||||||
|
import org.db3.airmq.features.common.AirMQOutlinedLightButton
|
||||||
|
import org.db3.airmq.features.login.EmailRegisterScreenContract.Action
|
||||||
|
import org.db3.airmq.features.login.EmailRegisterScreenContract.Event
|
||||||
|
import org.db3.airmq.features.login.EmailRegisterScreenContract.State
|
||||||
|
import org.db3.airmq.ui.theme.AirMQTheme
|
||||||
|
|
||||||
|
private val LegacyLoginGradientStart = Color(0xFF449CF5)
|
||||||
|
private val LegacyLoginGradientEnd = Color(0xFF5CE4BB)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmailRegisterScreen(
|
||||||
|
onRegisterSuccess: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: EmailRegisterViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel) {
|
||||||
|
viewModel.actions.collectLatest { action ->
|
||||||
|
when (action) {
|
||||||
|
Action.OpenManage -> onRegisterSuccess()
|
||||||
|
is Action.ShowMessage -> {
|
||||||
|
Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailRegisterScreenContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onEvent = viewModel::onEvent,
|
||||||
|
onBack = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmailRegisterScreenContent(
|
||||||
|
uiState: State,
|
||||||
|
onEvent: (Event) -> Unit,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val fieldEnabled = !uiState.isLoading
|
||||||
|
val scroll = rememberScrollState()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colorStops = arrayOf(
|
||||||
|
0.0f to LegacyLoginGradientStart,
|
||||||
|
0.35f to LegacyLoginGradientStart,
|
||||||
|
1.0f to LegacyLoginGradientEnd
|
||||||
|
),
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(1200f, 1200f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(horizontal = 40.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onBack, enabled = fieldEnabled) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_arrow_back),
|
||||||
|
contentDescription = stringResource(id = R.string.content_back),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(scroll),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.text_register_email_title),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 36.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.name,
|
||||||
|
onValueChange = { onEvent(Event.NameChanged(it)) },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = fieldEnabled,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
isError = uiState.nameError != null,
|
||||||
|
supportingText = uiState.nameError?.let { err ->
|
||||||
|
{ Text(err, color = Color.White.copy(alpha = 0.9f)) }
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.hint_register_name),
|
||||||
|
color = Color.White.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
colors = legacyLoginFieldColors()
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.email,
|
||||||
|
onValueChange = { onEvent(Event.EmailChanged(it)) },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = fieldEnabled,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
isError = uiState.emailError != null,
|
||||||
|
supportingText = uiState.emailError?.let { err ->
|
||||||
|
{ Text(err, color = Color.White.copy(alpha = 0.9f)) }
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.hint_email),
|
||||||
|
color = Color.White.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Email,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
colors = legacyLoginFieldColors()
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.password,
|
||||||
|
onValueChange = { onEvent(Event.PasswordChanged(it)) },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = fieldEnabled,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
isError = uiState.passwordError != null,
|
||||||
|
supportingText = uiState.passwordError?.let { err ->
|
||||||
|
{ Text(err, color = Color.White.copy(alpha = 0.9f)) }
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.hint_password),
|
||||||
|
color = Color.White.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
colors = legacyLoginFieldColors()
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.passwordConfirm,
|
||||||
|
onValueChange = { onEvent(Event.PasswordConfirmChanged(it)) },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = fieldEnabled,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
isError = uiState.passwordConfirmError != null,
|
||||||
|
supportingText = uiState.passwordConfirmError?.let { err ->
|
||||||
|
{ Text(err, color = Color.White.copy(alpha = 0.9f)) }
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.hint_password_confirm),
|
||||||
|
color = Color.White.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
colors = legacyLoginFieldColors()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
CircularProgressIndicator(color = Color.White)
|
||||||
|
} else {
|
||||||
|
AirMQOutlinedLightButton(
|
||||||
|
text = stringResource(id = R.string.button_register),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = { onEvent(Event.RegisterClicked) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun legacyLoginFieldColors() = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = Color.White,
|
||||||
|
unfocusedTextColor = Color.White,
|
||||||
|
cursorColor = Color.White,
|
||||||
|
focusedBorderColor = Color.White.copy(alpha = 0.8f),
|
||||||
|
unfocusedBorderColor = Color.White.copy(alpha = 0.55f),
|
||||||
|
focusedContainerColor = Color.White.copy(alpha = 0.1f),
|
||||||
|
unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||||
|
errorBorderColor = Color.White.copy(alpha = 0.95f),
|
||||||
|
errorCursorColor = Color.White,
|
||||||
|
errorSupportingTextColor = Color.White.copy(alpha = 0.95f)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun EmailRegisterScreenPreviewEmpty() {
|
||||||
|
AirMQTheme {
|
||||||
|
EmailRegisterScreenContent(
|
||||||
|
uiState = EmailRegisterScreenContract.previewState(),
|
||||||
|
onEvent = {},
|
||||||
|
onBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun EmailRegisterScreenPreviewFilled() {
|
||||||
|
AirMQTheme {
|
||||||
|
EmailRegisterScreenContent(
|
||||||
|
uiState = EmailRegisterScreenContract.previewState(
|
||||||
|
name = "Alex",
|
||||||
|
email = "alex@example.com",
|
||||||
|
password = "secret1",
|
||||||
|
passwordConfirm = "secret1"
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
onBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun EmailRegisterScreenPreviewLoading() {
|
||||||
|
AirMQTheme {
|
||||||
|
EmailRegisterScreenContent(
|
||||||
|
uiState = EmailRegisterScreenContract.previewState(
|
||||||
|
name = "Alex",
|
||||||
|
email = "alex@example.com",
|
||||||
|
password = "secret1",
|
||||||
|
passwordConfirm = "secret1",
|
||||||
|
isLoading = true
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
onBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun EmailRegisterScreenPreviewErrors() {
|
||||||
|
AirMQTheme {
|
||||||
|
EmailRegisterScreenContent(
|
||||||
|
uiState = EmailRegisterScreenContract.previewState(
|
||||||
|
name = "",
|
||||||
|
email = "bad",
|
||||||
|
password = "12",
|
||||||
|
passwordConfirm = "34",
|
||||||
|
nameError = "Enter your name",
|
||||||
|
emailError = "Invalid email",
|
||||||
|
passwordError = "Too short",
|
||||||
|
passwordConfirmError = "Does not match"
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
onBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package org.db3.airmq.features.login
|
||||||
|
|
||||||
|
object EmailRegisterScreenContract {
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val name: String = "",
|
||||||
|
val email: String = "",
|
||||||
|
val password: String = "",
|
||||||
|
val passwordConfirm: String = "",
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val nameError: String? = null,
|
||||||
|
val emailError: String? = null,
|
||||||
|
val passwordError: String? = null,
|
||||||
|
val passwordConfirmError: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
fun previewState(
|
||||||
|
name: String = "",
|
||||||
|
email: String = "",
|
||||||
|
password: String = "",
|
||||||
|
passwordConfirm: String = "",
|
||||||
|
isLoading: Boolean = false,
|
||||||
|
nameError: String? = null,
|
||||||
|
emailError: String? = null,
|
||||||
|
passwordError: String? = null,
|
||||||
|
passwordConfirmError: String? = null
|
||||||
|
): State = State(
|
||||||
|
name = name,
|
||||||
|
email = email,
|
||||||
|
password = password,
|
||||||
|
passwordConfirm = passwordConfirm,
|
||||||
|
isLoading = isLoading,
|
||||||
|
nameError = nameError,
|
||||||
|
emailError = emailError,
|
||||||
|
passwordError = passwordError,
|
||||||
|
passwordConfirmError = passwordConfirmError
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface Action {
|
||||||
|
data object OpenManage : Action
|
||||||
|
data class ShowMessage(val message: String) : Action
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Event {
|
||||||
|
data class NameChanged(val value: String) : Event
|
||||||
|
data class EmailChanged(val value: String) : Event
|
||||||
|
data class PasswordChanged(val value: String) : Event
|
||||||
|
data class PasswordConfirmChanged(val value: String) : Event
|
||||||
|
data object RegisterClicked : Event
|
||||||
|
data object BackClicked : Event
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package org.db3.airmq.features.login
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Patterns
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.db3.airmq.R
|
||||||
|
import org.db3.airmq.features.login.EmailRegisterScreenContract.Action
|
||||||
|
import org.db3.airmq.features.login.EmailRegisterScreenContract.Event
|
||||||
|
import org.db3.airmq.features.login.EmailRegisterScreenContract.State
|
||||||
|
import org.db3.airmq.sdk.auth.AuthService
|
||||||
|
|
||||||
|
private const val MinPasswordLength = 6
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class EmailRegisterViewModel @Inject constructor(
|
||||||
|
@ApplicationContext private val appContext: Context,
|
||||||
|
private val authService: AuthService
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(State())
|
||||||
|
val uiState: StateFlow<State> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
|
||||||
|
val actions: SharedFlow<Action> = _actions.asSharedFlow()
|
||||||
|
|
||||||
|
fun onEvent(event: Event) {
|
||||||
|
when (event) {
|
||||||
|
is Event.NameChanged -> {
|
||||||
|
_uiState.value = _uiState.value.copy(name = event.value, nameError = null)
|
||||||
|
}
|
||||||
|
is Event.EmailChanged -> {
|
||||||
|
_uiState.value = _uiState.value.copy(email = event.value, emailError = null)
|
||||||
|
}
|
||||||
|
is Event.PasswordChanged -> {
|
||||||
|
_uiState.value = _uiState.value.copy(password = event.value, passwordError = null)
|
||||||
|
}
|
||||||
|
is Event.PasswordConfirmChanged -> {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
passwordConfirm = event.value,
|
||||||
|
passwordConfirmError = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Event.RegisterClicked -> register()
|
||||||
|
Event.BackClicked -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun register() {
|
||||||
|
val s = _uiState.value
|
||||||
|
val nameError = if (s.name.isBlank()) {
|
||||||
|
appContext.getString(R.string.error_register_name_required)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val emailTrimmed = s.email.trim()
|
||||||
|
val emailError = when {
|
||||||
|
emailTrimmed.isBlank() -> appContext.getString(R.string.error_register_email_required)
|
||||||
|
!Patterns.EMAIL_ADDRESS.matcher(emailTrimmed).matches() -> {
|
||||||
|
appContext.getString(R.string.error_register_email_invalid)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val passwordError = when {
|
||||||
|
s.password.length < MinPasswordLength -> {
|
||||||
|
appContext.getString(R.string.error_register_password_short)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val passwordConfirmError = when {
|
||||||
|
s.password != s.passwordConfirm -> {
|
||||||
|
appContext.getString(R.string.error_register_password_mismatch)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (nameError != null || emailError != null || passwordError != null || passwordConfirmError != null) {
|
||||||
|
_uiState.value = s.copy(
|
||||||
|
nameError = nameError,
|
||||||
|
emailError = emailError,
|
||||||
|
passwordError = passwordError,
|
||||||
|
passwordConfirmError = passwordConfirmError
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = true,
|
||||||
|
nameError = null,
|
||||||
|
emailError = null,
|
||||||
|
passwordError = null,
|
||||||
|
passwordConfirmError = null
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
val result = authService.registerWithEmail(
|
||||||
|
email = emailTrimmed,
|
||||||
|
password = s.password,
|
||||||
|
name = s.name.trim()
|
||||||
|
)
|
||||||
|
if (result.isSuccess) {
|
||||||
|
_actions.tryEmit(Action.OpenManage)
|
||||||
|
} else {
|
||||||
|
val message = result.exceptionOrNull()?.message
|
||||||
|
?: appContext.getString(R.string.toast_email_auth_failed)
|
||||||
|
_actions.tryEmit(Action.ShowMessage(message))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ import org.db3.airmq.features.entry.SplashScreen
|
|||||||
import org.db3.airmq.features.entry.WizardScreen
|
import org.db3.airmq.features.entry.WizardScreen
|
||||||
import org.db3.airmq.features.location.LocationScreen
|
import org.db3.airmq.features.location.LocationScreen
|
||||||
import org.db3.airmq.features.login.EmailLoginScreen
|
import org.db3.airmq.features.login.EmailLoginScreen
|
||||||
|
import org.db3.airmq.features.login.EmailRegisterScreen
|
||||||
import org.db3.airmq.features.login.LoginScreen
|
import org.db3.airmq.features.login.LoginScreen
|
||||||
import org.db3.airmq.features.manage.ManageScreen
|
import org.db3.airmq.features.manage.ManageScreen
|
||||||
import org.db3.airmq.features.map.MapScreen
|
import org.db3.airmq.features.map.MapScreen
|
||||||
@@ -234,6 +235,17 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) {
|
|||||||
popUpTo(AirMqRoutes.LOGIN) { inclusive = true }
|
popUpTo(AirMqRoutes.LOGIN) { inclusive = true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onOpenRegister = { navController.navigate(AirMqRoutes.EMAIL_REGISTER) },
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(AirMqRoutes.EMAIL_REGISTER) {
|
||||||
|
EmailRegisterScreen(
|
||||||
|
onRegisterSuccess = {
|
||||||
|
navController.navigate(AirMqRoutes.MANAGE) {
|
||||||
|
popUpTo(AirMqRoutes.LOGIN) { inclusive = true }
|
||||||
|
}
|
||||||
|
},
|
||||||
onBack = { navController.popBackStack() }
|
onBack = { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ object AirMqRoutes {
|
|||||||
const val SETUP = "detail/setup"
|
const val SETUP = "detail/setup"
|
||||||
const val LOGIN = "detail/login"
|
const val LOGIN = "detail/login"
|
||||||
const val EMAIL_LOGIN = "detail/email-login"
|
const val EMAIL_LOGIN = "detail/email-login"
|
||||||
|
const val EMAIL_REGISTER = "detail/email-register"
|
||||||
const val NEWS = "detail/news"
|
const val NEWS = "detail/news"
|
||||||
const val NEWS_DETAIL = "detail/news/{newsId}"
|
const val NEWS_DETAIL = "detail/news/{newsId}"
|
||||||
const val DEVICE = "detail/device/{deviceId}"
|
const val DEVICE = "detail/device/{deviceId}"
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
<string name="button_connecting">Connecting…</string>
|
<string name="button_connecting">Connecting…</string>
|
||||||
|
|
||||||
<string name="text_sign_in_email">Sign in with email</string>
|
<string name="text_sign_in_email">Sign in with email</string>
|
||||||
|
<string name="text_register_email_title">Create account</string>
|
||||||
|
|
||||||
<!-- Hints -->
|
<!-- Hints -->
|
||||||
<string name="hint_email">Email</string>
|
<string name="hint_email">Email</string>
|
||||||
@@ -101,6 +102,14 @@
|
|||||||
<string name="hint_setup_name">Name</string>
|
<string name="hint_setup_name">Name</string>
|
||||||
<string name="hint_setup_model">Model</string>
|
<string name="hint_setup_model">Model</string>
|
||||||
<string name="hint_setup_wifi">WiFi network</string>
|
<string name="hint_setup_wifi">WiFi network</string>
|
||||||
|
<string name="hint_register_name">Your name</string>
|
||||||
|
<string name="hint_password_confirm">Confirm password</string>
|
||||||
|
|
||||||
|
<string name="error_register_name_required">Enter your name</string>
|
||||||
|
<string name="error_register_email_required">Enter your email</string>
|
||||||
|
<string name="error_register_email_invalid">Enter a valid email address</string>
|
||||||
|
<string name="error_register_password_short">Password must be at least 6 characters</string>
|
||||||
|
<string name="error_register_password_mismatch">Passwords do not match</string>
|
||||||
|
|
||||||
<!-- Content descriptors -->
|
<!-- Content descriptors -->
|
||||||
<string name="content_desc_user_pic">User Picture</string>
|
<string name="content_desc_user_pic">User Picture</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user