diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index 32f4f38de..48cabe4c3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -12,6 +12,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.navigate import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.appearanceDestination import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.navigateToAppearance import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.autoFillDestination +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.blockAutoFillDestination +import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill.navigateToBlockAutoFillScreen import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.navigateToAutoFill import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination @@ -51,12 +53,16 @@ fun NavGraphBuilder.settingsGraph( onNavigateToDeleteAccount = onNavigateToDeleteAccount, ) appearanceDestination(onNavigateBack = { navController.popBackStack() }) - autoFillDestination(onNavigateBack = { navController.popBackStack() }) + autoFillDestination( + onNavigateBack = { navController.popBackStack() }, + onNavigateToBlockAutoFillScreen = { navController.navigateToBlockAutoFillScreen() }, + ) otherDestination(onNavigateBack = { navController.popBackStack() }) vaultSettingsDestination( onNavigateBack = { navController.popBackStack() }, onNavigateToFolders = onNavigateToFolders, ) + blockAutoFillDestination(onNavigateBack = { navController.popBackStack() }) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt index b64e41e44..d35582a1d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt @@ -12,11 +12,15 @@ private const val AUTO_FILL_ROUTE = "settings_auto_fill" */ fun NavGraphBuilder.autoFillDestination( onNavigateBack: () -> Unit, + onNavigateToBlockAutoFillScreen: () -> Unit, ) { composableWithPushTransitions( route = AUTO_FILL_ROUTE, ) { - AutoFillScreen(onNavigateBack = onNavigateBack) + AutoFillScreen( + onNavigateBack = onNavigateBack, + onNavigateToBlockAutoFillScreen = onNavigateToBlockAutoFillScreen, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 9098f4573..12c821c06 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -53,6 +53,7 @@ fun AutoFillScreen( onNavigateBack: () -> Unit, viewModel: AutoFillViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, + onNavigateToBlockAutoFillScreen: () -> Unit, ) { val state by viewModel.stateFlow.collectAsState() val context = LocalContext.current @@ -71,6 +72,9 @@ fun AutoFillScreen( is AutoFillEvent.ShowToast -> { Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show() } + AutoFillEvent.NavigateToBlockAutoFill -> { + onNavigateToBlockAutoFillScreen() + } } } @@ -170,6 +174,16 @@ fun AutoFillScreen( { viewModel.trySendAction(AutoFillAction.UriDetectionMethodSelect(it)) } }, ) + BitwardenTextRow( + text = stringResource(id = R.string.block_auto_fill), + description = stringResource( + id = R.string.auto_fill_will_not_be_offered_for_these_ur_is, + ), + onClick = remember(viewModel) { + { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) } + }, + modifier = Modifier.fillMaxWidth(), + ) Spacer(modifier = Modifier.height(16.dp)) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt index f7e1cc28a..6b6a59410 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt @@ -57,6 +57,7 @@ class AutoFillViewModel @Inject constructor( AutoFillAction.BackClick -> handleBackClick() is AutoFillAction.CopyTotpAutomaticallyClick -> handleCopyTotpAutomaticallyClick(action) is AutoFillAction.UriDetectionMethodSelect -> handleUriDetectionMethodSelect(action) + AutoFillAction.BlockAutoFillClick -> handleBlockAutoFillClick() is AutoFillAction.UseInlineAutofillClick -> handleUseInlineAutofillClick(action) is AutoFillAction.Internal.AutofillEnabledUpdateReceive -> { handleAutofillEnabledUpdateReceive(action) @@ -109,6 +110,10 @@ class AutoFillViewModel @Inject constructor( it.copy(isAutoFillServicesEnabled = action.isAutofillEnabled) } } + + private fun handleBlockAutoFillClick() { + sendEvent(AutoFillEvent.NavigateToBlockAutoFill) + } } /** @@ -150,6 +155,11 @@ sealed class AutoFillEvent { */ data object NavigateToAutofillSettings : AutoFillEvent() + /** + * Navigate to block auto fill screen. + */ + data object NavigateToBlockAutoFill : AutoFillEvent() + /** * Displays a toast with the given [Text]. */ @@ -195,6 +205,11 @@ sealed class AutoFillAction { val uriDetectionMethod: AutoFillState.UriDetectionMethod, ) : AutoFillAction() + /** + * User clicked block auto fill button. + */ + data object BlockAutoFillClick : AutoFillAction() + /** * User clicked use inline autofill button. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillNavigation.kt new file mode 100644 index 000000000..186ab96cc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillNavigation.kt @@ -0,0 +1,28 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions + +private const val BLOCK_AUTO_FILL_ROUTE = "settings_block_auto_fill" + +/** + * Add block auto-fill destination to the nav graph. + */ +fun NavGraphBuilder.blockAutoFillDestination( + onNavigateBack: () -> Unit, +) { + composableWithPushTransitions( + route = BLOCK_AUTO_FILL_ROUTE, + ) { + BlockAutoFillScreen(onNavigateBack = onNavigateBack) + } +} + +/** + * Navigate to the block auto-fill screen. + */ +fun NavController.navigateToBlockAutoFillScreen(navOptions: NavOptions? = null) { + navigate(BLOCK_AUTO_FILL_ROUTE, navOptions) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt new file mode 100644 index 000000000..df672bcf2 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt @@ -0,0 +1,73 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +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.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.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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.components.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar + +/** + * Displays the block auto-fill screen. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BlockAutoFillScreen( + onNavigateBack: () -> Unit, + viewModel: BlockAutoFillViewModel = hiltViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel = viewModel) { event -> + when (event) { + BlockAutoFillEvent.NavigateBack -> onNavigateBack.invoke() + } + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + BitwardenScaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + BitwardenTopAppBar( + title = stringResource(id = R.string.autofill), + scrollBehavior = scrollBehavior, + navigationIcon = painterResource(id = R.drawable.ic_back), + navigationIconContentDescription = stringResource(id = R.string.back), + onNavigationIconClick = remember(viewModel) { + { viewModel.trySendAction(BlockAutoFillAction.BackClick) } + }, + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = "Not yet implemented") + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillViewModel.kt new file mode 100644 index 000000000..4e81f5870 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillViewModel.kt @@ -0,0 +1,84 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill + +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.android.parcel.Parcelize +import javax.inject.Inject + +private const val KEY_STATE = "state" + +/** + * View model for the blocked autofill URIs screen. + */ +@HiltViewModel +@Suppress("TooManyFunctions", "LargeClass") +class BlockAutoFillViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY_STATE] + ?: BlockAutoFillState( + viewState = BlockAutoFillState.ViewState.Empty, + ), +) { + + override fun handleAction(action: BlockAutoFillAction) { + when (action) { + BlockAutoFillAction.BackClick -> handleCloseClick() + } + } + + private fun handleCloseClick() { + sendEvent( + event = BlockAutoFillEvent.NavigateBack, + ) + } +} + +/** + * Represents the state for block auto fill. + * + * @property viewState indicates what view state the screen is in. + */ +@Parcelize +data class BlockAutoFillState( + val viewState: ViewState, +) : Parcelable { + + /** + * Represents the specific view states for the [BlockAutoFillScreen]. + */ + sealed class ViewState : Parcelable { + + /** + * Represents an empty content state for the [BlockAutoFillScreen]. + */ + @Parcelize + data object Empty : ViewState() + } +} + +/** + * Represents a set of events that can be emitted for the block auto fill screen. + * Each subclass of this sealed class denotes a distinct event that can occur. + */ +sealed class BlockAutoFillEvent { + + /** + * Navigate back to previous screen. + */ + data object NavigateBack : BlockAutoFillEvent() +} + +/** + * Represents a set of actions related to the block auto fill screen. + * Each subclass of this sealed class denotes a distinct action that can be taken. + */ +sealed class BlockAutoFillAction { + + /** + * User clicked close. + */ + data object BackClick : BlockAutoFillAction() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt index c1ff24688..1f532b3c5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt @@ -29,6 +29,7 @@ class AutoFillScreenTest : BaseComposeTest() { private var isSystemSettingsRequestSuccess = false private var onNavigateBackCalled = false + private var onNavigateToBlockAutoFillScreenCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) @@ -45,6 +46,7 @@ class AutoFillScreenTest : BaseComposeTest() { composeTestRule.setContent { AutoFillScreen( onNavigateBack = { onNavigateBackCalled = true }, + onNavigateToBlockAutoFillScreen = { onNavigateToBlockAutoFillScreenCalled = true }, viewModel = viewModel, intentManager = intentManager, ) @@ -234,6 +236,21 @@ class AutoFillScreenTest : BaseComposeTest() { mutableEventFlow.tryEmit(AutoFillEvent.NavigateBack) assertTrue(onNavigateBackCalled) } + + @Test + fun `on block auto fill click should send BlockAutoFillClick`() { + composeTestRule + .onNodeWithText("Block auto-fill") + .performScrollTo() + .performClick() + verify { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) } + } + + @Test + fun `on NavigateToBlockAutoFill should call onNavigateToBlockAutoFillScreen`() { + mutableEventFlow.tryEmit(AutoFillEvent.NavigateToBlockAutoFill) + assertTrue(onNavigateToBlockAutoFillScreenCalled) + } } private val DEFAULT_STATE: AutoFillState = AutoFillState( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt index a5e9605ab..89bce9d29 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt @@ -153,6 +153,15 @@ class AutoFillViewModelTest : BaseViewModelTest() { ) } + @Test + fun `on BlockAutoFillClick should emit NavigateToBlockAutoFill`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) + assertEquals(AutoFillEvent.NavigateToBlockAutoFill, awaitItem()) + } + } + private fun createViewModel( state: AutoFillState? = DEFAULT_STATE, ): AutoFillViewModel = AutoFillViewModel( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillViewModelTest.kt new file mode 100644 index 000000000..4a3200f50 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillViewModelTest.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.autofill.blockautofill + +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.jupiter.api.Test + +class BlockAutoFillViewModelTest : BaseViewModelTest() { + + @Test + fun `on BackClick should emit NavigateBack`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(BlockAutoFillAction.BackClick) + assertEquals(BlockAutoFillEvent.NavigateBack, awaitItem()) + } + } + + private fun createViewModel( + state: BlockAutoFillState? = DEFAULT_STATE, + ): BlockAutoFillViewModel = BlockAutoFillViewModel( + savedStateHandle = SavedStateHandle().apply { set("state", state) }, + ) +} + +private val DEFAULT_STATE: BlockAutoFillState = BlockAutoFillState( + BlockAutoFillState.ViewState.Empty, +)