diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/AddressEditorFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/address/AddressEditorFragment.kt index 5f716afd9095..1d21071363b0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/AddressEditorFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/AddressEditorFragment.kt @@ -5,9 +5,13 @@ package org.mozilla.fenix.settings.address import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R import org.mozilla.fenix.SecureFragment @@ -27,6 +31,16 @@ class AddressEditorFragment : SecureFragment(R.layout.fragment_address_editor) { private lateinit var addressEditorView: AddressEditorView private lateinit var interactor: AddressEditorInteractor + private val args by navArgs() + + /** + * Returns true if an existing address is being edited, and false otherwise. + */ + private val isEditing: Boolean + get() = args.address != null + + private lateinit var menu: Menu + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -41,18 +55,49 @@ class AddressEditorFragment : SecureFragment(R.layout.fragment_address_editor) { ) val binding = FragmentAddressEditorBinding.bind(view) + setHasOptionsMenu(true) - addressEditorView = AddressEditorView(binding, interactor) + addressEditorView = AddressEditorView(binding, interactor, args.address) addressEditorView.bind() } + override fun onPause() { + super.onPause() + menu.close() + } + override fun onResume() { super.onResume() - showToolbar(getString(R.string.addresses_add_address)) + if (isEditing) { + showToolbar(getString(R.string.addresses_edit_address)) + } else { + showToolbar(getString(R.string.addresses_add_address)) + } } override fun onStop() { super.onStop() this.view?.hideKeyboard() } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.address_editor, menu) + this.menu = menu + + menu.findItem(R.id.delete_address_button).isVisible = isEditing + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.delete_address_button -> { + args.address?.let { + addressEditorView.showConfirmDeleteAddressDialog(requireContext(), it.guid) + } + true + } + R.id.save_address_button -> { + addressEditorView.saveAddress() + true + } + else -> false + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/AddressManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/address/AddressManagementFragment.kt index 9a1a5ddf032d..25c4edfe71aa 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/AddressManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/AddressManagementFragment.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.ext.components @@ -66,6 +67,15 @@ class AddressManagementFragment : Fragment() { } } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + consumeFrom(store) { state -> + if (!state.isLoading && state.addresses.isEmpty()) { + findNavController().popBackStack() + return@consumeFrom + } + } + } + /** * Fetches all the addresses from the autofill storage and updates the * [AutofillFragmentStore] with the list of addresses. diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt index 2e69e49c4b42..5ee2ea230a83 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt @@ -28,6 +28,16 @@ interface AddressEditorController { * @see [AddressEditorInteractor.onSaveAddress] */ fun handleSaveAddress(addressFields: UpdatableAddressFields) + + /** + * @see [AddressEditorInteractor.onDeleteAddress] + */ + fun handleDeleteAddress(guid: String) + + /** + * @see [AddressEditorInteractor.onUpdateAddress] + */ + fun handleUpdateAddress(guid: String, addressFields: UpdatableAddressFields) } /** @@ -57,4 +67,24 @@ class DefaultAddressEditorController( } } } + + override fun handleDeleteAddress(guid: String) { + lifecycleScope.launch { + storage.deleteAddress(guid) + + lifecycleScope.launch(Dispatchers.Main) { + navController.popBackStack() + } + } + } + + override fun handleUpdateAddress(guid: String, addressFields: UpdatableAddressFields) { + lifecycleScope.launch { + storage.updateAddress(guid, addressFields) + + lifecycleScope.launch(Dispatchers.Main) { + navController.popBackStack() + } + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt index b7ef02c40d20..cbf87b532faa 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt @@ -37,17 +37,19 @@ class DefaultAddressManagementController( ) : AddressManagementController { override fun handleAddressClicked(address: Address) { - navigateToAddressEditor() + navigateToAddressEditor(address) } override fun handleAddAddressButtonClicked() { navigateToAddressEditor() } - private fun navigateToAddressEditor() { + private fun navigateToAddressEditor(address: Address? = null) { navController.navigate( AddressManagementFragmentDirections - .actionAddressManagementFragmentToAddressEditorFragment() + .actionAddressManagementFragmentToAddressEditorFragment( + address = address + ) ) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt index 17c30ea95b8b..5cc46c661733 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.settings.address.interactor +import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.UpdatableAddressFields import org.mozilla.fenix.settings.address.controller.AddressEditorController @@ -25,6 +26,22 @@ interface AddressEditorInteractor { * @param addressFields A [UpdatableAddressFields] record to add. */ fun onSaveAddress(addressFields: UpdatableAddressFields) + + /** + * Deletes the provided address from the autofill storage. Called when a user + * taps on the save menu item or "Save" button. + * + * @param guid The unique identifier for the [Address] record to delete. + */ + fun onDeleteAddress(guid: String) + + /** + * Updates the provided address in the autofill storage. Called when a user + * taps on the update menu item or "Update" button. + * + * @param addressFields A [UpdatableAddressFields] record to add. + */ + fun onUpdateAddress(guid: String, addressFields: UpdatableAddressFields) } /** @@ -44,4 +61,12 @@ class DefaultAddressEditorInteractor( override fun onSaveAddress(addressFields: UpdatableAddressFields) { controller.handleSaveAddress(addressFields) } + + override fun onDeleteAddress(guid: String) { + controller.handleDeleteAddress(guid) + } + + override fun onUpdateAddress(guid: String, addressFields: UpdatableAddressFields) { + controller.handleUpdateAddress(guid, addressFields) + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt b/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt index fe2363b4f73f..678c6b812668 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt @@ -4,9 +4,15 @@ package org.mozilla.fenix.settings.address.view +import android.content.Context +import android.content.DialogInterface +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import mozilla.components.concept.storage.Address 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.interactor.AddressEditorInteractor @@ -16,7 +22,8 @@ import org.mozilla.fenix.settings.address.interactor.AddressEditorInteractor */ class AddressEditorView( private val binding: FragmentAddressEditorBinding, - private val interactor: AddressEditorInteractor + private val interactor: AddressEditorInteractor, + private val address: Address? = null ) { /** @@ -36,26 +43,64 @@ class AddressEditorView( binding.saveButton.setOnClickListener { saveAddress() } + + address?.let { address -> + binding.emailInput.setText(address.email) + binding.phoneInput.setText(address.tel) + + binding.firstNameInput.setText(address.givenName) + binding.middleNameInput.setText(address.additionalName) + binding.lastNameInput.setText(address.familyName) + + binding.streetAddressInput.setText(address.streetAddress) + binding.cityInput.setText(address.addressLevel2) + binding.stateInput.setText(address.addressLevel1) + binding.zipInput.setText(address.postalCode) + + binding.deleteButton.apply { + isVisible = true + setOnClickListener { view -> + showConfirmDeleteAddressDialog(view.context, address.guid) + } + } + } } internal fun saveAddress() { binding.root.hideKeyboard() - interactor.onSaveAddress( - UpdatableAddressFields( - givenName = binding.firstNameInput.text.toString(), - additionalName = binding.middleNameInput.text.toString(), - familyName = binding.lastNameInput.text.toString(), - organization = "", - streetAddress = binding.streetAddressInput.text.toString(), - addressLevel3 = "", - addressLevel2 = "", - addressLevel1 = "", - postalCode = binding.zipInput.text.toString(), - country = "", - tel = binding.phoneInput.text.toString(), - email = binding.emailInput.text.toString() - ) + val addressFields = UpdatableAddressFields( + givenName = binding.firstNameInput.text.toString(), + additionalName = binding.middleNameInput.text.toString(), + familyName = binding.lastNameInput.text.toString(), + organization = "", + streetAddress = binding.streetAddressInput.text.toString(), + addressLevel3 = "", + addressLevel2 = "", + addressLevel1 = "", + postalCode = binding.zipInput.text.toString(), + country = "", + tel = binding.phoneInput.text.toString(), + email = binding.emailInput.text.toString() ) + + if (address != null) { + interactor.onUpdateAddress(address.guid, addressFields) + } else { + interactor.onSaveAddress(addressFields) + } + } + + internal fun showConfirmDeleteAddressDialog(context: Context, guid: String) { + AlertDialog.Builder(context).apply { + setMessage(R.string.addressess_confirm_dialog_message) + setNegativeButton(R.string.addressess_confirm_dialog_cancel_button) { dialog: DialogInterface, _ -> + dialog.cancel() + } + setPositiveButton(R.string.addressess_confirm_dialog_ok_button) { _, _ -> + interactor.onDeleteAddress(guid) + } + create() + }.show() } } diff --git a/app/src/main/res/menu/address_editor.xml b/app/src/main/res/menu/address_editor.xml new file mode 100644 index 000000000000..88b4be47fc69 --- /dev/null +++ b/app/src/main/res/menu/address_editor.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index c594faa2b685..4d847f29f5a3 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -1258,7 +1258,13 @@ + android:label="@string/addresses_add_address"> + + Unlock to use stored credit card information Add address + + Edit address Manage addresses @@ -1574,6 +1576,16 @@ Cancel Delete address + + Are you sure you want to delete this address? + + Delete + + Cancel + + Save address + + Delete address Add search engine diff --git a/app/src/test/java/org/mozilla/fenix/settings/address/AddressEditorViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/address/AddressEditorViewTest.kt index e57cbe08105c..514f9454b510 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/address/AddressEditorViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/address/AddressEditorViewTest.kt @@ -6,10 +6,14 @@ package org.mozilla.fenix.settings.address import android.view.LayoutInflater import android.view.View +import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.runBlocking +import mozilla.components.concept.storage.Address import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -26,12 +30,15 @@ class AddressEditorViewTest { private lateinit var interactor: AddressEditorInteractor private lateinit var addressEditorView: AddressEditorView private lateinit var binding: FragmentAddressEditorBinding + private lateinit var address: Address @Before fun setup() { view = LayoutInflater.from(testContext).inflate(R.layout.fragment_address_editor, null) binding = FragmentAddressEditorBinding.bind(view) interactor = mockk(relaxed = true) + address = mockk(relaxed = true) + every { address.guid } returns "123" addressEditorView = spyk(AddressEditorView(binding, interactor)) } @@ -44,4 +51,58 @@ class AddressEditorViewTest { verify { interactor.onCancelButtonClicked() } } + + @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 addressEditorView = spyk(AddressEditorView(binding, interactor, address)) + addressEditorView.bind() + + assertEquals("PostalCode", binding.zipInput.text.toString()) + assertEquals("State", binding.stateInput.text.toString()) + assertEquals("City", binding.cityInput.text.toString()) + assertEquals("Street", binding.streetAddressInput.text.toString()) + assertEquals("Family", binding.lastNameInput.text.toString()) + assertEquals("Given", binding.firstNameInput.text.toString()) + assertEquals("Additional", binding.middleNameInput.text.toString()) + assertEquals("email@mozilla.com", binding.emailInput.text.toString()) + assertEquals("Telephone", binding.phoneInput.text.toString()) + } + + @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)) + addressEditorView.bind() + + assertEquals(View.VISIBLE, binding.deleteButton.visibility) + } + + @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)) + addressEditorView.bind() + + binding.deleteButton.performClick() + + verify { addressEditorView.showConfirmDeleteAddressDialog(view.context, "123") } + } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressEditorControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressEditorControllerTest.kt index 10fa0059b136..aad0d4c1ff64 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressEditorControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressEditorControllerTest.kt @@ -6,11 +6,14 @@ package org.mozilla.fenix.settings.address.controller import androidx.navigation.NavController import io.mockk.coVerify +import io.mockk.coVerifySequence +import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest +import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.UpdatableAddressFields import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage import mozilla.components.support.test.rule.MainCoroutineRule @@ -73,4 +76,18 @@ class DefaultAddressEditorControllerTest { navController.popBackStack() } } + + @Test + fun `GIVEN an existing address record WHEN save address is called THEN update the address record to storage`() = runBlockingTest { + val address: Address = mockk() + val addressFields: UpdatableAddressFields = mockk() + every { address.guid } returns "123" + + controller.handleUpdateAddress(address.guid, addressFields) + + coVerifySequence { + storage.updateAddress("123", addressFields) + navController.popBackStack() + } + } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressManagementControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressManagementControllerTest.kt index d6a806a940b2..70d24b7c88a8 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressManagementControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/address/controller/DefaultAddressManagementControllerTest.kt @@ -37,7 +37,9 @@ class DefaultAddressManagementControllerTest { verify { navController.navigate( AddressManagementFragmentDirections - .actionAddressManagementFragmentToAddressEditorFragment() + .actionAddressManagementFragmentToAddressEditorFragment( + address = address + ) ) } }