mirror of
https://github.com/bitwarden/android.git
synced 2026-06-07 14:57:41 -05:00
BIT-814, BIT-815: Add UI for Enterprise Single Sign On screen (#437)
This commit is contained in:
committed by
Álison Fernandes
parent
a2e3984a5e
commit
800e0e018c
@@ -8,6 +8,8 @@ import androidx.navigation.navOptions
|
||||
import androidx.navigation.navigation
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
|
||||
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.navigateToEnterpriseSignOn
|
||||
import com.x8bit.bitwarden.ui.auth.feature.environment.environmentDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.environment.navigateToEnvironment
|
||||
import com.x8bit.bitwarden.ui.auth.feature.landing.LANDING_ROUTE
|
||||
@@ -37,6 +39,9 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
||||
)
|
||||
},
|
||||
)
|
||||
enterpriseSignOnDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
landingDestination(
|
||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
||||
onNavigateToLogin = { emailAddress ->
|
||||
@@ -51,6 +56,7 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
||||
)
|
||||
loginDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToEnterpriseSignOn = { navController.navigateToEnterpriseSignOn() },
|
||||
)
|
||||
environmentDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders
|
||||
|
||||
private const val ENTERPRISE_SIGN_ON_ROUTE = "enterprise_sign_on"
|
||||
|
||||
/**
|
||||
* Navigate to the enterprise single sign on screen.
|
||||
*/
|
||||
fun NavController.navigateToEnterpriseSignOn(navOptions: NavOptions? = null) {
|
||||
this.navigate(ENTERPRISE_SIGN_ON_ROUTE, navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the enterprise sign on screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.enterpriseSignOnDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composable(
|
||||
route = ENTERPRISE_SIGN_ON_ROUTE,
|
||||
enterTransition = TransitionProviders.Enter.slideUp,
|
||||
exitTransition = TransitionProviders.Exit.stay,
|
||||
popEnterTransition = TransitionProviders.Enter.stay,
|
||||
popExitTransition = TransitionProviders.Exit.slideDown,
|
||||
) {
|
||||
EnterpriseSignOnScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
|
||||
|
||||
import android.widget.Toast
|
||||
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.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
|
||||
/**
|
||||
* The top level composable for the Enterprise Single Sign On screen.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EnterpriseSignOnScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: EnterpriseSignOnViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
EnterpriseSignOnEvent.NavigateBack -> onNavigateBack()
|
||||
is EnterpriseSignOnEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val dialog = state.dialogState) {
|
||||
is EnterpriseSignOnState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = dialog.message,
|
||||
),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnterpriseSignOnAction.DialogDismiss) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = dialog.message,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.app_name),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnterpriseSignOnAction.CloseButtonClick) }
|
||||
},
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.log_in),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnterpriseSignOnAction.LogInClick) }
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
EnterpriseSignOnScreenContent(
|
||||
state = state,
|
||||
onOrgIdentifierInputChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnterpriseSignOnAction.OrgIdentifierInputChange(it)) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun EnterpriseSignOnScreenContent(
|
||||
state: EnterpriseSignOnState,
|
||||
onOrgIdentifierInputChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.semantics { testTagsAsResourceId = true }
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.log_in_sso_summary),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
value = state.orgIdentifierInput,
|
||||
onValueChange = onOrgIdentifierInputChange,
|
||||
label = stringResource(id = R.string.org_identifier),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* Manages application state for the enterprise single sign on screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class EnterpriseSignOnViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<EnterpriseSignOnState, EnterpriseSignOnEvent, EnterpriseSignOnAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: EnterpriseSignOnState(
|
||||
dialogState = null,
|
||||
orgIdentifierInput = "",
|
||||
),
|
||||
) {
|
||||
|
||||
override fun handleAction(action: EnterpriseSignOnAction) {
|
||||
when (action) {
|
||||
EnterpriseSignOnAction.CloseButtonClick -> handleCloseButtonClicked()
|
||||
EnterpriseSignOnAction.DialogDismiss -> handleDialogDismissed()
|
||||
EnterpriseSignOnAction.LogInClick -> handleLogInClicked()
|
||||
is EnterpriseSignOnAction.OrgIdentifierInputChange -> {
|
||||
handleOrgIdentifierInputChanged(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseButtonClicked() {
|
||||
sendEvent(EnterpriseSignOnEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleDialogDismissed() {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
|
||||
private fun handleLogInClicked() {
|
||||
// TODO BIT-816: submit request for single sign on
|
||||
sendEvent(EnterpriseSignOnEvent.ShowToast("Not yet implemented."))
|
||||
}
|
||||
|
||||
private fun handleOrgIdentifierInputChanged(
|
||||
action: EnterpriseSignOnAction.OrgIdentifierInputChange,
|
||||
) {
|
||||
mutableStateFlow.update { it.copy(orgIdentifierInput = action.input) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models state of the enterprise sign on screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class EnterpriseSignOnState(
|
||||
val dialogState: DialogState?,
|
||||
val orgIdentifierInput: String,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
*/
|
||||
sealed class DialogState : Parcelable {
|
||||
/**
|
||||
* Represents an error dialog with the given [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Represents a loading dialog with the given [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Loading(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for the enterprise sign on screen.
|
||||
*/
|
||||
sealed class EnterpriseSignOnEvent {
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
data object NavigateBack : EnterpriseSignOnEvent()
|
||||
|
||||
/**
|
||||
* Shows a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(
|
||||
val message: String,
|
||||
) : EnterpriseSignOnEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the enterprise sign on screen.
|
||||
*/
|
||||
sealed class EnterpriseSignOnAction {
|
||||
/**
|
||||
* Indicates that the top-bar close button was clicked.
|
||||
*/
|
||||
data object CloseButtonClick : EnterpriseSignOnAction()
|
||||
|
||||
/**
|
||||
* Indicates that the current dialog has been dismissed.
|
||||
*/
|
||||
data object DialogDismiss : EnterpriseSignOnAction()
|
||||
|
||||
/**
|
||||
* Indicates that the Log In button has been clicked.
|
||||
*/
|
||||
data object LogInClick : EnterpriseSignOnAction()
|
||||
|
||||
/**
|
||||
* Indicates that the organization identifier input has changed.
|
||||
*/
|
||||
data class OrgIdentifierInputChange(
|
||||
val input: String,
|
||||
) : EnterpriseSignOnAction()
|
||||
}
|
||||
@@ -44,6 +44,7 @@ fun NavController.navigateToLogin(
|
||||
*/
|
||||
fun NavGraphBuilder.loginDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
) {
|
||||
composable(
|
||||
route = LOGIN_ROUTE,
|
||||
@@ -55,12 +56,13 @@ fun NavGraphBuilder.loginDestination(
|
||||
},
|
||||
),
|
||||
enterTransition = TransitionProviders.Enter.slideUp,
|
||||
exitTransition = TransitionProviders.Exit.slideDown,
|
||||
popEnterTransition = TransitionProviders.Enter.slideUp,
|
||||
exitTransition = TransitionProviders.Exit.stay,
|
||||
popEnterTransition = TransitionProviders.Enter.stay,
|
||||
popExitTransition = TransitionProviders.Exit.slideDown,
|
||||
) {
|
||||
LoginScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToEnterpriseSignOn = onNavigateToEnterpriseSignOn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
@Suppress("LongMethod")
|
||||
fun LoginScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
viewModel: LoginViewModel = hiltViewModel(),
|
||||
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
|
||||
) {
|
||||
@@ -73,6 +74,7 @@ fun LoginScreen(
|
||||
intentHandler.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
LoginEvent.NavigateToEnterpriseSignOn -> onNavigateToEnterpriseSignOn()
|
||||
is LoginEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -164,7 +166,7 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun LoginScreenContent(
|
||||
@@ -233,13 +235,12 @@ private fun LoginScreenContent(
|
||||
|
||||
BitwardenOutlinedButtonWithIcon(
|
||||
label = stringResource(id = R.string.log_in_sso),
|
||||
icon = painterResource(id = R.drawable.ic_light_bulb),
|
||||
icon = painterResource(id = R.drawable.ic_briefcase),
|
||||
onClick = onSingleSignOnClick,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "LogInWithSsoButton" }
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
isEnabled = state.isLoginButtonEnabled,
|
||||
)
|
||||
|
||||
Text(
|
||||
|
||||
@@ -204,8 +204,7 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleSingleSignOnClicked() {
|
||||
// TODO BIT-204 navigate to single sign on
|
||||
sendEvent(LoginEvent.ShowToast("Not yet implemented."))
|
||||
sendEvent(LoginEvent.NavigateToEnterpriseSignOn)
|
||||
}
|
||||
|
||||
private fun handlePasswordInputChanged(action: LoginAction.PasswordInputChanged) {
|
||||
@@ -242,6 +241,11 @@ sealed class LoginEvent {
|
||||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : LoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the enterprise single sign on screen.
|
||||
*/
|
||||
data object NavigateToEnterpriseSignOn : LoginEvent()
|
||||
|
||||
/**
|
||||
* Shows a toast with the given [message].
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user