diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt index 557a1d757d1..07d9412d39c 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt @@ -13,7 +13,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.selector.findTabOrCustomTab @@ -47,6 +46,9 @@ import mozilla.components.concept.storage.CreditCardValidationDelegate import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import mozilla.components.concept.storage.LoginValidationDelegate +import mozilla.components.feature.prompts.address.AddressDelegate +import mozilla.components.feature.prompts.address.AddressPicker +import mozilla.components.feature.prompts.address.DefaultAddressDelegate import mozilla.components.feature.prompts.concept.SelectablePromptView import mozilla.components.feature.prompts.creditcard.CreditCardPicker import mozilla.components.feature.prompts.creditcard.CreditCardSaveDialogFragment @@ -133,6 +135,7 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog" * the select credit card prompt. * @property onSelectCreditCard A callback invoked when a user selects a credit card from the * select credit card prompt. + * @property addressDelegate Delegate for address picker. * @property onNeedToRequestPermissions A callback invoked when permissions * need to be requested before a prompt (e.g. a file picker) can be displayed. * Once the request is completed, [onPermissionsResult] needs to be invoked. @@ -155,6 +158,7 @@ class PromptFeature private constructor( private val creditCardPickerView: SelectablePromptView? = null, private val onManageCreditCards: () -> Unit = {}, private val onSelectCreditCard: () -> Unit = {}, + private val addressDelegate: AddressDelegate = DefaultAddressDelegate(), onNeedToRequestPermissions: OnNeedToRequestPermissions ) : LifecycleAwareFeature, PermissionsFeature, @@ -195,6 +199,7 @@ class PromptFeature private constructor( creditCardPickerView: SelectablePromptView? = null, onManageCreditCards: () -> Unit = {}, onSelectCreditCard: () -> Unit = {}, + addressDelegate: AddressDelegate = DefaultAddressDelegate(), onNeedToRequestPermissions: OnNeedToRequestPermissions ) : this( container = PromptContainer.Activity(activity), @@ -213,7 +218,8 @@ class PromptFeature private constructor( onManageLogins = onManageLogins, creditCardPickerView = creditCardPickerView, onManageCreditCards = onManageCreditCards, - onSelectCreditCard = onSelectCreditCard + onSelectCreditCard = onSelectCreditCard, + addressDelegate = addressDelegate ) constructor( @@ -233,6 +239,7 @@ class PromptFeature private constructor( creditCardPickerView: SelectablePromptView? = null, onManageCreditCards: () -> Unit = {}, onSelectCreditCard: () -> Unit = {}, + addressDelegate: AddressDelegate = DefaultAddressDelegate(), onNeedToRequestPermissions: OnNeedToRequestPermissions ) : this( container = PromptContainer.Fragment(fragment), @@ -251,7 +258,8 @@ class PromptFeature private constructor( onManageLogins = onManageLogins, creditCardPickerView = creditCardPickerView, onManageCreditCards = onManageCreditCards, - onSelectCreditCard = onSelectCreditCard + onSelectCreditCard = onSelectCreditCard, + addressDelegate = addressDelegate ) private val filePicker = FilePicker(container, store, customTabId, onNeedToRequestPermissions) @@ -272,6 +280,19 @@ class PromptFeature private constructor( ) } + @VisibleForTesting(otherwise = PRIVATE) + internal var addressPicker = + with(addressDelegate) { + addressPickerView?.let { + AddressPicker( + store = store, + addressSelectBar = it, + onManageAddresses = onManageAddresses, + sessionId = customTabId + ) + } + } + override val onNeedToRequestPermissions get() = filePicker.onNeedToRequestPermissions @@ -293,16 +314,29 @@ class PromptFeature private constructor( if (content.promptRequests.lastOrNull() != activePromptRequest) { // Dismiss any active select login or credit card prompt if it does // not match the current prompt request for the session. - if (activePromptRequest is SelectLoginPrompt) { - loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt) - } else if (activePromptRequest is SaveLoginPrompt) { - (activePrompt?.get() as? SaveLoginDialogFragment)?.dismissAllowingStateLoss() - } else if (activePromptRequest is SaveCreditCard) { - (activePrompt?.get() as? CreditCardSaveDialogFragment)?.dismissAllowingStateLoss() - } else if (activePromptRequest is SelectCreditCard) { - creditCardPicker?.dismissSelectCreditCardRequest( - activePromptRequest as SelectCreditCard - ) + when (activePromptRequest) { + is SelectLoginPrompt -> { + loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt) + } + is SaveLoginPrompt -> { + (activePrompt?.get() as? SaveLoginDialogFragment)?.dismissAllowingStateLoss() + } + is SaveCreditCard -> { + (activePrompt?.get() as? CreditCardSaveDialogFragment)?.dismissAllowingStateLoss() + } + is SelectCreditCard -> { + creditCardPicker?.dismissSelectCreditCardRequest( + activePromptRequest as SelectCreditCard + ) + } + is SelectAddress -> { + addressPicker?.dismissSelectAddressRequest( + activePromptRequest as SelectAddress + ) + } + else -> { + // no-op + } } onPromptRequested(state) @@ -429,15 +463,16 @@ class PromptFeature private constructor( creditCardPicker?.handleSelectCreditCardRequest(promptRequest) } } - is SelectAddress -> - if (isAddressAutofillEnabled() && promptRequest.addresses.isNotEmpty()) { - // Address picker will be implemented in #12061 - } is SelectLoginPrompt -> { if (promptRequest.logins.isNotEmpty()) { loginPicker?.handleSelectLoginRequest(promptRequest) } } + is SelectAddress -> { + if (isAddressAutofillEnabled() && promptRequest.addresses.isNotEmpty()) { + addressPicker?.handleSelectAddressRequest(promptRequest) + } + } else -> handleDialogsRequest(promptRequest, session) } } @@ -851,8 +886,8 @@ class PromptFeature private constructor( is SelectLoginPrompt, is SelectCreditCard, is SaveCreditCard, + is SelectAddress, is Share -> true - is SelectAddress -> false is Alert, is TextPrompt, is Confirm, is Repost, is Popup -> promptAbuserDetector.shouldShowMoreDialogs } } @@ -884,6 +919,15 @@ class PromptFeature private constructor( } } + (activePromptRequest as? SelectAddress)?.let { selectAddressPrompt -> + addressPicker?.let { addressPicker -> + if (addressDelegate.addressPickerView?.asView()?.isVisible == true) { + addressPicker.dismissSelectAddressRequest(selectAddressPrompt) + result = true + } + } + } + return result } diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt new file mode 100644 index 00000000000..f95086de126 --- /dev/null +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt @@ -0,0 +1,74 @@ +/* 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 mozilla.components.feature.prompts.address + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.concept.storage.Address +import mozilla.components.feature.prompts.R + +@VisibleForTesting +internal object AddressDiffCallback : DiffUtil.ItemCallback
() { + override fun areItemsTheSame(oldItem: Address, newItem: Address) = + oldItem.guid == newItem.guid + + override fun areContentsTheSame(oldItem: Address, newItem: Address) = + oldItem == newItem +} + +/** + * RecyclerView adapter for displaying address items. + */ +internal class AddressAdapter( + private val onAddressSelected: (Address) -> Unit +) : ListAdapter(AddressDiffCallback) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressViewHolder { + val view = LayoutInflater + .from(parent.context) + .inflate(R.layout.mozac_feature_prompts_address_list_item, parent, false) + return AddressViewHolder(view, onAddressSelected) + } + + override fun onBindViewHolder(holder: AddressViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +/** + * View holder for a address item. + */ +@VisibleForTesting +internal class AddressViewHolder( + itemView: View, + private val onAddressSelected: (Address) -> Unit +) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + @VisibleForTesting + lateinit var address: Address + + init { + itemView.setOnClickListener(this) + } + + fun bind(address: Address) { + this.address = address + itemView.findViewById(R.id.address_name)?.text = address.displayFormat() + } + + override fun onClick(v: View?) { + onAddressSelected(address) + } +} + +/** + * Format the address details to be displayed to the user. + */ +fun Address.displayFormat(): String = + "${this.streetAddress}, ${this.addressLevel2}, ${this.addressLevel1}, ${this.postalCode}" diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt new file mode 100644 index 00000000000..038455b4ede --- /dev/null +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt @@ -0,0 +1,33 @@ +/* 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 mozilla.components.feature.prompts.address + +import mozilla.components.concept.storage.Address +import mozilla.components.feature.prompts.concept.SelectablePromptView + +/** + * Delegate for address picker + */ +interface AddressDelegate { + /** + * The [SelectablePromptView] used for [AddressPicker] to display a + * selectable prompt list of address options. + */ + val addressPickerView: SelectablePromptView
? + + /** + * Callback invoked when the user clicks "Manage addresses" from + * select address prompt. + */ + val onManageAddresses: () -> Unit +} + +/** + * Default implementation for address picker delegate + */ +class DefaultAddressDelegate( + override val addressPickerView: SelectablePromptView
? = null, + override val onManageAddresses: () -> Unit = {} +) : AddressDelegate diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt new file mode 100644 index 00000000000..877c2a43528 --- /dev/null +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt @@ -0,0 +1,85 @@ +/* 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 mozilla.components.feature.prompts.address + +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.prompt.PromptRequest +import mozilla.components.concept.storage.Address +import mozilla.components.feature.prompts.concept.SelectablePromptView +import mozilla.components.feature.prompts.consumePromptFrom +import mozilla.components.support.base.log.logger.Logger + +/** + * Interactor that implements [SelectablePromptView.Listener] and notifies the feature about actions + * the user performed in the address picker. + * + * @property store The [BrowserStore] this feature should subscribe to. + * @property addressSelectBar The [SelectablePromptView] view into which the select address + * prompt will be inflated. + * @property onManageAddresses Callback invoked when user clicks on "Manage adresses" button from + * select address prompt. + * @property sessionId The session ID which requested the prompt. + */ +class AddressPicker( + private val store: BrowserStore, + private val addressSelectBar: SelectablePromptView
, + private val onManageAddresses: () -> Unit = {}, + private var sessionId: String? = null +) : SelectablePromptView.Listener
{ + + init { + addressSelectBar.listener = this + } + + /** + * Shows the select address prompt in response to the [PromptRequest] event. + * + * @param request The [PromptRequest] containing the the address request data to be shown. + */ + internal fun handleSelectAddressRequest(request: PromptRequest.SelectAddress) { + addressSelectBar.showPrompt(request.addresses) + } + + /** + * Dismisses the active [PromptRequest.SelectAddress] request. + * + * @param promptRequest The current active [PromptRequest.SelectAddress] or null + * otherwise. + */ + @Suppress("TooGenericExceptionCaught") + fun dismissSelectAddressRequest(promptRequest: PromptRequest.SelectAddress? = null) { + addressSelectBar.hidePrompt() + + try { + if (promptRequest != null) { + promptRequest.onDismiss() + sessionId?.let { + store.dispatch(ContentAction.ConsumePromptRequestAction(it, promptRequest)) + } + return + } + + store.consumePromptFrom(sessionId) { + it.onDismiss() + } + } catch (e: RuntimeException) { + Logger.error("Can't dismiss select address prompt", e) + } + } + + override fun onOptionSelect(option: Address) { + store.consumePromptFrom(sessionId) { + it.onConfirm(option) + } + + addressSelectBar.hidePrompt() + } + + override fun onManageOptions() { + onManageAddresses.invoke() + dismissSelectAddressRequest() + } +} diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt new file mode 100644 index 00000000000..ca6d9362b2a --- /dev/null +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt @@ -0,0 +1,147 @@ +/* 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 mozilla.components.feature.prompts.address + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.View +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.withStyledAttributes +import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat +import androidx.core.widget.TextViewCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.concept.storage.Address +import mozilla.components.feature.prompts.R +import mozilla.components.feature.prompts.concept.SelectablePromptView +import mozilla.components.support.ktx.android.view.hideKeyboard + +/** + * A customizable "Select addresses" bar implementing [SelectablePromptView]. + */ +class AddressSelectBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), SelectablePromptView
{ + + private var view: View? = null + private var recyclerView: RecyclerView? = null + private var headerView: AppCompatTextView? = null + private var expanderView: AppCompatImageView? = null + private var manageAddressesView: AppCompatTextView? = null + private var headerTextStyle: Int? = null + + private val listAdapter = AddressAdapter { address -> + listener?.apply { + onOptionSelect(address) + } + } + + override var listener: SelectablePromptView.Listener
? = null + + init { + context.withStyledAttributes( + attrs, + R.styleable.AddressSelectBar, + defStyleAttr, + 0 + ) { + val textStyle = + getResourceId( + R.styleable.AddressSelectBar_mozacSelectAddressHeaderTextStyle, + 0 + ) + + if (textStyle > 0) { + headerTextStyle = textStyle + } + } + } + + override fun hidePrompt() { + this.isVisible = false + recyclerView?.isVisible = false + manageAddressesView?.isVisible = false + + listAdapter.submitList(null) + + toggleSelectAddressHeader(shouldExpand = false) + } + + override fun showPrompt(options: List
) { + if (view == null) { + view = View.inflate(context, LAYOUT_ID, this) + bindViews() + } + + listAdapter.submitList(options) + view?.isVisible = true + } + + private fun bindViews() { + recyclerView = findViewById(R.id.address_list).apply { + layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + adapter = listAdapter + } + + headerView = findViewById(R.id.select_address_header).apply { + setOnClickListener { + toggleSelectAddressHeader(shouldExpand = recyclerView?.isVisible != true) + } + + headerTextStyle?.let { appearance -> + TextViewCompat.setTextAppearance(this, appearance) + currentTextColor.let { color -> + TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(color)) + } + } + } + + expanderView = + findViewById(R.id.mozac_feature_address_expander).apply { + headerView?.currentTextColor?.let { + ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(it)) + } + } + + manageAddressesView = findViewById(R.id.manage_addresses).apply { + setOnClickListener { + listener?.onManageOptions() + } + } + } + + /** + * Toggles the visibility of the list of address items in the prompt. + * + * @param shouldExpand True if the list of addresses should be displayed, false otherwise. + */ + private fun toggleSelectAddressHeader(shouldExpand: Boolean) { + recyclerView?.isVisible = shouldExpand + manageAddressesView?.isVisible = shouldExpand + + if (shouldExpand) { + view?.hideKeyboard() + expanderView?.rotation = ROTATE_180 + headerView?.contentDescription = + context.getString(R.string.mozac_feature_prompts_collapse_address_content_description) + } else { + expanderView?.rotation = 0F + headerView?.contentDescription = + context.getString(R.string.mozac_feature_prompts_expand_address_content_description) + } + } + + companion object { + val LAYOUT_ID = R.layout.mozac_feature_prompts_address_select_prompt + + private const val ROTATE_180 = 180F + } +} diff --git a/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_list_item.xml b/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_list_item.xml new file mode 100644 index 00000000000..3a63084b7e3 --- /dev/null +++ b/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_list_item.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_select_prompt.xml b/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_select_prompt.xml new file mode 100644 index 00000000000..64064fd0d60 --- /dev/null +++ b/components/feature/prompts/src/main/res/layout/mozac_feature_prompts_address_select_prompt.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + diff --git a/components/feature/prompts/src/main/res/values/attrs.xml b/components/feature/prompts/src/main/res/values/attrs.xml index 8f6ed0ab246..de216bfa8b5 100644 --- a/components/feature/prompts/src/main/res/values/attrs.xml +++ b/components/feature/prompts/src/main/res/values/attrs.xml @@ -16,4 +16,8 @@ + + + + diff --git a/components/feature/prompts/src/main/res/values/strings.xml b/components/feature/prompts/src/main/res/values/strings.xml index 6990737970f..42cf481ab1f 100644 --- a/components/feature/prompts/src/main/res/values/strings.xml +++ b/components/feature/prompts/src/main/res/values/strings.xml @@ -116,4 +116,14 @@ Update card expiration date? Card number will be encrypted. Security code won’t be saved. + + + + Select addresses + + Expand suggested addresses + + Collapse suggested addresses + + Manage addresses diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt index a8fa655168f..da6758d873a 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt @@ -38,9 +38,12 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry +import mozilla.components.feature.prompts.address.AddressDelegate +import mozilla.components.feature.prompts.address.AddressPicker import mozilla.components.feature.prompts.concept.SelectablePromptView import mozilla.components.feature.prompts.creditcard.CreditCardPicker import mozilla.components.feature.prompts.creditcard.CreditCardSaveDialogFragment @@ -89,6 +92,7 @@ class PromptFeatureTest { private lateinit var fragmentManager: FragmentManager private lateinit var loginPicker: LoginPicker private lateinit var creditCardPicker: CreditCardPicker + private lateinit var addressPicker: AddressPicker private val tabId = "test-tab" private fun tab(): TabSessionState? { @@ -111,6 +115,7 @@ class PromptFeatureTest { ) loginPicker = mock() creditCardPicker = mock() + addressPicker = mock() fragmentManager = mockFragmentManager() } @@ -592,6 +597,64 @@ class PromptFeatureTest { verify(feature.creditCardPicker!!).dismissSelectCreditCardRequest(selectCreditCardRequest) } + @Test + fun `WHEN dismissSelectPrompts is called THEN the active addressPicker dismiss should be called`() { + val addressPickerView: SelectablePromptView
= mock() + val addressDelegate: AddressDelegate = mock() + val feature = spy( + PromptFeature( + mock(), + store, + fragmentManager = fragmentManager, + addressDelegate = addressDelegate + ) { } + ) + feature.addressPicker = addressPicker + feature.activePromptRequest = mock() + + whenever(addressDelegate.addressPickerView).thenReturn(addressPickerView) + whenever(addressPickerView.asView()).thenReturn(mock()) + whenever(addressPickerView.asView().visibility).thenReturn(View.VISIBLE) + + feature.dismissSelectPrompts() + verify(feature.addressPicker!!, never()).dismissSelectAddressRequest(any()) + + val selectAddressPromptRequest = mock() + feature.activePromptRequest = selectAddressPromptRequest + + feature.dismissSelectPrompts() + + verify(feature.addressPicker!!).dismissSelectAddressRequest(selectAddressPromptRequest) + + store.waitUntilIdle() + assertTrue(tab()!!.content.promptRequests.isEmpty()) + } + + @Test + fun `GIVEN addressPickerView is not visible WHEN dismissSelectPrompts is called THEN dismissSelectPrompts returns false`() { + val addressPickerView: SelectablePromptView
= mock() + val addressDelegate: AddressDelegate = mock() + val feature = spy( + PromptFeature( + mock(), + store, + fragmentManager = fragmentManager, + addressDelegate = addressDelegate + ) { } + ) + val selectAddressRequest = mock() + feature.addressPicker = addressPicker + feature.activePromptRequest = selectAddressRequest + + whenever(addressDelegate.addressPickerView).thenReturn(addressPickerView) + whenever(addressPickerView.asView()).thenReturn(mock()) + whenever(addressPickerView.asView().visibility).thenReturn(View.GONE) + + val result = feature.dismissSelectPrompts() + + assertEquals(false, result) + } + @Test fun `Calling onCancel will consume promptRequest`() { val feature = diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt new file mode 100644 index 00000000000..d6d5ca2200b --- /dev/null +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt @@ -0,0 +1,79 @@ + +package mozilla.components.feature.prompts.address + +import android.view.LayoutInflater +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.concept.storage.Address +import mozilla.components.feature.prompts.R +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AddressAdapterTest { + + private val address = Address( + guid = "1", + givenName = "Location", + additionalName = "Location", + familyName = "Location", + organization = "Mozilla", + streetAddress = "1230 Main st", + addressLevel3 = "Location3", + addressLevel2 = "Location2", + addressLevel1 = "Location1", + postalCode = "90237", + country = "USA", + tel = "00", + email = "email" + ) + + @Test + fun testAddressDiffCallback() { + val address2 = address.copy() + + assertTrue( + AddressDiffCallback.areItemsTheSame(address, address2) + ) + assertTrue( + AddressDiffCallback.areContentsTheSame(address, address2) + ) + + val address3 = address.copy(guid = "2") + + assertFalse( + AddressDiffCallback.areItemsTheSame(address, address3) + ) + assertFalse( + AddressDiffCallback.areItemsTheSame(address, address3) + ) + } + + @Test + fun `WHEN an address is bound to the adapter THEN set the address display name`() { + val view = + LayoutInflater.from(testContext).inflate(R.layout.mozac_feature_prompts_address_list_item, null) + val addressName: TextView = view.findViewById(R.id.address_name) + + AddressViewHolder(view) {}.bind(address) + + assertEquals(address.displayFormat(), addressName.text) + } + + @Test + fun `WHEN an address item is clicked THEN call the onAddressSelected callback`() { + var addressSelected = false + val view = + LayoutInflater.from(testContext).inflate(R.layout.mozac_feature_prompts_address_list_item, null) + val onAddressSelect: (Address) -> Unit = { addressSelected = true } + + AddressViewHolder(view, onAddressSelect).bind(address) + view.performClick() + + assertTrue(addressSelected) + } +} diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt new file mode 100644 index 00000000000..59d3466157a --- /dev/null +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt @@ -0,0 +1,101 @@ +/* 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 mozilla.components.feature.prompts.address + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.prompt.PromptRequest +import mozilla.components.concept.storage.Address +import mozilla.components.support.test.mock +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class AddressPickerTest { + + private lateinit var store: BrowserStore + private lateinit var state: BrowserState + private lateinit var addressPicker: AddressPicker + private lateinit var addressSelectBar: AddressSelectBar + + private val address = Address( + guid = "1", + givenName = "Location", + additionalName = "Location", + familyName = "Location", + organization = "Mozilla", + streetAddress = "1230 Main st", + addressLevel3 = "Location3", + addressLevel2 = "Location2", + addressLevel1 = "Location1", + postalCode = "90237", + country = "USA", + tel = "00", + email = "email" + ) + + private var onDismissCalled = false + private var confirmedAddress: Address? = null + + private val promptRequest = PromptRequest.SelectAddress( + addresses = listOf(address), + onDismiss = { onDismissCalled = true }, + onConfirm = { confirmedAddress = it } + ) + + @Before + fun setup() { + store = mock() + state = mock() + addressSelectBar = mock() + addressPicker = AddressPicker( + store = store, + addressSelectBar = addressSelectBar + ) + + whenever(store.state).thenReturn(state) + } + + @Test + fun `WHEN onOptionSelect is called with an address THEN selectAddressCallback is invoked and prompt is hidden`() { + val content: ContentState = mock() + whenever(content.promptRequests).thenReturn(listOf(promptRequest)) + val selectedTab = TabSessionState("browser-tab", content, mock(), mock()) + whenever(state.selectedTabId).thenReturn(selectedTab.id) + whenever(state.tabs).thenReturn(listOf(selectedTab)) + + addressPicker.onOptionSelect(address) + + verify(addressSelectBar).hidePrompt() + assertEquals(address, confirmedAddress) + } + + @Test + fun `GIVEN a prompt request WHEN handleSelectAddressRequest is called THEN the prompt is shown with the provided addresses`() { + addressPicker.handleSelectAddressRequest(promptRequest) + + verify(addressSelectBar).showPrompt(promptRequest.addresses) + } + + @Test + fun `GIVEN a custom tab and a prompt request WHEN handleSelectAddressRequest is called THEN the prompt is shown with the provided addresses`() { + val customTabContent: ContentState = mock() + val customTab = CustomTabSessionState("custom-tab", customTabContent, mock(), mock()) + whenever(customTabContent.promptRequests).thenReturn(listOf(promptRequest)) + whenever(state.customTabs).thenReturn(listOf(customTab)) + + addressPicker.handleSelectAddressRequest(promptRequest) + + verify(addressSelectBar).showPrompt(promptRequest.addresses) + } +} diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt new file mode 100644 index 00000000000..1d7751e2e78 --- /dev/null +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt @@ -0,0 +1,99 @@ +/* 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 mozilla.components.feature.prompts.address + +import android.widget.LinearLayout +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.concept.storage.Address +import mozilla.components.feature.prompts.R +import mozilla.components.feature.prompts.concept.SelectablePromptView +import mozilla.components.support.test.ext.appCompatContext +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class AddressSelectBarTest { + + private lateinit var addressSelectBar: AddressSelectBar + + private val address = Address( + guid = "1", + givenName = "Location", + additionalName = "Location", + familyName = "Location", + organization = "Mozilla", + streetAddress = "1230 Main st", + addressLevel3 = "Location3", + addressLevel2 = "Location2", + addressLevel1 = "Location1", + postalCode = "90237", + country = "USA", + tel = "00", + email = "email" + ) + + @Before + fun setup() { + addressSelectBar = AddressSelectBar(appCompatContext) + } + + @Test + fun `WHEN showPrompt is called THEN the select bar is shown`() { + val addresses = listOf(address) + + addressSelectBar.showPrompt(addresses) + + assertTrue(addressSelectBar.isVisible) + } + + @Test + fun `WHEN hidePrompt is called THEN the select bar is hidden`() { + assertTrue(addressSelectBar.isVisible) + + addressSelectBar.hidePrompt() + + assertFalse(addressSelectBar.isVisible) + } + + @Test + fun `WHEN the selectBar header is clicked two times THEN the list of addresses is shown, then hidden`() { + addressSelectBar.showPrompt(listOf(address)) + addressSelectBar.findViewById(R.id.select_address_header).performClick() + + assertTrue(addressSelectBar.findViewById(R.id.address_list).isVisible) + + addressSelectBar.findViewById(R.id.select_address_header).performClick() + + assertFalse(addressSelectBar.findViewById(R.id.address_list).isVisible) + } + + @Test + fun `GIVEN a listener WHEN an address is clicked THEN onOptionSelected is called`() { + val listener: SelectablePromptView.Listener
= mock() + + assertNull(addressSelectBar.listener) + + addressSelectBar.listener = listener + + addressSelectBar.showPrompt(listOf(address)) + val adapter = addressSelectBar.findViewById(R.id.address_list).adapter as AddressAdapter + val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0) + adapter.bindViewHolder(holder, 0) + + holder.itemView.performClick() + + verify(listener).onOptionSelect(address) + } +} diff --git a/docs/changelog.md b/docs/changelog.md index e53933fbb20..e5d560cc2c5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,9 @@ permalink: /changelog/ * [Gecko](https://github.com/mozilla-mobile/android-components/blob/main/buildSrc/src/main/java/Gecko.kt) * [Configuration](https://github.com/mozilla-mobile/android-components/blob/main/.config.yml) +* **feature-prompts**: + * Added optional `addressPickerView` and `onManageAddresses` parameters through `AddressDelegate` to `PromptFeature` for a new `AddressPicker` to display a view for selecting addresses to autofill into a site. [#12061](https://github.com/mozilla-mobile/android-components/issues/12061) + # 102.0.0 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v101.0.0...v102.0.1) * [Milestone](https://github.com/mozilla-mobile/android-components/milestone/149?closed=1)