Refine social auth UI and simplify manage auth state.

Extract Google sign-in into a dedicated manager with explicit cancel handling, update shared social button variants, and switch manage UI state from enum modes to a direct authorization flag.

Made-with: Cursor
This commit is contained in:
2026-03-02 03:16:46 +01:00
parent 28ad63fb4a
commit 8bf076697e
9 changed files with 257 additions and 182 deletions

View File

@@ -9,9 +9,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -54,7 +53,7 @@ enum class AirMQButtonStyle {
} }
private val LegacyButtonShape = RoundedCornerShape(18.dp) private val LegacyButtonShape = RoundedCornerShape(18.dp)
private val LegacyButtonHeight = 48.dp private val LegacyButtonHeight = 36.dp
private val LegacyDisabledContainer = Color(0xFFE0E0E0) private val LegacyDisabledContainer = Color(0xFFE0E0E0)
private val LegacyDisabledContent = Color(0x61000000) private val LegacyDisabledContent = Color(0x61000000)
@@ -99,6 +98,69 @@ fun AirMQButton(
} }
} }
@Composable
fun AirMQSocialButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIconRes: Int? = null,
iconTint: Color = Color.Unspecified,
containerColor: Color = Color.White,
contentColor: Color = Color(0xFF202124)
) {
Button(
onClick = onClick,
enabled = enabled,
shape = LegacyButtonShape,
modifier = modifier.height(LegacyButtonHeight),
colors = ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = LegacyDisabledContainer,
disabledContentColor = LegacyDisabledContent
)
) {
AirMQButtonLabel(
text = text,
leadingIconRes = leadingIconRes,
iconTint = if (enabled) iconTint else LegacyDisabledContent,
textColor = if (enabled) contentColor else LegacyDisabledContent
)
}
}
@Composable
fun AirMQOutlinedLightButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIconRes: Int? = null
) {
OutlinedButton(
onClick = onClick,
enabled = enabled,
shape = LegacyButtonShape,
border = BorderStroke(
width = 1.dp,
color = if (enabled) Color.White.copy(alpha = 0.55f) else LegacyDisabledContent
),
modifier = modifier.height(LegacyButtonHeight),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = Color.White,
disabledContentColor = LegacyDisabledContent
)
) {
AirMQButtonLabel(
text = text,
leadingIconRes = leadingIconRes,
iconTint = if (enabled) Color.White else LegacyDisabledContent,
textColor = if (enabled) Color.White else LegacyDisabledContent
)
}
}
@Composable @Composable
fun AirMQContainedButton( fun AirMQContainedButton(
text: String, text: String,
@@ -111,7 +173,7 @@ fun AirMQContainedButton(
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
shape = LegacyButtonShape, shape = LegacyButtonShape,
modifier = modifier.heightIn(min = LegacyButtonHeight), modifier = modifier.height(LegacyButtonHeight),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = LegacyButtonContained, containerColor = LegacyButtonContained,
contentColor = LegacyButtonOnContained, contentColor = LegacyButtonOnContained,
@@ -143,7 +205,7 @@ fun AirMQOutlinedButton(
width = 1.dp, width = 1.dp,
color = if (enabled) LegacyOutlineLight else LegacyDisabledContent color = if (enabled) LegacyOutlineLight else LegacyDisabledContent
), ),
modifier = modifier.heightIn(min = LegacyButtonHeight), modifier = modifier.height(LegacyButtonHeight),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
contentColor = LegacyButtonOnOutlined, contentColor = LegacyButtonOnOutlined,
disabledContentColor = LegacyDisabledContent disabledContentColor = LegacyDisabledContent
@@ -169,7 +231,7 @@ fun AirMQTextButton(
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
shape = LegacyButtonShape, shape = LegacyButtonShape,
modifier = modifier.heightIn(min = LegacyButtonHeight), modifier = modifier.height(LegacyButtonHeight),
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
contentColor = LegacyButtonOnText, contentColor = LegacyButtonOnText,
disabledContentColor = LegacyDisabledContent disabledContentColor = LegacyDisabledContent
@@ -205,7 +267,7 @@ fun AirMQGradientButton(
shape = LegacyButtonShape, shape = LegacyButtonShape,
interactionSource = interactionSource, interactionSource = interactionSource,
modifier = modifier modifier = modifier
.heightIn(min = LegacyButtonHeight) .height(LegacyButtonHeight)
.clip(LegacyButtonShape), .clip(LegacyButtonShape),
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
@@ -218,7 +280,7 @@ fun AirMQGradientButton(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = LegacyButtonHeight) .height(LegacyButtonHeight)
.clip(LegacyButtonShape) .clip(LegacyButtonShape)
.background( .background(
brush = Brush.horizontalGradient( brush = Brush.horizontalGradient(
@@ -330,3 +392,71 @@ private fun AirMQButtonsPreviewGradientWithIcon() {
} }
} }
} }
@Preview(name = "Buttons - Social", showBackground = true)
@Composable
private fun AirMQButtonsPreviewSocial() {
AirMQTheme {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
AirMQSocialButton(
text = "Sign in with Google",
leadingIconRes = R.drawable.ic_google,
iconTint = Color.Unspecified,
containerColor = Color.White,
contentColor = Color(0xFF202124),
onClick = {},
modifier = Modifier.fillMaxWidth()
)
AirMQSocialButton(
text = "Sign in with Facebook",
leadingIconRes = R.drawable.ic_facebook,
iconTint = Color.White,
containerColor = Color(0xFF3B5998),
contentColor = Color.White,
onClick = {},
modifier = Modifier.fillMaxWidth()
)
AirMQOutlinedLightButton(
text = "Continue anonymously",
onClick = {},
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Preview(name = "Buttons - Social Icon Comparison", showBackground = true)
@Composable
private fun AirMQButtonsPreviewSocialIconComparison() {
AirMQTheme {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
AirMQSocialButton(
text = "Sign in with Google",
leadingIconRes = R.drawable.ic_google,
iconTint = Color.Unspecified,
containerColor = Color.White,
contentColor = Color(0xFF202124),
onClick = {},
modifier = Modifier.fillMaxWidth()
)
AirMQSocialButton(
text = "Sign in with Google",
leadingIconRes = null,
containerColor = Color.White,
contentColor = Color(0xFF202124),
onClick = {},
modifier = Modifier.fillMaxWidth()
)
}
}
}

View File

@@ -19,10 +19,6 @@ fun DashboardScreen(
title = stringResource(id = R.string.title_dashboard), title = stringResource(id = R.string.title_dashboard),
subtitle = stringResource(id = R.string.dashboard_subtitle), subtitle = stringResource(id = R.string.dashboard_subtitle),
actions = listOf( actions = listOf(
ScreenAction(stringResource(id = R.string.dashboard_open_map), onOpenMap),
ScreenAction(stringResource(id = R.string.dashboard_open_manage), onOpenManage),
ScreenAction(stringResource(id = R.string.dashboard_open_city), onOpenCity),
ScreenAction(stringResource(id = R.string.dashboard_open_device), onOpenDevice),
ScreenAction(stringResource(id = R.string.dashboard_open_news), onOpenNews), ScreenAction(stringResource(id = R.string.dashboard_open_news), onOpenNews),
ScreenAction(stringResource(id = R.string.manage_open_widget_constructor), onOpenWidgetConstructor) ScreenAction(stringResource(id = R.string.manage_open_widget_constructor), onOpenWidgetConstructor)
) )

View File

@@ -0,0 +1,82 @@
package org.db3.airmq.features.login
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.util.Log
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
import androidx.credentials.exceptions.NoCredentialException
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import org.db3.airmq.R
sealed interface GoogleSignInResult {
data class Success(val idToken: String) : GoogleSignInResult
data object Cancelled : GoogleSignInResult
data class Error(val message: String) : GoogleSignInResult
}
private const val GOOGLE_SIGN_IN_TAG = "GoogleSignIn"
suspend fun launchGoogleSignIn(context: Context): GoogleSignInResult {
return try {
val activity = context.findActivity()
val request = GetCredentialRequest.Builder()
.addCredentialOption(
GetGoogleIdOption.Builder()
.setServerClientId(context.getString(R.string.default_web_client_id))
.setFilterByAuthorizedAccounts(false)
.build()
)
.build()
val response = CredentialManager.create(context).getCredential(
context = activity,
request = request
)
val credential = response.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
) {
GoogleSignInResult.Success(GoogleIdTokenCredential.createFrom(credential.data).idToken)
} else {
GoogleSignInResult.Error("Unsupported credential type for Google sign-in.")
}
} catch (error: GetCredentialCancellationException) {
logGoogleSignInError(error)
GoogleSignInResult.Cancelled
} catch (error: GetCredentialException) {
logGoogleSignInError(error)
GoogleSignInResult.Error(context.getString(R.string.toast_oauth_failed))
} catch (error: GoogleIdTokenParsingException) {
Log.e(GOOGLE_SIGN_IN_TAG, "Google ID token parsing failed", error)
GoogleSignInResult.Error(context.getString(R.string.toast_oauth_failed))
} catch (error: Throwable) {
GoogleSignInResult.Error(error.message ?: context.getString(R.string.toast_oauth_failed))
}
}
private fun logGoogleSignInError(exception: GetCredentialException) {
val message = when (exception) {
is GetCredentialCancellationException -> "Google sign-in cancelled by user"
is NoCredentialException -> "No Google credential available on device"
is GetCredentialProviderConfigurationException -> "Credential provider is not configured correctly"
is GetCredentialInterruptedException -> "Credential flow interrupted; try again"
else -> "CredentialManager returned an unknown sign-in error"
}
Log.e(GOOGLE_SIGN_IN_TAG, message, exception)
}
private tailrec fun Context.findActivity(): Activity {
return when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> error("Unable to find Activity context for Google sign-in.")
}
}

View File

@@ -1,21 +1,8 @@
package org.db3.airmq.features.login package org.db3.airmq.features.login
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialInterruptedException
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
import androidx.credentials.exceptions.NoCredentialException
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -25,13 +12,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -57,20 +40,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Icon
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import org.db3.airmq.R import org.db3.airmq.R
import org.db3.airmq.features.common.AirMQOutlinedLightButton
import org.db3.airmq.features.common.AirMQSocialButton
import org.db3.airmq.features.login.LoginScreenContract.Action import org.db3.airmq.features.login.LoginScreenContract.Action
import org.db3.airmq.features.login.LoginScreenContract.Event import org.db3.airmq.features.login.LoginScreenContract.Event
import org.db3.airmq.features.login.LoginScreenContract.State import org.db3.airmq.features.login.LoginScreenContract.State
@@ -79,7 +57,6 @@ import org.db3.airmq.ui.theme.AirMQTheme
private val LegacyLoginGradientStart = Color(0xFF449CF5) private val LegacyLoginGradientStart = Color(0xFF449CF5)
private val LegacyLoginGradientEnd = Color(0xFF5CE4BB) private val LegacyLoginGradientEnd = Color(0xFF5CE4BB)
private val LegacyFacebookBlue = Color(0xFF3B5998) private val LegacyFacebookBlue = Color(0xFF3B5998)
private const val LOGIN_TAG = "LoginScreen"
@Composable @Composable
fun LoginScreen( fun LoginScreen(
@@ -95,13 +72,11 @@ fun LoginScreen(
when (action) { when (action) {
Action.OpenManage -> onLogInToManage() Action.OpenManage -> onLogInToManage()
Action.LaunchGoogleSignIn -> { Action.LaunchGoogleSignIn -> {
val result = launchGoogleSignIn(context) when (val result = launchGoogleSignIn(context)) {
result.fold( is GoogleSignInResult.Success -> viewModel.onEvent(Event.GoogleTokenReceived(result.idToken))
onSuccess = { viewModel.onEvent(Event.GoogleTokenReceived(it)) }, GoogleSignInResult.Cancelled -> viewModel.onEvent(Event.GoogleSignInCancelled)
onFailure = { error -> is GoogleSignInResult.Error -> viewModel.onEvent(Event.GoogleSignInFailed(result.message))
viewModel.onEvent(Event.GoogleSignInFailed(error.message))
} }
)
} }
Action.ShowContinueAnonymousDialog -> showContinueAnonymousDialog = true Action.ShowContinueAnonymousDialog -> showContinueAnonymousDialog = true
Action.OpenPrivacyPolicy -> { Action.OpenPrivacyPolicy -> {
@@ -230,41 +205,32 @@ private fun LoginScreenContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
SocialSignInButton( AirMQSocialButton(
text = stringResource(id = R.string.button_sign_in_google), text = stringResource(id = R.string.button_sign_in_google),
iconRes = R.drawable.ic_google, leadingIconRes = R.drawable.ic_google,
iconTint = Color.Unspecified, iconTint = Color.Unspecified,
backgroundColor = Color.White, containerColor = Color.White,
textColor = Color(0xFF202124), contentColor = Color(0xFF202124),
modifier = Modifier.fillMaxWidth(),
onClick = { onEvent(Event.GoogleClicked) } onClick = { onEvent(Event.GoogleClicked) }
) )
SocialSignInButton( AirMQSocialButton(
text = stringResource(id = R.string.button_sign_in_facebook), text = stringResource(id = R.string.button_sign_in_facebook),
iconRes = R.drawable.ic_facebook, leadingIconRes = R.drawable.ic_facebook,
iconTint = Color.White, iconTint = Color.White,
backgroundColor = LegacyFacebookBlue, containerColor = LegacyFacebookBlue,
textColor = Color.White, contentColor = Color.White,
modifier = Modifier.fillMaxWidth(),
onClick = { onEvent(Event.FacebookClicked) } onClick = { onEvent(Event.FacebookClicked) }
) )
OutlinedButton( AirMQOutlinedLightButton(
onClick = { onEvent(Event.ContinueAnonymousClicked) }, text = stringResource(id = R.string.button_continue_anonym),
shape = RoundedCornerShape(18.dp), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors( onClick = { onEvent(Event.ContinueAnonymousClicked) }
contentColor = Color.White
),
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.55f)),
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
) {
Text(
text = stringResource(id = R.string.button_continue_anonym).uppercase(),
style = MaterialTheme.typography.labelLarge
) )
} }
}
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@@ -275,45 +241,6 @@ private fun LoginScreenContent(
} }
} }
@Composable
private fun SocialSignInButton(
text: String,
iconRes: Int,
iconTint: Color,
backgroundColor: Color,
textColor: Color,
onClick: () -> Unit
) {
Button(
onClick = onClick,
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = backgroundColor,
contentColor = textColor
),
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxHeight()
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = text.uppercase(),
style = MaterialTheme.typography.labelLarge
)
}
}
}
@Composable @Composable
private fun PrivacyAndTermsFooter(onEvent: (Event) -> Unit) { private fun PrivacyAndTermsFooter(onEvent: (Event) -> Unit) {
val privacyPolicy = "Privacy Policy" val privacyPolicy = "Privacy Policy"
@@ -388,61 +315,6 @@ private fun PrivacyAndTermsFooter(onEvent: (Event) -> Unit) {
) )
} }
private suspend fun launchGoogleSignIn(context: Context): Result<String> = runCatching {
val activity = context.findActivity()
val request = GetCredentialRequest.Builder()
.addCredentialOption(
GetGoogleIdOption.Builder()
.setServerClientId(context.getString(R.string.default_web_client_id))
.setFilterByAuthorizedAccounts(false)
.build()
)
.build()
val response = CredentialManager.create(context).getCredential(
context = activity,
request = request
)
val credential = response.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
) {
GoogleIdTokenCredential.createFrom(credential.data).idToken
} else {
error("Unsupported credential type for Google sign-in.")
}
}.recoverCatching {
when (it) {
is GetCredentialException -> {
logGoogleSignInError(it)
throw IllegalStateException(context.getString(R.string.toast_oauth_failed), it)
}
is GoogleIdTokenParsingException -> {
Log.e(LOGIN_TAG, "Google ID token parsing failed", it)
throw IllegalStateException(context.getString(R.string.toast_oauth_failed), it)
}
else -> throw it
}
}
private fun logGoogleSignInError(exception: GetCredentialException) {
val message = when (exception) {
is GetCredentialCancellationException -> "Google sign-in cancelled by user"
is NoCredentialException -> "No Google credential available on device"
is GetCredentialProviderConfigurationException -> "Credential provider is not configured correctly"
is GetCredentialInterruptedException -> "Credential flow interrupted; try again"
else -> "CredentialManager returned an unknown sign-in error"
}
Log.e(LOGIN_TAG, message, exception)
}
private tailrec fun Context.findActivity(): Activity {
return when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> error("Unable to find Activity context for Google sign-in.")
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun LoginScreenPreview() { private fun LoginScreenPreview() {

View File

@@ -18,6 +18,7 @@ object LoginScreenContract {
sealed interface Event { sealed interface Event {
data object GoogleClicked : Event data object GoogleClicked : Event
data class GoogleTokenReceived(val idToken: String) : Event data class GoogleTokenReceived(val idToken: String) : Event
data object GoogleSignInCancelled : Event
data class GoogleSignInFailed(val message: String? = null) : Event data class GoogleSignInFailed(val message: String? = null) : Event
data object FacebookClicked : Event data object FacebookClicked : Event
data object ContinueAnonymousClicked : Event data object ContinueAnonymousClicked : Event

View File

@@ -49,6 +49,9 @@ class LoginViewModel @Inject constructor(
) )
) )
} }
Event.GoogleSignInCancelled -> {
_uiState.value = _uiState.value.copy(isLoading = false)
}
Event.FacebookClicked -> { Event.FacebookClicked -> {
_actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon))) _actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon)))
} }

View File

@@ -47,7 +47,6 @@ import org.db3.airmq.features.manage.ManageScreenContract.Action
import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem
import org.db3.airmq.features.manage.ManageScreenContract.Event import org.db3.airmq.features.manage.ManageScreenContract.Event
import org.db3.airmq.features.manage.ManageScreenContract.State import org.db3.airmq.features.manage.ManageScreenContract.State
import org.db3.airmq.features.manage.ManageScreenContract.UserMode
import org.db3.airmq.ui.theme.AirMQTheme import org.db3.airmq.ui.theme.AirMQTheme
import org.db3.airmq.ui.theme.LegacyNavGradientEnd import org.db3.airmq.ui.theme.LegacyNavGradientEnd
import org.db3.airmq.ui.theme.LegacyNavGradientStart import org.db3.airmq.ui.theme.LegacyNavGradientStart
@@ -99,16 +98,16 @@ private fun ManageScreenContent(
ProfileHeader( ProfileHeader(
name = uiState.userName, name = uiState.userName,
email = uiState.userEmail, email = uiState.userEmail,
isAnonymous = uiState.userMode == UserMode.ANONYMOUS, isAnonymous = !uiState.isAuthorized,
onSettingsClick = { onEvent(Event.SettingsClicked) } onSettingsClick = { onEvent(Event.SettingsClicked) }
) )
when (uiState.userMode) { when (uiState.isAuthorized) {
UserMode.ANONYMOUS -> AnonymousContent( false -> AnonymousContent(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
devicesLabel = uiState.devicesLabel, devicesLabel = uiState.devicesLabel,
onSignIn = { onEvent(Event.SignInClicked) } onSignIn = { onEvent(Event.SignInClicked) }
) )
UserMode.AUTHORIZED -> AuthorizedContent( true -> AuthorizedContent(
devicesLabel = uiState.devicesLabel, devicesLabel = uiState.devicesLabel,
devices = uiState.devices, devices = uiState.devices,
onOpenSetup = { onEvent(Event.SetupClicked) }, onOpenSetup = { onEvent(Event.SetupClicked) },
@@ -116,7 +115,7 @@ private fun ManageScreenContent(
onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) } onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) }
) )
} }
if (uiState.userMode == UserMode.AUTHORIZED) { if (uiState.isAuthorized) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
AirMQButton( AirMQButton(
text = stringResource(id = R.string.manage_open_widget_constructor), text = stringResource(id = R.string.manage_open_widget_constructor),
@@ -147,7 +146,7 @@ private fun ProfileHeader(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.background( .background(
brush = Brush.verticalGradient( brush = Brush.verticalGradient(
colors = listOf(LegacyNavGradientEnd, LegacyNavGradientStart) colors = listOf(LegacyNavGradientEnd, LegacyNavGradientStart)
@@ -245,7 +244,6 @@ private fun AnonymousContent(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.fillMaxWidth(0.46f) .fillMaxWidth(0.46f)
.height(48.dp)
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
) )
} }
@@ -336,7 +334,7 @@ private fun ManageScreenAnonymousPreview() {
AirMQTheme { AirMQTheme {
ManageScreenContent( ManageScreenContent(
uiState = State( uiState = State(
userMode = UserMode.ANONYMOUS, isAuthorized = false,
userName = "Anonymous user", userName = "Anonymous user",
userEmail = "Your preferences are not being synced, please sign in", userEmail = "Your preferences are not being synced, please sign in",
devicesLabel = "Sign in to add devices" devicesLabel = "Sign in to add devices"
@@ -354,7 +352,7 @@ private fun ManageScreenAuthorizedPreview() {
AirMQTheme { AirMQTheme {
ManageScreenContent( ManageScreenContent(
uiState = State( uiState = State(
userMode = UserMode.AUTHORIZED, isAuthorized = true,
userName = "Anton Betsun", userName = "Anton Betsun",
userEmail = "messbees@gmail.com", userEmail = "messbees@gmail.com",
devicesLabel = "My devices", devicesLabel = "My devices",

View File

@@ -1,12 +1,6 @@
package org.db3.airmq.features.manage package org.db3.airmq.features.manage
object ManageScreenContract { object ManageScreenContract {
enum class UserMode {
ANONYMOUS,
AUTHORIZED
}
data class DeviceItem( data class DeviceItem(
val id: String, val id: String,
val name: String, val name: String,
@@ -15,7 +9,7 @@ object ManageScreenContract {
) )
data class State( data class State(
val userMode: UserMode = UserMode.ANONYMOUS, val isAuthorized: Boolean = false,
val userName: String = "", val userName: String = "",
val userEmail: String = "", val userEmail: String = "",
val devicesLabel: String = "", val devicesLabel: String = "",

View File

@@ -18,7 +18,6 @@ import org.db3.airmq.features.manage.ManageScreenContract.Action
import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem
import org.db3.airmq.features.manage.ManageScreenContract.Event import org.db3.airmq.features.manage.ManageScreenContract.Event
import org.db3.airmq.features.manage.ManageScreenContract.State import org.db3.airmq.features.manage.ManageScreenContract.State
import org.db3.airmq.features.manage.ManageScreenContract.UserMode
import org.db3.airmq.sdk.auth.AuthService import org.db3.airmq.sdk.auth.AuthService
import org.db3.airmq.sdk.auth.model.User import org.db3.airmq.sdk.auth.model.User
@@ -62,14 +61,14 @@ class ManageViewModel @Inject constructor(
} }
private fun anonymousState(): State = State( private fun anonymousState(): State = State(
userMode = UserMode.ANONYMOUS, isAuthorized = false,
userName = appContext.getString(R.string.text_anonymous_user), userName = appContext.getString(R.string.text_anonymous_user),
userEmail = appContext.getString(R.string.text_please_sign_in), userEmail = appContext.getString(R.string.text_please_sign_in),
devicesLabel = appContext.getString(R.string.text_sign_in_small) devicesLabel = appContext.getString(R.string.text_sign_in_small)
) )
private fun authorizedState(user: User): State = State( private fun authorizedState(user: User): State = State(
userMode = UserMode.AUTHORIZED, isAuthorized = true,
userName = user.displayName ?: appContext.getString(R.string.text_anonymous_user), userName = user.displayName ?: appContext.getString(R.string.text_anonymous_user),
userEmail = user.email ?: "", userEmail = user.email ?: "",
devicesLabel = appContext.getString(R.string.text_your_devices), devicesLabel = appContext.getString(R.string.text_your_devices),