BIT-1957: Update Email Alias generator UI to include 'ForwardEmail' (#1082)

This commit is contained in:
Patrick Honkonen 2024-03-04 14:32:05 -05:00 committed by Álison Fernandes
parent 54f5026047
commit 35e204d9c1
6 changed files with 442 additions and 0 deletions

View file

@ -1062,6 +1062,31 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent(
)
}
is ServiceType.ForwardEmail -> {
BitwardenPasswordField(
label = stringResource(id = R.string.api_key_required_parenthesis),
value = usernameTypeState.selectedServiceType.apiKey,
onValueChange = forwardedEmailAliasHandlers.onForwardEmailApiKeyTextChange,
showPasswordTestTag = "ShowForwardedEmailApiSecretButton",
modifier = Modifier
.padding(horizontal = 16.dp)
.semantics { testTag = "ForwardedEmailApiSecretEntry" }
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.domain_name_required_parenthesis),
value = usernameTypeState.selectedServiceType.domainName,
onValueChange = forwardedEmailAliasHandlers.onForwardEmailDomainNameTextChange,
modifier = Modifier
.padding(horizontal = 16.dp)
.semantics { testTag = "ForwardedEmailDomainNameEntry" }
.fillMaxWidth(),
)
}
is ServiceType.SimpleLogin -> {
BitwardenPasswordField(
label = stringResource(id = R.string.api_key_required_parenthesis),
@ -1429,6 +1454,8 @@ private data class ForwardedEmailAliasHandlers(
val onDuckDuckGoApiKeyTextChange: (String) -> Unit,
val onFastMailApiKeyTextChange: (String) -> Unit,
val onFirefoxRelayAccessTokenTextChange: (String) -> Unit,
val onForwardEmailApiKeyTextChange: (String) -> Unit,
val onForwardEmailDomainNameTextChange: (String) -> Unit,
val onSimpleLoginApiKeyTextChange: (String) -> Unit,
) {
companion object {
@ -1512,6 +1539,32 @@ private data class ForwardedEmailAliasHandlers(
),
)
},
onForwardEmailApiKeyTextChange = { newApiKey ->
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.ApiKeyTextChange(
apiKey = newApiKey,
),
)
},
onForwardEmailDomainNameTextChange = { newDomainName ->
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.DomainNameTextChange(
domainName = newDomainName,
),
)
},
onSimpleLoginApiKeyTextChange = { newApiKey ->
viewModel.trySendAction(
GeneratorAction

View file

@ -41,6 +41,7 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Us
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.ForwardEmail
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.PlusAddressedEmail
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.RandomWord
@ -169,6 +170,10 @@ class GeneratorViewModel @Inject constructor(
handleSimpleLoginTextInputChange(action)
}
is GeneratorAction.MainType.Username.UsernameType.ForwardedEmailAlias.ForwardEmail -> {
handleForwardEmailSpecificAction(action)
}
is GeneratorAction.MainType.Username.UsernameType.PlusAddressedEmail.EmailTextChange -> {
handlePlusAddressedEmailTextInputChange(action)
}
@ -971,6 +976,15 @@ class GeneratorViewModel @Inject constructor(
)
}
ForwardedEmailAlias.ServiceTypeOption.FORWARD_EMAIL -> updateForwardedEmailAliasType {
ForwardedEmailAlias(
selectedServiceType = ForwardEmail(
apiKey = options.forwardEmailApiAccessToken.orEmpty(),
domainName = options.forwardEmailDomainName.orEmpty(),
),
)
}
ForwardedEmailAlias.ServiceTypeOption.SIMPLE_LOGIN -> updateForwardedEmailAliasType {
ForwardedEmailAlias(
selectedServiceType = SimpleLogin(
@ -1102,6 +1116,73 @@ class GeneratorViewModel @Inject constructor(
//endregion FirefoxRelay Service Specific Handlers
//region ForwardEmail Email Specific Handlers
private fun handleForwardEmailSpecificAction(
action: GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail,
) {
when (action) {
is GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.ApiKeyTextChange,
-> {
handleForwardEmailApiKeyTextChange(action)
}
is GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.DomainNameTextChange,
-> {
handleForwardEmailDomainNameTextChange(action)
}
}
}
private fun handleForwardEmailApiKeyTextChange(
action: GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.ApiKeyTextChange,
) {
updateForwardEmailServiceType { forwardEmailServiceType ->
val newApiKey = action.apiKey
forwardEmailServiceType.copy(apiKey = newApiKey)
}
}
private fun handleForwardEmailDomainNameTextChange(
action: GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.DomainNameTextChange,
) {
updateForwardEmailServiceType { forwardEmailServiceType ->
val newDomainName = action.domainName
forwardEmailServiceType.copy(domainName = newDomainName)
}
}
//endregion ForwardEmail Email Specific Handlers
//region SimpleLogin Service Specific Handlers
private fun handleSimpleLoginTextInputChange(
@ -1485,6 +1566,28 @@ class GeneratorViewModel @Inject constructor(
}
}
private inline fun updateForwardEmailServiceType(
crossinline block: (ForwardEmail) -> ForwardEmail,
) {
updateGeneratorMainTypeUsername { currentUsernameType ->
if (currentUsernameType.selectedType !is ForwardedEmailAlias) {
return@updateGeneratorMainTypeUsername currentUsernameType
}
val currentServiceType = currentUsernameType.selectedType.selectedServiceType
if (currentServiceType !is ForwardEmail) {
return@updateGeneratorMainTypeUsername currentUsernameType
}
currentUsernameType.copy(
selectedType = ForwardedEmailAlias(
selectedServiceType = block(currentServiceType),
obfuscatedText = currentUsernameType.selectedType.obfuscatedText,
),
)
}
}
private inline fun updateCatchAllEmailType(
crossinline block: (CatchAllEmail) -> CatchAllEmail,
) {
@ -1810,6 +1913,7 @@ data class GeneratorState(
DUCK_DUCK_GO(R.string.duck_duck_go),
FAST_MAIL(R.string.fastmail),
FIREFOX_RELAY(R.string.firefox_relay),
FORWARD_EMAIL(R.string.forward_email),
SIMPLE_LOGIN(R.string.simple_login),
}
@ -1886,6 +1990,22 @@ data class GeneratorState(
get() = ServiceTypeOption.FIREFOX_RELAY.labelRes
}
/**
* Represents the ForwardEmail service type, with configurable options for
* api key and domain name.
*
* @property apiKey The api key used for generation.
* @property domainName The domain name used for generation.
*/
@Parcelize
data class ForwardEmail(
val apiKey: String = "",
val domainName: String = "",
) : ServiceType(), Parcelable {
override val displayStringResId: Int
get() = ServiceTypeOption.FORWARD_EMAIL.labelRes
}
/**
* Represents the SimpleLogin service type, with a configurable option for
* api key.
@ -2206,6 +2326,26 @@ sealed class GeneratorAction {
data class AccessTokenTextChange(val accessToken: String) : FirefoxRelay()
}
/**
* Represents actions specifically related to the ForwardEmail service.
*/
sealed class ForwardEmail : ForwardedEmailAlias() {
/**
* Fired when the api key input text is changed.
*
* @property apiKey The new api key text.
*/
data class ApiKeyTextChange(val apiKey: String) : ForwardEmail()
/**
* Fires when the domain name input text is changed.
*
* @property domainName The new domain name text.
*/
data class DomainNameTextChange(val domainName: String) : ForwardEmail()
}
/**
* Represents actions specifically related to the SimpleLogin service.
*/

View file

@ -60,6 +60,15 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded
}
}
is ServiceType.ForwardEmail -> {
val apiKey = this.apiKey.orNullIfBlank() ?: return null
val domainName = this.domainName.orNullIfBlank() ?: return null
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.ForwardEmail(apiKey, domainName),
website = null,
)
}
is ServiceType.SimpleLogin -> {
this
.apiKey

View file

@ -1429,6 +1429,92 @@ class GeneratorScreenTest : BaseComposeTest() {
//endregion SimpleLogin Service Type Tests
//region ForwardEmail Service Type Tests
@Suppress("MaxLineLength")
@Test
fun `in Username_ForwardedEmailAlias_ForwardEmail state, updating api key text input should send ApiKeyTextChange action`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ServiceType
.ForwardEmail(),
),
),
),
)
val newApiKey = "apiKey"
composeTestRule
.onNodeWithText("API key (required)")
.performScrollTo()
.performTextInput(newApiKey)
verify {
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.ApiKeyTextChange(
apiKey = newApiKey,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in Username_ForwardedEmailAlias_ForwardEmail state, updating domain name text input should send DomainNameChange action`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ServiceType
.ForwardEmail(),
),
),
),
)
val newDomainName = "domainName"
composeTestRule
.onNodeWithText("Domain name (required)")
.performScrollTo()
.performTextInput(newDomainName)
verify {
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.DomainNameTextChange(
domainName = newDomainName,
),
)
}
}
//endregion ForwardEmail Service Type Tests
//region Username Type Tests
@Test

View file

@ -1588,6 +1588,99 @@ class GeneratorViewModelTest : BaseViewModelTest() {
}
}
@Nested
inner class ForwardEmailActions {
private val defaultForwardEmailState = createForwardEmailState()
private lateinit var viewModel: GeneratorViewModel
@BeforeEach
fun setUp() {
viewModel = createViewModel(defaultForwardEmailState)
}
@Test
fun `ApiKeyTextChange should update api key text correctly`() {
val newApiKey = "newApiKey"
val action = GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.ApiKeyTextChange(
apiKey = newApiKey,
)
fakeGeneratorRepository.setMockGenerateForwardedServiceResult(
GeneratedForwardedServiceUsernameResult.Success(
generatedEmailAddress = "defaultForwardEmail",
),
)
viewModel.actionChannel.trySend(action)
val expectedState = defaultForwardEmailState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ServiceType
.ForwardEmail(
apiKey = newApiKey,
),
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `DomainNameTextChange should update domain name text correctly`() {
val newDomainName = "newDomainName"
val action = GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ForwardEmail
.DomainNameTextChange(
domainName = newDomainName,
)
fakeGeneratorRepository.setMockGenerateForwardedServiceResult(
GeneratedForwardedServiceUsernameResult.Success(
generatedEmailAddress = "defaultForwardEmail",
),
)
viewModel.actionChannel.trySend(action)
val expectedState = defaultForwardEmailState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ServiceType
.ForwardEmail(
domainName = newDomainName,
),
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
}
@Nested
inner class SimpleLoginActions {
private val defaultSimpleLoginState = createSimpleLoginState()
@ -1943,6 +2036,25 @@ class GeneratorViewModelTest : BaseViewModelTest() {
currentEmailAddress = "currentEmail",
)
private fun createForwardEmailState(
generatedText: String = "defaultForwardEmail",
): GeneratorState =
GeneratorState(
generatedText = generatedText,
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ServiceType
.ForwardEmail(),
),
),
currentEmailAddress = "currentEmail",
)
private fun createSimpleLoginState(
generatedText: String = "defaultSimpleLogin",
): GeneratorState =

View file

@ -121,6 +121,48 @@ internal class ServiceTypeExtensionsTest {
)
}
@Test
fun `toUsernameGeneratorRequest for ForwardEmail returns null when apiKey is blank`() {
val forwardMailServiceType = ServiceType.ForwardEmail(
apiKey = "",
domainName = "domainName",
)
val request = forwardMailServiceType.toUsernameGeneratorRequest()
assertNull(request)
}
@Test
fun `toUsernameGeneratorRequest for ForwardEmail returns null when domainName is blank`() {
val forwardMailServiceType = ServiceType.ForwardEmail(
apiKey = "apiKey",
domainName = "",
)
val request = forwardMailServiceType.toUsernameGeneratorRequest()
assertNull(request)
}
@Test
fun `toUsernameGeneratorRequest for ForwardEmail returns correct request`() {
val forwardEmailServiceType = ServiceType.ForwardEmail(
apiKey = "apiKey",
domainName = "domainName",
)
val request = forwardEmailServiceType.toUsernameGeneratorRequest()
assertEquals(
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.ForwardEmail(
apiToken = "apiKey",
domain = "domainName",
),
website = null,
),
request,
)
}
@Test
fun `toUsernameGeneratorRequest for SimpleLogin returns null when apiKey is blank`() {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "")