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 -> { is ServiceType.SimpleLogin -> {
BitwardenPasswordField( BitwardenPasswordField(
label = stringResource(id = R.string.api_key_required_parenthesis), label = stringResource(id = R.string.api_key_required_parenthesis),
@ -1429,6 +1454,8 @@ private data class ForwardedEmailAliasHandlers(
val onDuckDuckGoApiKeyTextChange: (String) -> Unit, val onDuckDuckGoApiKeyTextChange: (String) -> Unit,
val onFastMailApiKeyTextChange: (String) -> Unit, val onFastMailApiKeyTextChange: (String) -> Unit,
val onFirefoxRelayAccessTokenTextChange: (String) -> Unit, val onFirefoxRelayAccessTokenTextChange: (String) -> Unit,
val onForwardEmailApiKeyTextChange: (String) -> Unit,
val onForwardEmailDomainNameTextChange: (String) -> Unit,
val onSimpleLoginApiKeyTextChange: (String) -> Unit, val onSimpleLoginApiKeyTextChange: (String) -> Unit,
) { ) {
companion object { 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 -> onSimpleLoginApiKeyTextChange = { newApiKey ->
viewModel.trySendAction( viewModel.trySendAction(
GeneratorAction 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.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.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.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.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.PlusAddressedEmail
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.RandomWord import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.RandomWord
@ -169,6 +170,10 @@ class GeneratorViewModel @Inject constructor(
handleSimpleLoginTextInputChange(action) handleSimpleLoginTextInputChange(action)
} }
is GeneratorAction.MainType.Username.UsernameType.ForwardedEmailAlias.ForwardEmail -> {
handleForwardEmailSpecificAction(action)
}
is GeneratorAction.MainType.Username.UsernameType.PlusAddressedEmail.EmailTextChange -> { is GeneratorAction.MainType.Username.UsernameType.PlusAddressedEmail.EmailTextChange -> {
handlePlusAddressedEmailTextInputChange(action) 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.ServiceTypeOption.SIMPLE_LOGIN -> updateForwardedEmailAliasType {
ForwardedEmailAlias( ForwardedEmailAlias(
selectedServiceType = SimpleLogin( selectedServiceType = SimpleLogin(
@ -1102,6 +1116,73 @@ class GeneratorViewModel @Inject constructor(
//endregion FirefoxRelay Service Specific Handlers //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 //region SimpleLogin Service Specific Handlers
private fun handleSimpleLoginTextInputChange( 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( private inline fun updateCatchAllEmailType(
crossinline block: (CatchAllEmail) -> CatchAllEmail, crossinline block: (CatchAllEmail) -> CatchAllEmail,
) { ) {
@ -1810,6 +1913,7 @@ data class GeneratorState(
DUCK_DUCK_GO(R.string.duck_duck_go), DUCK_DUCK_GO(R.string.duck_duck_go),
FAST_MAIL(R.string.fastmail), FAST_MAIL(R.string.fastmail),
FIREFOX_RELAY(R.string.firefox_relay), FIREFOX_RELAY(R.string.firefox_relay),
FORWARD_EMAIL(R.string.forward_email),
SIMPLE_LOGIN(R.string.simple_login), SIMPLE_LOGIN(R.string.simple_login),
} }
@ -1886,6 +1990,22 @@ data class GeneratorState(
get() = ServiceTypeOption.FIREFOX_RELAY.labelRes 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 * Represents the SimpleLogin service type, with a configurable option for
* api key. * api key.
@ -2206,6 +2326,26 @@ sealed class GeneratorAction {
data class AccessTokenTextChange(val accessToken: String) : FirefoxRelay() 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. * 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 -> { is ServiceType.SimpleLogin -> {
this this
.apiKey .apiKey

View file

@ -1429,6 +1429,92 @@ class GeneratorScreenTest : BaseComposeTest() {
//endregion SimpleLogin Service Type Tests //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 //region Username Type Tests
@Test @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 @Nested
inner class SimpleLoginActions { inner class SimpleLoginActions {
private val defaultSimpleLoginState = createSimpleLoginState() private val defaultSimpleLoginState = createSimpleLoginState()
@ -1943,6 +2036,25 @@ class GeneratorViewModelTest : BaseViewModelTest() {
currentEmailAddress = "currentEmail", 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( private fun createSimpleLoginState(
generatedText: String = "defaultSimpleLogin", generatedText: String = "defaultSimpleLogin",
): GeneratorState = ): 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 @Test
fun `toUsernameGeneratorRequest for SimpleLogin returns null when apiKey is blank`() { fun `toUsernameGeneratorRequest for SimpleLogin returns null when apiKey is blank`() {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "") val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "")