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:
@@ -9,9 +9,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -54,7 +53,7 @@ enum class AirMQButtonStyle {
|
||||
}
|
||||
|
||||
private val LegacyButtonShape = RoundedCornerShape(18.dp)
|
||||
private val LegacyButtonHeight = 48.dp
|
||||
private val LegacyButtonHeight = 36.dp
|
||||
private val LegacyDisabledContainer = Color(0xFFE0E0E0)
|
||||
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
|
||||
fun AirMQContainedButton(
|
||||
text: String,
|
||||
@@ -111,7 +173,7 @@ fun AirMQContainedButton(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
shape = LegacyButtonShape,
|
||||
modifier = modifier.heightIn(min = LegacyButtonHeight),
|
||||
modifier = modifier.height(LegacyButtonHeight),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = LegacyButtonContained,
|
||||
contentColor = LegacyButtonOnContained,
|
||||
@@ -143,7 +205,7 @@ fun AirMQOutlinedButton(
|
||||
width = 1.dp,
|
||||
color = if (enabled) LegacyOutlineLight else LegacyDisabledContent
|
||||
),
|
||||
modifier = modifier.heightIn(min = LegacyButtonHeight),
|
||||
modifier = modifier.height(LegacyButtonHeight),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = LegacyButtonOnOutlined,
|
||||
disabledContentColor = LegacyDisabledContent
|
||||
@@ -169,7 +231,7 @@ fun AirMQTextButton(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
shape = LegacyButtonShape,
|
||||
modifier = modifier.heightIn(min = LegacyButtonHeight),
|
||||
modifier = modifier.height(LegacyButtonHeight),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = LegacyButtonOnText,
|
||||
disabledContentColor = LegacyDisabledContent
|
||||
@@ -205,7 +267,7 @@ fun AirMQGradientButton(
|
||||
shape = LegacyButtonShape,
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier
|
||||
.heightIn(min = LegacyButtonHeight)
|
||||
.height(LegacyButtonHeight)
|
||||
.clip(LegacyButtonShape),
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -218,7 +280,7 @@ fun AirMQGradientButton(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = LegacyButtonHeight)
|
||||
.height(LegacyButtonHeight)
|
||||
.clip(LegacyButtonShape)
|
||||
.background(
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@ fun DashboardScreen(
|
||||
title = stringResource(id = R.string.title_dashboard),
|
||||
subtitle = stringResource(id = R.string.dashboard_subtitle),
|
||||
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.manage_open_widget_constructor), onOpenWidgetConstructor)
|
||||
)
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,8 @@
|
||||
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 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.layout.Arrangement
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.sp
|
||||
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.material3.Icon
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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 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.Event
|
||||
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 LegacyLoginGradientEnd = Color(0xFF5CE4BB)
|
||||
private val LegacyFacebookBlue = Color(0xFF3B5998)
|
||||
private const val LOGIN_TAG = "LoginScreen"
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
@@ -95,13 +72,11 @@ fun LoginScreen(
|
||||
when (action) {
|
||||
Action.OpenManage -> onLogInToManage()
|
||||
Action.LaunchGoogleSignIn -> {
|
||||
val result = launchGoogleSignIn(context)
|
||||
result.fold(
|
||||
onSuccess = { viewModel.onEvent(Event.GoogleTokenReceived(it)) },
|
||||
onFailure = { error ->
|
||||
viewModel.onEvent(Event.GoogleSignInFailed(error.message))
|
||||
}
|
||||
)
|
||||
when (val result = launchGoogleSignIn(context)) {
|
||||
is GoogleSignInResult.Success -> viewModel.onEvent(Event.GoogleTokenReceived(result.idToken))
|
||||
GoogleSignInResult.Cancelled -> viewModel.onEvent(Event.GoogleSignInCancelled)
|
||||
is GoogleSignInResult.Error -> viewModel.onEvent(Event.GoogleSignInFailed(result.message))
|
||||
}
|
||||
}
|
||||
Action.ShowContinueAnonymousDialog -> showContinueAnonymousDialog = true
|
||||
Action.OpenPrivacyPolicy -> {
|
||||
@@ -230,40 +205,31 @@ private fun LoginScreenContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
SocialSignInButton(
|
||||
AirMQSocialButton(
|
||||
text = stringResource(id = R.string.button_sign_in_google),
|
||||
iconRes = R.drawable.ic_google,
|
||||
leadingIconRes = R.drawable.ic_google,
|
||||
iconTint = Color.Unspecified,
|
||||
backgroundColor = Color.White,
|
||||
textColor = Color(0xFF202124),
|
||||
containerColor = Color.White,
|
||||
contentColor = Color(0xFF202124),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { onEvent(Event.GoogleClicked) }
|
||||
)
|
||||
|
||||
SocialSignInButton(
|
||||
AirMQSocialButton(
|
||||
text = stringResource(id = R.string.button_sign_in_facebook),
|
||||
iconRes = R.drawable.ic_facebook,
|
||||
leadingIconRes = R.drawable.ic_facebook,
|
||||
iconTint = Color.White,
|
||||
backgroundColor = LegacyFacebookBlue,
|
||||
textColor = Color.White,
|
||||
containerColor = LegacyFacebookBlue,
|
||||
contentColor = Color.White,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { onEvent(Event.FacebookClicked) }
|
||||
)
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { onEvent(Event.ContinueAnonymousClicked) },
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
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
|
||||
)
|
||||
}
|
||||
AirMQOutlinedLightButton(
|
||||
text = stringResource(id = R.string.button_continue_anonym),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { onEvent(Event.ContinueAnonymousClicked) }
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
private fun PrivacyAndTermsFooter(onEvent: (Event) -> Unit) {
|
||||
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)
|
||||
@Composable
|
||||
private fun LoginScreenPreview() {
|
||||
|
||||
@@ -18,6 +18,7 @@ object LoginScreenContract {
|
||||
sealed interface Event {
|
||||
data object GoogleClicked : Event
|
||||
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 ContinueAnonymousClicked : Event
|
||||
|
||||
@@ -49,6 +49,9 @@ 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)))
|
||||
}
|
||||
|
||||
@@ -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.Event
|
||||
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.LegacyNavGradientEnd
|
||||
import org.db3.airmq.ui.theme.LegacyNavGradientStart
|
||||
@@ -99,16 +98,16 @@ private fun ManageScreenContent(
|
||||
ProfileHeader(
|
||||
name = uiState.userName,
|
||||
email = uiState.userEmail,
|
||||
isAnonymous = uiState.userMode == UserMode.ANONYMOUS,
|
||||
isAnonymous = !uiState.isAuthorized,
|
||||
onSettingsClick = { onEvent(Event.SettingsClicked) }
|
||||
)
|
||||
when (uiState.userMode) {
|
||||
UserMode.ANONYMOUS -> AnonymousContent(
|
||||
when (uiState.isAuthorized) {
|
||||
false -> AnonymousContent(
|
||||
modifier = Modifier.weight(1f),
|
||||
devicesLabel = uiState.devicesLabel,
|
||||
onSignIn = { onEvent(Event.SignInClicked) }
|
||||
)
|
||||
UserMode.AUTHORIZED -> AuthorizedContent(
|
||||
true -> AuthorizedContent(
|
||||
devicesLabel = uiState.devicesLabel,
|
||||
devices = uiState.devices,
|
||||
onOpenSetup = { onEvent(Event.SetupClicked) },
|
||||
@@ -116,7 +115,7 @@ private fun ManageScreenContent(
|
||||
onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) }
|
||||
)
|
||||
}
|
||||
if (uiState.userMode == UserMode.AUTHORIZED) {
|
||||
if (uiState.isAuthorized) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AirMQButton(
|
||||
text = stringResource(id = R.string.manage_open_widget_constructor),
|
||||
@@ -147,7 +146,7 @@ private fun ProfileHeader(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(LegacyNavGradientEnd, LegacyNavGradientStart)
|
||||
@@ -245,7 +244,6 @@ private fun AnonymousContent(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(0.46f)
|
||||
.height(48.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
@@ -336,7 +334,7 @@ private fun ManageScreenAnonymousPreview() {
|
||||
AirMQTheme {
|
||||
ManageScreenContent(
|
||||
uiState = State(
|
||||
userMode = UserMode.ANONYMOUS,
|
||||
isAuthorized = false,
|
||||
userName = "Anonymous user",
|
||||
userEmail = "Your preferences are not being synced, please sign in",
|
||||
devicesLabel = "Sign in to add devices"
|
||||
@@ -354,7 +352,7 @@ private fun ManageScreenAuthorizedPreview() {
|
||||
AirMQTheme {
|
||||
ManageScreenContent(
|
||||
uiState = State(
|
||||
userMode = UserMode.AUTHORIZED,
|
||||
isAuthorized = true,
|
||||
userName = "Anton Betsun",
|
||||
userEmail = "messbees@gmail.com",
|
||||
devicesLabel = "My devices",
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
package org.db3.airmq.features.manage
|
||||
|
||||
object ManageScreenContract {
|
||||
|
||||
enum class UserMode {
|
||||
ANONYMOUS,
|
||||
AUTHORIZED
|
||||
}
|
||||
|
||||
data class DeviceItem(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@@ -15,7 +9,7 @@ object ManageScreenContract {
|
||||
)
|
||||
|
||||
data class State(
|
||||
val userMode: UserMode = UserMode.ANONYMOUS,
|
||||
val isAuthorized: Boolean = false,
|
||||
val userName: String = "",
|
||||
val userEmail: String = "",
|
||||
val devicesLabel: String = "",
|
||||
|
||||
@@ -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.Event
|
||||
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.model.User
|
||||
|
||||
@@ -62,14 +61,14 @@ class ManageViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun anonymousState(): State = State(
|
||||
userMode = UserMode.ANONYMOUS,
|
||||
isAuthorized = false,
|
||||
userName = appContext.getString(R.string.text_anonymous_user),
|
||||
userEmail = appContext.getString(R.string.text_please_sign_in),
|
||||
devicesLabel = appContext.getString(R.string.text_sign_in_small)
|
||||
)
|
||||
|
||||
private fun authorizedState(user: User): State = State(
|
||||
userMode = UserMode.AUTHORIZED,
|
||||
isAuthorized = true,
|
||||
userName = user.displayName ?: appContext.getString(R.string.text_anonymous_user),
|
||||
userEmail = user.email ?: "",
|
||||
devicesLabel = appContext.getString(R.string.text_your_devices),
|
||||
|
||||
Reference in New Issue
Block a user