add bml login, accounts and contacts

This commit is contained in:
2026-05-14 05:24:28 +05:00
parent 135f0659dd
commit e4684ec017
14 changed files with 634 additions and 33 deletions
@@ -61,7 +61,11 @@ class AccountsAdapter(accounts: List<MibAccount>) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item.Header) {
binding.tvProfileName.text = item.profileName
binding.tvProfileType.text = if (item.profileType == "0") "Personal" else "Business"
binding.tvProfileType.text = when (item.profileType) {
"BML" -> "Bank of Maldives"
"0" -> "Personal"
else -> "Business"
}
}
}
@@ -14,6 +14,8 @@ 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.MibAccount
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding
import sh.sar.basedbank.ui.login.LoginActivity
@@ -63,30 +65,42 @@ class HomeActivity : AppCompatActivity() {
// Load data
val app = application as BasedBankApp
if (app.accounts.isNotEmpty()) {
if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty()) {
// Came from fresh manual login — accounts ready, rest fetched in background
viewModel.accounts.value = app.accounts
AccountCache.save(this, app.accounts)
val cached = FinancingCache.load(this)
if (cached.isNotEmpty()) viewModel.financing.value = cached
val cachedContacts = ContactsCache.loadContacts(this)
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
val cachedCats = ContactsCache.loadCategories(this)
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles)
} else {
// Came from lock screen — show caches immediately, refresh everything in background
val cached = AccountCache.load(this)
if (cached.isNotEmpty()) viewModel.accounts.value = cached
val mibAccounts = app.accounts.filter { it.profileType != "BML" }
val merged = mibAccounts + app.bmlAccounts
viewModel.accounts.value = merged
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
if (app.bmlAccounts.isNotEmpty()) AccountCache.saveBml(this, app.bmlAccounts)
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedContacts = ContactsCache.loadContacts(this)
val cachedContacts = mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this))
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
val cachedCats = ContactsCache.loadCategories(this)
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
val creds = CredentialStore(this).loadMibCredentials()
if (creds != null) autoRefresh(creds)
refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles)
if (app.bmlSession != null) refreshBmlContacts(app)
} else {
// Came from lock screen — show caches immediately, refresh everything in background
val cachedMib = AccountCache.load(this)
val cachedBml = AccountCache.loadBml(this)
val merged = cachedMib + cachedBml
if (merged.isNotEmpty()) viewModel.accounts.value = merged
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedContacts = mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this))
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
val cachedCats = ContactsCache.loadCategories(this)
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)
}
// Show dashboard on first create
@@ -109,32 +123,96 @@ class HomeActivity : AppCompatActivity() {
.commit()
}
private fun autoRefresh(creds: CredentialStore.MibCredentials) {
private fun autoRefreshMib(
mibCreds: CredentialStore.MibCredentials,
bmlCreds: CredentialStore.BmlCredentials?
) {
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 {
val accounts = withContext(Dispatchers.IO) {
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
mibAccounts = withContext(Dispatchers.IO) {
flow.login(mibCreds.username, mibCreds.passwordHash, mibCreds.otpSeed)
}
val app = application as BasedBankApp
app.accounts = accounts
app.accounts = mibAccounts
app.mibSession = flow.lastSession
app.mibProfiles = flow.lastProfiles
AccountCache.save(this@HomeActivity, accounts)
viewModel.accounts.postValue(accounts)
} catch (_: Exception) {
// Keep cached data silently
} finally {
binding.refreshIndicator.visibility = View.GONE
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 */ }
}
viewModel.accounts.postValue(mibAccounts + bmlAccounts)
val app = application as BasedBankApp
refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles)
}
}
private fun autoRefreshBml(bmlCreds: CredentialStore.BmlCredentials) {
binding.refreshIndicator.visibility = View.VISIBLE
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 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 }
}
}
private fun refreshBmlContacts(app: BasedBankApp) {
val session = app.bmlSession ?: return
val bmlFlow = BmlLoginFlow()
lifecycleScope.launch {
try {
val bmlContacts = withContext(Dispatchers.IO) { bmlFlow.fetchContacts(session) }
if (bmlContacts.isNotEmpty()) {
ContactsCache.saveBml(this@HomeActivity, bmlContacts)
val mibContacts = viewModel.contacts.value ?: ContactsCache.loadContacts(this@HomeActivity)
viewModel.contacts.postValue(mergeContacts(mibContacts, bmlContacts))
}
} catch (_: Exception) { /* keep cached */ }
}
}
private fun mergeContacts(
mib: List<MibBeneficiary>,
bml: List<MibBeneficiary>
): List<MibBeneficiary> {
val seen = mutableSetOf<String>()
val result = mutableListOf<MibBeneficiary>()
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
}
private fun refreshContacts(session: MibSession?, profiles: List<MibProfile>) {
if (session == null || profiles.isEmpty()) return
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
@@ -162,7 +240,8 @@ class HomeActivity : AppCompatActivity() {
}
if (allContacts.isNotEmpty()) {
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
viewModel.contacts.postValue(allContacts)
val bmlContacts = ContactsCache.loadBml(this@HomeActivity)
viewModel.contacts.postValue(mergeContacts(allContacts, bmlContacts))
viewModel.contactCategories.postValue(allCategories)
}
} catch (_: Exception) { /* keep cached data */ }
@@ -23,6 +23,10 @@ class BankSelectionFragment : Fragment() {
binding.cardMib.setOnClickListener {
findNavController().navigate(R.id.action_bankSelection_to_credentials)
}
binding.cardBml.setOnClickListener {
val args = android.os.Bundle().apply { putString("bankType", "BML") }
findNavController().navigate(R.id.action_bankSelection_to_credentials_bml, args)
}
}
override fun onDestroyView() {
@@ -9,6 +9,7 @@ import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
@@ -16,6 +17,8 @@ import kotlinx.coroutines.launch
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.BmlLoginFlow
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.CredentialStore
@@ -24,7 +27,8 @@ import sh.sar.basedbank.ui.home.HomeActivity
class CredentialsFragment : Fragment() {
private val TAG = "CredentialsFragment"
private val bankType: String get() = arguments?.getString("bankType") ?: "MIB"
private val otpHandler = Handler(Looper.getMainLooper())
private val otpRunnable = object : Runnable {
override fun run() {
@@ -42,6 +46,11 @@ class CredentialsFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (bankType == "BML") {
binding.ivBankLogo.setImageResource(R.drawable.bml_logo_vector)
binding.tvSignInDesc.setText(R.string.bml_sign_in_desc)
}
binding.btnLogin.setOnClickListener { attemptLogin() }
binding.etOtpSeed.addTextChangedListener(object : TextWatcher {
@@ -82,11 +91,15 @@ class CredentialsFragment : Fragment() {
}
private fun attemptLogin() {
if (bankType == "BML") {
attemptBmlLogin()
return
}
val username = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString()
val otpSeed = binding.etOtpSeed.text.toString().trim()
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
binding.tvError.text = "Please fill in all fields"
binding.tvError.visibility = View.VISIBLE
@@ -125,6 +138,47 @@ class CredentialsFragment : Fragment() {
}
}
private fun attemptBmlLogin() {
val username = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString()
val otpSeed = binding.etOtpSeed.text.toString().trim()
if (username.isEmpty() || password.isEmpty() || otpSeed.isEmpty()) {
binding.tvError.text = "Please fill in all fields"
binding.tvError.visibility = View.VISIBLE
return
}
binding.tvError.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
binding.btnLogin.isEnabled = false
val flow = BmlLoginFlow()
viewLifecycleOwner.lifecycleScope.launch {
try {
val (session, accounts) = withContext(Dispatchers.IO) {
flow.login(username, password, otpSeed)
}
CredentialStore(requireContext()).saveBmlCredentials(username, password, otpSeed)
AccountCache.saveBml(requireContext(), accounts)
val app = requireActivity().application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
// Merge with any existing MIB accounts already in app
app.accounts = app.accounts + accounts
val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
} catch (e: Exception) {
binding.tvError.text = e.message ?: "Login failed"
binding.tvError.visibility = View.VISIBLE
} finally {
binding.progressBar.visibility = View.GONE
binding.btnLogin.isEnabled = true
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null