autocomplete with saved contacts in transfer page #51
@@ -40,9 +40,11 @@ import kotlinx.coroutines.withContext
|
|||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.models.BankAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
||||||
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||||
|
import sh.sar.basedbank.databinding.ItemPickerRowBinding
|
||||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||||
import sh.sar.basedbank.ui.home.transfer.BmlTransferHandler
|
import sh.sar.basedbank.ui.home.transfer.BmlTransferHandler
|
||||||
import sh.sar.basedbank.ui.home.transfer.FahipayTransferHandler
|
import sh.sar.basedbank.ui.home.transfer.FahipayTransferHandler
|
||||||
@@ -92,6 +94,7 @@ class TransferFragment : Fragment() {
|
|||||||
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
|
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
|
||||||
|
|
||||||
private var accountDropdownAdapter: AccountDropdownAdapter? = null
|
private var accountDropdownAdapter: AccountDropdownAdapter? = null
|
||||||
|
private var contactDropdownAdapter: ContactDropdownAdapter? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owns everything BML-specific: sessions, the transfer itself, the business-profile OTP
|
* Owns everything BML-specific: sessions, the transfer itself, the business-profile OTP
|
||||||
@@ -305,6 +308,9 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
setupFromDropdown()
|
setupFromDropdown()
|
||||||
setupAccountLookup()
|
setupAccountLookup()
|
||||||
|
// Contact search dropdown on the To field needs viewModel.contacts populated —
|
||||||
|
// otherwise it stays empty until the contacts tab or picker sheet is opened first.
|
||||||
|
(activity as? HomeActivity)?.loadAllContacts()
|
||||||
|
|
||||||
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
||||||
accountDropdownAdapter?.notifyDataSetChanged()
|
accountDropdownAdapter?.notifyDataSetChanged()
|
||||||
@@ -327,7 +333,7 @@ class TransferFragment : Fragment() {
|
|||||||
// MFAISA source + a phone-number pick (e.g. a tagged M-Faisa recent) — re-run the
|
// MFAISA source + a phone-number pick (e.g. a tagged M-Faisa recent) — re-run the
|
||||||
// basicBeneDetails lookup so the recipient gets fully resolved before Send is enabled.
|
// basicBeneDetails lookup so the recipient gets fully resolved before Send is enabled.
|
||||||
if (selectedAccount?.bank == "MFAISA") {
|
if (selectedAccount?.bank == "MFAISA") {
|
||||||
binding.etTo.setText(accountNumber)
|
binding.etTo.setText(accountNumber, false)
|
||||||
mfaisaHandler().searchRecipient(accountNumber)
|
mfaisaHandler().searchRecipient(accountNumber)
|
||||||
return@setFragmentResultListener
|
return@setFragmentResultListener
|
||||||
}
|
}
|
||||||
@@ -408,7 +414,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.cardToInfo.visibility = View.VISIBLE
|
binding.cardToInfo.visibility = View.VISIBLE
|
||||||
if (savedToImageHash != null) loadToPhoto(savedToImageHash!!, isProfile = resolvedToOwnAccount != null)
|
if (savedToImageHash != null) loadToPhoto(savedToImageHash!!, isProfile = resolvedToOwnAccount != null)
|
||||||
} else if (savedToText.isNotEmpty()) {
|
} else if (savedToText.isNotEmpty()) {
|
||||||
binding.etTo.setText(savedToText)
|
binding.etTo.setText(savedToText, false)
|
||||||
}
|
}
|
||||||
if (savedAmount.isNotEmpty()) binding.etAmount.setText(savedAmount)
|
if (savedAmount.isNotEmpty()) binding.etAmount.setText(savedAmount)
|
||||||
if (savedRemarks.isNotEmpty()) binding.etRemarks.setText(savedRemarks)
|
if (savedRemarks.isNotEmpty()) binding.etRemarks.setText(savedRemarks)
|
||||||
@@ -537,7 +543,7 @@ class TransferFragment : Fragment() {
|
|||||||
resolvedToOwnAccount = null
|
resolvedToOwnAccount = null
|
||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.etTo.setText("")
|
binding.etTo.setText("", false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.tilTo.hint = getString(R.string.transfer_to)
|
binding.tilTo.hint = getString(R.string.transfer_to)
|
||||||
@@ -551,7 +557,7 @@ class TransferFragment : Fragment() {
|
|||||||
resolvedToOwnAccount = null
|
resolvedToOwnAccount = null
|
||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.etTo.setText("")
|
binding.etTo.setText("", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The picker and QR-scan icons live alongside the tilTo input. Keep them in sync with
|
// The picker and QR-scan icons live alongside the tilTo input. Keep them in sync with
|
||||||
@@ -764,6 +770,30 @@ class TransferFragment : Fragment() {
|
|||||||
updateTransferButton()
|
updateTransferButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupContactDropdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Live "search contacts as you type" dropdown on the To field (shown once 3+ chars are entered). */
|
||||||
|
private fun setupContactDropdown() {
|
||||||
|
val adapter = ContactDropdownAdapter(requireContext()) { selectedAccount?.bank == "MFAISA" }
|
||||||
|
contactDropdownAdapter = adapter
|
||||||
|
binding.etTo.setAdapter(adapter)
|
||||||
|
|
||||||
|
viewModel.contacts.observe(viewLifecycleOwner) { contacts ->
|
||||||
|
adapter.updateSource(contacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.etTo.setOnItemClickListener { _, _, position, _ ->
|
||||||
|
val contact = adapter.getContact(position) ?: return@setOnItemClickListener
|
||||||
|
prefillToDirectly(
|
||||||
|
accountNumber = contact.benefAccount,
|
||||||
|
displayName = contact.benefNickName,
|
||||||
|
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||||
|
colorHex = contact.bankColor,
|
||||||
|
imageHash = contact.customerImgHash
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupAccount() {
|
private fun lookupAccount() {
|
||||||
@@ -975,7 +1005,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
binding.btnScanQr.visibility = View.VISIBLE
|
binding.btnScanQr.visibility = View.VISIBLE
|
||||||
binding.tilTo.error = null
|
binding.tilTo.error = null
|
||||||
binding.etTo.setText(accountNumber)
|
binding.etTo.setText(accountNumber, false)
|
||||||
lookupAccount()
|
lookupAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1512,7 +1542,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
binding.btnScanQr.visibility = View.VISIBLE
|
binding.btnScanQr.visibility = View.VISIBLE
|
||||||
binding.etTo.setText("")
|
binding.etTo.setText("", false)
|
||||||
binding.tilTo.error = null
|
binding.tilTo.error = null
|
||||||
binding.tilAmount.error = null
|
binding.tilAmount.error = null
|
||||||
}
|
}
|
||||||
@@ -1782,4 +1812,59 @@ class TransferFragment : Fragment() {
|
|||||||
override fun convertResultToString(r: Any?) = ""
|
override fun convertResultToString(r: Any?) = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Filters [source] contacts by nickname/name/account number, shown once the query is 3+ chars. */
|
||||||
|
private inner class ContactDropdownAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val isDisabled: () -> Boolean
|
||||||
|
) : BaseAdapter(), Filterable {
|
||||||
|
|
||||||
|
private var source: List<BankContact> = emptyList()
|
||||||
|
private var filtered: List<BankContact> = emptyList()
|
||||||
|
|
||||||
|
fun updateSource(contacts: List<BankContact>) {
|
||||||
|
source = contacts
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getContact(position: Int): BankContact? = filtered.getOrNull(position)
|
||||||
|
|
||||||
|
override fun getCount() = filtered.size
|
||||||
|
override fun getItem(position: Int) = filtered[position]
|
||||||
|
override fun getItemId(position: Int) = position.toLong()
|
||||||
|
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val contact = filtered[position]
|
||||||
|
val b = if (convertView?.tag is ItemPickerRowBinding) {
|
||||||
|
convertView.tag as ItemPickerRowBinding
|
||||||
|
} else {
|
||||||
|
ItemPickerRowBinding.inflate(LayoutInflater.from(context), parent, false).also { it.root.tag = it }
|
||||||
|
}
|
||||||
|
b.tvPrimary.text = contact.benefNickName
|
||||||
|
b.tvSecondary.text = "${contact.benefBankName} · ${contact.benefAccount}"
|
||||||
|
b.tvBalance.visibility = View.GONE
|
||||||
|
b.ivIcon.scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
||||||
|
b.ivIcon.setImageBitmap(makeInitialsBitmap(contact.benefNickName, contact.bankColor))
|
||||||
|
return b.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilter() = object : Filter() {
|
||||||
|
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||||
|
val query = constraint?.toString()?.trim().orEmpty()
|
||||||
|
val matches = if (isDisabled() || query.length < 3) emptyList() else source.filter {
|
||||||
|
it.benefNickName.contains(query, ignoreCase = true) ||
|
||||||
|
it.benefName.contains(query, ignoreCase = true) ||
|
||||||
|
it.benefAccount.contains(query, ignoreCase = true)
|
||||||
|
}.take(8)
|
||||||
|
return FilterResults().apply { values = matches; count = matches.size }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
|
||||||
|
filtered = results?.values as? List<BankContact> ?: emptyList()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertResultToString(r: Any?) = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,12 +148,15 @@
|
|||||||
app:endIconDrawable="@android:drawable/ic_menu_search"
|
app:endIconDrawable="@android:drawable/ic_menu_search"
|
||||||
app:endIconContentDescription="@string/transfer_lookup_account">
|
app:endIconContentDescription="@string/transfer_lookup_account">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||||
android:id="@+id/etTo"
|
android:id="@+id/etTo"
|
||||||
|
style="@style/Widget.Material3.AutoCompleteTextView.OutlinedBox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textNoSuggestions"
|
android:inputType="textNoSuggestions"
|
||||||
android:maxLines="1" />
|
android:maxLines="1"
|
||||||
|
android:completionThreshold="3"
|
||||||
|
android:dropDownHeight="wrap_content" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user