From 9a80ce5dff7dffc4d687c24ae833fd09d4fd9ad4 Mon Sep 17 00:00:00 2001 From: beetzung Date: Mon, 2 Mar 2026 20:38:09 +0100 Subject: [PATCH] feat(login): replace Facebook with email-password auth screen - Remove Facebook provider from Login flow - Add EmailLoginScreen with gradient background, email/password fields - Add EmailLoginScreenContract and EmailLoginViewModel with stub logic - Add navigation: Sign in with email -> EmailLoginScreen - Use back arrow icon instead of back text - Move header above email field, add Register button - Update run command to launch app after install - Add ic_arrow_back drawable, update strings Made-with: Cursor --- .cursor/commands/run.md | 5 +- .../db3/airmq/features/common/AirMQButtons.kt | 10 +- .../airmq/features/login/EmailLoginScreen.kt | 237 ++++++++++++++++++ .../login/EmailLoginScreenContract.kt | 23 ++ .../features/login/EmailLoginViewModel.kt | 53 ++++ .../db3/airmq/features/login/LoginScreen.kt | 15 +- .../features/login/LoginScreenContract.kt | 3 +- .../airmq/features/login/LoginViewModel.kt | 4 +- .../features/navigation/AirMQNavGraph.kt | 14 +- .../airmq/features/navigation/AirMqRoutes.kt | 1 + app/src/main/res/drawable/ic_arrow_back.xml | 9 + app/src/main/res/values-be-rBY/strings.xml | 2 +- app/src/main/res/values-ru-rRU/strings.xml | 2 +- app/src/main/res/values/strings.xml | 7 +- 14 files changed, 362 insertions(+), 23 deletions(-) create mode 100644 app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreen.kt create mode 100644 app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreenContract.kt create mode 100644 app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginViewModel.kt create mode 100644 app/src/main/res/drawable/ic_arrow_back.xml diff --git a/.cursor/commands/run.md b/.cursor/commands/run.md index 69b82ea..3ccae51 100644 --- a/.cursor/commands/run.md +++ b/.cursor/commands/run.md @@ -1,9 +1,10 @@ # Run app on device -Build and install the app on the connected USB Android device. +Build, install, and launch the app on the connected USB Android device. ## What to do 1. Run `./gradlew installDebug` (use `gradlew.bat` on Windows). 2. Ensure the device is connected via USB with USB debugging enabled. -3. After a successful install, report the device name and that the app is ready to launch. +3. After a successful install, launch the app with `adb shell am start -n org.db3.airmq/.MainActivity`. +4. Report the device name and that the app was launched. diff --git a/app/src/main/kotlin/org/db3/airmq/features/common/AirMQButtons.kt b/app/src/main/kotlin/org/db3/airmq/features/common/AirMQButtons.kt index 47c43dc..c94c3fe 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/common/AirMQButtons.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/common/AirMQButtons.kt @@ -413,11 +413,11 @@ private fun AirMQButtonsPreviewSocial() { modifier = Modifier.fillMaxWidth() ) AirMQSocialButton( - text = "Sign in with Facebook", - leadingIconRes = R.drawable.ic_facebook, - iconTint = Color.White, - containerColor = Color(0xFF3B5998), - contentColor = Color.White, + text = "Sign in with email", + leadingIconRes = R.drawable.ic_account, + iconTint = Color.Unspecified, + containerColor = Color.White, + contentColor = Color(0xFF202124), onClick = {}, modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreen.kt new file mode 100644 index 0000000..bcfd197 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreen.kt @@ -0,0 +1,237 @@ +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.text.KeyboardOptions +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.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.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.flow.collectLatest +import org.db3.airmq.R +import androidx.compose.material3.TextButton +import org.db3.airmq.features.common.AirMQOutlinedLightButton +import org.db3.airmq.features.login.EmailLoginScreenContract.Action +import org.db3.airmq.features.login.EmailLoginScreenContract.Event +import org.db3.airmq.features.login.EmailLoginScreenContract.State +import org.db3.airmq.ui.theme.AirMQTheme + +private val LegacyLoginGradientStart = Color(0xFF449CF5) +private val LegacyLoginGradientEnd = Color(0xFF5CE4BB) + +@Composable +fun EmailLoginScreen( + onLogInToManage: () -> Unit, + onBack: () -> Unit, + viewModel: EmailLoginViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + + LaunchedEffect(viewModel) { + viewModel.actions.collectLatest { action -> + when (action) { + Action.OpenManage -> onLogInToManage() + is Action.ShowMessage -> { + Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show() + } + } + } + } + + EmailLoginScreenContent( + uiState = uiState, + onEvent = viewModel::onEvent, + onBack = onBack + ) +} + +@Composable +private fun EmailLoginScreenContent( + uiState: State, + onEvent: (Event) -> Unit, + onBack: () -> Unit +) { + 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) { + 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)) + + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(id = R.string.text_sign_in_email), + color = Color.White, + fontSize = 36.sp, + fontWeight = FontWeight.Light, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = uiState.email, + onValueChange = { onEvent(Event.EmailChanged(it)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + stringResource(id = R.string.hint_email), + color = Color.White.copy(alpha = 0.7f) + ) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + colors = 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) + ) + ) + + OutlinedTextField( + value = uiState.password, + onValueChange = { onEvent(Event.PasswordChanged(it)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + stringResource(id = R.string.hint_password), + color = Color.White.copy(alpha = 0.7f) + ) + }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + colors = 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) + ) + ) + + if (uiState.isLoading) { + CircularProgressIndicator(color = Color.White) + } else { + AirMQOutlinedLightButton( + text = stringResource(id = R.string.button_sign_in), + modifier = Modifier.fillMaxWidth(), + onClick = { onEvent(Event.SignInClicked) } + ) + } + + TextButton( + onClick = { onEvent(Event.RegisterClicked) }, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.button_register), + color = Color.White + ) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun EmailLoginScreenPreview() { + AirMQTheme { + EmailLoginScreenContent( + uiState = State(), + onEvent = {}, + onBack = {} + ) + } +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreenContract.kt b/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreenContract.kt new file mode 100644 index 0000000..f6ba9d5 --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginScreenContract.kt @@ -0,0 +1,23 @@ +package org.db3.airmq.features.login + +object EmailLoginScreenContract { + + data class State( + val email: String = "", + val password: String = "", + val isLoading: Boolean = false + ) + + sealed interface Action { + data object OpenManage : Action + data class ShowMessage(val message: String) : Action + } + + sealed interface Event { + data class EmailChanged(val value: String) : Event + data class PasswordChanged(val value: String) : Event + data object SignInClicked : Event + data object RegisterClicked : Event + data object BackClicked : Event + } +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginViewModel.kt b/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginViewModel.kt new file mode 100644 index 0000000..47a07ce --- /dev/null +++ b/app/src/main/kotlin/org/db3/airmq/features/login/EmailLoginViewModel.kt @@ -0,0 +1,53 @@ +package org.db3.airmq.features.login + +import android.content.Context +import androidx.lifecycle.ViewModel +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 org.db3.airmq.R +import org.db3.airmq.features.login.EmailLoginScreenContract.Action +import org.db3.airmq.features.login.EmailLoginScreenContract.Event +import org.db3.airmq.features.login.EmailLoginScreenContract.State + +@HiltViewModel +class EmailLoginViewModel @Inject constructor( + @ApplicationContext private val appContext: Context +) : ViewModel() { + + private val _uiState = MutableStateFlow(State()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _actions = MutableSharedFlow(extraBufferCapacity = 1) + val actions: SharedFlow = _actions.asSharedFlow() + + fun onEvent(event: Event) { + when (event) { + is Event.EmailChanged -> { + _uiState.value = _uiState.value.copy(email = event.value) + } + is Event.PasswordChanged -> { + _uiState.value = _uiState.value.copy(password = event.value) + } + Event.SignInClicked -> { + _uiState.value = _uiState.value.copy(isLoading = true) + _actions.tryEmit( + Action.ShowMessage(appContext.getString(R.string.coming_soon)) + ) + _uiState.value = _uiState.value.copy(isLoading = false) + } + Event.RegisterClicked -> { + _actions.tryEmit( + Action.ShowMessage(appContext.getString(R.string.coming_soon)) + ) + } + Event.BackClicked -> Unit + } + } +} diff --git a/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreen.kt index 5a23ccc..0873160 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreen.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreen.kt @@ -56,11 +56,11 @@ import org.db3.airmq.ui.theme.AirMQTheme private val LegacyLoginGradientStart = Color(0xFF449CF5) private val LegacyLoginGradientEnd = Color(0xFF5CE4BB) -private val LegacyFacebookBlue = Color(0xFF3B5998) @Composable fun LoginScreen( onLogInToManage: () -> Unit, + onOpenEmailLogin: () -> Unit, viewModel: LoginViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -78,6 +78,7 @@ fun LoginScreen( is GoogleSignInResult.Error -> viewModel.onEvent(Event.GoogleSignInFailed(result.message)) } } + Action.NavigateToEmailLogin -> onOpenEmailLogin() Action.ShowContinueAnonymousDialog -> showContinueAnonymousDialog = true Action.OpenPrivacyPolicy -> { Toast.makeText(context, context.getString(R.string.coming_soon), Toast.LENGTH_SHORT).show() @@ -216,13 +217,13 @@ private fun LoginScreenContent( ) AirMQSocialButton( - text = stringResource(id = R.string.button_sign_in_facebook), - leadingIconRes = R.drawable.ic_facebook, - iconTint = Color.White, - containerColor = LegacyFacebookBlue, - contentColor = Color.White, + text = stringResource(id = R.string.button_sign_in_email), + leadingIconRes = R.drawable.ic_account, + iconTint = Color.Unspecified, + containerColor = Color.White, + contentColor = Color(0xFF202124), modifier = Modifier.fillMaxWidth(), - onClick = { onEvent(Event.FacebookClicked) } + onClick = { onEvent(Event.EmailClicked) } ) AirMQOutlinedLightButton( diff --git a/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreenContract.kt b/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreenContract.kt index 7cb4c29..1646180 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreenContract.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/login/LoginScreenContract.kt @@ -9,6 +9,7 @@ object LoginScreenContract { sealed interface Action { data object OpenManage : Action data object LaunchGoogleSignIn : Action + data object NavigateToEmailLogin : Action data object ShowContinueAnonymousDialog : Action data object OpenPrivacyPolicy : Action data object OpenTermsAndConditions : Action @@ -20,7 +21,7 @@ object LoginScreenContract { data class GoogleTokenReceived(val idToken: String) : Event data object GoogleSignInCancelled : Event data class GoogleSignInFailed(val message: String? = null) : Event - data object FacebookClicked : Event + data object EmailClicked : Event data object ContinueAnonymousClicked : Event data object ContinueAnonymousConfirmed : Event data object ContinueAnonymousDismissed : Event diff --git a/app/src/main/kotlin/org/db3/airmq/features/login/LoginViewModel.kt b/app/src/main/kotlin/org/db3/airmq/features/login/LoginViewModel.kt index 0b76969..e995f13 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/login/LoginViewModel.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/login/LoginViewModel.kt @@ -52,8 +52,8 @@ class LoginViewModel @Inject constructor( Event.GoogleSignInCancelled -> { _uiState.value = _uiState.value.copy(isLoading = false) } - Event.FacebookClicked -> { - _actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon))) + Event.EmailClicked -> { + _actions.tryEmit(Action.NavigateToEmailLogin) } Event.ContinueAnonymousClicked -> { _actions.tryEmit(Action.ShowContinueAnonymousDialog) diff --git a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt index 292bed5..92c948a 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt @@ -38,6 +38,7 @@ import org.db3.airmq.features.device.DeviceScreen import org.db3.airmq.features.entry.SplashScreen import org.db3.airmq.features.entry.WizardScreen import org.db3.airmq.features.location.LocationScreen +import org.db3.airmq.features.login.EmailLoginScreen import org.db3.airmq.features.login.LoginScreen import org.db3.airmq.features.manage.ManageScreen import org.db3.airmq.features.map.MapScreen @@ -204,7 +205,18 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) { } composable(AirMqRoutes.LOGIN) { LoginScreen( - onLogInToManage = { navController.navigate(AirMqRoutes.MANAGE) } + onLogInToManage = { navController.navigate(AirMqRoutes.MANAGE) }, + onOpenEmailLogin = { navController.navigate(AirMqRoutes.EMAIL_LOGIN) } + ) + } + composable(AirMqRoutes.EMAIL_LOGIN) { + EmailLoginScreen( + onLogInToManage = { + navController.navigate(AirMqRoutes.MANAGE) { + popUpTo(AirMqRoutes.LOGIN) { inclusive = true } + } + }, + onBack = { navController.popBackStack() } ) } composable(AirMqRoutes.NEWS) { diff --git a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqRoutes.kt b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqRoutes.kt index a844cf2..4f7fad1 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqRoutes.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMqRoutes.kt @@ -12,6 +12,7 @@ object AirMqRoutes { const val LOCATION = "detail/location" const val SETUP = "detail/setup" const val LOGIN = "detail/login" + const val EMAIL_LOGIN = "detail/email-login" const val NEWS = "detail/news" const val NEWS_DETAIL = "detail/news/{newsId}" const val DEVICE = "detail/device/{deviceId}" diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..bc04ed6 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-be-rBY/strings.xml b/app/src/main/res/values-be-rBY/strings.xml index 1f16dfa..dea8c14 100644 --- a/app/src/main/res/values-be-rBY/strings.xml +++ b/app/src/main/res/values-be-rBY/strings.xml @@ -23,7 +23,7 @@ Адмяніць Палітыка канфідэнцыйнасці Увайсці з дапамогай Google - Увайсці з дапамогай Facebook + Увайсці па email Пошук прылады Завяршыць Працягнуць ананімна diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index a647e69..5f957f7 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -23,7 +23,7 @@ Отменить Политикой конфиденциальности Войти с помощью Google - Войти с помощью Facebook + Войти по email Поиск устройства Завершить Продолжить анонимно diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96f9506..8f06260 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,7 +81,7 @@ Cancel Register Sign in with Google - Sign in with Facebook + Sign in with email Copy log Look for a device Finish @@ -92,7 +92,10 @@ Add new device Connecting… + Sign in with email + + Email Password Device name Name @@ -142,8 +145,6 @@ Copied Device requires registration - 356744979027664 - fb356744979027664 Air quality Borderless layout Error