forked from thijooree/android
refactor codebase to be more module for later adding new banks.. add support for single profile mib accounts.. add suport for disabling mib profiles in settings
This commit is contained in:
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.Transaction
|
||||
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||
import sh.sar.basedbank.databinding.ItemAccountHistoryHeaderBinding
|
||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
|
||||
@@ -20,7 +21,8 @@ import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class AccountHistoryAdapter(
|
||||
private val account: MibAccount
|
||||
private val account: MibAccount,
|
||||
private val display: AccountHistoryDisplay
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private sealed class Item {
|
||||
@@ -138,7 +140,7 @@ class AccountHistoryAdapter(
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is HeaderVH -> holder.bind(account)
|
||||
is HeaderVH -> holder.bind(display)
|
||||
is DateHeaderVH -> holder.bind((displayItems[position - 1] as Item.DateHeader).label)
|
||||
is TransactionVH -> holder.bind((displayItems[position - 1] as Item.Trx).transaction)
|
||||
else -> Unit
|
||||
@@ -147,37 +149,20 @@ class AccountHistoryAdapter(
|
||||
|
||||
inner class HeaderVH(private val b: ItemAccountHistoryHeaderBinding) :
|
||||
RecyclerView.ViewHolder(b.root) {
|
||||
fun bind(acc: MibAccount) {
|
||||
b.tvHeaderAccountName.text = acc.accountBriefName
|
||||
b.tvHeaderAccountNumber.text = acc.accountNumber
|
||||
b.tvHeaderPillBank.text = when {
|
||||
acc.profileType.startsWith("BML") -> "BML"
|
||||
acc.profileType == "FAHIPAY" -> "FP"
|
||||
else -> null
|
||||
}
|
||||
b.tvHeaderPillType.text = friendlyType(acc.accountTypeName)
|
||||
b.tvHeaderAvailable.text = "${acc.currencyName} ${acc.availableBalance}"
|
||||
b.tvHeaderBalance.text = "${acc.currencyName} ${acc.currentBalance}"
|
||||
val blocked = acc.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||
if (blocked > 0.0) {
|
||||
b.tvHeaderBlocked.text = "${acc.currencyName} ${acc.blockedAmount}"
|
||||
fun bind(d: AccountHistoryDisplay) {
|
||||
b.tvHeaderAccountName.text = d.name
|
||||
b.tvHeaderAccountNumber.text = d.number
|
||||
b.tvHeaderPillBank.text = d.bankPill
|
||||
b.tvHeaderPillType.text = d.typeLabel
|
||||
b.tvHeaderAvailable.text = d.availableBalance
|
||||
b.tvHeaderBalance.text = d.workingBalance
|
||||
if (d.blockedBalance != null) {
|
||||
b.tvHeaderBlocked.text = d.blockedBalance
|
||||
b.llHeaderBlocked.visibility = View.VISIBLE
|
||||
} else {
|
||||
b.llHeaderBlocked.visibility = View.GONE
|
||||
}
|
||||
b.btnHeaderTransfer.setOnClickListener { onTransferClick?.invoke(acc) }
|
||||
}
|
||||
|
||||
private fun friendlyType(raw: String): String {
|
||||
val u = raw.trim().uppercase()
|
||||
return when {
|
||||
u.contains("SAVING") -> "Savings"
|
||||
u.contains("CURRENT") -> "Current"
|
||||
u.contains("WADIAH") -> "Islamic"
|
||||
u.contains("VISA") || u.contains("MASTERCARD") || u.contains("AMEX") -> "Card"
|
||||
u.contains("PREPAID") -> "Prepaid"
|
||||
else -> raw.trim().take(12)
|
||||
}
|
||||
b.btnHeaderTransfer.setOnClickListener { onTransferClick?.invoke(account) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,23 +17,18 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.api.mib.MibHistoryClient
|
||||
import sh.sar.basedbank.api.mib.Transaction
|
||||
import sh.sar.basedbank.api.mib.TransactionCache
|
||||
import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding
|
||||
import sh.sar.basedbank.util.AccountHistoryParser
|
||||
import sh.sar.basedbank.util.ContactImageCache
|
||||
import sh.sar.basedbank.util.HistoryFetcher
|
||||
import sh.sar.basedbank.util.MerchantIconCache
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
class AccountHistoryFragment : Fragment() {
|
||||
|
||||
@@ -43,21 +38,13 @@ class AccountHistoryFragment : Fragment() {
|
||||
|
||||
private lateinit var adapter: AccountHistoryAdapter
|
||||
private lateinit var account: MibAccount
|
||||
private lateinit var fetcher: HistoryFetcher
|
||||
|
||||
private val allTransactions = mutableListOf<Transaction>()
|
||||
private var searchQuery = ""
|
||||
private var firstPageDone = false
|
||||
private val pendingImageNames = mutableSetOf<String>()
|
||||
private val pendingIconUrls = mutableSetOf<String>()
|
||||
|
||||
// Pagination state
|
||||
private var mibNextStart = 1
|
||||
private var mibTotalCount = -1 // -1 = unknown; loaded on first fetch
|
||||
private var bmlNextPage = 1
|
||||
private var bmlTotalPages = -1
|
||||
private var cardMonthOffset = 0 // 0 = current month, 1 = prev, etc.
|
||||
private var fahipayNextStart = 0
|
||||
private var fahipayTotal = -1
|
||||
private var isLoading = false
|
||||
private val pageSize = 10
|
||||
|
||||
@@ -79,8 +66,10 @@ class AccountHistoryFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val accountNumber = requireArguments().getString(ARG_ACCOUNT_NUMBER) ?: return
|
||||
account = viewModel.accounts.value?.find { it.accountNumber == accountNumber } ?: return
|
||||
fetcher = HistoryFetcher(account)
|
||||
|
||||
adapter = AccountHistoryAdapter(account)
|
||||
val historyDisplay = AccountHistoryParser.from(account) ?: return
|
||||
adapter = AccountHistoryAdapter(account, historyDisplay)
|
||||
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
||||
adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) }
|
||||
adapter.onTransferClick = { acc ->
|
||||
@@ -104,11 +93,10 @@ class AccountHistoryFragment : Fragment() {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
searchQuery = s?.toString()?.trim() ?: ""
|
||||
filterAndDisplay()
|
||||
if (searchQuery.isNotBlank() && hasMore() && !isLoading) loadNextPage()
|
||||
if (searchQuery.isNotBlank() && fetcher.hasMore() && !isLoading) loadNextPage()
|
||||
}
|
||||
})
|
||||
|
||||
// Load cache immediately, then fetch fresh data in background
|
||||
val cached = TransactionCache.load(requireContext(), account.accountNumber)
|
||||
if (cached.isNotEmpty()) {
|
||||
allTransactions.addAll(cached)
|
||||
@@ -133,19 +121,8 @@ class AccountHistoryFragment : Fragment() {
|
||||
binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun isMib() = !account.profileType.startsWith("BML") && account.profileType != "FAHIPAY"
|
||||
private fun isBmlCard() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||
private fun isFahipay() = account.profileType == "FAHIPAY"
|
||||
|
||||
private fun hasMore(): Boolean = when {
|
||||
isFahipay() -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||
isMib() -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||
isBmlCard() -> cardMonthOffset < 3 // load up to 3 months
|
||||
else -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||
}
|
||||
|
||||
private fun loadNextPage() {
|
||||
if (isLoading || !hasMore()) return
|
||||
if (isLoading || !fetcher.hasMore()) return
|
||||
isLoading = true
|
||||
|
||||
if (firstPageDone && allTransactions.isNotEmpty()) {
|
||||
@@ -155,68 +132,7 @@ class AccountHistoryFragment : Fragment() {
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
|
||||
lifecycleScope.launch {
|
||||
val transactions: List<Transaction> = withContext(Dispatchers.IO) {
|
||||
when {
|
||||
isFahipay() -> {
|
||||
val session = app.fahipaySession ?: return@withContext emptyList()
|
||||
val flow = FahipayLoginFlow()
|
||||
flow.setSessionCookie(session.sessionCookie)
|
||||
val (list, total) = flow.fetchHistory(
|
||||
session = session,
|
||||
accountDisplayName = account.accountBriefName,
|
||||
accountNumber = account.accountNumber,
|
||||
start = fahipayNextStart
|
||||
)
|
||||
if (total > 0) fahipayTotal = total
|
||||
fahipayNextStart += list.size
|
||||
list
|
||||
}
|
||||
isMib() -> {
|
||||
val session = app.mibSession ?: return@withContext emptyList()
|
||||
app.mibMutex.withLock {
|
||||
val profile = app.mibProfiles.firstOrNull { it.profileId == account.profileId }
|
||||
if (profile != null) app.mibLoginFlow.switchProfile(session, profile)
|
||||
val (list, total) = MibHistoryClient().fetchHistory(
|
||||
session = session,
|
||||
accountNo = account.accountNumber,
|
||||
accountDisplayName = account.accountBriefName,
|
||||
start = mibNextStart,
|
||||
pageSize = pageSize
|
||||
)
|
||||
if (total > 0) mibTotalCount = total
|
||||
mibNextStart += list.size.coerceAtLeast(pageSize)
|
||||
list
|
||||
}
|
||||
}
|
||||
isBmlCard() -> {
|
||||
val session = app.bmlSessionFor(account) ?: return@withContext emptyList()
|
||||
val cal = Calendar.getInstance()
|
||||
cal.add(Calendar.MONTH, -cardMonthOffset)
|
||||
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
|
||||
cardMonthOffset++
|
||||
BmlLoginFlow().fetchCardHistory(
|
||||
session = session,
|
||||
cardId = account.internalId,
|
||||
accountDisplayName = account.accountBriefName,
|
||||
accountNumber = account.accountNumber,
|
||||
month = month
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val session = app.bmlSessionFor(account) ?: return@withContext emptyList()
|
||||
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
||||
session = session,
|
||||
accountId = account.internalId,
|
||||
accountDisplayName = account.accountBriefName,
|
||||
accountNumber = account.accountNumber,
|
||||
page = bmlNextPage
|
||||
)
|
||||
if (totalPages > 0) bmlTotalPages = totalPages
|
||||
bmlNextPage++
|
||||
list
|
||||
}
|
||||
}
|
||||
}
|
||||
val transactions = fetcher.fetchNextPage(app, pageSize)
|
||||
|
||||
isLoading = false
|
||||
|
||||
@@ -233,7 +149,6 @@ class AccountHistoryFragment : Fragment() {
|
||||
allTransactions.sortByDescending { AccountHistoryAdapter.parseDateMillis(it.date) }
|
||||
TransactionCache.save(requireContext(), account.accountNumber, allTransactions)
|
||||
if (searchQuery.isBlank()) {
|
||||
// Append incrementally to preserve scroll position
|
||||
val sorted = newOnes.sortedByDescending { AccountHistoryAdapter.parseDateMillis(it.date) }
|
||||
adapter.appendTransactions(sorted)
|
||||
binding.emptyView.visibility = View.GONE
|
||||
@@ -243,7 +158,7 @@ class AccountHistoryFragment : Fragment() {
|
||||
} else {
|
||||
adapter.showLoadingFooter = false
|
||||
}
|
||||
if (searchQuery.isNotBlank() && hasMore()) loadNextPage()
|
||||
if (searchQuery.isNotBlank() && fetcher.hasMore()) loadNextPage()
|
||||
} else {
|
||||
adapter.showLoadingFooter = false
|
||||
if (allTransactions.isEmpty()) binding.emptyView.visibility = View.VISIBLE
|
||||
@@ -288,8 +203,7 @@ class AccountHistoryFragment : Fragment() {
|
||||
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
||||
val bytes = response.body?.bytes() ?: return@launch
|
||||
response.close()
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
?: return@launch
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||
MerchantIconCache.save(requireContext(), url, bitmap)
|
||||
withContext(Dispatchers.Main) { adapter.updateIconUrl(url, bitmap) }
|
||||
} catch (_: Exception) {
|
||||
|
||||
@@ -8,23 +8,24 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.databinding.ItemAccountBinding
|
||||
import sh.sar.basedbank.databinding.ItemCardBinding
|
||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||
import sh.sar.basedbank.util.BmlDashboardParser
|
||||
import sh.sar.basedbank.util.MibAccountParser
|
||||
import sh.sar.basedbank.util.AccountListDisplay
|
||||
import sh.sar.basedbank.util.AccountListParser
|
||||
|
||||
class AccountsAdapter(
|
||||
accounts: List<MibAccount>,
|
||||
private val onAccountClick: (MibAccount) -> Unit = {}
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
var onTransferClick: ((MibAccount) -> Unit)? = null
|
||||
|
||||
private sealed class Item {
|
||||
data class SectionTitle(val label: String) : Item()
|
||||
data class Account(val account: MibAccount) : Item()
|
||||
data class Card(val account: MibAccount) : Item()
|
||||
data class Account(val account: MibAccount, val display: AccountListDisplay) : Item()
|
||||
data class Card(val account: MibAccount, val display: AccountListDisplay) : Item()
|
||||
}
|
||||
|
||||
private val items: MutableList<Item> = buildItems(accounts).toMutableList()
|
||||
@@ -36,38 +37,38 @@ class AccountsAdapter(
|
||||
}
|
||||
|
||||
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
||||
val nonPrepaid = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||
val displayed = accounts.mapNotNull { acc -> AccountListParser.from(acc)?.let { acc to it } }
|
||||
val nonCards = displayed.filter { !it.second.isCard }
|
||||
val cards = displayed.filter { it.second.isCard }
|
||||
|
||||
// Group non-prepaid accounts by their derived section title, preserving order
|
||||
val groups = LinkedHashMap<String, MutableList<MibAccount>>()
|
||||
for (acc in nonPrepaid) {
|
||||
val groups = LinkedHashMap<String, MutableList<Pair<MibAccount, AccountListDisplay>>>()
|
||||
for ((acc, display) in nonCards) {
|
||||
val title = sectionTitle(acc)
|
||||
groups.getOrPut(title) { mutableListOf() }.add(acc)
|
||||
groups.getOrPut(title) { mutableListOf() }.add(acc to display)
|
||||
}
|
||||
for ((title, group) in groups) {
|
||||
add(Item.SectionTitle(title))
|
||||
group.forEach { add(Item.Account(it)) }
|
||||
group.forEach { (acc, display) -> add(Item.Account(acc, display)) }
|
||||
}
|
||||
|
||||
if (prepaid.isNotEmpty()) {
|
||||
if (cards.isNotEmpty()) {
|
||||
add(Item.SectionTitle("Cards · Bank of Maldives"))
|
||||
prepaid.forEach { add(Item.Card(it)) }
|
||||
cards.forEach { (acc, display) -> add(Item.Card(acc, display)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun sectionTitle(account: MibAccount): String {
|
||||
val profileLabel = when (account.profileType) {
|
||||
"0" -> "Personal"
|
||||
"1" -> "Business"
|
||||
else -> account.profileName
|
||||
val bankName = when (account.bank) {
|
||||
"BML" -> "Bank of Maldives"
|
||||
"FAHIPAY" -> "Fahipay"
|
||||
"MIB" -> "Maldives Islamic Bank"
|
||||
else -> account.bank
|
||||
}
|
||||
val bank = when {
|
||||
account.profileType.startsWith("BML") -> "Bank of Maldives"
|
||||
account.profileType == "FAHIPAY" -> "Fahipay"
|
||||
else -> "Maldives Islamic Bank"
|
||||
val profileLabel = when (account.bank) {
|
||||
"MIB" -> account.cifType.ifBlank { account.profileName }
|
||||
else -> account.profileName
|
||||
}
|
||||
return if (profileLabel.isNotBlank()) "$profileLabel · $bank" else bank
|
||||
return if (profileLabel.isNotBlank()) "$profileLabel · $bankName" else bankName
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int) = when (items[position]) {
|
||||
@@ -79,17 +80,17 @@ class AccountsAdapter(
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
TYPE_HEADER -> SectionViewHolder(ItemDateHeaderBinding.inflate(inflater, parent, false))
|
||||
TYPE_CARD -> CardViewHolder(ItemCardBinding.inflate(inflater, parent, false))
|
||||
else -> AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
|
||||
TYPE_HEADER -> SectionViewHolder(ItemDateHeaderBinding.inflate(inflater, parent, false))
|
||||
TYPE_CARD -> CardViewHolder(ItemCardBinding.inflate(inflater, parent, false))
|
||||
else -> AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = items[position]) {
|
||||
is Item.SectionTitle -> (holder as SectionViewHolder).bind(item)
|
||||
is Item.Account -> (holder as AccountViewHolder).bind(item.account)
|
||||
is Item.Card -> (holder as CardViewHolder).bind(item.account)
|
||||
is Item.Account -> (holder as AccountViewHolder).bind(item.account, item.display)
|
||||
is Item.Card -> (holder as CardViewHolder).bind(item.account, item.display)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,18 +105,15 @@ class AccountsAdapter(
|
||||
|
||||
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(account: MibAccount) {
|
||||
binding.tvAccountName.text = account.accountBriefName
|
||||
binding.tvAccountNumber.text = account.accountNumber
|
||||
val label = if (account.profileType.startsWith("BML"))
|
||||
BmlDashboardParser.productLabel(account.accountTypeName)
|
||||
else
|
||||
MibAccountParser.productLabel(account.accountTypeName)
|
||||
binding.tvPillType.text = label
|
||||
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||
fun bind(account: MibAccount, display: AccountListDisplay) {
|
||||
binding.tvAccountName.text = display.name
|
||||
binding.tvAccountNumber.text = display.number
|
||||
binding.tvAccountType.text = display.typeLabel
|
||||
binding.tvBalance.text = display.balance
|
||||
binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) }
|
||||
binding.root.setOnClickListener { onAccountClick(account) }
|
||||
binding.root.setOnLongClickListener {
|
||||
copyToClipboard(it.context, account.accountNumber)
|
||||
copyToClipboard(it.context, display.number)
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -123,23 +121,22 @@ class AccountsAdapter(
|
||||
|
||||
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(account: MibAccount) {
|
||||
binding.ivCardBrand.setImageResource(cardBrandIcon(account.accountTypeName))
|
||||
binding.tvCardName.text = account.accountBriefName
|
||||
binding.tvCardNumber.text = account.accountNumber
|
||||
binding.tvCardProduct.text = BmlDashboardParser.productLabel(account.accountTypeName)
|
||||
fun bind(account: MibAccount, display: AccountListDisplay) {
|
||||
binding.ivCardBrand.setImageResource(display.cardBrandIcon)
|
||||
binding.tvCardName.text = display.name
|
||||
binding.tvCardNumber.text = display.number
|
||||
binding.tvCardProduct.text = display.typeLabel
|
||||
binding.layoutCardBalance.visibility = View.VISIBLE
|
||||
binding.tvCardBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||
|
||||
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
||||
if (isActive) {
|
||||
binding.tvCardStatus.visibility = View.GONE
|
||||
binding.root.alpha = 1f
|
||||
} else {
|
||||
binding.tvCardStatus.text = account.statusDesc
|
||||
binding.tvCardBalance.text = display.balance
|
||||
if (display.statusLabel != null) {
|
||||
binding.tvCardStatus.text = display.statusLabel
|
||||
binding.tvCardStatus.visibility = View.VISIBLE
|
||||
binding.root.alpha = 0.45f
|
||||
} else {
|
||||
binding.tvCardStatus.visibility = View.GONE
|
||||
binding.root.alpha = 1f
|
||||
}
|
||||
binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) }
|
||||
binding.root.setOnClickListener { onAccountClick(account) }
|
||||
}
|
||||
}
|
||||
@@ -154,13 +151,5 @@ class AccountsAdapter(
|
||||
cm.setPrimaryClip(ClipData.newPlainText("Account Number", accountNumber))
|
||||
Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun cardBrandIcon(productName: String): Int = when {
|
||||
productName.contains("AMEX", ignoreCase = true) ||
|
||||
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> R.drawable.americanexpress
|
||||
productName.contains("VISA", ignoreCase = true) -> R.drawable.visa
|
||||
productName.contains("MASTERCARD", ignoreCase = true) -> R.drawable.mastercard
|
||||
else -> R.drawable.ic_nav_card
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ class AccountsFragment : Fragment() {
|
||||
adapter = AccountsAdapter(emptyList()) { account ->
|
||||
(activity as? HomeActivity)?.showWithBackStack(AccountHistoryFragment.newInstance(account))
|
||||
}
|
||||
adapter.onTransferClick = { account ->
|
||||
(activity as? HomeActivity)?.navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFrom(account))
|
||||
}
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
||||
private fun buildDestinations(): List<DestinationOption> {
|
||||
val list = mutableListOf<DestinationOption>()
|
||||
for (profile in app.mibProfiles) {
|
||||
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile))
|
||||
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile, subtitle = profile.cifType))
|
||||
}
|
||||
val store = CredentialStore(requireContext())
|
||||
for ((loginId, _) in app.bmlSessions) {
|
||||
|
||||
@@ -12,23 +12,23 @@ import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||
import sh.sar.basedbank.databinding.ItemContactBinding
|
||||
import sh.sar.basedbank.util.ContactDisplay
|
||||
|
||||
class ContactsAdapter(
|
||||
private val imageCache: MutableMap<String, Bitmap>,
|
||||
private val onImageNeeded: (hash: String) -> Unit,
|
||||
private val onDeleteClick: (MibBeneficiary) -> Unit,
|
||||
private val onTransferClick: (MibBeneficiary) -> Unit
|
||||
private val onDeleteClick: (ContactDisplay) -> Unit,
|
||||
private val onTransferClick: (ContactDisplay) -> Unit
|
||||
) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
|
||||
|
||||
private var allContacts: List<MibBeneficiary> = emptyList()
|
||||
private var displayed: List<MibBeneficiary> = emptyList()
|
||||
private var allContacts: List<ContactDisplay> = emptyList()
|
||||
private var displayed: List<ContactDisplay> = emptyList()
|
||||
|
||||
private var activeCategoryId: String? = null
|
||||
private var searchQuery: String = ""
|
||||
|
||||
fun updateContacts(contacts: List<MibBeneficiary>) {
|
||||
fun updateContacts(contacts: List<ContactDisplay>) {
|
||||
allContacts = contacts
|
||||
applyFilter()
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class ContactsAdapter(
|
||||
fun updateImage(hash: String, bitmap: Bitmap) {
|
||||
imageCache[hash] = bitmap
|
||||
displayed.forEachIndexed { index, contact ->
|
||||
if (contact.customerImgHash == hash) notifyItemChanged(index)
|
||||
if (contact.imageHash == hash) notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +48,11 @@ class ContactsAdapter(
|
||||
|
||||
private fun applyFilter() {
|
||||
displayed = allContacts.filter { contact ->
|
||||
val matchesCategory = activeCategoryId == null || contact.benefCategoryId == activeCategoryId
|
||||
val matchesCategory = activeCategoryId == null || contact.categoryId == activeCategoryId
|
||||
val matchesSearch = searchQuery.isBlank() ||
|
||||
contact.benefNickName.contains(searchQuery, ignoreCase = true) ||
|
||||
contact.benefName.contains(searchQuery, ignoreCase = true) ||
|
||||
contact.benefAccount.contains(searchQuery)
|
||||
contact.name.contains(searchQuery, ignoreCase = true) ||
|
||||
contact.realName.contains(searchQuery, ignoreCase = true) ||
|
||||
contact.accountNumber.contains(searchQuery)
|
||||
matchesCategory && matchesSearch
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
@@ -76,7 +76,7 @@ class ContactsAdapter(
|
||||
binding.root.setOnLongClickListener {
|
||||
val pos = holder.bindingAdapterPosition
|
||||
if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
||||
val account = displayed[pos].benefAccount
|
||||
val account = displayed[pos].accountNumber
|
||||
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
|
||||
Toast.makeText(it.context, account, Toast.LENGTH_SHORT).show()
|
||||
@@ -88,7 +88,7 @@ class ContactsAdapter(
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val contact = displayed[position]
|
||||
val cachedImage = contact.customerImgHash?.let { hash ->
|
||||
val cachedImage = contact.imageHash?.let { hash ->
|
||||
imageCache[hash] ?: run { onImageNeeded(hash); null }
|
||||
}
|
||||
holder.bind(contact, cachedImage)
|
||||
@@ -99,21 +99,24 @@ class ContactsAdapter(
|
||||
inner class ViewHolder(val binding: ItemContactBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(contact: MibBeneficiary, photo: Bitmap?) {
|
||||
val isFahipay = contact.benefType == "FAHIPAY"
|
||||
binding.tvContactName.text = contact.benefNickName
|
||||
binding.tvContactAccount.text = contact.benefAccount
|
||||
binding.tvRealName.text = if (isFahipay) "" else "${contact.benefName} · ${contact.transferCyDesc} · ${contact.benefBankName}"
|
||||
binding.tvRealName.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
||||
binding.btnTransferContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
||||
binding.btnEditContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
||||
binding.btnDeleteContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
||||
fun bind(contact: ContactDisplay, photo: Bitmap?) {
|
||||
binding.tvContactName.text = contact.name
|
||||
binding.tvContactAccount.text = contact.accountNumber
|
||||
binding.tvRealName.text = contact.detail ?: ""
|
||||
binding.tvRealName.visibility =
|
||||
if (contact.detail != null) android.view.View.VISIBLE else android.view.View.GONE
|
||||
binding.btnTransferContact.visibility =
|
||||
if (contact.canTransfer) android.view.View.VISIBLE else android.view.View.GONE
|
||||
binding.btnEditContact.visibility =
|
||||
if (contact.canEdit) android.view.View.VISIBLE else android.view.View.GONE
|
||||
binding.btnDeleteContact.visibility =
|
||||
if (contact.canDelete) android.view.View.VISIBLE else android.view.View.GONE
|
||||
|
||||
if (photo != null) {
|
||||
binding.ivContactPhoto.setImageBitmap(photo)
|
||||
} else {
|
||||
binding.ivContactPhoto.setImageBitmap(
|
||||
makeInitialsBitmap(contact.benefNickName, contact.bankColor)
|
||||
makeInitialsBitmap(contact.name, contact.bankColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,15 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.databinding.FragmentContactsBinding
|
||||
import sh.sar.basedbank.util.ContactDisplay
|
||||
import sh.sar.basedbank.util.ContactImageCache
|
||||
import sh.sar.basedbank.util.ContactListParser
|
||||
import sh.sar.basedbank.util.ContactManager
|
||||
import sh.sar.basedbank.util.ContactsCache
|
||||
import sh.sar.basedbank.util.TransferNetwork
|
||||
|
||||
class ContactsFragment : Fragment() {
|
||||
|
||||
@@ -40,7 +42,7 @@ class ContactsFragment : Fragment() {
|
||||
private val app get() = requireActivity().application as BasedBankApp
|
||||
private val session get() = app.mibSession
|
||||
|
||||
private var allContacts: List<MibBeneficiary> = emptyList()
|
||||
private var allContacts: List<ContactDisplay> = emptyList()
|
||||
private var currentSearch: String = ""
|
||||
private var mediator: TabLayoutMediator? = null
|
||||
private lateinit var pagerAdapter: ContactsPagerAdapter
|
||||
@@ -53,9 +55,9 @@ class ContactsFragment : Fragment() {
|
||||
private val density get() = resources.displayMetrics.density
|
||||
val contactAdapters: List<ContactsAdapter> = pages.map { page ->
|
||||
ContactsAdapter(
|
||||
imageCache = sharedImageCache,
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onDeleteClick = { contact -> confirmDelete(contact) },
|
||||
imageCache = sharedImageCache,
|
||||
onImageNeeded = { hash -> fetchImage(hash) },
|
||||
onDeleteClick = { contact -> confirmDelete(contact) },
|
||||
onTransferClick = { contact -> openTransfer(contact) }
|
||||
).also { a ->
|
||||
a.setFilter(page.categoryId, currentSearch)
|
||||
@@ -63,7 +65,7 @@ class ContactsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContacts(contacts: List<MibBeneficiary>) =
|
||||
fun updateContacts(contacts: List<ContactDisplay>) =
|
||||
contactAdapters.forEach { it.updateContacts(contacts) }
|
||||
|
||||
fun updateSearch(query: String) =
|
||||
@@ -124,8 +126,8 @@ class ContactsFragment : Fragment() {
|
||||
}
|
||||
|
||||
viewModel.contacts.observe(viewLifecycleOwner) { contacts ->
|
||||
allContacts = contacts
|
||||
pagerAdapter.updateContacts(contacts)
|
||||
allContacts = ContactListParser.fromList(contacts)
|
||||
pagerAdapter.updateContacts(allContacts)
|
||||
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.loadingView.visibility = View.GONE
|
||||
}
|
||||
@@ -150,31 +152,29 @@ class ContactsFragment : Fragment() {
|
||||
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
|
||||
}
|
||||
|
||||
private fun openTransfer(contact: MibBeneficiary) {
|
||||
private fun openTransfer(contact: ContactDisplay) {
|
||||
val fragment = TransferFragment.newInstance(
|
||||
accountNumber = contact.benefAccount,
|
||||
displayName = contact.benefNickName,
|
||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||
accountNumber = contact.accountNumber,
|
||||
displayName = contact.name,
|
||||
subtitle = contact.transferSubtitle,
|
||||
colorHex = contact.bankColor,
|
||||
imageHash = contact.customerImgHash
|
||||
imageHash = contact.imageHash
|
||||
)
|
||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, fragment)
|
||||
}
|
||||
|
||||
private fun confirmDelete(contact: MibBeneficiary) {
|
||||
private fun confirmDelete(contact: ContactDisplay) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.contact_delete_title)
|
||||
.setMessage(getString(R.string.contact_delete_message, contact.benefNickName))
|
||||
.setMessage(getString(R.string.contact_delete_message, contact.name))
|
||||
.setPositiveButton(R.string.contact_delete) { _, _ -> deleteContact(contact) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteContact(contact: MibBeneficiary) {
|
||||
private fun deleteContact(contact: ContactDisplay) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
if (contact.benefCategoryId == "BML") deleteBml(contact) else deleteMib(contact)
|
||||
}
|
||||
val success = withContext(Dispatchers.IO) { ContactManager.delete(contact, app) }
|
||||
if (success) {
|
||||
Toast.makeText(requireContext(), R.string.contact_deleted, Toast.LENGTH_SHORT).show()
|
||||
removeFromViewModel(contact)
|
||||
@@ -184,27 +184,10 @@ class ContactsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteBml(contact: MibBeneficiary): Boolean {
|
||||
val sess = app.bmlSessions[contact.profileId] ?: app.anyBmlSession() ?: return false
|
||||
val contactId = contact.benefNo.removePrefix("bml_")
|
||||
return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
private fun deleteMib(contact: MibBeneficiary): Boolean {
|
||||
val sess = session ?: return false
|
||||
return try {
|
||||
if (contact.profileId.isNotBlank()) {
|
||||
val profile = app.mibProfiles.firstOrNull { it.profileId == contact.profileId }
|
||||
if (profile != null) app.mibLoginFlow.switchProfile(sess, profile)
|
||||
}
|
||||
MibContactsClient().deleteContact(sess, contact.benefNo)
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
private fun removeFromViewModel(contact: MibBeneficiary) {
|
||||
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.benefNo } ?: return
|
||||
private fun removeFromViewModel(contact: ContactDisplay) {
|
||||
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.id } ?: return
|
||||
viewModel.contacts.value = updated
|
||||
if (contact.benefCategoryId == "BML") {
|
||||
if (contact.network == TransferNetwork.BML) {
|
||||
updated.filter { it.benefCategoryId == "BML" }
|
||||
.groupBy { it.profileId }
|
||||
.forEach { (loginId, contacts) ->
|
||||
@@ -221,7 +204,6 @@ class ContactsFragment : Fragment() {
|
||||
|
||||
private fun fetchImage(hash: String) {
|
||||
if (!pendingHashes.add(hash)) return
|
||||
// Check disk cache first — if hash matches we already have the image
|
||||
val cached = ContactImageCache.load(requireContext(), hash)
|
||||
if (cached != null) {
|
||||
view?.post { pagerAdapter.updateImage(hash, cached) }
|
||||
|
||||
@@ -130,9 +130,9 @@ class HomeActivity : AppCompatActivity() {
|
||||
val app = application as BasedBankApp
|
||||
if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
|
||||
// Came from fresh manual login — accounts ready, rest fetched in background
|
||||
val mibAccounts = app.accounts.filter { !it.profileType.startsWith("BML") && it.profileType != "FAHIPAY" }
|
||||
val mibAccounts = app.accounts.filter { it.bank == "MIB" }
|
||||
val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
||||
viewModel.accounts.value = merged
|
||||
viewModel.accounts.value = merged.filterVisibleAccounts()
|
||||
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
|
||||
if (app.bmlAccounts.isNotEmpty()) {
|
||||
val byLoginId = app.bmlAccounts.groupBy { it.loginTag.removePrefix("bml_") }
|
||||
@@ -145,7 +145,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
val cachedLimits = ForeignLimitsCache.load(this)
|
||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||
|
||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||
refreshFinancing(app.mibSession, app.mibProfiles.filterVisibleProfiles())
|
||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||
} else {
|
||||
// Came from lock screen — show caches immediately, refresh everything in background
|
||||
@@ -364,12 +364,12 @@ class HomeActivity : AppCompatActivity() {
|
||||
// Immediately drop accounts for logged-out banks from the displayed list
|
||||
val current = viewModel.accounts.value ?: emptyList()
|
||||
viewModel.accounts.value = current.filter { acc ->
|
||||
if (!hasMib && !acc.profileType.startsWith("BML") && acc.profileType != "FAHIPAY") return@filter false
|
||||
if (acc.profileType.startsWith("BML")) {
|
||||
if (!hasMib && acc.bank == "MIB") return@filter false
|
||||
if (acc.bank == "BML") {
|
||||
val loginId = acc.loginTag.removePrefix("bml_")
|
||||
return@filter loginId in bmlLoginIds
|
||||
}
|
||||
if (!hasFahipay && acc.profileType == "FAHIPAY") return@filter false
|
||||
if (!hasFahipay && acc.bank == "FAHIPAY") return@filter false
|
||||
true
|
||||
}
|
||||
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
|
||||
@@ -395,6 +395,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
app.mibSession = flow.lastSession
|
||||
app.mibProfiles = flow.lastProfiles
|
||||
AccountCache.save(this@HomeActivity, accounts)
|
||||
CredentialStore(this@HomeActivity).saveMibProfiles(flow.lastProfiles)
|
||||
accounts
|
||||
} catch (_: Exception) { AccountCache.load(this@HomeActivity) }
|
||||
}
|
||||
@@ -488,14 +489,34 @@ class HomeActivity : AppCompatActivity() {
|
||||
|
||||
val app = application as BasedBankApp
|
||||
app.bmlAccounts = bmlAccounts
|
||||
viewModel.accounts.postValue(mibAccounts + bmlAccounts + fahipayAccounts)
|
||||
viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts())
|
||||
binding.refreshIndicator.visibility = View.GONE
|
||||
|
||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||
refreshFinancing(app.mibSession, app.mibProfiles.filterVisibleProfiles())
|
||||
}
|
||||
}
|
||||
|
||||
/** Filters MIB accounts whose profileId the user has hidden in settings. */
|
||||
private fun List<MibAccount>.filterVisibleAccounts(): List<MibAccount> {
|
||||
val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds()
|
||||
if (hidden.isEmpty()) return this
|
||||
return filter { it.bank != "MIB" || it.profileId !in hidden }
|
||||
}
|
||||
|
||||
/** Filters MIB profiles the user has hidden — prevents API requests for hidden profiles. */
|
||||
private fun List<MibProfile>.filterVisibleProfiles(): List<MibProfile> {
|
||||
val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds()
|
||||
if (hidden.isEmpty()) return this
|
||||
return filter { it.profileId !in hidden }
|
||||
}
|
||||
|
||||
/** Called by SettingsLoginsFragment after the user changes profile visibility. */
|
||||
fun applyProfileVisibility() {
|
||||
val current = viewModel.accounts.value ?: return
|
||||
viewModel.accounts.value = current.filterVisibleAccounts()
|
||||
}
|
||||
|
||||
private fun refreshBmlLimits(session: BmlSession) {
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
lifecycleScope.launch {
|
||||
@@ -550,7 +571,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
if (cats.isNotEmpty()) viewModel.contactCategories.value = cats
|
||||
}
|
||||
// Refresh all banks in background
|
||||
refreshContacts(app.mibSession, app.mibProfiles)
|
||||
refreshContacts(app.mibSession, app.mibProfiles.filterVisibleProfiles())
|
||||
refreshBmlContacts(app)
|
||||
if (app.fahipaySession != null) refreshFahipayContacts(app.fahipaySession!!)
|
||||
}
|
||||
@@ -630,7 +651,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
val app = application as BasedBankApp
|
||||
lifecycleScope.launch {
|
||||
val current = viewModel.accounts.value ?: emptyList()
|
||||
if (src.profileType == "FAHIPAY") {
|
||||
if (src.bank == "FAHIPAY") {
|
||||
val fresh = withContext(Dispatchers.IO) {
|
||||
val sess = app.fahipaySession ?: return@withContext null
|
||||
try {
|
||||
@@ -645,9 +666,9 @@ class HomeActivity : AppCompatActivity() {
|
||||
accounts
|
||||
} catch (_: Exception) { null }
|
||||
} ?: return@launch
|
||||
val others = current.filter { it.profileType != "FAHIPAY" }
|
||||
val others = current.filter { it.bank != "FAHIPAY" }
|
||||
viewModel.accounts.postValue(others + fresh)
|
||||
} else if (src.profileType.startsWith("BML")) {
|
||||
} else if (src.bank == "BML") {
|
||||
val loginId = src.loginTag.removePrefix("bml_")
|
||||
val fresh = withContext(Dispatchers.IO) {
|
||||
val sess = app.bmlSessionFor(src) ?: return@withContext null
|
||||
@@ -659,20 +680,36 @@ class HomeActivity : AppCompatActivity() {
|
||||
accounts
|
||||
} catch (_: Exception) { null }
|
||||
} ?: return@launch
|
||||
val otherAccounts = current.filter { !it.profileType.startsWith("BML") || it.loginTag != src.loginTag }
|
||||
val otherAccounts = current.filter { it.bank != "BML" || it.loginTag != src.loginTag }
|
||||
viewModel.accounts.postValue(otherAccounts + fresh)
|
||||
} else {
|
||||
val fresh = withContext(Dispatchers.IO) {
|
||||
val sess = app.mibSession ?: return@withContext null
|
||||
val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId } ?: return@withContext null
|
||||
try { MibLoginFlow(CredentialStore(this@HomeActivity)).fetchAllProfiles(sess, listOf(profile), src.loginTag) }
|
||||
catch (_: Exception) { null }
|
||||
val store = CredentialStore(this@HomeActivity)
|
||||
val hidden = store.getHiddenMibProfileIds()
|
||||
val allVisible = app.mibProfiles.filter { hidden.isEmpty() || it.profileId !in hidden }
|
||||
// Try P47 for all visible profiles (fast path, works for multi-profile)
|
||||
val sess = app.mibSession
|
||||
if (sess != null && allVisible.isNotEmpty()) {
|
||||
try {
|
||||
val accounts = MibLoginFlow(store).fetchAllProfiles(sess, allVisible, src.loginTag)
|
||||
if (accounts.isNotEmpty()) return@withContext accounts
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
// P47 returned nothing — re-login to get fresh balances (handles single-profile A41 path)
|
||||
val creds = store.loadMibCredentials() ?: return@withContext null
|
||||
try {
|
||||
val flow = MibLoginFlow(store)
|
||||
val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||
app.mibSession = flow.lastSession
|
||||
app.mibProfiles = flow.lastProfiles
|
||||
store.saveMibProfiles(flow.lastProfiles)
|
||||
accounts.takeIf { it.isNotEmpty() }
|
||||
} catch (_: Exception) { null }
|
||||
} ?: return@launch
|
||||
// Replace accounts from this profile only, keep everything else
|
||||
val others = current.filter { it.profileId != src.profileId || it.profileType.startsWith("BML") }
|
||||
val merged = others + fresh
|
||||
AccountCache.save(this@HomeActivity, merged.filter { !it.profileType.startsWith("BML") })
|
||||
viewModel.accounts.postValue(merged)
|
||||
// Replace all MIB accounts with fresh data
|
||||
val others = current.filter { it.bank != "MIB" }
|
||||
AccountCache.save(this@HomeActivity, fresh)
|
||||
viewModel.accounts.postValue(others + fresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
@@ -14,6 +15,7 @@ import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibProfile
|
||||
import sh.sar.basedbank.api.mib.TransactionCache
|
||||
import sh.sar.basedbank.databinding.FragmentSettingsLoginsBinding
|
||||
import sh.sar.basedbank.ui.login.LoginActivity
|
||||
@@ -67,22 +69,9 @@ class SettingsLoginsFragment : Fragment() {
|
||||
if (hasMib) {
|
||||
val profile = store.loadMibUserProfile()
|
||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name)
|
||||
val profileNames = AccountCache.load(ctx).map { it.profileName }.filter { it.isNotBlank() }.distinct()
|
||||
val mibProfiles = store.loadMibProfiles()
|
||||
addLoginRow(container, R.drawable.mib_logo, displayName) {
|
||||
showLoginDetails(
|
||||
title = getString(R.string.mib_name),
|
||||
details = buildString {
|
||||
if (!profile?.fullName.isNullOrBlank()) appendLine("${getString(R.string.login_detail_name)}: ${profile!!.fullName}")
|
||||
if (!profile?.email.isNullOrBlank()) appendLine("${getString(R.string.login_detail_email)}: ${profile!!.email}")
|
||||
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
|
||||
if (profileNames.isNotEmpty()) {
|
||||
appendLine()
|
||||
appendLine(getString(R.string.login_detail_profiles))
|
||||
profileNames.forEach { appendLine(" • $it") }
|
||||
}
|
||||
}.trim(),
|
||||
onLogout = { confirmLogout(getString(R.string.mib_name)) { logoutMib(store) } }
|
||||
)
|
||||
showMibLoginDetails(store, profile, mibProfiles)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +143,117 @@ class SettingsLoginsFragment : Fragment() {
|
||||
container.addView(row)
|
||||
}
|
||||
|
||||
private fun showMibLoginDetails(
|
||||
store: CredentialStore,
|
||||
profile: CredentialStore.MibUserProfile?,
|
||||
mibProfiles: List<MibProfile>
|
||||
) {
|
||||
val ctx = requireContext()
|
||||
val dp = ctx.resources.displayMetrics.density
|
||||
val originalHidden = store.getHiddenMibProfileIds()
|
||||
val hidden = originalHidden.toMutableSet()
|
||||
|
||||
val scroll = android.widget.ScrollView(ctx)
|
||||
val container = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
val pad = (16 * dp).toInt()
|
||||
setPadding(pad, (8 * dp).toInt(), pad, pad)
|
||||
}
|
||||
scroll.addView(container)
|
||||
|
||||
// Account info lines
|
||||
listOfNotNull(
|
||||
profile?.fullName?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_name)}: $it" },
|
||||
profile?.email?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_email)}: $it" },
|
||||
profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: $it" }
|
||||
).forEach { line ->
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = line
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||
it.bottomMargin = (4 * dp).toInt()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (mibProfiles.isNotEmpty()) {
|
||||
if (profile != null) {
|
||||
container.addView(View(ctx).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (1 * dp).toInt()).also {
|
||||
it.topMargin = (12 * dp).toInt(); it.bottomMargin = (12 * dp).toInt()
|
||||
}
|
||||
setBackgroundColor(0x1F000000)
|
||||
})
|
||||
}
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = getString(R.string.login_detail_profiles)
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelMedium)
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||
it.bottomMargin = (8 * dp).toInt()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Build checkbox rows — wired up after dialog.show() so we can reference the Save button
|
||||
val checkboxRows = mibProfiles.map { p ->
|
||||
val row = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||
it.bottomMargin = (4 * dp).toInt()
|
||||
}
|
||||
}
|
||||
val textCol = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
textCol.addView(TextView(ctx).apply {
|
||||
text = p.name
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
|
||||
})
|
||||
if (p.cifType.isNotBlank()) {
|
||||
textCol.addView(TextView(ctx).apply {
|
||||
text = p.cifType
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall)
|
||||
alpha = 0.6f
|
||||
})
|
||||
}
|
||||
val cb = CheckBox(ctx).apply { isChecked = p.profileId !in hidden }
|
||||
row.addView(textCol)
|
||||
row.addView(cb)
|
||||
container.addView(row)
|
||||
p to cb
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(ctx)
|
||||
.setTitle(getString(R.string.mib_name))
|
||||
.setView(scroll)
|
||||
.setPositiveButton(R.string.save, null) // null — set manually after show()
|
||||
.setNeutralButton(R.string.close, null)
|
||||
.setNegativeButton(R.string.settings_logout) { _, _ ->
|
||||
confirmLogout(getString(R.string.mib_name)) { logoutMib(store) }
|
||||
}
|
||||
.show()
|
||||
|
||||
val saveBtn = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
|
||||
saveBtn.isEnabled = false
|
||||
|
||||
checkboxRows.forEach { (p, cb) ->
|
||||
cb.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId)
|
||||
val atLeastOneVisible = mibProfiles.any { it.profileId !in hidden }
|
||||
saveBtn.isEnabled = hidden != originalHidden && atLeastOneVisible
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.setOnClickListener {
|
||||
store.setHiddenMibProfileIds(hidden)
|
||||
clearAllCaches(ctx)
|
||||
dialog.dismiss()
|
||||
(activity as? HomeActivity)?.relogin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(title)
|
||||
|
||||
@@ -49,6 +49,7 @@ import sh.sar.basedbank.api.mib.MibTransferResult
|
||||
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||
import sh.sar.basedbank.util.AccountListParser
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.util.AccountInputParser
|
||||
import sh.sar.basedbank.util.PaymvQrParser
|
||||
@@ -209,16 +210,15 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun showFromCard(account: MibAccount) {
|
||||
val isBml = account.profileType.startsWith("BML")
|
||||
val colorHex = when {
|
||||
isBml -> "#0066A1"
|
||||
account.profileType == "FAHIPAY" -> "#15BEA7"
|
||||
else -> "#FE860E"
|
||||
val colorHex = when (account.bank) {
|
||||
"BML" -> "#0066A1"
|
||||
"FAHIPAY" -> "#15BEA7"
|
||||
else -> "#FE860E"
|
||||
}
|
||||
val bankLabel = when {
|
||||
isBml -> "BML"
|
||||
account.profileType == "FAHIPAY" -> "FP"
|
||||
else -> null
|
||||
val bankLabel = when (account.bank) {
|
||||
"BML" -> "BML"
|
||||
"FAHIPAY" -> "FP"
|
||||
else -> null
|
||||
}
|
||||
val typeLabel = when {
|
||||
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
||||
@@ -234,7 +234,7 @@ class TransferFragment : Fragment() {
|
||||
binding.tilFrom.visibility = View.GONE
|
||||
binding.cardFromInfo.visibility = View.VISIBLE
|
||||
|
||||
if (!isBml && account.profileImageHash != null) {
|
||||
if (account.bank != "BML" && account.profileImageHash != null) {
|
||||
loadFromPhoto(account.profileImageHash)
|
||||
}
|
||||
}
|
||||
@@ -312,7 +312,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
// Fahipay source: only phone numbers are supported
|
||||
if (selectedAccount?.profileType == "FAHIPAY") {
|
||||
if (selectedAccount?.bank == "FAHIPAY") {
|
||||
if (AccountInputParser.detect(accountNumber) == AccountInputParser.InputType.PHONE) {
|
||||
lookupFahipayTarget(accountNumber)
|
||||
} else {
|
||||
@@ -328,7 +328,7 @@ class TransferFragment : Fragment() {
|
||||
return
|
||||
}
|
||||
|
||||
val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true
|
||||
val isBmlSource = selectedAccount?.bank == "BML"
|
||||
|
||||
startLookupLoading()
|
||||
|
||||
@@ -567,7 +567,7 @@ class TransferFragment : Fragment() {
|
||||
binding.tilAmount.error = null
|
||||
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
||||
|
||||
val isSrcBml = src.profileType.startsWith("BML")
|
||||
val isSrcBml = src.bank == "BML"
|
||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||
val currency = src.currencyName.ifBlank { "MVR" }
|
||||
@@ -1009,11 +1009,11 @@ class TransferFragment : Fragment() {
|
||||
.also { it.root.tag = it }
|
||||
}
|
||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
val isBmlAccount = acc.profileType.startsWith("BML")
|
||||
val isBmlAccount = acc.bank == "BML"
|
||||
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"
|
||||
b.tvDropdownBalance.text = AccountListParser.from(acc)?.balance ?: ""
|
||||
b.root.alpha = if (inactive) 0.4f else 1f
|
||||
b.root
|
||||
}
|
||||
|
||||
@@ -61,10 +61,10 @@ class TransferHistoryFragment : Fragment() {
|
||||
var fahipayTotal: Int = -1
|
||||
) {
|
||||
fun hasMore(): Boolean = when {
|
||||
account.profileType == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
|
||||
account.profileType.startsWith("BML") -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||
account.bank == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
|
||||
account.bank == "BML" -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ class TransferHistoryFragment : Fragment() {
|
||||
val results = mutableListOf<Transaction>()
|
||||
|
||||
// BML accounts: fetch in parallel
|
||||
val bmlStates = activeStates.filter { it.account.profileType.startsWith("BML") }
|
||||
val bmlStates = activeStates.filter { it.account.bank == "BML" }
|
||||
results.addAll(bmlStates.map { state ->
|
||||
async {
|
||||
try {
|
||||
@@ -187,7 +187,7 @@ class TransferHistoryFragment : Fragment() {
|
||||
}.awaitAll().flatten())
|
||||
|
||||
// Fahipay accounts
|
||||
val fahipayStates = activeStates.filter { it.account.profileType == "FAHIPAY" }
|
||||
val fahipayStates = activeStates.filter { it.account.bank == "FAHIPAY" }
|
||||
for (state in fahipayStates) {
|
||||
val session = app.fahipaySession ?: continue
|
||||
try {
|
||||
@@ -206,9 +206,7 @@ class TransferHistoryFragment : Fragment() {
|
||||
}
|
||||
|
||||
// MIB accounts: serialized per profile, protected by mutex to prevent session race
|
||||
val mibStates = activeStates.filter {
|
||||
!it.account.profileType.startsWith("BML") && it.account.profileType != "FAHIPAY"
|
||||
}
|
||||
val mibStates = activeStates.filter { it.account.bank == "MIB" }
|
||||
for ((profileId, states) in mibStates.groupBy { it.account.profileId }) {
|
||||
val session = mibSession ?: break
|
||||
app.mibMutex.withLock {
|
||||
|
||||
@@ -194,6 +194,7 @@ class CredentialsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
AccountCache.save(requireContext(), accounts)
|
||||
CredentialStore(requireContext()).saveMibProfiles(flow.lastProfiles)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
app.accounts = accounts
|
||||
app.mibSession = flow.lastSession
|
||||
|
||||
Reference in New Issue
Block a user