Fixes #24917: add country dropdown to address editor

pull/543/head
MatthewTighe 2 years ago committed by mergify[bot]
parent 9a965e420d
commit 676d7090f0

@ -17,6 +17,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.SecureFragment
import org.mozilla.fenix.databinding.FragmentAddressEditorBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.address.controller.DefaultAddressEditorController
import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor
@ -57,7 +58,8 @@ class AddressEditorFragment : SecureFragment(R.layout.fragment_address_editor) {
val binding = FragmentAddressEditorBinding.bind(view)
setHasOptionsMenu(true)
addressEditorView = AddressEditorView(binding, interactor, args.address)
val searchRegion = requireComponents.core.store.state.search.region
addressEditorView = AddressEditorView(binding, interactor, searchRegion, args.address)
addressEditorView.bind()
}

@ -9,25 +9,62 @@ import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import mozilla.components.concept.storage.Address
import android.widget.ArrayAdapter
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.search.RegionState
import mozilla.components.concept.storage.UpdatableAddressFields
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddressEditorBinding
import org.mozilla.fenix.ext.placeCursorAtEnd
import org.mozilla.fenix.settings.address.AddressEditorFragment
import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor
internal const val DEFAULT_COUNTRY = "US"
/**
* Shows an address editor for adding or updating an address.
*/
class AddressEditorView(
private val binding: FragmentAddressEditorBinding,
private val interactor: AddressEditorInteractor,
private val region: RegionState? = RegionState.Default,
private val address: Address? = null
) {
/**
* Binds the view.
* Value type representing properties determined by the country used in an [Address].
* This data is meant to mirror the data currently represented on desktop here:
* https://searchfox.org/mozilla-central/source/toolkit/components/formautofill/addressmetadata/addressReferences.js
*
* This can be expanded to included things like a list of applicable states/provinces per country
* or the names that should be used for each form field.
*
* Note: Most properties here need to be kept in sync with the data in the above desktop
* address reference file in order to prevent duplications when sync is enabled. There are
* ongoing conversations about how best to share that data cross-platform, if at all.
* Some more detail: https://bugzilla.mozilla.org/show_bug.cgi?id=1769809
*
* Exceptions: [displayName] is a local property and stop-gap to a more robust solution.
*
* @property key The country code used to lookup the address data. Should match desktop entries.
* @property displayName The name to display when selected.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal data class Country(
val key: String,
val displayName: String,
)
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val countries = mapOf(
"CA" to Country("CA", "Canada"),
"US" to Country("US", "United States"),
)
/**
* Binds the view in the [AddressEditorFragment], using the current [Address] if available.
*/
fun bind() {
binding.firstNameInput.apply {
@ -64,6 +101,8 @@ class AddressEditorView(
}
}
}
bindCountryDropdown()
}
internal fun saveAddress() {
@ -79,7 +118,7 @@ class AddressEditorView(
addressLevel2 = "",
addressLevel1 = "",
postalCode = binding.zipInput.text.toString(),
country = "",
country = binding.countryDropDown.selectedItem.toString().toCountryCode(),
tel = binding.phoneInput.text.toString(),
email = binding.emailInput.text.toString()
)
@ -103,4 +142,27 @@ class AddressEditorView(
create()
}.show()
}
private fun bindCountryDropdown() {
val adapter = ArrayAdapter<String>(
binding.root.context,
android.R.layout.simple_spinner_dropdown_item,
)
val selectedCountryKey = (address?.country ?: region?.home).takeIf {
it in countries.keys
} ?: DEFAULT_COUNTRY
var selectedPosition = -1
countries.values.forEachIndexed { index, country ->
if (country.key == selectedCountryKey) selectedPosition = index
adapter.add(country.displayName)
}
binding.countryDropDown.adapter = adapter
binding.countryDropDown.setSelection(selectedPosition)
}
private fun String.toCountryCode() = countries.values.find {
it.displayName == this
}?.key ?: DEFAULT_COUNTRY
}

@ -324,6 +324,41 @@
</com.google.android.material.textfield.TextInputLayout>
<!-- Country -->
<LinearLayout
android:id="@+id/country_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingStart="3dp"
android:paddingEnd="0dp"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/zip_layout"
app:layout_constraintBottom_toTopOf="@id/phone_title">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/textPrimary"
android:textSize="12sp"
android:labelFor="@id/country_drop_down"
app:fontFamily="@font/metropolis_semibold"
android:text="@string/addresses_country" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/country_drop_down"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/textPrimary" />
</LinearLayout>
<!-- Phone -->
<TextView
android:id="@+id/phone_title"
@ -340,7 +375,7 @@
android:labelFor="@id/phone_input"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/state_layout" />
app:layout_constraintTop_toBottomOf="@+id/country_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/phone_layout"

@ -1259,11 +1259,11 @@
android:id="@+id/addressEditorFragment"
android:name="org.mozilla.fenix.settings.address.AddressEditorFragment"
android:label="@string/addresses_add_address">
<argument
android:name="address"
android:defaultValue="@null"
app:argType="mozilla.components.concept.storage.Address"
app:nullable="true" />
<argument
android:name="address"
android:defaultValue="@null"
app:argType="mozilla.components.concept.storage.Address"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/addressManagementFragment"

@ -1566,6 +1566,8 @@
<string name="addresses_state">State</string>
<!-- The header for the zip code of an address -->
<string name="addresses_zip">Zip</string>
<!-- The header for the country or region of an address -->
<string name="addresses_country">Country or region</string>
<!-- The header for the phone number of an address -->
<string name="addresses_phone">Phone</string>
<!-- The header for the email of an address -->

@ -11,6 +11,7 @@ import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.search.RegionState
import mozilla.components.concept.storage.Address
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
@ -22,6 +23,7 @@ import org.mozilla.fenix.databinding.FragmentAddressEditorBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor
import org.mozilla.fenix.settings.address.view.AddressEditorView
import org.mozilla.fenix.settings.address.view.DEFAULT_COUNTRY
@RunWith(FenixRobolectricTestRunner::class)
class AddressEditorViewTest {
@ -54,27 +56,15 @@ class AddressEditorViewTest {
@Test
fun `GIVEN an existing address WHEN editor is opened THEN the form fields are correctly mapped to the address fields`() {
val address = Address(
guid = "123",
givenName = "Given",
additionalName = "Additional",
familyName = "Family",
organization = "Organization",
streetAddress = "Street",
addressLevel3 = "Suburb",
addressLevel2 = "City",
addressLevel1 = "State",
postalCode = "PostalCode",
country = "Country",
tel = "Telephone",
email = "email@mozilla.com",
timeCreated = 0L,
timeLastUsed = 1L,
timeLastModified = 1L,
timesUsed = 2L
val address = generateAddress()
val addressEditorView = spyk(
AddressEditorView(
binding = binding,
interactor = interactor,
address = address,
)
)
val addressEditorView = spyk(AddressEditorView(binding, interactor, address))
addressEditorView.bind()
assertEquals("PostalCode", binding.zipInput.text.toString())
@ -90,7 +80,13 @@ class AddressEditorViewTest {
@Test
fun `GIVEN an existing address WHEN editor is opened THEN the delete address button is visible`() = runBlocking {
val addressEditorView = spyk(AddressEditorView(binding, interactor, address))
val addressEditorView = spyk(
AddressEditorView(
binding = binding,
interactor = interactor,
address = address,
)
)
addressEditorView.bind()
assertEquals(View.VISIBLE, binding.deleteButton.visibility)
@ -98,11 +94,105 @@ class AddressEditorViewTest {
@Test
fun `GIVEN an existing address WHEN the delete address button is clicked THEN confirm delete dialog is shown`() = runBlocking {
val addressEditorView = spyk(AddressEditorView(binding, interactor, address))
val addressEditorView = spyk(
AddressEditorView(
binding = binding,
interactor = interactor,
address = address,
)
)
addressEditorView.bind()
binding.deleteButton.performClick()
verify { addressEditorView.showConfirmDeleteAddressDialog(view.context, "123") }
}
@Test
fun `GIVEN existing address WHEN country dropdown is bound THEN adapter sets country dropdown to address`() {
val addressEditorView = spyk(
AddressEditorView(
binding = binding,
interactor = interactor,
address = generateAddress(country = "CA"),
)
)
addressEditorView.bind()
assertEquals(addressEditorView.countries["CA"]?.displayName, binding.countryDropDown.selectedItem.toString())
}
@Test
fun `GIVEN existing address and region not in supported countries WHEN country dropdown is bound THEN adapter sets dropdown to lower priority`() {
val addressEditorView = spyk(
AddressEditorView(
binding = binding,
interactor = interactor,
region = RegionState.Default,
address = generateAddress(country = "XX"),
)
)
addressEditorView.bind()
assertEquals(addressEditorView.countries[DEFAULT_COUNTRY]!!.displayName, binding.countryDropDown.selectedItem.toString())
}
@Test
fun `GIVEN search region and no address WHEN country dropdown is bound THEN adapter sets country dropdown to home region`() {
val addressEditorView = AddressEditorView(
binding = binding,
interactor = interactor,
region = RegionState("CA", "US"),
address = null,
)
addressEditorView.bind()
assertEquals(addressEditorView.countries["CA"]?.displayName, binding.countryDropDown.selectedItem.toString())
}
@Test
fun `GIVEN search region not in supported countries WHEN country dropdown is bound THEN adapter sets dropdown to lower priority`() {
val addressEditorView = AddressEditorView(
binding = binding,
interactor = interactor,
region = RegionState.Default,
address = null,
)
addressEditorView.bind()
assertEquals(addressEditorView.countries[DEFAULT_COUNTRY]!!.displayName, binding.countryDropDown.selectedItem.toString())
}
@Test
fun `GIVEN no address or search region WHEN country dropdown is bound THEN adapter sets dropdown to default`() {
val addressEditorView = AddressEditorView(
binding = binding,
interactor = interactor,
region = null,
address = null,
)
addressEditorView.bind()
assertEquals(addressEditorView.countries[DEFAULT_COUNTRY]!!.displayName, binding.countryDropDown.selectedItem.toString())
}
private fun generateAddress(country: String = "US") = Address(
guid = "123",
givenName = "Given",
additionalName = "Additional",
familyName = "Family",
organization = "Organization",
streetAddress = "Street",
addressLevel3 = "Suburb",
addressLevel2 = "City",
addressLevel1 = "State",
postalCode = "PostalCode",
country = country,
tel = "Telephone",
email = "email@mozilla.com",
timeCreated = 0L,
timeLastUsed = 1L,
timeLastModified = 1L,
timesUsed = 2L
)
}

Loading…
Cancel
Save