BIT-711: Adding UI for AddyIo service (#456)

This commit is contained in:
joshua-livefront 2023-12-29 14:16:46 -05:00 committed by Álison Fernandes
parent 5ec810d4d6
commit 78461394f3
4 changed files with 367 additions and 19 deletions

View file

@ -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,
),
)
},
)

View file

@ -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.
*/

View file

@ -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")

View file

@ -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 =