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 =