[fenix] For https://github.com/mozilla-mobile/fenix/issues/24850 - Display an address list for managing existing addresses

pull/600/head
Gabriel Luong 2 years ago committed by mergify[bot]
parent 3bc3ca9610
commit 0782aba4b0

@ -39,6 +39,7 @@ private val ICON_SIZE = 24.dp
* an optional, interactable icon at the end. * an optional, interactable icon at the end.
* *
* @param label The label in the list item. * @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout.
* @param description An optional description text below the label. * @param description An optional description text below the label.
* @param onClick Called when the user clicks on the item. * @param onClick Called when the user clicks on the item.
* @param iconPainter [Painter] used to display a [ListItemIcon] after the list item. * @param iconPainter [Painter] used to display a [ListItemIcon] after the list item.
@ -48,6 +49,7 @@ private val ICON_SIZE = 24.dp
@Composable @Composable
fun TextListItem( fun TextListItem(
label: String, label: String,
modifier: Modifier = Modifier,
description: String? = null, description: String? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
iconPainter: Painter? = null, iconPainter: Painter? = null,
@ -56,6 +58,7 @@ fun TextListItem(
) { ) {
ListItem( ListItem(
label = label, label = label,
modifier = modifier,
description = description, description = description,
onClick = onClick, onClick = onClick,
afterListAction = { afterListAction = {
@ -167,6 +170,7 @@ fun IconListItem(
* the flexibility to add custom UI to either end of the item. * the flexibility to add custom UI to either end of the item.
* *
* @param label The label in the list item. * @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout.
* @param description An optional description text below the label. * @param description An optional description text below the label.
* @param onClick Called when the user clicks on the item. * @param onClick Called when the user clicks on the item.
* @param beforeListAction Optional Composable for adding UI before the list item. * @param beforeListAction Optional Composable for adding UI before the list item.
@ -175,6 +179,7 @@ fun IconListItem(
@Composable @Composable
private fun ListItem( private fun ListItem(
label: String, label: String,
modifier: Modifier = Modifier,
description: String? = null, description: String? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
beforeListAction: @Composable RowScope.() -> Unit = {}, beforeListAction: @Composable RowScope.() -> Unit = {},
@ -192,7 +197,7 @@ private fun ListItem(
beforeListAction() beforeListAction()
Column( Column(
modifier = Modifier modifier = modifier
.padding(horizontal = 16.dp, vertical = 6.dp) .padding(horizontal = 16.dp, vertical = 6.dp)
.weight(1f), .weight(1f),
) { ) {

@ -0,0 +1,82 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.address
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.address.controller.DefaultAddressManagementController
import org.mozilla.fenix.settings.address.interactor.AddressManagementInteractor
import org.mozilla.fenix.settings.address.interactor.DefaultAddressManagementInteractor
import org.mozilla.fenix.settings.address.view.AddressList
import org.mozilla.fenix.settings.autofill.AutofillAction
import org.mozilla.fenix.settings.autofill.AutofillFragmentState
import org.mozilla.fenix.settings.autofill.AutofillFragmentStore
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Displays a list of saved addresses.
*/
class AddressManagementFragment : Fragment() {
private lateinit var store: AutofillFragmentStore
private lateinit var interactor: AddressManagementInteractor
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
store = StoreProvider.get(this) {
AutofillFragmentStore(AutofillFragmentState())
}
interactor = DefaultAddressManagementInteractor(
controller = DefaultAddressManagementController(
navController = findNavController()
)
)
loadAddresses()
return ComposeView(requireContext()).apply {
setContent {
FirefoxTheme {
val addresses = store.observeAsComposableState { state -> state.addresses }
AddressList(
addresses = addresses.value ?: emptyList(),
onAddressClick = interactor::onSelectAddress,
onAddAddressButtonClick = interactor::onAddAddressButtonClick
)
}
}
}
}
/**
* Fetches all the addresses from the autofill storage and updates the
* [AutofillFragmentStore] with the list of addresses.
*/
private fun loadAddresses() {
lifecycleScope.launch {
val addresses = requireContext().components.core.autofillStorage.getAllAddresses()
lifecycleScope.launch(Dispatchers.Main) {
store.dispatch(AutofillAction.UpdateAddresses(addresses))
}
}
}
}

@ -0,0 +1,53 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.address.controller
import androidx.navigation.NavController
import mozilla.components.concept.storage.Address
import org.mozilla.fenix.settings.address.AddressManagementFragment
import org.mozilla.fenix.settings.address.AddressManagementFragmentDirections
import org.mozilla.fenix.settings.address.interactor.AddressManagementInteractor
/**
* [AddressManagementFragment] controller. An interface that handles the view manipulation of
* the address manager triggered by the interactor.
*/
interface AddressManagementController {
/**
* @see [AddressManagementInteractor.onSelectAddress]
*/
fun handleAddressClicked(address: Address)
/**
* @see [AddressManagementInteractor.onAddAddressClick]
*/
fun handleAddAddressButtonClicked()
}
/**
* The default implementation of [AddressManagementController].
*
* @param navController [NavController] used for navigation.
*/
class DefaultAddressManagementController(
private val navController: NavController
) : AddressManagementController {
override fun handleAddressClicked(address: Address) {
navigateToAddressEditor()
}
override fun handleAddAddressButtonClicked() {
navigateToAddressEditor()
}
private fun navigateToAddressEditor() {
navController.navigate(
AddressManagementFragmentDirections
.actionAddressManagementFragmentToAddressEditorFragment()
)
}
}

@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.address.interactor
import mozilla.components.concept.storage.Address
import org.mozilla.fenix.settings.address.controller.AddressManagementController
/**
* Interface for the address management interactor.
*/
interface AddressManagementInteractor {
/**
* Navigates to the address editor to edit the selected address. Called when a user
* taps on an address item.
*
* @param address The selected [Address] to edit.
*/
fun onSelectAddress(address: Address)
/**
* Navigates to the address editor to add a new address. Called when a user
* taps on 'Add address' button.
*/
fun onAddAddressButtonClick()
}
/**
* The default implementation of [AddressManagementInteractor].
*
* @param controller An instance of [AddressManagementController] which will be delegated for
* all user interactions.
*/
class DefaultAddressManagementInteractor(
private val controller: AddressManagementController
) : AddressManagementInteractor {
override fun onSelectAddress(address: Address) {
controller.handleAddressClicked(address)
}
override fun onAddAddressButtonClick() {
controller.handleAddAddressButtonClicked()
}
}

@ -0,0 +1,90 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.address.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.components.concept.storage.Address
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.list.IconListItem
import org.mozilla.fenix.compose.list.TextListItem
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* A list of addresses.
*
* @param addresses A list of [Address] to display.
* @param onAddressClick Invoked when the user clicks on an address.
* @param onAddAddressButtonClick Invoked when the user clicks on the "Add address" button.
*/
@Composable
fun AddressList(
addresses: List<Address>,
onAddressClick: (Address) -> Unit,
onAddAddressButtonClick: () -> Unit,
) {
LazyColumn {
items(addresses) { address ->
TextListItem(
label = address.givenName + " " + address.familyName,
modifier = Modifier.padding(start = 56.dp),
description = address.streetAddress,
onClick = { onAddressClick(address) },
)
}
item {
IconListItem(
label = stringResource(R.string.preferences_addresses_add_address),
beforeIconPainter = painterResource(R.drawable.ic_new),
onClick = onAddAddressButtonClick,
)
}
}
}
@Preview
@Composable
private fun AddressListPreview() {
FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) {
Box(Modifier.background(FirefoxTheme.colors.layer2)) {
AddressList(
addresses = listOf(
Address(
guid = "1",
givenName = "Banana",
additionalName = "",
familyName = "Apple",
organization = "Mozilla",
streetAddress = "123 Sesame Street",
addressLevel3 = "",
addressLevel2 = "",
addressLevel1 = "",
postalCode = "90210",
country = "US",
tel = "+1 519 555-5555",
email = "foo@bar.com",
timeCreated = 0L,
timeLastUsed = 0L,
timeLastModified = 0L,
timesUsed = 0L
)
),
onAddressClick = {},
onAddAddressButtonClick = {},
)
}
}
}

@ -179,9 +179,15 @@ class AutofillSettingFragment : BiometricPromptPreferenceFragment() {
manageAddressesPreference.setOnPreferenceClickListener { manageAddressesPreference.setOnPreferenceClickListener {
navController.navigate( navController.navigate(
AutofillSettingFragmentDirections if (hasAddresses) {
.actionAutofillSettingFragmentToAddressEditorFragment() AutofillSettingFragmentDirections
.actionAutofillSettingFragmentToAddressManagementFragment()
} else {
AutofillSettingFragmentDirections
.actionAutofillSettingFragmentToAddressEditorFragment()
}
) )
super.onPreferenceTreeClick(it) super.onPreferenceTreeClick(it)
} }
} }

@ -1213,6 +1213,13 @@
app:exitAnim="@anim/slide_out_left" app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left" app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" /> app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_autofillSettingFragment_to_addressManagementFragment"
app:destination="@id/addressManagementFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/creditCardEditorFragment" android:id="@+id/creditCardEditorFragment"
@ -1240,5 +1247,17 @@
android:id="@+id/addressEditorFragment" android:id="@+id/addressEditorFragment"
android:name="org.mozilla.fenix.settings.address.AddressEditorFragment" android:name="org.mozilla.fenix.settings.address.AddressEditorFragment"
android:label="@string/addresses_add_address" /> android:label="@string/addresses_add_address" />
<fragment
android:id="@+id/addressManagementFragment"
android:name="org.mozilla.fenix.settings.address.AddressManagementFragment"
android:label="@string/addresses_manage_addresses">
<action
android:id="@+id/action_addressManagementFragment_to_addressEditorFragment"
app:destination="@id/addressEditorFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
</navigation> </navigation>
</navigation> </navigation>

@ -1553,6 +1553,8 @@
<string name="credit_cards_biometric_prompt_unlock_message">Unlock to use stored credit card information</string> <string name="credit_cards_biometric_prompt_unlock_message">Unlock to use stored credit card information</string>
<!-- Title of the "Add address" screen --> <!-- Title of the "Add address" screen -->
<string name="addresses_add_address">Add address</string> <string name="addresses_add_address">Add address</string>
<!-- Title of the "Manage addresses" screen -->
<string name="addresses_manage_addresses">Manage addresses</string>
<!-- The header for the full name of an address --> <!-- The header for the full name of an address -->
<string name="addresses_full_name">Full Name</string> <string name="addresses_full_name">Full Name</string>
<!-- The header for the street address of an address --> <!-- The header for the street address of an address -->

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.address.controller
import androidx.navigation.NavController
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.concept.storage.Address
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.settings.address.AddressManagementFragmentDirections
class DefaultAddressManagementControllerTest {
private val navController: NavController = mockk(relaxed = true)
private lateinit var controller: AddressManagementController
@Before
fun setup() {
controller = spyk(
DefaultAddressManagementController(
navController = navController
)
)
}
@Test
fun `WHEN an address is selected THEN navigate to the address editor`() {
val address: Address = mockk(relaxed = true)
controller.handleAddressClicked(address)
verify {
navController.navigate(
AddressManagementFragmentDirections
.actionAddressManagementFragmentToAddressEditorFragment()
)
}
}
@Test
fun `WHEN the add address button is clicked THEN navigate to the address editor`() {
controller.handleAddAddressButtonClicked()
verify {
navController.navigate(
AddressManagementFragmentDirections
.actionAddressManagementFragmentToAddressEditorFragment()
)
}
}
}

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.address.interactor
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.concept.storage.Address
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.settings.address.controller.AddressManagementController
class DefaultAddressManagementInteractorTest {
private val controller: AddressManagementController = mockk(relaxed = true)
private lateinit var interactor: AddressManagementInteractor
@Before
fun setup() {
interactor = DefaultAddressManagementInteractor(controller)
}
@Test
fun `WHEN an address is selected THEN forward to controller handler`() {
val address: Address = mockk(relaxed = true)
interactor.onSelectAddress(address)
verify { controller.handleAddressClicked(address) }
}
@Test
fun `WHEN add address button is clicked THEN forward to controller handler`() {
interactor.onAddAddressButtonClick()
verify { controller.handleAddAddressButtonClicked() }
}
}

@ -125,7 +125,7 @@ class AutofillSettingFragmentTest {
verify { verify {
navController.navigate( navController.navigate(
AutofillSettingFragmentDirections AutofillSettingFragmentDirections
.actionAutofillSettingFragmentToAddressEditorFragment() .actionAutofillSettingFragmentToAddressManagementFragment()
) )
} }
} }

Loading…
Cancel
Save