huge refactor.. might need to revert later

This commit is contained in:
2026-05-20 22:43:29 +05:00
parent e894f81887
commit 6d48c27391
49 changed files with 1224 additions and 1072 deletions
@@ -9,8 +9,8 @@ import android.view.View
import android.view.ViewGroup
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.api.models.BankAccount
import sh.sar.basedbank.api.models.BankTransaction
import sh.sar.basedbank.util.AccountHistoryDisplay
import sh.sar.basedbank.databinding.ItemAccountHistoryHeaderBinding
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
@@ -21,13 +21,13 @@ import java.util.Date
import java.util.Locale
class AccountHistoryAdapter(
private val account: MibAccount,
private val account: BankAccount,
private val display: AccountHistoryDisplay
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private sealed class Item {
data class DateHeader(val label: String) : Item()
data class Trx(val transaction: Transaction) : Item()
data class Trx(val transaction: BankTransaction) : Item()
}
private val displayItems = mutableListOf<Item>()
@@ -36,7 +36,7 @@ class AccountHistoryAdapter(
private val iconUrlCache = mutableMapOf<String, Bitmap>()
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
var onIconUrlNeeded: ((url: String) -> Unit)? = null
var onTransferClick: ((MibAccount) -> Unit)? = null
var onTransferClick: ((BankAccount) -> Unit)? = null
private var hideAmounts: Boolean = false
fun setHideAmounts(hide: Boolean) {
@@ -84,7 +84,7 @@ class AccountHistoryAdapter(
* Display the given (already sorted + filtered) list with date group headers.
* Silently resets the loading footer so notifyDataSetChanged covers everything.
*/
fun setTransactions(transactions: List<Transaction>) {
fun setTransactions(transactions: List<BankTransaction>) {
_showLoadingFooter = false
displayItems.clear()
lastInsertedDateKey = ""
@@ -105,7 +105,7 @@ class AccountHistoryAdapter(
* Appends [newTransactions] (assumed to be older than all existing items) using incremental
* notifications, so the RecyclerView doesn't reset scroll position.
*/
fun appendTransactions(newTransactions: List<Transaction>) {
fun appendTransactions(newTransactions: List<BankTransaction>) {
if (newTransactions.isEmpty()) return
if (_showLoadingFooter) {
val pos = itemCount - 1
@@ -184,7 +184,7 @@ class AccountHistoryAdapter(
inner class TransactionVH(private val b: ItemTransactionBinding) :
RecyclerView.ViewHolder(b.root) {
fun bind(trx: Transaction) {
fun bind(trx: BankTransaction) {
val isCredit = trx.amount >= 0
val color = sourceColor(trx.source)
val name = trx.counterpartyName ?: trx.description
@@ -237,7 +237,7 @@ class AccountHistoryAdapter(
b.root.setOnClickListener { showDetail(trx) }
}
private fun showDetail(trx: Transaction) {
private fun showDetail(trx: BankTransaction) {
val ctx = b.root.context
val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description
val details = buildString {
@@ -22,9 +22,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.Transaction
import sh.sar.basedbank.api.models.BankTransaction
import sh.sar.basedbank.api.mib.TransactionCache
import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding
import sh.sar.basedbank.util.AccountHistoryParser
@@ -39,10 +39,10 @@ class AccountHistoryFragment : Fragment() {
private val viewModel: HomeViewModel by activityViewModels()
private lateinit var adapter: AccountHistoryAdapter
private lateinit var account: MibAccount
private lateinit var account: BankAccount
private lateinit var fetcher: HistoryFetcher
private val allTransactions = mutableListOf<Transaction>()
private val allTransactions = mutableListOf<BankTransaction>()
private var searchQuery = ""
private var firstPageDone = false
private val pendingImageNames = mutableSetOf<String>()
@@ -53,7 +53,7 @@ class AccountHistoryFragment : Fragment() {
companion object {
private const val ARG_ACCOUNT_NUMBER = "account_number"
fun newInstance(account: MibAccount) = AccountHistoryFragment().apply {
fun newInstance(account: BankAccount) = AccountHistoryFragment().apply {
arguments = Bundle().apply {
putString(ARG_ACCOUNT_NUMBER, account.accountNumber)
}
@@ -8,7 +8,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.databinding.ItemAccountBinding
import sh.sar.basedbank.databinding.ItemCardBinding
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
@@ -16,22 +16,22 @@ import sh.sar.basedbank.util.AccountListDisplay
import sh.sar.basedbank.util.AccountListParser
class AccountsAdapter(
accounts: List<MibAccount>,
private val onAccountClick: (MibAccount) -> Unit = {}
accounts: List<BankAccount>,
private val onAccountClick: (BankAccount) -> Unit = {}
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var onTransferClick: ((MibAccount) -> Unit)? = null
var onTransferClick: ((BankAccount) -> Unit)? = null
private var hideAmounts: Boolean = false
private sealed class Item {
data class SectionTitle(val label: String) : Item()
data class Account(val account: MibAccount, val display: AccountListDisplay) : Item()
data class Card(val account: MibAccount, val display: AccountListDisplay) : Item()
data class Account(val account: BankAccount, val display: AccountListDisplay) : Item()
data class Card(val account: BankAccount, val display: AccountListDisplay) : Item()
}
private val items: MutableList<Item> = buildItems(accounts).toMutableList()
fun updateAccounts(accounts: List<MibAccount>) {
fun updateAccounts(accounts: List<BankAccount>) {
items.clear()
items.addAll(buildItems(accounts))
notifyDataSetChanged()
@@ -43,12 +43,12 @@ class AccountsAdapter(
notifyDataSetChanged()
}
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
private fun buildItems(accounts: List<BankAccount>): List<Item> = buildList {
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 }
val groups = LinkedHashMap<String, MutableList<Pair<MibAccount, AccountListDisplay>>>()
val groups = LinkedHashMap<String, MutableList<Pair<BankAccount, AccountListDisplay>>>()
for ((acc, display) in nonCards) {
val title = sectionTitle(acc)
groups.getOrPut(title) { mutableListOf() }.add(acc to display)
@@ -64,7 +64,7 @@ class AccountsAdapter(
}
}
private fun sectionTitle(account: MibAccount): String {
private fun sectionTitle(account: BankAccount): String {
val bankName = when (account.bank) {
"BML" -> "Bank of Maldives"
"FAHIPAY" -> "Fahipay"
@@ -112,7 +112,7 @@ class AccountsAdapter(
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: MibAccount, display: AccountListDisplay) {
fun bind(account: BankAccount, display: AccountListDisplay) {
binding.tvAccountName.text = display.name
binding.tvAccountNumber.text = display.number
binding.tvAccountType.text = display.typeLabel
@@ -128,7 +128,7 @@ class AccountsAdapter(
private inner class CardViewHolder(private val binding: ItemCardBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: MibAccount, display: AccountListDisplay) {
fun bind(account: BankAccount, display: AccountListDisplay) {
binding.ivCardBrand.setImageResource(display.cardBrandIcon)
binding.tvCardName.text = display.name
binding.tvCardNumber.text = display.number
@@ -30,8 +30,9 @@ import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlAccountValidation
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
import sh.sar.basedbank.api.bml.BmlContactsClient
import sh.sar.basedbank.api.bml.BmlValidateClient
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibProfile
import sh.sar.basedbank.api.mib.MibTransferClient
@@ -64,7 +65,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
private var selectedImageBase64: String = ""
private var selectedCategoryId: String = "0"
private var categories: List<MibBeneficiaryCategory> = emptyList()
private var categories: List<BankContactCategory> = emptyList()
private val imagePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri ?: return@registerForActivityResult
@@ -244,14 +245,12 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
private fun lookupForBml(input: String): BmlAccountValidation? {
val loginId = selectedDest?.bmlLoginId ?: return null
val bmlSess = app.bmlSessions[loginId] ?: return null
val bmlFlow = BmlLoginFlow()
// 1) Try BML validate
val validated = try { bmlFlow.validateAccount(bmlSess, input) } catch (_: Exception) { null }
val validated = try { BmlValidateClient().validateAccount(bmlSess, input) } catch (_: Exception) { null }
if (validated != null) return validated
// 2) Try BML MIB verify
val mibVerified = try { bmlFlow.verifyMibAccount(bmlSess, input) } catch (_: Exception) { null }
val mibVerified = try { BmlValidateClient().verifyMibAccount(bmlSess, input) } catch (_: Exception) { null }
if (mibVerified != null) return mibVerified
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
@@ -295,7 +294,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
// MIB lookup failed (e.g. BML USD account) — fall back to BML validate
val bmlSess = app.anyBmlSession() ?: return null
return try { BmlLoginFlow().validateAccount(bmlSess, input) } catch (_: Exception) { null }
return try { BmlValidateClient().validateAccount(bmlSess, input) } catch (_: Exception) { null }
}
private fun showLookupResult(validation: BmlAccountValidation, input: String) {
@@ -421,15 +420,14 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
val loginId = selectedDest?.bmlLoginId ?: return false
val bmlSess = app.bmlSessions[loginId] ?: return false
val lookup = bmlLookup ?: return false
val bmlFlow = BmlLoginFlow()
val account = lookup.account
return when {
account.matches(Regex("^7\\d{12}$")) ->
// BML account → IAT
bmlFlow.saveContact(bmlSess, "IAT", account, alias)
BmlContactsClient().saveContact(bmlSess, "IAT", account, alias)
account.matches(Regex("^9\\d{16}$")) ->
// MIB internal → DOT; swift is BML's internal UUID for MIB bank
bmlFlow.saveContact(bmlSess, "DOT", account, alias,
BmlContactsClient().saveContact(bmlSess, "DOT", account, alias,
currency = lookup.currency, name = lookup.name, swift = MIB_SWIFT_ON_BML)
else -> false
}
@@ -489,7 +487,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
if (dest.isBml) {
val loginId = dest.bmlLoginId ?: return@launch
val bmlSess = app.bmlSessions[loginId] ?: return@launch
val fresh = BmlLoginFlow().fetchContacts(bmlSess, loginId)
val fresh = BmlContactsClient().fetchContacts(bmlSess, loginId)
val existing = viewModel.contacts.value ?: emptyList()
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
viewModel.contacts.postValue(merged)
@@ -23,7 +23,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.databinding.FragmentContactsBinding
import sh.sar.basedbank.util.ContactDisplay
@@ -153,7 +153,7 @@ class ContactsFragment : Fragment() {
}.also { it.attach() }
}
private fun rebuildPager(cats: List<MibBeneficiaryCategory>) {
private fun rebuildPager(cats: List<BankContactCategory>) {
val pages = buildList {
add(TabPage(null, getString(R.string.contacts_tab_all)))
cats.forEach { add(TabPage(it.id, it.categoryName)) }
@@ -12,7 +12,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlForeignLimit
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.databinding.FragmentDashboardBinding
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
@@ -68,7 +68,7 @@ class DashboardFragment : Fragment() {
}
}
private fun updateBalances(accounts: List<MibAccount>) {
private fun updateBalances(accounts: List<BankAccount>) {
val hide = viewModel.hideAmounts.value ?: false
if (hide) {
binding.tvMvrBalance.text = "MVR ••••••"
@@ -36,18 +36,22 @@ import okhttp3.RequestBody.Companion.toRequestBody
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.AuthExpiredException
import sh.sar.basedbank.api.bml.BmlAccountClient
import sh.sar.basedbank.api.bml.BmlActivationResult
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.bml.BmlContactsClient
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
import sh.sar.basedbank.api.bml.BmlProfile
import sh.sar.basedbank.api.bml.BmlSession
import sh.sar.basedbank.api.fahipay.FahipayAccountClient
import sh.sar.basedbank.api.fahipay.FahipayContactsClient
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
import sh.sar.basedbank.api.fahipay.FahipaySession
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding
import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.api.mib.MibBeneficiary
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.api.mib.MibProfile
@@ -490,7 +494,7 @@ fun applyNavLabelVisibility() {
val loginTag = "bml_$loginId"
val app = application as BasedBankApp
val savedProfiles = store.loadBmlProfiles(loginId)
val allAccounts = mutableListOf<MibAccount>()
val allAccounts = mutableListOf<BankAccount>()
var anyExpired = savedProfiles.isEmpty()
// Try each saved profile's cached session
@@ -499,7 +503,7 @@ fun applyNavLabelVisibility() {
if (saved != null) {
try {
val session = BmlSession(saved.first, saved.second)
val accounts = BmlLoginFlow().fetchAccounts(session, loginTag, profile.name, profile.profileId)
val accounts = BmlAccountClient().fetchAccounts(session, loginTag, profile.name, profile.profileId)
app.bmlSessions[profile.profileId] = session
allAccounts += accounts
} catch (_: AuthExpiredException) { anyExpired = true
@@ -517,7 +521,7 @@ fun applyNavLabelVisibility() {
if (legacyToken != null) {
try {
val session = BmlSession(legacyToken.first, legacyToken.second)
val accounts = BmlLoginFlow().fetchAccounts(session, loginTag)
val accounts = BmlAccountClient().fetchAccounts(session, loginTag)
app.bmlSessions[loginId] = session
allAccounts += accounts
anyExpired = false
@@ -565,7 +569,7 @@ fun applyNavLabelVisibility() {
}
if (allAccounts.isNotEmpty()) AccountCache.saveBml(this@HomeActivity, loginId, allAccounts)
allAccounts as List<MibAccount>
allAccounts as List<BankAccount>
}
}
@@ -581,10 +585,9 @@ fun applyNavLabelVisibility() {
if (savedSession != null) {
try {
val session = FahipaySession(savedSession.first, savedSession.second)
fahipayFlow.setSessionCookie(session.sessionCookie)
val balance = fahipayFlow.fetchBalance(session)
val profile = fahipayFlow.fetchProfile(session)
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
val balance = FahipayAccountClient().fetchBalance(session)
val profile = FahipayAccountClient().fetchProfile(session)
val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag))
val app = application as BasedBankApp
app.fahipaySessions[loginId] = session
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
@@ -601,9 +604,9 @@ fun applyNavLabelVisibility() {
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
val session = FahipaySession(authId, cookieValue)
store.saveFahipaySession(loginId, authId, cookieValue)
val profile = fahipayFlow.fetchProfile(session)
val balance = fahipayFlow.fetchBalance(session)
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
val profile = FahipayAccountClient().fetchProfile(session)
val balance = FahipayAccountClient().fetchBalance(session)
val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag))
val app = application as BasedBankApp
app.fahipaySessions[loginId] = session
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
@@ -636,7 +639,7 @@ fun applyNavLabelVisibility() {
}
/** Filters accounts whose profileId the user has hidden in settings. */
private fun List<MibAccount>.filterVisibleAccounts(): List<MibAccount> {
private fun List<BankAccount>.filterVisibleAccounts(): List<BankAccount> {
val store = CredentialStore(this@HomeActivity)
return filter { acc ->
when (acc.bank) {
@@ -669,11 +672,10 @@ fun applyNavLabelVisibility() {
}
private fun refreshBmlLimits(session: BmlSession) {
val bmlFlow = BmlLoginFlow()
lifecycleScope.launch {
try {
val (userName, limits) = withContext(Dispatchers.IO) {
Pair(bmlFlow.fetchUserInfo(session)?.fullName ?: "", bmlFlow.fetchForeignLimits(session))
Pair(BmlAccountClient().fetchUserInfo(session)?.fullName ?: "", BmlForeignLimitsClient().fetchForeignLimits(session))
}
val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
val idx = existing.indexOfFirst { it.userName == userName }
@@ -693,7 +695,7 @@ fun applyNavLabelVisibility() {
val allBmlContacts = withContext(Dispatchers.IO) {
store.getBmlLoginIds().flatMap { loginId ->
val session = app.anyBmlSessionFor(loginId) ?: return@flatMap emptyList()
val contacts = BmlLoginFlow().fetchContacts(session, loginId)
val contacts = BmlContactsClient().fetchContacts(session, loginId)
if (contacts.isNotEmpty()) ContactsCache.saveBml(this@HomeActivity, loginId, contacts)
contacts
}
@@ -736,13 +738,11 @@ fun applyNavLabelVisibility() {
lifecycleScope.launch {
try {
val groups = withContext(Dispatchers.IO) {
val flow = FahipayLoginFlow()
flow.setSessionCookie(session.sessionCookie)
flow.fetchContacts(session)
FahipayContactsClient().fetchContacts(session)
}
if (groups.isEmpty()) return@launch
val contacts = groups.flatMap { it.contacts }
val categories = groups.map { MibBeneficiaryCategory(it.categoryId, it.label, it.contacts.size) }
val categories = groups.map { BankContactCategory(it.categoryId, it.label, it.contacts.size) }
ContactsCache.saveFahipay(this@HomeActivity, contacts, categories)
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
@@ -755,11 +755,11 @@ fun applyNavLabelVisibility() {
}
private fun mergeContacts(
mib: List<MibBeneficiary>,
bml: List<MibBeneficiary>
): List<MibBeneficiary> {
mib: List<BankContact>,
bml: List<BankContact>
): List<BankContact> {
val seen = mutableSetOf<String>()
val result = mutableListOf<MibBeneficiary>()
val result = mutableListOf<BankContact>()
for (c in mib) if (seen.add(c.benefNo)) result.add(c)
for (c in bml) if (seen.add(c.benefNo)) result.add(c)
return result
@@ -774,8 +774,8 @@ fun applyNavLabelVisibility() {
val (allContacts, allCategories) = withContext(Dispatchers.IO) {
val seenContacts = mutableSetOf<String>()
val seenCategories = mutableSetOf<String>()
val contacts = mutableListOf<MibBeneficiary>()
val categories = mutableListOf<MibBeneficiaryCategory>()
val contacts = mutableListOf<BankContact>()
val categories = mutableListOf<BankContactCategory>()
for (profile in profiles) {
try {
flow.switchProfile(session, profile)
@@ -803,7 +803,7 @@ fun applyNavLabelVisibility() {
}
}
fun refreshBalances(src: MibAccount) {
fun refreshBalances(src: BankAccount) {
val app = application as BasedBankApp
lifecycleScope.launch {
val current = viewModel.accounts.value ?: emptyList()
@@ -811,12 +811,10 @@ fun applyNavLabelVisibility() {
val fresh = withContext(Dispatchers.IO) {
val sess = app.fahipaySessionFor(src) ?: return@withContext null
try {
val flow = FahipayLoginFlow()
flow.setSessionCookie(sess.sessionCookie)
val balance = flow.fetchBalance(sess)
val profile = flow.fetchProfile(sess)
val balance = FahipayAccountClient().fetchBalance(sess)
val profile = FahipayAccountClient().fetchProfile(sess)
val loginTag = "fahipay_${profile.profileId}"
val accounts = listOf(flow.buildAccount(profile, balance, loginTag))
val accounts = listOf(FahipayAccountClient().buildAccount(profile, balance, loginTag))
val loginId = src.loginTag.removePrefix("fahipay_")
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != src.loginTag } + accounts
@@ -830,7 +828,7 @@ fun applyNavLabelVisibility() {
val fresh = withContext(Dispatchers.IO) {
val sess = app.bmlSessionFor(src) ?: return@withContext null
try {
val accounts = BmlLoginFlow().fetchAccounts(sess, src.loginTag)
val accounts = BmlAccountClient().fetchAccounts(sess, src.loginTag)
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
val otherBml = app.bmlAccounts.filter { it.loginTag != src.loginTag }
app.bmlAccounts = otherBml + accounts
@@ -3,16 +3,16 @@ package sh.sar.basedbank.ui.home
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import sh.sar.basedbank.api.bml.BmlForeignLimit
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibBeneficiary
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibFinanceDeal
class HomeViewModel : ViewModel() {
val accounts = MutableLiveData<List<MibAccount>>(emptyList())
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(emptyList())
val contacts = MutableLiveData<List<BankContact>>(emptyList())
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
@@ -14,7 +14,8 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.bml.BmlAccountClient
import sh.sar.basedbank.api.mib.MibProfileClient
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.FragmentOtpBinding
import sh.sar.basedbank.databinding.ItemOtpCardBinding
@@ -94,7 +95,7 @@ class OtpFragment : Fragment() {
val session = app.mibSessions[loginId] ?: continue
val flow = app.mibFlowFor(loginId)
val profile = withContext(Dispatchers.IO) {
try { flow.fetchPersonalProfile(session) } catch (_: Exception) { null }
try { MibProfileClient().fetchPersonalProfile(session) } catch (_: Exception) { null }
}
if (profile != null) {
store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile(
@@ -114,7 +115,7 @@ class OtpFragment : Fragment() {
if (store.loadBmlUserProfile(loginId)?.fullName.isNullOrBlank()) {
val session = app.bmlSessions[loginId] ?: continue
val info = withContext(Dispatchers.IO) {
try { BmlLoginFlow().fetchUserInfo(session) } catch (_: Exception) { null }
try { BmlAccountClient().fetchUserInfo(session) } catch (_: Exception) { null }
}
if (info != null) {
store.saveBmlUserProfile(loginId, CredentialStore.BmlUserProfile(
@@ -33,7 +33,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
import sh.sar.basedbank.util.PaymvQrParser
@@ -46,7 +46,7 @@ class PayMvQrFragment : Fragment() {
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
private var selectedAccount: MibAccount? = null
private var selectedAccount: BankAccount? = null
private var generatedBitmap: Bitmap? = null
private var generateJob: Job? = null
@@ -186,7 +186,7 @@ class PayMvQrFragment : Fragment() {
private fun renderQrCard(
ctx: Context,
account: MibAccount,
account: BankAccount,
qrPayload: String,
amountStr: String?
): Bitmap {
@@ -378,10 +378,10 @@ class PayMvQrFragment : Fragment() {
private inner class QrAccountAdapter(
private val context: Context,
private val accounts: List<MibAccount>
private val accounts: List<BankAccount>
) : BaseAdapter(), Filterable {
fun getAccount(position: Int): MibAccount? = accounts.getOrNull(position)
fun getAccount(position: Int): BankAccount? = accounts.getOrNull(position)
override fun getCount() = accounts.size
override fun getItem(position: Int) = accounts.getOrNull(position)
@@ -411,7 +411,7 @@ class PayMvQrFragment : Fragment() {
FilterResults().apply { values = accounts; count = accounts.size }
override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged()
override fun convertResultToString(r: Any?) =
(r as? MibAccount)?.let {
(r as? BankAccount)?.let {
val prefix = if (it.bank == "BML" && it.profileName.isNotBlank()) "${it.profileName} · " else ""
"$prefix${it.accountBriefName}"
} ?: ""
@@ -9,17 +9,17 @@ import android.view.ViewGroup
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import sh.sar.basedbank.api.mib.Transaction
import sh.sar.basedbank.api.models.BankTransaction
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
import sh.sar.basedbank.databinding.ItemTransactionBinding
/** Adapter for Transaction History — date-grouped, shows account name in secondary line. */
/** Adapter for BankTransaction History — date-grouped, shows account name in secondary line. */
class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private sealed class Item {
data class DateHeader(val label: String) : Item()
data class Trx(val transaction: Transaction) : Item()
data class Trx(val transaction: BankTransaction) : Item()
}
private val displayItems = mutableListOf<Item>()
@@ -67,7 +67,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
/** Replace the full sorted transaction list and rebuild date groups. */
fun setTransactions(transactions: List<Transaction>) {
fun setTransactions(transactions: List<BankTransaction>) {
_showLoadingFooter = false
displayItems.clear()
var lastDateKey = ""
@@ -114,7 +114,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
inner class TransactionVH(private val b: ItemTransactionBinding) :
RecyclerView.ViewHolder(b.root) {
fun bind(trx: Transaction) {
fun bind(trx: BankTransaction) {
val isCredit = trx.amount >= 0
val color = AccountHistoryAdapter.sourceColor(trx.source)
val name = trx.counterpartyName ?: trx.description
@@ -141,7 +141,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
b.tvDescription.text = trx.description
// Show account name in secondary line for Transaction History
// Show account name in secondary line for BankTransaction History
b.tvCounterparty.text = trx.accountDisplayName
b.tvCounterparty.visibility = View.VISIBLE
@@ -162,7 +162,7 @@ class TransactionAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
b.root.setOnClickListener { showDetail(trx) }
}
private fun showDetail(trx: Transaction) {
private fun showDetail(trx: BankTransaction) {
val ctx = b.root.context
val title = trx.counterpartyName?.takeIf { it.isNotBlank() } ?: trx.description
val details = buildString {
@@ -36,12 +36,13 @@ 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.bml.BmlTransferClient
import sh.sar.basedbank.api.bml.BmlTransferResult
import sh.sar.basedbank.api.bml.BmlValidateClient
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
import sh.sar.basedbank.api.fahipay.OoredooClient
import sh.sar.basedbank.api.bml.BmlTransferResult
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibBeneficiary
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
import sh.sar.basedbank.api.mib.MibLookupException
@@ -65,11 +66,11 @@ class TransferFragment : Fragment() {
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
private var selectedAccount: MibAccount? = null
private var selectedAccount: BankAccount? = null
private val session get() = selectedAccount
?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) }
?: (requireActivity().application as BasedBankApp).anyMibSession()
private fun bmlSessionFor(account: MibAccount?) =
private fun bmlSessionFor(account: BankAccount?) =
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
?: (requireActivity().application as BasedBankApp).anyBmlSession()
@@ -105,7 +106,7 @@ class TransferFragment : Fragment() {
private const val ARG_AMOUNT_PREFILL = "amount_prefill"
private const val ARG_REMARKS_PREFILL = "remarks_prefill"
fun newInstanceFrom(account: MibAccount) = TransferFragment().apply {
fun newInstanceFrom(account: BankAccount) = TransferFragment().apply {
arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) }
}
@@ -239,7 +240,7 @@ class TransferFragment : Fragment() {
}
}
private fun showFromCard(account: MibAccount) {
private fun showFromCard(account: BankAccount) {
val colorHex = when (account.bank) {
"BML" -> "#0066A1"
"FAHIPAY" -> "#15BEA7"
@@ -284,7 +285,7 @@ class TransferFragment : Fragment() {
}
}
private fun updateAmountPrefix(account: MibAccount) {
private fun updateAmountPrefix(account: BankAccount) {
binding.tilAmount.prefixText = if (account.currencyName == "USD") "USD " else "MVR "
}
@@ -369,10 +370,9 @@ class TransferFragment : Fragment() {
val info = withContext(Dispatchers.IO) {
if (isBmlSource && bmlSess != null) {
val inputType = AccountInputParser.detect(accountNumber)
val bmlFlow = BmlLoginFlow()
val bmlResult = try {
if (inputType == AccountInputParser.InputType.MIB_ACCOUNT) bmlFlow.verifyMibAccount(bmlSess, accountNumber)
else bmlFlow.validateAccount(bmlSess, accountNumber)
if (inputType == AccountInputParser.InputType.MIB_ACCOUNT) BmlValidateClient().verifyMibAccount(bmlSess, accountNumber)
else BmlValidateClient().validateAccount(bmlSess, accountNumber)
} catch (_: Exception) { null }
if (bmlResult != null) {
val bankId = when (bmlResult.trnType) {
@@ -393,7 +393,7 @@ class TransferFragment : Fragment() {
catch (e: MibLookupException) { errorMsg = e.message; null }
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
} else {
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
val bmlResult = try { BmlValidateClient().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
if (bmlResult != null) {
val bankId = when (bmlResult.trnType) {
"IAT" -> "MALBMVMV"
@@ -731,7 +731,7 @@ class TransferFragment : Fragment() {
}
private fun doMibTransfer(
src: MibAccount,
src: BankAccount,
destAccount: String,
destName: String,
destDisplay: String,
@@ -790,7 +790,7 @@ class TransferFragment : Fragment() {
mibReferenceNo = result.trxId,
mibTransactionDate = result.date
)
Triple(true, "Transaction ID: ${result.trxId}\n${result.date}", receipt)
Triple(true, "BankTransaction ID: ${result.trxId}\n${result.date}", receipt)
} else {
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
}
@@ -800,7 +800,7 @@ class TransferFragment : Fragment() {
}
private fun doBmlTransfer(
src: MibAccount,
src: BankAccount,
destAccount: String,
destDisplay: String,
amount: Double,
@@ -809,8 +809,8 @@ class TransferFragment : Fragment() {
isSrcCard: Boolean,
isDestMib: Boolean,
currency: String,
allAccounts: List<MibAccount>,
allContacts: List<MibBeneficiary>
allAccounts: List<BankAccount>,
allContacts: List<BankContact>
): Triple<Boolean, String, TransferReceiptData?> {
val loginId = src.loginTag.removePrefix("bml_")
val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
@@ -845,10 +845,9 @@ class TransferFragment : Fragment() {
}
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
val bmlFlow = BmlLoginFlow()
// Step 1: initiate
val initiated = try {
bmlFlow.initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
BmlTransferClient().initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
} catch (e: Exception) { return Triple(false, e.message ?: "Initiation failed", null) }
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
@@ -858,7 +857,7 @@ class TransferFragment : Fragment() {
?.let { Totp.generate(it) } ?: otp
return try {
val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
val result = BmlTransferClient().confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
if (result.success) {
val receipt = TransferReceiptData(
bank = "BML",
@@ -1000,12 +999,12 @@ class TransferFragment : Fragment() {
_binding = null
}
private fun MibAccount.toDisplayString() = "$accountBriefName · $accountNumber"
private fun BankAccount.toDisplayString() = "$accountBriefName · $accountNumber"
// items: String = section header, MibAccount = selectable row
// items: String = section header, BankAccount = selectable row
private inner class AccountDropdownAdapter(
private val context: Context,
accounts: List<MibAccount>
accounts: List<BankAccount>
) : BaseAdapter(), Filterable {
private val items: List<Any> = buildList {
@@ -1018,7 +1017,7 @@ class TransferFragment : Fragment() {
}
}
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
fun getAccount(position: Int): BankAccount? = (items.getOrNull(position) as? BankAccount)
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
override fun getCount() = items.size
@@ -1043,7 +1042,7 @@ class TransferFragment : Fragment() {
b.tvHeader.text = item
b.root
} else {
val acc = item as MibAccount
val acc = item as BankAccount
val b = if (convertView?.tag is ItemAccountDropdownBinding) {
convertView.tag as ItemAccountDropdownBinding
} else {
@@ -25,12 +25,12 @@ 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.bml.BmlHistoryClient
import sh.sar.basedbank.api.fahipay.FahipayHistoryClient
import sh.sar.basedbank.api.models.BankAccount
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.models.BankTransaction
import sh.sar.basedbank.api.mib.TransactionCache
import sh.sar.basedbank.databinding.FragmentTransferHistoryBinding
import sh.sar.basedbank.util.ContactImageCache
@@ -48,12 +48,12 @@ class TransferHistoryFragment : Fragment() {
private lateinit var adapter: TransactionAdapter
// All merged transactions sorted by date desc
private val allTransactions = mutableListOf<Transaction>()
private val allTransactions = mutableListOf<BankTransaction>()
private var searchQuery = ""
// Per-account pagination state
private data class AccountState(
val account: MibAccount,
val account: BankAccount,
var mibNextStart: Int = 1,
var mibTotalCount: Int = -1,
var bmlNextPage: Int = 1,
@@ -158,7 +158,7 @@ class TransferHistoryFragment : Fragment() {
lifecycleScope.launch {
val newTransactions = withContext(Dispatchers.IO) {
val results = mutableListOf<Transaction>()
val results = mutableListOf<BankTransaction>()
// BML accounts: fetch in parallel
val bmlStates = activeStates.filter { it.account.bank == "BML" }
@@ -172,7 +172,7 @@ class TransferHistoryFragment : Fragment() {
cal.add(Calendar.MONTH, -state.cardMonthOffset)
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
state.cardMonthOffset++
BmlLoginFlow().fetchCardHistory(
BmlHistoryClient().fetchCardHistory(
session = session,
cardId = state.account.internalId,
accountDisplayName = state.account.accountBriefName,
@@ -182,7 +182,7 @@ class TransferHistoryFragment : Fragment() {
}
else -> {
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
val (list, totalPages) = BmlHistoryClient().fetchAccountHistory(
session = session,
accountId = state.account.internalId,
accountDisplayName = state.account.accountBriefName,
@@ -194,7 +194,7 @@ class TransferHistoryFragment : Fragment() {
list
}
}
} catch (_: Exception) { emptyList<Transaction>() }
} catch (_: Exception) { emptyList<BankTransaction>() }
}
}.awaitAll().flatten())
@@ -203,9 +203,7 @@ class TransferHistoryFragment : Fragment() {
for (state in fahipayStates) {
val session = app.fahipaySessionFor(state.account) ?: continue
try {
val flow = FahipayLoginFlow()
flow.setSessionCookie(session.sessionCookie)
val (list, total) = flow.fetchHistory(
val (list, total) = FahipayHistoryClient().fetchHistory(
session = session,
accountDisplayName = state.account.accountBriefName,
accountNumber = state.account.accountNumber,
@@ -18,14 +18,17 @@ import kotlinx.coroutines.withContext
import sh.sar.basedbank.util.Totp
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlAccountClient
import sh.sar.basedbank.api.bml.BmlActivationResult
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.bml.BmlOtpChannel
import sh.sar.basedbank.api.bml.BmlProfile
import sh.sar.basedbank.api.fahipay.FahipayAccountClient
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
import sh.sar.basedbank.api.fahipay.FahipaySession
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.api.mib.MibProfileClient
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
@@ -56,7 +59,7 @@ class CredentialsFragment : Fragment() {
// BML multi-profile state
private var bmlFlow: BmlLoginFlow? = null
private var bmlLoginId: String = ""
private var bmlAccumulatedAccounts = mutableListOf<MibAccount>()
private var bmlAccumulatedAccounts = mutableListOf<BankAccount>()
private var bmlPendingBusinessProfiles = ArrayDeque<Pair<BmlProfile, List<BmlOtpChannel>>>()
private var bmlCurrentBusinessProfile: BmlProfile? = null
private var bmlSelectedChannel: String? = null
@@ -202,7 +205,7 @@ class CredentialsFragment : Fragment() {
store.saveMibCredentials(loginId, username, passwordHash, otpSeed)
withContext(Dispatchers.IO) {
flow.lastSession?.let { s ->
val profile = flow.fetchPersonalProfile(s)
val profile = MibProfileClient().fetchPersonalProfile(s)
if (profile != null) store.saveMibUserProfile(
loginId,
CredentialStore.MibUserProfile(
@@ -428,7 +431,7 @@ class CredentialsFragment : Fragment() {
val anySession = app.anyBmlSessionFor(bmlLoginId)
if (anySession != null) {
withContext(Dispatchers.IO) {
val info = BmlLoginFlow().fetchUserInfo(anySession)
val info = BmlAccountClient().fetchUserInfo(anySession)
if (info != null) {
store.saveBmlUserProfile(
bmlLoginId,
@@ -582,13 +585,13 @@ class CredentialsFragment : Fragment() {
store: CredentialStore
) {
val (profile, balance) = withContext(Dispatchers.IO) {
val p = flow.fetchProfile(session)
val b = flow.fetchBalance(session)
val p = FahipayAccountClient().fetchProfile(session)
val b = FahipayAccountClient().fetchBalance(session)
Pair(p, b)
}
val loginId = profile.profileId
val loginTag = "fahipay_$loginId"
val account = flow.buildAccount(profile, balance, loginTag)
val account = FahipayAccountClient().buildAccount(profile, balance, loginTag)
store.saveFahipayCredentials(loginId, idCard, password)
store.saveFahipaySession(loginId, session.authId, session.sessionCookie)
store.saveFahipayUserProfile(