From 78461394f39616a42ad0f38f89c215ec1afe978b Mon Sep 17 00:00:00 2001 From: joshua-livefront <139182194+joshua-livefront@users.noreply.github.com> Date: Fri, 29 Dec 2023 14:16:46 -0500 Subject: [PATCH] BIT-711: Adding UI for AddyIo service (#456) --- .../feature/generator/GeneratorScreen.kt | 53 ++++++- .../feature/generator/GeneratorViewModel.kt | 139 ++++++++++++++++-- .../feature/generator/GeneratorScreenTest.kt | 90 +++++++++++- .../generator/GeneratorViewModelTest.kt | 104 ++++++++++++- 4 files changed, 367 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index 2b3236500..8d684c839 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -817,6 +817,7 @@ private fun UsernameOptionsItem( //region ForwardedEmailAliasType Composables +@Suppress("LongMethod") @Composable private fun ColumnScope.ForwardedEmailAliasTypeContent( usernameTypeState: GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias, @@ -833,8 +834,26 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( when (usernameTypeState.selectedServiceType) { - is ServiceType.AnonAddy -> { - // TODO: AnonAddy Service Implementation (BIT-711) + is ServiceType.AddyIo -> { + BitwardenPasswordField( + label = stringResource(id = R.string.api_access_token), + value = usernameTypeState.selectedServiceType.apiAccessToken, + onValueChange = forwardedEmailAliasHandlers.onAddyIoAccessTokenTextChange, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + BitwardenTextField( + label = stringResource(id = R.string.domain_name_required_parenthesis), + value = usernameTypeState.selectedServiceType.domainName, + onValueChange = forwardedEmailAliasHandlers.onAddyIoDomainNameTextChange, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) } is ServiceType.DuckDuckGo -> { @@ -1185,10 +1204,13 @@ private class PassphraseHandlers( */ private class ForwardedEmailAliasHandlers( val onServiceChange: (ServiceTypeOption) -> Unit, + val onAddyIoAccessTokenTextChange: (String) -> Unit, + val onAddyIoDomainNameTextChange: (String) -> Unit, val onDuckDuckGoApiKeyTextChange: (String) -> Unit, val onFirefoxRelayAccessTokenTextChange: (String) -> Unit, ) { companion object { + @Suppress("LongMethod") fun create(viewModel: GeneratorViewModel): ForwardedEmailAliasHandlers { return ForwardedEmailAliasHandlers( onServiceChange = { newServiceTypeOption -> @@ -1203,6 +1225,32 @@ private class ForwardedEmailAliasHandlers( ), ) }, + onAddyIoAccessTokenTextChange = { newAccessToken -> + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .AccessTokenTextChange( + accessToken = newAccessToken, + ), + ) + }, + onAddyIoDomainNameTextChange = { newDomainName -> + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .DomainTextChange( + domain = newDomainName, + ), + ) + }, onDuckDuckGoApiKeyTextChange = { newApiKey -> viewModel.trySendAction( GeneratorAction @@ -1227,7 +1275,6 @@ private class ForwardedEmailAliasHandlers( .AccessTokenTextChange( accessToken = newAccessToken, ), - ) }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt index ae63a3c4f..5f78fd407 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt @@ -22,8 +22,11 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Pa import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.CatchAllEmail import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AddyIo import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.DuckDuckGo +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.FastMail import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.FirefoxRelay +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.SimpleLogin import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.RandomWord @@ -115,6 +118,10 @@ class GeneratorViewModel @Inject constructor( handleServiceTypeOptionSelect(action) } + is GeneratorAction.MainType.Username.UsernameType.ForwardedEmailAlias.AddyIo -> { + handleAddyIoSpecificAction(action) + } + is GeneratorAction.MainType.Username.UsernameType.ForwardedEmailAlias.DuckDuckGo.ApiKeyTextChange -> { handleDuckDuckGoTextInputChange(action) } @@ -554,15 +561,15 @@ class GeneratorViewModel @Inject constructor( ) Username.UsernameTypeOption.CATCH_ALL_EMAIL -> loadUsernameOptions( - selectedType = Username(selectedType = Username.UsernameType.CatchAllEmail()), + selectedType = Username(selectedType = CatchAllEmail()), ) Username.UsernameTypeOption.FORWARDED_EMAIL_ALIAS -> loadUsernameOptions( - selectedType = Username(selectedType = Username.UsernameType.ForwardedEmailAlias()), + selectedType = Username(selectedType = ForwardedEmailAlias()), ) Username.UsernameTypeOption.RANDOM_WORD -> loadUsernameOptions( - selectedType = Username(selectedType = Username.UsernameType.RandomWord()), + selectedType = Username(selectedType = RandomWord()), ) } } @@ -580,30 +587,92 @@ class GeneratorViewModel @Inject constructor( .ServiceTypeOptionSelect, ) { when (action.serviceTypeOption) { - ForwardedEmailAlias.ServiceTypeOption.ANON_ADDY -> updateForwardedEmailAliasType { - ForwardedEmailAlias(selectedServiceType = ServiceType.AnonAddy()) + ForwardedEmailAlias.ServiceTypeOption.ADDY_IO -> updateForwardedEmailAliasType { + ForwardedEmailAlias(selectedServiceType = AddyIo()) } ForwardedEmailAlias.ServiceTypeOption.DUCK_DUCK_GO -> updateForwardedEmailAliasType { - ForwardedEmailAlias(selectedServiceType = ServiceType.DuckDuckGo()) + ForwardedEmailAlias(selectedServiceType = DuckDuckGo()) } ForwardedEmailAlias.ServiceTypeOption.FAST_MAIL -> updateForwardedEmailAliasType { - ForwardedEmailAlias(selectedServiceType = ServiceType.FastMail()) + ForwardedEmailAlias(selectedServiceType = FastMail()) } ForwardedEmailAlias.ServiceTypeOption.FIREFOX_RELAY -> updateForwardedEmailAliasType { - ForwardedEmailAlias(selectedServiceType = ServiceType.FirefoxRelay()) + ForwardedEmailAlias(selectedServiceType = FirefoxRelay()) } ForwardedEmailAlias.ServiceTypeOption.SIMPLE_LOGIN -> updateForwardedEmailAliasType { - ForwardedEmailAlias(selectedServiceType = ServiceType.SimpleLogin()) + ForwardedEmailAlias(selectedServiceType = SimpleLogin()) } } } //endregion Forwarded Email Alias Specific Handlers + //region Addy.Io Service Specific Handlers + + private fun handleAddyIoSpecificAction( + action: GeneratorAction.MainType.Username.UsernameType.ForwardedEmailAlias.AddyIo, + ) { + when (action) { + is GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .AccessTokenTextChange, + -> { + handleAddyIoAccessTokenTextChange(action) + } + + is GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .DomainTextChange, + -> { + handleAddyIoDomainNameTextChange(action) + } + } + } + + private fun handleAddyIoAccessTokenTextChange( + action: GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .AccessTokenTextChange, + ) { + updateAddyIoServiceType { addyIoServiceType -> + val newAccessToken = action.accessToken + addyIoServiceType.copy(apiAccessToken = newAccessToken) + } + } + + private fun handleAddyIoDomainNameTextChange( + action: GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .DomainTextChange, + ) { + updateAddyIoServiceType { addyIoServiceType -> + val newDomain = action.domain + addyIoServiceType.copy(domainName = newDomain) + } + } + + //endregion Addy.Io Service Specific Handlers + //region DuckDuckGo Service Specific Handlers private fun handleDuckDuckGoTextInputChange( @@ -800,6 +869,30 @@ class GeneratorViewModel @Inject constructor( } } + private inline fun updateAddyIoServiceType( + crossinline block: (AddyIo) -> AddyIo, + ) { + updateGeneratorMainTypeUsername { currentUsernameType -> + if (currentUsernameType.selectedType !is ForwardedEmailAlias) { + return@updateGeneratorMainTypeUsername currentUsernameType + } + + val currentServiceType = (currentUsernameType.selectedType).selectedServiceType + if (currentServiceType !is AddyIo) { + return@updateGeneratorMainTypeUsername currentUsernameType + } + + val updatedServiceType = block(currentServiceType) + + currentUsernameType.copy( + selectedType = ForwardedEmailAlias( + selectedServiceType = updatedServiceType, + obfuscatedText = currentUsernameType.selectedType.obfuscatedText, + ), + ) + } + } + private inline fun updateDuckDuckGoServiceType( crossinline block: (DuckDuckGo) -> DuckDuckGo, ) { @@ -1173,7 +1266,7 @@ data class GeneratorState( * the label for each type. */ enum class ServiceTypeOption(val labelRes: Int) { - ANON_ADDY(R.string.addy_io), + ADDY_IO(R.string.addy_io), DUCK_DUCK_GO(R.string.duck_duck_go), FAST_MAIL(R.string.fastmail), FIREFOX_RELAY(R.string.firefox_relay), @@ -1195,19 +1288,19 @@ data class GeneratorState( abstract val displayStringResId: Int? /** - * Represents the Anon Addy service type, with a configurable option for + * Represents the Addy Io service type, with a configurable option for * service and api key. * * @property apiAccessToken The token used for generation. * @property domainName The domain name used for generation. */ @Parcelize - data class AnonAddy( + data class AddyIo( val apiAccessToken: String = "", val domainName: String = "", ) : ServiceType(), Parcelable { override val displayStringResId: Int - get() = ServiceTypeOption.ANON_ADDY.labelRes + get() = ServiceTypeOption.ADDY_IO.labelRes } /** @@ -1498,6 +1591,26 @@ sealed class GeneratorAction { .ServiceTypeOption, ) : ForwardedEmailAlias() + /** + * Represents actions specifically related to the AddyIo service. + */ + sealed class AddyIo : ForwardedEmailAlias() { + + /** + * Fired when the access token input text is changed. + * + * @property accessToken The new access token text. + */ + data class AccessTokenTextChange(val accessToken: String) : AddyIo() + + /** + * Fired when the domain text input is changed. + * + * @property domain The new domain text. + */ + data class DomainTextChange(val domain: String) : AddyIo() + } + /** * Represents actions specifically related to the DuckDuckGo service. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index 4ba0d72e0..a5edecf9a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -896,7 +896,7 @@ class GeneratorScreenTest : BaseComposeTest() { .UsernameType .ForwardedEmailAlias .ServiceTypeOption - .ANON_ADDY + .ADDY_IO // Opens the menu composeTestRule @@ -927,6 +927,94 @@ class GeneratorScreenTest : BaseComposeTest() { //endregion Forwarded Email Alias Tests + //region Addy.Io Service Type Tests + + @Suppress("MaxLineLength") + @Test + fun `in Username_ForwardedEmailAlias_AddyIo state, updating access token text input should send AccessTokenTextChange action`() { + updateState( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .AddyIo(), + ), + ), + ), + ) + + val newAccessToken = "accessToken" + + composeTestRule + .onNodeWithText("API access token") + .performScrollTo() + .performTextInput(newAccessToken) + + verify { + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .AccessTokenTextChange( + accessToken = newAccessToken, + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in Username_ForwardedEmailAlias_AddyIo state, updating domain name text input should send DomainTextChange action`() { + updateState( + GeneratorState( + generatedText = "Placeholder", + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .AddyIo(), + ), + ), + ), + ) + + val newDomainName = "domainName" + + composeTestRule + .onNodeWithText("Domain name (required)") + .performScrollTo() + .performTextInput(newDomainName) + + verify { + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .DomainTextChange( + domain = newDomainName, + ), + ) + } + } + + //endregion Addy.Io Service Type Tests + //region DuckDuckGo Service Type Tests @Suppress("MaxLineLength") diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt index 4d93f7860..4f248b95d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt @@ -31,6 +31,9 @@ class GeneratorViewModelTest : BaseViewModelTest() { private val forwardedEmailAliasSavedStateHandle = createSavedStateHandleWithState(initialForwardedEmailAliasState) + private val initialAddyIoState = createAddyIoState() + private val addyIoSavedStateHandle = createSavedStateHandleWithState(initialAddyIoState) + private val initialDuckDuckGoState = createDuckDuckGoState() private val duckDuckGoSavedStateHandle = createSavedStateHandleWithState(initialDuckDuckGoState) @@ -930,7 +933,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { .UsernameType .ForwardedEmailAlias .ServiceTypeOption - .ANON_ADDY, + .ADDY_IO, ) viewModel.actionChannel.trySend(action) @@ -948,7 +951,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { .UsernameType .ForwardedEmailAlias .ServiceType - .AnonAddy(), + .AddyIo(), ), ), ) @@ -957,6 +960,85 @@ class GeneratorViewModelTest : BaseViewModelTest() { } } + @Nested + inner class AddyIoActions { + private val defaultAddyIoState = createAddyIoState() + private lateinit var viewModel: GeneratorViewModel + + @BeforeEach + fun setup() { + viewModel = GeneratorViewModel(addyIoSavedStateHandle, fakeGeneratorRepository) + } + + @Test + fun `AccessTokenTextChange should update access token text correctly`() = runTest { + val newAccessToken = "newAccessToken" + val action = GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .AccessTokenTextChange( + accessToken = newAccessToken, + ) + + viewModel.actionChannel.trySend(action) + + val expectedState = defaultAddyIoState.copy( + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .AddyIo( + apiAccessToken = newAccessToken, + ), + ), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + } + + @Test + fun `DomainTextChange should update the domain text correctly`() = runTest { + val newDomainName = "newDomain" + val action = GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .AddyIo + .DomainTextChange( + domain = newDomainName, + ) + + viewModel.actionChannel.trySend(action) + + val expectedState = defaultAddyIoState.copy( + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .AddyIo( + domainName = newDomainName, + ), + ), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + } + } + @Nested inner class DuckDuckGoActions { private val defaultDuckDuckGoState = createDuckDuckGoState() @@ -1253,6 +1335,24 @@ class GeneratorViewModelTest : BaseViewModelTest() { ), ) + private fun createAddyIoState( + generatedText: String = "defaultAddyIo", + ): GeneratorState = + GeneratorState( + generatedText = generatedText, + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .AddyIo(), + ), + ), + ) + private fun createDuckDuckGoState( generatedText: String = "defaultDuckDuckGo", ): GeneratorState =