Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caf0db60da
|
||
|
|
b48022d249
|
@@ -359,55 +359,87 @@ class SettingsLoginsFragment : Fragment() {
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private data class LoginEntry(val logoRes: Int, val displayName: String, val onClick: () -> Unit)
|
||||
|
||||
private fun buildLoginsSection() {
|
||||
val ctx = requireContext()
|
||||
val store = CredentialStore(ctx)
|
||||
val container = binding.loginsContainer
|
||||
container.removeAllViews()
|
||||
|
||||
val mibLoginIds = store.getMibLoginIds()
|
||||
val bmlLoginIds = store.getBmlLoginIds()
|
||||
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||
val mfaisaLoginIds = store.getMfaisaLoginIds()
|
||||
var anyLogins = false
|
||||
|
||||
binding.tvLoginsTitle.visibility = if (mibLoginIds.isNotEmpty() || bmlLoginIds.isNotEmpty() || fahipayLoginIds.isNotEmpty() || mfaisaLoginIds.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
fun addGroup(loginIds: List<String>, entryFor: (String) -> LoginEntry, persistOrder: (List<String>) -> Unit) {
|
||||
if (loginIds.isEmpty()) return
|
||||
anyLogins = true
|
||||
val groupContainer = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL }
|
||||
loginIds.forEachIndexed { index, loginId ->
|
||||
val entry = entryFor(loginId)
|
||||
fun swapAndPersist(otherIndex: Int) {
|
||||
val newOrder = loginIds.toMutableList().apply {
|
||||
val tmp = this[index]; this[index] = this[otherIndex]; this[otherIndex] = tmp
|
||||
}
|
||||
persistOrder(newOrder)
|
||||
buildLoginsSection()
|
||||
}
|
||||
addLoginRow(
|
||||
groupContainer, entry.logoRes, entry.displayName, entry.onClick,
|
||||
canMoveUp = index > 0,
|
||||
canMoveDown = index < loginIds.size - 1,
|
||||
onMoveUp = { swapAndPersist(index - 1) },
|
||||
onMoveDown = { swapAndPersist(index + 1) }
|
||||
)
|
||||
}
|
||||
container.addView(groupContainer)
|
||||
}
|
||||
|
||||
for (loginId in mibLoginIds) {
|
||||
addGroup(store.getMibLoginIds(), { loginId ->
|
||||
val profile = store.loadMibUserProfile(loginId)
|
||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name)
|
||||
val mibProfiles = store.loadMibProfiles(loginId)
|
||||
addLoginRow(container, R.drawable.mib_logo, displayName) {
|
||||
LoginEntry(R.drawable.mib_logo, displayName) {
|
||||
showMibLoginDetails(store, loginId, profile, mibProfiles)
|
||||
}
|
||||
}
|
||||
}, store::setMibLoginIds)
|
||||
|
||||
for (loginId in bmlLoginIds) {
|
||||
addGroup(store.getBmlLoginIds(), { loginId ->
|
||||
val profile = store.loadBmlUserProfile(loginId)
|
||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name)
|
||||
val bmlProfiles = store.loadBmlProfiles(loginId)
|
||||
addLoginRow(container, R.drawable.bml_logo_vector, displayName) {
|
||||
LoginEntry(R.drawable.bml_logo_vector, displayName) {
|
||||
showBmlLoginDetails(store, loginId, profile, bmlProfiles)
|
||||
}
|
||||
}
|
||||
}, store::setBmlLoginIds)
|
||||
|
||||
for (loginId in fahipayLoginIds) {
|
||||
addGroup(store.getFahipayLoginIds(), { loginId ->
|
||||
val profile = store.loadFahipayUserProfile(loginId)
|
||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
||||
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
||||
LoginEntry(R.drawable.fahipay_logo, displayName) {
|
||||
showFahipayLoginDetails(store, loginId, profile)
|
||||
}
|
||||
}
|
||||
}, store::setFahipayLoginIds)
|
||||
|
||||
for (loginId in mfaisaLoginIds) {
|
||||
addGroup(store.getMfaisaLoginIds(), { loginId ->
|
||||
val profile = store.loadMfaisaUserProfile(loginId)
|
||||
val displayName = profile?.name?.takeIf { it.isNotBlank() } ?: getString(R.string.ooredoo_name)
|
||||
addLoginRow(container, R.drawable.ooredoo_logo, displayName) {
|
||||
LoginEntry(R.drawable.ooredoo_logo, displayName) {
|
||||
showMfaisaLoginDetails(store, loginId, profile)
|
||||
}
|
||||
}
|
||||
}, store::setMfaisaLoginIds)
|
||||
|
||||
binding.tvLoginsTitle.visibility = if (anyLogins) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun addLoginRow(container: LinearLayout, logoRes: Int, displayName: String, onClick: () -> Unit) {
|
||||
private fun addLoginRow(
|
||||
container: LinearLayout,
|
||||
logoRes: Int,
|
||||
displayName: String,
|
||||
onClick: () -> Unit,
|
||||
canMoveUp: Boolean,
|
||||
canMoveDown: Boolean,
|
||||
onMoveUp: () -> Unit,
|
||||
onMoveDown: () -> Unit
|
||||
) {
|
||||
val ctx = requireContext()
|
||||
val dp = ctx.resources.displayMetrics.density
|
||||
val row = LinearLayout(ctx).apply {
|
||||
@@ -429,10 +461,31 @@ class SettingsLoginsFragment : Fragment() {
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge)
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
row.addView(logo); row.addView(tvName)
|
||||
val btnUp = makeMoveButton(ctx, rotationDeg = -90f, enabled = canMoveUp, onClick = onMoveUp)
|
||||
val btnDown = makeMoveButton(ctx, rotationDeg = 90f, enabled = canMoveDown, onClick = onMoveDown)
|
||||
row.addView(logo); row.addView(tvName); row.addView(btnUp); row.addView(btnDown)
|
||||
container.addView(row)
|
||||
}
|
||||
|
||||
private fun makeMoveButton(ctx: Context, rotationDeg: Float, enabled: Boolean, onClick: () -> Unit): ImageView {
|
||||
val dp = ctx.resources.displayMetrics.density
|
||||
val size = (32 * dp).toInt()
|
||||
return ImageView(ctx).apply {
|
||||
setImageResource(R.drawable.ic_arrow_right)
|
||||
rotation = rotationDeg
|
||||
scaleType = ImageView.ScaleType.CENTER_INSIDE
|
||||
val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackgroundBorderless))
|
||||
background = ta.getDrawable(0); ta.recycle()
|
||||
val iconColor = com.google.android.material.color.MaterialColors.getColor(
|
||||
ctx, com.google.android.material.R.attr.colorOnSurfaceVariant, android.graphics.Color.GRAY)
|
||||
imageTintList = android.content.res.ColorStateList.valueOf(iconColor)
|
||||
alpha = if (enabled) 1f else 0.3f
|
||||
isClickable = enabled; isFocusable = enabled
|
||||
layoutParams = LinearLayout.LayoutParams(size, size).apply { marginStart = (4 * dp).toInt() }
|
||||
if (enabled) setOnClickListener { onClick() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMibLoginDetails(
|
||||
store: CredentialStore,
|
||||
loginId: String,
|
||||
|
||||
@@ -40,9 +40,11 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
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.databinding.FragmentTransferBinding
|
||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||
import sh.sar.basedbank.databinding.ItemPickerRowBinding
|
||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||
import sh.sar.basedbank.ui.home.transfer.BmlTransferHandler
|
||||
import sh.sar.basedbank.ui.home.transfer.FahipayTransferHandler
|
||||
@@ -92,6 +94,7 @@ class TransferFragment : Fragment() {
|
||||
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
|
||||
|
||||
private var accountDropdownAdapter: AccountDropdownAdapter? = null
|
||||
private var contactDropdownAdapter: ContactDropdownAdapter? = null
|
||||
|
||||
/**
|
||||
* Owns everything BML-specific: sessions, the transfer itself, the business-profile OTP
|
||||
@@ -305,6 +308,9 @@ class TransferFragment : Fragment() {
|
||||
|
||||
setupFromDropdown()
|
||||
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) {
|
||||
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
|
||||
// basicBeneDetails lookup so the recipient gets fully resolved before Send is enabled.
|
||||
if (selectedAccount?.bank == "MFAISA") {
|
||||
binding.etTo.setText(accountNumber)
|
||||
binding.etTo.setText(accountNumber, false)
|
||||
mfaisaHandler().searchRecipient(accountNumber)
|
||||
return@setFragmentResultListener
|
||||
}
|
||||
@@ -408,7 +414,7 @@ class TransferFragment : Fragment() {
|
||||
binding.cardToInfo.visibility = View.VISIBLE
|
||||
if (savedToImageHash != null) loadToPhoto(savedToImageHash!!, isProfile = resolvedToOwnAccount != null)
|
||||
} else if (savedToText.isNotEmpty()) {
|
||||
binding.etTo.setText(savedToText)
|
||||
binding.etTo.setText(savedToText, false)
|
||||
}
|
||||
if (savedAmount.isNotEmpty()) binding.etAmount.setText(savedAmount)
|
||||
if (savedRemarks.isNotEmpty()) binding.etRemarks.setText(savedRemarks)
|
||||
@@ -537,7 +543,7 @@ class TransferFragment : Fragment() {
|
||||
resolvedToOwnAccount = null
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.etTo.setText("")
|
||||
binding.etTo.setText("", false)
|
||||
}
|
||||
} else {
|
||||
binding.tilTo.hint = getString(R.string.transfer_to)
|
||||
@@ -551,7 +557,7 @@ class TransferFragment : Fragment() {
|
||||
resolvedToOwnAccount = null
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
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
|
||||
@@ -764,6 +770,30 @@ class TransferFragment : Fragment() {
|
||||
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() {
|
||||
@@ -975,7 +1005,7 @@ class TransferFragment : Fragment() {
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
binding.tilTo.error = null
|
||||
binding.etTo.setText(accountNumber)
|
||||
binding.etTo.setText(accountNumber, false)
|
||||
lookupAccount()
|
||||
}
|
||||
|
||||
@@ -1512,7 +1542,7 @@ class TransferFragment : Fragment() {
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
binding.etTo.setText("")
|
||||
binding.etTo.setText("", false)
|
||||
binding.tilTo.error = null
|
||||
binding.tilAmount.error = null
|
||||
}
|
||||
@@ -1782,4 +1812,59 @@ class TransferFragment : Fragment() {
|
||||
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?) = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ class CredentialStore(context: Context) {
|
||||
|
||||
fun hasMibCredentials(): Boolean = getMibLoginIds().isNotEmpty()
|
||||
|
||||
fun setMibLoginIds(order: List<String>) {
|
||||
if (order.toSet() != getMibLoginIds().toSet()) return
|
||||
prefs.edit().putString("mib_login_ids", org.json.JSONArray(order).toString()).apply()
|
||||
}
|
||||
|
||||
private fun addMibLoginId(loginId: String) {
|
||||
val ids = getMibLoginIds().toMutableList()
|
||||
if (loginId !in ids) {
|
||||
@@ -160,6 +165,11 @@ class CredentialStore(context: Context) {
|
||||
|
||||
fun hasBmlCredentials(): Boolean = getBmlLoginIds().isNotEmpty()
|
||||
|
||||
fun setBmlLoginIds(order: List<String>) {
|
||||
if (order.toSet() != getBmlLoginIds().toSet()) return
|
||||
prefs.edit().putString("bml_login_ids", org.json.JSONArray(order).toString()).apply()
|
||||
}
|
||||
|
||||
private fun addBmlLoginId(loginId: String) {
|
||||
val ids = getBmlLoginIds().toMutableList()
|
||||
if (loginId !in ids) {
|
||||
@@ -316,6 +326,11 @@ class CredentialStore(context: Context) {
|
||||
|
||||
fun hasFahipayCredentials(): Boolean = getFahipayLoginIds().isNotEmpty()
|
||||
|
||||
fun setFahipayLoginIds(order: List<String>) {
|
||||
if (order.toSet() != getFahipayLoginIds().toSet()) return
|
||||
prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(order).toString()).apply()
|
||||
}
|
||||
|
||||
private fun addFahipayLoginId(loginId: String) {
|
||||
val ids = getFahipayLoginIds().toMutableList()
|
||||
if (loginId !in ids) {
|
||||
@@ -473,6 +488,11 @@ class CredentialStore(context: Context) {
|
||||
|
||||
fun hasMfaisaCredentials(): Boolean = getMfaisaLoginIds().isNotEmpty()
|
||||
|
||||
fun setMfaisaLoginIds(order: List<String>) {
|
||||
if (order.toSet() != getMfaisaLoginIds().toSet()) return
|
||||
prefs.edit().putString("mfaisa_login_ids", org.json.JSONArray(order).toString()).apply()
|
||||
}
|
||||
|
||||
private fun addMfaisaLoginId(loginId: String) {
|
||||
val ids = getMfaisaLoginIds().toMutableList()
|
||||
if (loginId !in ids) {
|
||||
|
||||
@@ -148,12 +148,15 @@
|
||||
app:endIconDrawable="@android:drawable/ic_menu_search"
|
||||
app:endIconContentDescription="@string/transfer_lookup_account">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/etTo"
|
||||
style="@style/Widget.Material3.AutoCompleteTextView.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:maxLines="1" />
|
||||
android:maxLines="1"
|
||||
android:completionThreshold="3"
|
||||
android:dropDownHeight="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user