PM-23321: Replace two-factor screen toasts with snackbars (#5473)

This commit is contained in:
David Perez
2025-07-03 13:10:48 -05:00
committed by GitHub
parent ef9dda5159
commit 348e14e52d
4 changed files with 51 additions and 22 deletions

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.auth.feature.twofactorlogin
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -26,7 +25,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -59,6 +57,8 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.composition.LocalNfcManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@@ -78,7 +78,6 @@ fun TwoFactorLoginScreen(
nfcManager: NfcManager = LocalNfcManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
LifecycleEventEffect { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
@@ -96,6 +95,7 @@ fun TwoFactorLoginScreen(
else -> Unit
}
}
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
TwoFactorLoginEvent.NavigateBack -> onNavigateBack()
@@ -116,9 +116,7 @@ fun TwoFactorLoginScreen(
intentManager.startCustomTabsActivity(uri = event.uri)
}
is TwoFactorLoginEvent.ShowToast -> {
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
}
is TwoFactorLoginEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
}
}
@@ -171,6 +169,9 @@ fun TwoFactorLoginScreen(
},
)
},
snackbarHost = {
BitwardenSnackbarHost(bitwardenHostState = snackbarHostState)
},
) {
TwoFactorLoginScreenContent(
state = state,

View File

@@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.imageRes
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.isContinueButtonEnabled
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.shouldUseNfc
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.showPasswordInput
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
@@ -249,7 +250,7 @@ class TwoFactorLoginViewModel @Inject constructor(
)
TwoFactorLoginEvent.NavigateToWebAuth(uri = uri)
}
?: TwoFactorLoginEvent.ShowToast(
?: TwoFactorLoginEvent.ShowSnackbar(
message = R.string.there_was_an_error_starting_web_authn_two_factor_authentication.asText(),
),
)
@@ -453,7 +454,7 @@ class TwoFactorLoginViewModel @Inject constructor(
ResendEmailResult.Success -> {
if (action.isUserInitiated) {
sendEvent(
TwoFactorLoginEvent.ShowToast(
TwoFactorLoginEvent.ShowSnackbar(
message = R.string.verification_email_sent.asText(),
),
)
@@ -710,11 +711,25 @@ sealed class TwoFactorLoginEvent {
data class NavigateToRecoveryCode(val uri: Uri) : TwoFactorLoginEvent()
/**
* Shows a toast with the given [message].
* Shows a snackbar with the given [data].
*/
data class ShowToast(
val message: Text,
) : TwoFactorLoginEvent()
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : TwoFactorLoginEvent() {
constructor(
message: Text,
messageHeader: Text? = null,
actionLabel: Text? = null,
withDismissAction: Boolean = false,
) : this(
data = BitwardenSnackbarData(
message = message,
messageHeader = messageHeader,
actionLabel = actionLabel,
withDismissAction = withDismissAction,
),
)
}
}
/**

View File

@@ -18,6 +18,7 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.network.model.TwoFactorAuthMethod
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.nfc.NfcManager
import io.mockk.every
@@ -60,6 +61,15 @@ class TwoFactorLoginScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `on ShowSnackbar should display snackbar content`() {
val message = "message"
val data = BitwardenSnackbarData(message = message.asText())
composeTestRule.onNodeWithText(text = message).assertDoesNotExist()
mutableEventFlow.tryEmit(TwoFactorLoginEvent.ShowSnackbar(data = data))
composeTestRule.onNodeWithText(text = message).assertIsDisplayed()
}
@Test
fun `basicDialog should update according to state`() {
composeTestRule.onNodeWithText("Error message").assertDoesNotExist()

View File

@@ -513,7 +513,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `ContinueButtonClick login should emit ShowToast when auth method is WEB_AUTH and data is null`() =
fun `ContinueButtonClick login should emit ShowSnackbar when auth method is WEB_AUTH and data is null`() =
runTest {
val response = GetTokenResponseJson.TwoFactorRequired(
authMethodsData = emptyMap(),
@@ -528,8 +528,10 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
assertEquals(
TwoFactorLoginEvent.ShowToast(
message = R.string.there_was_an_error_starting_web_authn_two_factor_authentication.asText(),
TwoFactorLoginEvent.ShowSnackbar(
message = R.string
.there_was_an_error_starting_web_authn_two_factor_authentication
.asText(),
),
awaitItem(),
)
@@ -899,7 +901,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
@Test
fun `ResendEmailClick returns success should emit ShowToast`() = runTest {
fun `ResendEmailClick returns success should emit ShowSnackbar`() = runTest {
coEvery {
authRepository.resendVerificationCodeEmail()
} returns ResendEmailResult.Success
@@ -924,7 +926,9 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
)
assertEquals(
TwoFactorLoginEvent.ShowToast(message = R.string.verification_email_sent.asText()),
TwoFactorLoginEvent.ShowSnackbar(
message = R.string.verification_email_sent.asText(),
),
awaitItem(),
)
}
@@ -1050,7 +1054,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
@Test
@Suppress("MaxLineLength")
fun `ReceiveResendEmailResult with ResendEmailResult Success and isUserInitiated true should ShowToast`() =
fun `ReceiveResendEmailResult with ResendEmailResult Success and isUserInitiated true should ShowSnackbar`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
@@ -1061,7 +1065,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
),
)
assertEquals(
TwoFactorLoginEvent.ShowToast(
TwoFactorLoginEvent.ShowSnackbar(
message = R.string.verification_email_sent.asText(),
),
awaitItem(),
@@ -1192,8 +1196,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
@Test
@Suppress("MaxLineLength")
fun `ReceiveResendEmailResult with ResendEmailResult Success should ShowToast`() =
fun `ReceiveResendEmailResult with ResendEmailResult Success should ShowSnackbar`() =
runTest {
val initialState = DEFAULT_STATE.copy(
authMethod = TwoFactorAuthMethod.EMAIL,
@@ -1208,7 +1211,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
),
)
assertEquals(
TwoFactorLoginEvent.ShowToast(
TwoFactorLoginEvent.ShowSnackbar(
message = R.string.verification_email_sent.asText(),
),
awaitItem(),