add BML usd limits to dashboard and optmize login flow/session expirey

This commit is contained in:
2026-05-14 05:24:33 +05:00
parent 8119d554cf
commit 25bbbe926f
10 changed files with 506 additions and 55 deletions
@@ -8,9 +8,11 @@ import android.widget.Toast
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.mib.MibFinanceDeal
import sh.sar.basedbank.databinding.FragmentDashboardBinding
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
class DashboardFragment : Fragment() {
@@ -26,6 +28,7 @@ class DashboardFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
binding.btnTransfer.setOnClickListener {
(requireActivity() as HomeActivity).showWithBackStack(TransferFragment())
@@ -52,6 +55,29 @@ class DashboardFragment : Fragment() {
binding.tvUsdBalance.text = "USD %,.2f".format(usdTotal)
}
private fun updateForeignLimits(entries: List<HomeViewModel.BmlLimitsData>) {
binding.containerForeignLimits.removeAllViews()
for (entry in entries) {
for (limit in entry.limits) {
val card = ItemForeignLimitBinding.inflate(layoutInflater, binding.containerForeignLimits, false)
card.tvLimitUserName.text = entry.userName.ifBlank { "BML" }
card.tvLimitType.text = limit.type
card.tvLimitGeneral.text = "USD %,.0f / %,.0f".format(limit.generalRemaining, limit.generalCap)
card.tvLimitMedical.text = "USD %,.0f".format(limit.medicalRemaining)
card.tvLimitAtm.text = if (!limit.isAtmEnabled)
"USD %,.0f / %,.0f · Disabled".format(limit.atmRemaining, limit.atmLimit)
else
"USD %,.0f / %,.0f".format(limit.atmRemaining, limit.atmLimit)
card.tvLimitEcom.text = "USD %,.0f / %,.0f".format(limit.ecomRemaining, limit.ecomLimit)
card.tvLimitPos.text = if (!limit.isPosEnabled)
"USD %,.0f / %,.0f · Disabled".format(limit.posRemaining, limit.posLimit)
else
"USD %,.0f / %,.0f".format(limit.posRemaining, limit.posLimit)
binding.containerForeignLimits.addView(card.root)
}
}
}
private fun updatePendingFinances(deals: List<MibFinanceDeal>) {
val total = deals.sumOf { it.outstandingAmount }
binding.tvPendingFinances.text = "MVR %,.2f".format(total)
@@ -10,11 +10,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.AuthExpiredException
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.bml.BmlSession
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding
@@ -30,6 +33,7 @@ import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.ContactsCache
import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.FinancingCache
import sh.sar.basedbank.util.ForeignLimitsCache
class HomeActivity : AppCompatActivity() {
@@ -82,7 +86,10 @@ class HomeActivity : AppCompatActivity() {
refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles)
if (app.bmlSession != null) refreshBmlContacts(app)
if (app.bmlSession != null) {
refreshBmlContacts(app)
refreshBmlLimits(app.bmlSession!!)
}
} else {
// Came from lock screen — show caches immediately, refresh everything in background
val cachedMib = AccountCache.load(this)
@@ -97,10 +104,7 @@ class HomeActivity : AppCompatActivity() {
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
val store = CredentialStore(this)
val mibCreds = store.loadMibCredentials()
val bmlCreds = store.loadBmlCredentials()
if (mibCreds != null) autoRefreshMib(mibCreds, bmlCreds)
else if (bmlCreds != null) autoRefreshBml(bmlCreds)
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store)
}
// Show dashboard on first create
@@ -123,67 +127,99 @@ class HomeActivity : AppCompatActivity() {
.commit()
}
private fun autoRefreshMib(
mibCreds: CredentialStore.MibCredentials,
bmlCreds: CredentialStore.BmlCredentials?
private fun autoRefresh(
mibCreds: CredentialStore.MibCredentials?,
bmlCreds: CredentialStore.BmlCredentials?,
store: CredentialStore
) {
if (mibCreds == null && bmlCreds == null) return
binding.refreshIndicator.visibility = View.VISIBLE
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
lifecycleScope.launch {
var mibAccounts: List<MibAccount> = AccountCache.load(this@HomeActivity)
try {
mibAccounts = withContext(Dispatchers.IO) {
flow.login(mibCreds.username, mibCreds.passwordHash, mibCreds.otpSeed)
}
val app = application as BasedBankApp
app.accounts = mibAccounts
app.mibSession = flow.lastSession
app.mibProfiles = flow.lastProfiles
AccountCache.save(this@HomeActivity, mibAccounts)
} catch (_: Exception) { /* keep cached */ }
finally { binding.refreshIndicator.visibility = View.GONE }
val bmlAccounts = AccountCache.loadBml(this@HomeActivity).toMutableList()
if (bmlCreds != null) {
try {
val bmlFlow = BmlLoginFlow()
val (session, accounts) = withContext(Dispatchers.IO) {
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
}
val app = application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
AccountCache.saveBml(this@HomeActivity, accounts)
bmlAccounts.clear()
bmlAccounts.addAll(accounts)
refreshBmlContacts(app)
} catch (_: Exception) { /* keep cached */ }
lifecycleScope.launch {
// MIB and BML login run in parallel
val mibJob = mibCreds?.let {
async(Dispatchers.IO) {
try {
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
val accounts = flow.login(it.username, it.passwordHash, it.otpSeed)
val app = application as BasedBankApp
app.accounts = accounts
app.mibSession = flow.lastSession
app.mibProfiles = flow.lastProfiles
AccountCache.save(this@HomeActivity, accounts)
accounts
} catch (_: Exception) { AccountCache.load(this@HomeActivity) }
}
}
val bmlJob = bmlCreds?.let {
async(Dispatchers.IO) {
val bmlFlow = BmlLoginFlow()
val savedToken = store.loadBmlSession()
// Try cached token first
if (savedToken != null) {
try {
val session = BmlSession(savedToken.first, savedToken.second)
val accounts = bmlFlow.fetchAccounts(session)
val app = application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
AccountCache.saveBml(this@HomeActivity, accounts)
return@async Pair(session, accounts)
} catch (_: AuthExpiredException) {
// Token expired — fall through to full login
} catch (_: Exception) {
// Network or other error — fall through to full login
}
}
// Full login (token missing or expired)
try {
val (session, accounts) = bmlFlow.login(it.username, it.password, it.otpSeed)
store.saveBmlSession(session.accessToken, session.deviceId)
val app = application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
AccountCache.saveBml(this@HomeActivity, accounts)
Pair(session, accounts)
} catch (_: Exception) {
Pair(null, AccountCache.loadBml(this@HomeActivity))
}
}
}
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
val (bmlSession, bmlAccounts) = bmlJob?.await() ?: Pair(null, AccountCache.loadBml(this@HomeActivity))
viewModel.accounts.postValue(mibAccounts + bmlAccounts)
binding.refreshIndicator.visibility = View.GONE
val app = application as BasedBankApp
if (bmlSession != null) {
refreshBmlContacts(app)
refreshBmlLimits(bmlSession)
}
refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles)
}
}
private fun autoRefreshBml(bmlCreds: CredentialStore.BmlCredentials) {
binding.refreshIndicator.visibility = View.VISIBLE
private fun refreshBmlLimits(session: BmlSession) {
val bmlFlow = BmlLoginFlow()
lifecycleScope.launch {
val cachedMib = AccountCache.load(this@HomeActivity)
try {
val bmlFlow = BmlLoginFlow()
val (session, accounts) = withContext(Dispatchers.IO) {
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
val (userName, limits) = withContext(Dispatchers.IO) {
Pair(bmlFlow.fetchUserInfo(session), bmlFlow.fetchForeignLimits(session))
}
val app = application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
AccountCache.saveBml(this@HomeActivity, accounts)
viewModel.accounts.postValue(cachedMib + accounts)
refreshBmlContacts(app)
} catch (_: Exception) { /* keep cached */ }
finally { binding.refreshIndicator.visibility = View.GONE }
val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
val idx = existing.indexOfFirst { it.userName == userName }
val entry = HomeViewModel.BmlLimitsData(userName, limits)
if (idx >= 0) existing[idx] = entry else existing.add(entry)
viewModel.bmlLimits.postValue(existing)
ForeignLimitsCache.save(this@HomeActivity, existing)
} catch (_: Exception) { /* keep previous */ }
}
}
@@ -2,6 +2,7 @@ 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
@@ -12,4 +13,7 @@ class HomeViewModel : ViewModel() {
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(emptyList())
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
}
@@ -159,7 +159,9 @@ class CredentialsFragment : Fragment() {
val (session, accounts) = withContext(Dispatchers.IO) {
flow.login(username, password, otpSeed)
}
CredentialStore(requireContext()).saveBmlCredentials(username, password, otpSeed)
val store = CredentialStore(requireContext())
store.saveBmlCredentials(username, password, otpSeed)
store.saveBmlSession(session.accessToken, session.deviceId)
AccountCache.saveBml(requireContext(), accounts)
val app = requireActivity().application as BasedBankApp
app.bmlSession = session