add support for multiple BML accounts, and BML credit cards
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s

This commit is contained in:
2026-05-18 00:29:52 +05:00
parent cd4b3fef8b
commit 00e109562b
16 changed files with 289 additions and 200 deletions

View File

@@ -19,11 +19,19 @@ class BasedBankApp : Application() {
var fullName: String = ""
var mibSession: MibSession? = null
var mibProfiles: List<MibProfile> = emptyList()
var bmlSession: BmlSession? = null
/** Active BML sessions keyed by loginId (= BML username). */
val bmlSessions: MutableMap<String, BmlSession> = mutableMapOf()
var bmlAccounts: List<MibAccount> = emptyList()
var fahipaySession: FahipaySession? = null
var fahipayAccounts: List<MibAccount> = emptyList()
/** Returns the BML session for the given account (matched via loginTag). */
fun bmlSessionFor(account: MibAccount): BmlSession? =
bmlSessions[account.loginTag.removePrefix("bml_")]
/** Returns any available BML session (for non-account-specific operations). */
fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull()
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
val mibMutex = Mutex()

View File

@@ -166,17 +166,17 @@ class BmlLoginFlow {
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
val accounts = fetchAccounts(session)
val accounts = fetchAccounts(session, "bml_$username")
return Pair(session, accounts)
}
fun fetchAccounts(session: BmlSession): List<MibAccount> {
fun fetchAccounts(session: BmlSession, loginTag: String): List<MibAccount> {
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
val code = resp.code
val json = resp.body?.string()
resp.close()
if (code == 401 || code == 419) throw AuthExpiredException()
return parseDashboard(json ?: return emptyList(), "bml_${session.deviceId}")
return parseDashboard(json ?: return emptyList(), loginTag)
}
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
@@ -324,11 +324,11 @@ class BmlLoginFlow {
return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false }
}
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
fun fetchContacts(session: BmlSession, loginId: String): List<MibBeneficiary> {
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute()
val json = resp.body?.string() ?: return emptyList()
resp.close()
return parseContacts(json)
return parseContacts(json, loginId)
}
/**
@@ -634,29 +634,28 @@ class BmlLoginFlow {
internalId = internalId
))
} else if (accountType == "Card") {
val isVisible = item.optBoolean("account_visible", false)
if (!isVisible) continue // debit cards and other hidden cards — skip
val isPrepaid = item.optBoolean("prepaid_card", false)
if (isPrepaid) {
val cardBalance = item.optJSONObject("cardBalance")
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
prepaidCards.add(MibAccount(
profileName = "Personal",
profileType = "BML_PREPAID",
accountNumber = accountNumber,
accountBriefName = product,
currencyName = currency,
accountTypeName = product,
availableBalance = "%.2f".format(available),
currentBalance = "%.2f".format(cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0),
blockedAmount = "0.00",
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
statusDesc = status,
profileImageHash = null,
loginTag = loginTag,
internalId = internalId
))
} else {
// Linked debit cards have no independent balance or account link — skip
}
val cardBalance = item.optJSONObject("cardBalance")
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
prepaidCards.add(MibAccount(
profileName = "Personal",
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
accountNumber = accountNumber,
accountBriefName = item.optString("alias").ifBlank { product },
currencyName = currency,
accountTypeName = product,
availableBalance = "%.2f".format(available),
currentBalance = "%.2f".format(current),
blockedAmount = "0.00",
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
statusDesc = status,
profileImageHash = null,
loginTag = loginTag,
internalId = internalId
))
}
}
@@ -694,7 +693,7 @@ class BmlLoginFlow {
} catch (_: Exception) { emptyList() }
}
private fun parseContacts(json: String): List<MibBeneficiary> {
private fun parseContacts(json: String, loginId: String = ""): List<MibBeneficiary> {
val root = JSONObject(json)
if (!root.optBoolean("success")) return emptyList()
val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList()
@@ -715,7 +714,8 @@ class BmlLoginFlow {
benefStatus = item.optString("status", "S"),
transferCyDesc = item.optString("currency", "MVR"),
customerImgHash = null,
benefCategoryId = "BML"
benefCategoryId = "BML",
profileId = loginId
))
}
return result

View File

@@ -134,7 +134,7 @@ class AccountHistoryFragment : Fragment() {
}
private fun isMib() = !account.profileType.startsWith("BML") && account.profileType != "FAHIPAY"
private fun isBmlCard() = account.profileType == "BML_PREPAID"
private fun isBmlCard() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
private fun isFahipay() = account.profileType == "FAHIPAY"
private fun hasMore(): Boolean = when {
@@ -189,7 +189,7 @@ class AccountHistoryFragment : Fragment() {
}
}
isBmlCard() -> {
val session = app.bmlSession ?: return@withContext emptyList()
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)
@@ -203,7 +203,7 @@ class AccountHistoryFragment : Fragment() {
)
}
else -> {
val session = app.bmlSession ?: return@withContext emptyList()
val session = app.bmlSessionFor(account) ?: return@withContext emptyList()
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
session = session,
accountId = account.internalId,

View File

@@ -35,8 +35,8 @@ class AccountsAdapter(
}
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
val nonPrepaid = accounts.filter { it.profileType != "BML_PREPAID" }
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" }
val nonPrepaid = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
// Group non-prepaid accounts by their derived section title, preserving order
val groups = LinkedHashMap<String, MutableList<MibAccount>>()

View File

@@ -86,7 +86,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
for (profile in app.mibProfiles) {
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile))
}
if (app.bmlSession != null) {
if (app.anyBmlSession() != null) {
list.add(DestinationOption("BML · Personal", isBml = true))
}
return list
@@ -186,7 +186,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
}
private fun lookupForBml(input: String): BmlAccountValidation? {
val bmlSess = app.bmlSession ?: return null
val bmlSess = app.anyBmlSession() ?: return null
val bmlFlow = BmlLoginFlow()
// 1) Try BML validate
@@ -236,7 +236,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
if (mibResult != null) return mibResult
// MIB lookup failed (e.g. BML USD account) — fall back to BML validate
val bmlSess = app.bmlSession ?: return null
val bmlSess = app.anyBmlSession() ?: return null
return try { BmlLoginFlow().validateAccount(bmlSess, input) } catch (_: Exception) { null }
}
@@ -358,7 +358,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
}
private fun saveToBml(alias: String): Boolean {
val bmlSess = app.bmlSession ?: return false
val bmlSess = app.anyBmlSession() ?: return false
val lookup = bmlLookup ?: return false
val bmlFlow = BmlLoginFlow()
val account = lookup.account
@@ -425,12 +425,13 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
requireActivity().lifecycleScope.launch(Dispatchers.IO) {
try {
if (dest.isBml) {
val bmlSess = app.bmlSession ?: return@launch
val fresh = BmlLoginFlow().fetchContacts(bmlSess)
val bmlSess = app.anyBmlSession() ?: return@launch
val loginId = app.bmlSessions.entries.firstOrNull { it.value == bmlSess }?.key ?: ""
val fresh = BmlLoginFlow().fetchContacts(bmlSess, loginId)
val existing = viewModel.contacts.value ?: emptyList()
val merged = existing.filter { it.benefCategoryId != "BML" } + fresh
viewModel.contacts.postValue(merged)
ContactsCache.saveBml(requireContext(), fresh)
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, fresh)
} else {
val profile = dest.mibProfile ?: return@launch
val mibSess = app.mibSession ?: return@launch

View File

@@ -203,11 +203,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
val fromAccount = accounts.find { it.accountNumber == fromAccountNumber }
val fromCurrency = fromAccount?.currencyName ?: ""
val fromLoginTag = fromAccount?.loginTag ?: ""
val fromIsCard = fromAccount?.profileType == "BML_PREPAID"
val fromIsCard = fromAccount?.profileType == "BML_PREPAID" || fromAccount?.profileType == "BML_CREDIT"
if (tabTag == MY_ACCOUNTS_TAG) {
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" }
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
val filteredRegular = if (search.isBlank()) regularAccounts else regularAccounts.filter {
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)

View File

@@ -185,7 +185,7 @@ class ContactsFragment : Fragment() {
}
private fun deleteBml(contact: MibBeneficiary): Boolean {
val sess = app.bmlSession ?: return false
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 }
}
@@ -205,7 +205,11 @@ class ContactsFragment : Fragment() {
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.benefNo } ?: return
viewModel.contacts.value = updated
if (contact.benefCategoryId == "BML") {
ContactsCache.saveBml(requireContext(), updated.filter { it.benefCategoryId == "BML" })
updated.filter { it.benefCategoryId == "BML" }
.groupBy { it.profileId }
.forEach { (loginId, contacts) ->
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, contacts)
}
} else {
ContactsCache.save(
requireContext(),

View File

@@ -119,7 +119,10 @@ class HomeActivity : AppCompatActivity() {
val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts
viewModel.accounts.value = merged
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
if (app.bmlAccounts.isNotEmpty()) AccountCache.saveBml(this, app.bmlAccounts)
if (app.bmlAccounts.isNotEmpty()) {
val byLoginId = app.bmlAccounts.groupBy { it.loginTag.removePrefix("bml_") }
byLoginId.forEach { (loginId, accounts) -> AccountCache.saveBml(this, loginId, accounts) }
}
if (app.fahipayAccounts.isNotEmpty()) AccountCache.saveFahipay(this, app.fahipayAccounts)
val cachedFinancing = FinancingCache.load(this)
@@ -128,11 +131,12 @@ class HomeActivity : AppCompatActivity() {
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
refreshFinancing(app.mibSession, app.mibProfiles)
if (app.bmlSession != null) refreshBmlLimits(app.bmlSession!!)
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
} else {
// Came from lock screen — show caches immediately, refresh everything in background
val store = CredentialStore(this)
val cachedMib = AccountCache.load(this)
val cachedBml = AccountCache.loadBml(this)
val cachedBml = AccountCache.loadBml(this, store.getBmlLoginIds())
val cachedFahipay = AccountCache.loadFahipay(this)
val merged = cachedMib + cachedBml + cachedFahipay
if (merged.isNotEmpty()) viewModel.accounts.value = merged
@@ -141,8 +145,7 @@ class HomeActivity : AppCompatActivity() {
val cachedLimits = ForeignLimitsCache.load(this)
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
val store = CredentialStore(this)
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store.loadFahipayCredentials(), store)
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
}
// Show dashboard on first create
@@ -324,9 +327,9 @@ class HomeActivity : AppCompatActivity() {
fun relogin() {
val store = CredentialStore(this)
val hasMib = store.hasMibCredentials()
val hasBml = store.hasBmlCredentials()
val bmlLoginIds = store.getBmlLoginIds()
val hasFahipay = store.hasFahipayCredentials()
if (!hasMib && !hasBml && !hasFahipay) {
if (!hasMib && bmlLoginIds.isEmpty() && !hasFahipay) {
startActivity(Intent(this, LoginActivity::class.java))
finish()
return
@@ -335,24 +338,26 @@ class HomeActivity : AppCompatActivity() {
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 (!hasBml && acc.profileType.startsWith("BML")) return@filter false
if (acc.profileType.startsWith("BML")) {
val loginId = acc.loginTag.removePrefix("bml_")
return@filter loginId in bmlLoginIds
}
if (!hasFahipay && acc.profileType == "FAHIPAY") return@filter false
true
}
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store.loadFahipayCredentials(), store)
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
}
private fun autoRefresh(
mibCreds: CredentialStore.MibCredentials?,
bmlCreds: CredentialStore.BmlCredentials?,
fahipayCreds: CredentialStore.FahipayCredentials?,
store: CredentialStore
) {
if (mibCreds == null && bmlCreds == null && fahipayCreds == null) return
val bmlLoginIds = store.getBmlLoginIds()
if (mibCreds == null && bmlLoginIds.isEmpty() && fahipayCreds == null) return
binding.refreshIndicator.visibility = View.VISIBLE
lifecycleScope.launch {
// MIB and BML login run in parallel
val mibJob = mibCreds?.let {
async(Dispatchers.IO) {
try {
@@ -368,39 +373,36 @@ class HomeActivity : AppCompatActivity() {
}
}
val bmlJob = bmlCreds?.let {
async(Dispatchers.IO) {
// One async job per BML login, all run in parallel
val bmlJobs = bmlLoginIds.mapNotNull { loginId ->
val creds = store.loadBmlCredentials(loginId) ?: return@mapNotNull null
loginId to async(Dispatchers.IO) {
val bmlFlow = BmlLoginFlow()
val savedToken = store.loadBmlSession()
val loginTag = "bml_$loginId"
val savedToken = store.loadBmlSession(loginId)
// Try cached token first
if (savedToken != null) {
try {
val session = BmlSession(savedToken.first, savedToken.second)
val accounts = bmlFlow.fetchAccounts(session)
val accounts = bmlFlow.fetchAccounts(session, loginTag)
val app = application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
AccountCache.saveBml(this@HomeActivity, accounts)
app.bmlSessions[loginId] = session
AccountCache.saveBml(this@HomeActivity, loginId, 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 (session, accounts) = bmlFlow.login(creds.username, creds.password, creds.otpSeed)
store.saveBmlSession(loginId, session.accessToken, session.deviceId)
val app = application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
AccountCache.saveBml(this@HomeActivity, accounts)
app.bmlSessions[loginId] = session
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
Pair(session, accounts)
} catch (_: Exception) {
Pair(null, AccountCache.loadBml(this@HomeActivity))
Pair(null, AccountCache.loadBml(this@HomeActivity, loginId))
}
}
}
@@ -410,7 +412,6 @@ class HomeActivity : AppCompatActivity() {
val fahipayFlow = FahipayLoginFlow()
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
// Try cached session first
val savedSession = store.loadFahipaySession()
if (savedSession != null) {
try {
@@ -426,15 +427,12 @@ class HomeActivity : AppCompatActivity() {
AccountCache.saveFahipay(this@HomeActivity, accounts)
return@async Pair(session, accounts)
} catch (_: Exception) {
// Session expired — fall through to full login
}
}
// Full re-login (only works if user has no 2FA, or 2FA was skipped)
try {
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
if (step.twoFactorRequired) {
// Can't auto-complete 2FA — use cached data
return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
}
val authId = step.authId ?: return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
@@ -457,14 +455,16 @@ class HomeActivity : AppCompatActivity() {
}
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
val (bmlSession, bmlAccounts) = bmlJob?.await() ?: Pair(null, AccountCache.loadBml(this@HomeActivity))
val bmlResults = bmlJobs.map { (_, job) -> job.await() }
val bmlAccounts = bmlResults.flatMap { it.second }
val (_, fahipayAccounts) = fahipayJob?.await() ?: Pair(null, AccountCache.loadFahipay(this@HomeActivity))
val app = application as BasedBankApp
app.bmlAccounts = bmlAccounts
viewModel.accounts.postValue(mibAccounts + bmlAccounts + fahipayAccounts)
binding.refreshIndicator.visibility = View.GONE
val app = application as BasedBankApp
if (bmlSession != null) refreshBmlLimits(bmlSession)
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
refreshFinancing(app.mibSession, app.mibProfiles)
}
}
@@ -487,16 +487,21 @@ class HomeActivity : AppCompatActivity() {
}
private fun refreshBmlContacts(app: BasedBankApp) {
val session = app.bmlSession ?: return
val bmlFlow = BmlLoginFlow()
if (app.bmlSessions.isEmpty()) return
lifecycleScope.launch {
try {
val bmlContacts = withContext(Dispatchers.IO) { bmlFlow.fetchContacts(session) }
if (bmlContacts.isNotEmpty()) {
ContactsCache.saveBml(this@HomeActivity, bmlContacts)
val allBmlContacts = withContext(Dispatchers.IO) {
app.bmlSessions.flatMap { (loginId, session) ->
val contacts = BmlLoginFlow().fetchContacts(session, loginId)
if (contacts.isNotEmpty()) ContactsCache.saveBml(this@HomeActivity, loginId, contacts)
contacts
}
}
if (allBmlContacts.isNotEmpty()) {
val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity)
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
viewModel.contacts.postValue(mergeContacts(mergeContacts(mibContacts, bmlContacts), fahipayContacts))
viewModel.contacts.postValue(mergeContacts(mergeContacts(mibContacts, allBmlContacts), fahipayContacts))
}
} catch (_: Exception) { /* keep cached */ }
}
@@ -504,10 +509,11 @@ class HomeActivity : AppCompatActivity() {
fun loadAllContacts() {
val app = application as BasedBankApp
val store = sh.sar.basedbank.util.CredentialStore(this)
// Populate ViewModel from cache immediately if empty
if (viewModel.contacts.value.isNullOrEmpty()) {
val cached = mergeContacts(
mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this)),
mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this, store.getBmlLoginIds())),
ContactsCache.loadFahipay(this)
)
if (cached.isNotEmpty()) viewModel.contacts.value = cached
@@ -535,7 +541,8 @@ class HomeActivity : AppCompatActivity() {
val categories = groups.map { MibBeneficiaryCategory(it.categoryId, it.label, it.contacts.size) }
ContactsCache.saveFahipay(this@HomeActivity, contacts, categories)
val mibContacts = ContactsCache.loadContacts(this@HomeActivity)
val bmlContacts = ContactsCache.loadBml(this@HomeActivity)
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, bmlLoginIds)
viewModel.contacts.postValue(mergeContacts(mergeContacts(mibContacts, bmlContacts), contacts))
val mibCategories = ContactsCache.loadCategories(this@HomeActivity)
viewModel.contactCategories.postValue(mibCategories + categories)
@@ -581,7 +588,8 @@ class HomeActivity : AppCompatActivity() {
}
if (allContacts.isNotEmpty()) {
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
val bmlContacts = ContactsCache.loadBml(this@HomeActivity)
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, bmlLoginIds)
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
@@ -613,17 +621,19 @@ class HomeActivity : AppCompatActivity() {
val others = current.filter { it.profileType != "FAHIPAY" }
viewModel.accounts.postValue(others + fresh)
} else if (src.profileType.startsWith("BML")) {
val loginId = src.loginTag.removePrefix("bml_")
val fresh = withContext(Dispatchers.IO) {
val sess = app.bmlSession ?: return@withContext null
val sess = app.bmlSessionFor(src) ?: return@withContext null
try {
val accounts = BmlLoginFlow().fetchAccounts(sess)
AccountCache.saveBml(this@HomeActivity, accounts)
app.bmlAccounts = accounts
val accounts = BmlLoginFlow().fetchAccounts(sess, src.loginTag)
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
val otherBml = app.bmlAccounts.filter { it.loginTag != src.loginTag }
app.bmlAccounts = otherBml + accounts
accounts
} catch (_: Exception) { null }
} ?: return@launch
val mibOnly = current.filter { !it.profileType.startsWith("BML") }
viewModel.accounts.postValue(mibOnly + fresh)
val otherAccounts = current.filter { !it.profileType.startsWith("BML") || it.loginTag != src.loginTag }
viewModel.accounts.postValue(otherAccounts + fresh)
} else {
val fresh = withContext(Dispatchers.IO) {
val sess = app.mibSession ?: return@withContext null

View File

@@ -75,9 +75,10 @@ class OtpFragment : Fragment() {
val name = store.loadMibFullName()
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
}
store.loadBmlCredentials()?.let { creds ->
val name = store.loadBmlFullName()
entries.add(OtpEntry(if (name != null) "BML · $name" else "BML", creds.otpSeed))
for (loginId in store.getBmlLoginIds()) {
val creds = store.loadBmlCredentials(loginId) ?: continue
val name = store.loadBmlUserProfile(loginId)?.fullName
entries.add(OtpEntry(if (!name.isNullOrBlank()) "BML · $name" else "BML", creds.otpSeed))
}
val adapter = OtpAdapter(entries)
@@ -105,13 +106,14 @@ class OtpFragment : Fragment() {
}
}
}
if (store.loadBmlFullName() == null) {
app.bmlSession?.let { session ->
for (loginId in store.getBmlLoginIds()) {
if (store.loadBmlUserProfile(loginId)?.fullName.isNullOrBlank()) {
val session = app.bmlSessions[loginId] ?: continue
val info = withContext(Dispatchers.IO) {
try { BmlLoginFlow().fetchUserInfo(session) } catch (_: Exception) { null }
}
if (info != null) {
store.saveBmlUserProfile(CredentialStore.BmlUserProfile(
store.saveBmlUserProfile(loginId, CredentialStore.BmlUserProfile(
fullName = info.fullName,
email = info.email,
mobile = info.mobile,
@@ -119,7 +121,8 @@ class OtpFragment : Fragment() {
idCard = info.idCard,
birthdate = info.birthdate
))
val idx = entries.indexOfFirst { it.seed == store.loadBmlCredentials()?.otpSeed }
val seed = store.loadBmlCredentials(loginId)?.otpSeed
val idx = entries.indexOfFirst { it.seed == seed }
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "BML · ${info.fullName}"); changed = true }
}
}

View File

@@ -59,10 +59,10 @@ class SettingsLoginsFragment : Fragment() {
container.removeAllViews()
val hasMib = store.hasMibCredentials()
val hasBml = store.hasBmlCredentials()
val bmlLoginIds = store.getBmlLoginIds()
val hasFahipay = store.hasFahipayCredentials()
binding.tvLoginsTitle.visibility = if (hasMib || hasBml || hasFahipay) View.VISIBLE else View.GONE
binding.tvLoginsTitle.visibility = if (hasMib || bmlLoginIds.isNotEmpty() || hasFahipay) View.VISIBLE else View.GONE
if (hasMib) {
val profile = store.loadMibUserProfile()
@@ -86,10 +86,10 @@ class SettingsLoginsFragment : Fragment() {
}
}
if (hasBml) {
val profile = store.loadBmlUserProfile()
for (loginId in bmlLoginIds) {
val profile = store.loadBmlUserProfile(loginId)
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name)
val profileNames = AccountCache.loadBml(ctx).map { it.profileName }.filter { it.isNotBlank() }.distinct()
val profileNames = AccountCache.loadBml(ctx, loginId).map { it.profileName }.filter { it.isNotBlank() }.distinct()
addLoginRow(container, R.drawable.bml_logo_vector, displayName) {
showLoginDetails(
title = getString(R.string.bml_name),
@@ -105,7 +105,7 @@ class SettingsLoginsFragment : Fragment() {
profileNames.forEach { appendLine("$it") }
}
}.trim(),
onLogout = { confirmLogout(getString(R.string.bml_name)) { logoutBml(store) } }
onLogout = { confirmLogout(getString(R.string.bml_name)) { logoutBml(store, loginId) } }
)
}
}
@@ -183,11 +183,12 @@ class SettingsLoginsFragment : Fragment() {
buildLoginsSection()
}
private fun logoutBml(store: CredentialStore) {
private fun logoutBml(store: CredentialStore, loginId: String) {
val ctx = requireContext()
store.clearBmlCredentials(); store.clearBmlSession()
store.clearBmlCredentials(loginId); store.clearBmlSession(loginId)
val app = requireActivity().application as BasedBankApp
app.bmlSession = null; app.bmlAccounts = emptyList()
app.bmlSessions.remove(loginId)
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$loginId" }
clearAllCaches(ctx)
(activity as HomeActivity).relogin()
buildLoginsSection()

View File

@@ -63,7 +63,9 @@ class TransferFragment : Fragment() {
private var selectedAccount: MibAccount? = null
private val session get() = (requireActivity().application as BasedBankApp).mibSession
private val bmlSession get() = (requireActivity().application as BasedBankApp).bmlSession
private fun bmlSessionFor(account: MibAccount?) =
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
?: (requireActivity().application as BasedBankApp).anyBmlSession()
// Resolved recipient info — set after successful lookup or prefill
private var resolvedAccountNumber = ""
@@ -203,6 +205,7 @@ class TransferFragment : Fragment() {
}
val typeLabel = when {
account.profileType == "BML_PREPAID" -> "Prepaid Card"
account.profileType == "BML_CREDIT" -> "Credit Card"
account.accountTypeName.isNotBlank() -> account.accountTypeName
else -> account.profileType
}
@@ -302,7 +305,7 @@ class TransferFragment : Fragment() {
}
val mibSess = session
val bmlSess = bmlSession
val bmlSess = bmlSessionFor(selectedAccount)
if (mibSess == null && bmlSess == null) {
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
return
@@ -548,7 +551,7 @@ class TransferFragment : Fragment() {
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
val isSrcBml = src.profileType.startsWith("BML")
val isSrcCard = src.profileType == "BML_PREPAID"
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" }
val allAccounts = viewModel.accounts.value ?: emptyList()
@@ -577,6 +580,7 @@ class TransferFragment : Fragment() {
?.transferCyDesc?.ifBlank { "MVR" }
?: if (isDestMib) "MVR" else "MVR"
val isUsdToMvr = currency.equals("USD", ignoreCase = true) && destCurrency.equals("MVR", ignoreCase = true)
val isSrcCredit = src.profileType == "BML_CREDIT"
val mainMsg = "Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}"
@@ -607,7 +611,7 @@ class TransferFragment : Fragment() {
.setPositiveButton(R.string.transfer_confirm) { _, _ -> doTransfer() }
.setNegativeButton(android.R.string.cancel, null)
if (isUsdToMvr) {
if (isUsdToMvr || isSrcCredit) {
val ctx = requireContext()
val dp = resources.displayMetrics.density
val container = LinearLayout(ctx).apply {
@@ -615,13 +619,24 @@ class TransferFragment : Fragment() {
setPadding((24 * dp).toInt(), (16 * dp).toInt(), (24 * dp).toInt(), 0)
}
container.addView(TextView(ctx).apply { text = mainMsg })
container.addView(TextView(ctx).apply {
text = "⚠ You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
setTextColor(Color.RED)
textSize = 16f
typeface = Typeface.DEFAULT_BOLD
setPadding(0, (16 * dp).toInt(), 0, 0)
})
if (isUsdToMvr) {
container.addView(TextView(ctx).apply {
text = "⚠ You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
setTextColor(Color.RED)
textSize = 16f
typeface = Typeface.DEFAULT_BOLD
setPadding(0, (16 * dp).toInt(), 0, 0)
})
}
if (isSrcCredit) {
container.addView(TextView(ctx).apply {
text = "⚠ Transferring from a credit card is treated as a cash advance. Cash advance fees will be charged on the 10th of the month."
setTextColor(Color.RED)
textSize = 16f
typeface = Typeface.DEFAULT_BOLD
setPadding(0, (16 * dp).toInt(), 0, 0)
})
}
dialogBuilder.setView(container)
} else {
dialogBuilder.setMessage(mainMsg)
@@ -741,8 +756,9 @@ class TransferFragment : Fragment() {
allAccounts: List<MibAccount>,
allContacts: List<MibBeneficiary>
): Triple<Boolean, String, TransferReceiptData?> {
val sess = bmlSession ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
val otp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
val loginId = src.loginTag.removePrefix("bml_")
val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
val otp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
?.let { Totp.generate(it) }
?: return Triple(false, "OTP unavailable", null)
val debitAccount = src.internalId.ifBlank {
@@ -750,7 +766,7 @@ class TransferFragment : Fragment() {
}
// Determine type + credit account
val isDestMyCard = allAccounts.any { it.profileType == "BML_PREPAID" && it.accountNumber == destAccount }
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
val (transferType, creditAccount, bank) = when {
isSrcCard -> {
// CAD: card → own BML account
@@ -759,7 +775,7 @@ class TransferFragment : Fragment() {
}
isDestMyCard -> {
// CPA: BML CASA → own card top-up
val card = allAccounts.first { it.profileType == "BML_PREPAID" && it.accountNumber == destAccount }
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
}
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
@@ -782,7 +798,7 @@ class TransferFragment : Fragment() {
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
// Step 2: confirm with fresh OTP
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
?.let { Totp.generate(it) } ?: otp
return try {
@@ -932,8 +948,8 @@ class TransferFragment : Fragment() {
) : BaseAdapter(), Filterable {
private val items: List<Any> = buildList {
val regular = accounts.filter { it.profileType != "BML_PREPAID" }
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
addAll(regular)
if (cards.isNotEmpty()) {
add(getString(R.string.cards))
@@ -942,7 +958,7 @@ class TransferFragment : Fragment() {
}
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
?.takeUnless { it.profileType == "BML_PREPAID" && !it.statusDesc.equals("Active", ignoreCase = true) }
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
override fun getCount() = items.size
override fun getItem(position: Int) = items[position]
@@ -973,7 +989,7 @@ class TransferFragment : Fragment() {
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
.also { it.root.tag = it }
}
val inactive = acc.profileType == "BML_PREPAID" && !acc.statusDesc.equals("Active", ignoreCase = true)
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
b.tvDropdownAccountName.text = acc.accountBriefName
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"

View File

@@ -62,7 +62,7 @@ class TransferHistoryFragment : Fragment() {
) {
fun hasMore(): Boolean = when {
account.profileType == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
account.profileType == "BML_PREPAID" -> cardMonthOffset < 2
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
account.profileType.startsWith("BML") -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
}
@@ -143,7 +143,6 @@ class TransferHistoryFragment : Fragment() {
val app = requireActivity().application as BasedBankApp
val mibSession = app.mibSession
val bmlSession = app.bmlSession
lifecycleScope.launch {
val newTransactions = withContext(Dispatchers.IO) {
@@ -155,8 +154,8 @@ class TransferHistoryFragment : Fragment() {
async {
try {
when {
state.account.profileType == "BML_PREPAID" -> {
val session = bmlSession ?: return@async emptyList()
state.account.profileType == "BML_PREPAID" || state.account.profileType == "BML_CREDIT" -> {
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
val cal = Calendar.getInstance()
cal.add(Calendar.MONTH, -state.cardMonthOffset)
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
@@ -170,7 +169,7 @@ class TransferHistoryFragment : Fragment() {
)
}
else -> {
val session = bmlSession ?: return@async emptyList()
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
session = session,
accountId = state.account.internalId,

View File

@@ -185,6 +185,7 @@ class CredentialsFragment : Fragment() {
binding.progressBar.visibility = View.VISIBLE
binding.btnLogin.isEnabled = false
val loginId = username
val flow = BmlLoginFlow()
viewLifecycleOwner.lifecycleScope.launch {
try {
@@ -192,11 +193,12 @@ class CredentialsFragment : Fragment() {
flow.login(username, password, otpSeed)
}
val store = CredentialStore(requireContext())
store.saveBmlCredentials(username, password, otpSeed)
store.saveBmlSession(session.accessToken, session.deviceId)
store.saveBmlCredentials(loginId, username, password, otpSeed)
store.saveBmlSession(loginId, session.accessToken, session.deviceId)
withContext(Dispatchers.IO) {
val info = flow.fetchUserInfo(session)
if (info != null) store.saveBmlUserProfile(
loginId,
CredentialStore.BmlUserProfile(
fullName = info.fullName,
email = info.email,
@@ -207,11 +209,11 @@ class CredentialsFragment : Fragment() {
)
)
}
AccountCache.saveBml(requireContext(), accounts)
AccountCache.saveBml(requireContext(), loginId, accounts)
val app = requireActivity().application as BasedBankApp
app.bmlSession = session
app.bmlAccounts = accounts
// Merge with any existing MIB accounts already in app
app.bmlSessions[loginId] = session
// Merge with any existing BML accounts from other logins
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$loginId" } + accounts
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

View File

@@ -9,9 +9,10 @@ object AccountCache {
private const val PREFS = "account_cache"
private const val KEY_MIB = "mib_accounts"
private const val KEY_BML = "bml_accounts"
private const val KEY_FAHIPAY = "fahipay_accounts"
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
fun save(context: Context, accounts: List<MibAccount>) {
val arr = JSONArray()
for (acc in accounts) {
@@ -36,7 +37,7 @@ object AccountCache {
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
}
fun saveBml(context: Context, accounts: List<MibAccount>) {
fun saveBml(context: Context, loginId: String, accounts: List<MibAccount>) {
val arr = JSONArray()
for (acc in accounts) {
arr.put(JSONObject().apply {
@@ -56,15 +57,14 @@ object AccountCache {
})
}
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putString(KEY_BML, CacheEncryption.encrypt(arr.toString())).apply()
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
}
fun loadBml(context: Context): List<MibAccount> {
fun loadBml(context: Context, loginId: String): List<MibAccount> {
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_BML, null) ?: return emptyList()
.getString(bmlKey(loginId), null) ?: return emptyList()
return try {
val json = CacheEncryption.decrypt(raw)
val arr = JSONArray(json)
val arr = JSONArray(CacheEncryption.decrypt(raw))
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
MibAccount(
@@ -84,9 +84,12 @@ object AccountCache {
internalId = o.optString("internalId", "")
)
}
} catch (e: Exception) { emptyList() }
} catch (_: Exception) { emptyList() }
}
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
loginIds.flatMap { loadBml(context, it) }
fun saveFahipay(context: Context, accounts: List<MibAccount>) {
val arr = JSONArray()
for (acc in accounts) {

View File

@@ -84,7 +84,9 @@ object ContactsCache {
}
}
fun saveBml(context: Context, contacts: List<MibBeneficiary>) {
private fun bmlKey(loginId: String) = "bml_contacts_$loginId"
fun saveBml(context: Context, loginId: String, contacts: List<MibBeneficiary>) {
val arr = JSONArray()
for (c in contacts) {
arr.put(JSONObject().apply {
@@ -99,18 +101,18 @@ object ContactsCache {
put("benefStatus", c.benefStatus)
put("transferCyDesc", c.transferCyDesc)
put("benefCategoryId", c.benefCategoryId)
put("profileId", c.profileId)
})
}
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putString("bml_contacts", CacheEncryption.encrypt(arr.toString())).apply()
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
}
fun loadBml(context: Context): List<MibBeneficiary> {
fun loadBml(context: Context, loginId: String): List<MibBeneficiary> {
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString("bml_contacts", null) ?: return emptyList()
.getString(bmlKey(loginId), null) ?: return emptyList()
return try {
val json = CacheEncryption.decrypt(raw)
val arr = JSONArray(json)
val arr = JSONArray(CacheEncryption.decrypt(raw))
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
MibBeneficiary(
@@ -125,12 +127,16 @@ object ContactsCache {
benefStatus = o.optString("benefStatus"),
transferCyDesc = o.optString("transferCyDesc", "MVR"),
customerImgHash = null,
benefCategoryId = o.optString("benefCategoryId", "BML")
benefCategoryId = o.optString("benefCategoryId", "BML"),
profileId = o.optString("profileId", "")
)
}
} catch (e: Exception) { emptyList() }
} catch (_: Exception) { emptyList() }
}
fun loadBml(context: Context, loginIds: List<String>): List<MibBeneficiary> =
loginIds.flatMap { loadBml(context, it) }
fun saveFahipay(context: Context, contacts: List<MibBeneficiary>, categories: List<MibBeneficiaryCategory>) {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
val arr = JSONArray()

View File

@@ -24,7 +24,6 @@ class CredentialStore(context: Context) {
// ── MIB login credentials ─────────────────────────────────────────────────
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
fun hasBmlCredentials(): Boolean = prefs.contains("bml_enc_username")
fun hasFahipayCredentials(): Boolean = prefs.contains("fahipay_enc_id_card")
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
@@ -91,58 +90,106 @@ class CredentialStore(context: Context) {
return try { decrypt(enc, key) } catch (_: Exception) { null }
}
// ── BML login credentials ─────────────────────────────────────────────────
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
fun saveBmlCredentials(username: String, password: String, otpSeed: String) {
fun getBmlLoginIds(): List<String> {
val json = prefs.getString("bml_login_ids", null)
if (json != null) {
return try {
val arr = org.json.JSONArray(json)
(0 until arr.length()).map { arr.getString(it) }
} catch (_: Exception) { emptyList() }
}
// One-time migration from single-slot BML storage
val oldEncUsername = prefs.getString("bml_enc_username", null) ?: return emptyList()
return try {
val key = getOrCreateKey()
val loginId = decrypt(oldEncUsername, key)
val edit = prefs.edit()
prefs.getString("bml_enc_password", null)?.let { edit.putString("bml_${loginId}_enc_password", it) }
prefs.getString("bml_enc_otp_seed", null)?.let { edit.putString("bml_${loginId}_enc_otp_seed", it) }
prefs.getString("bml_enc_token", null)?.let { edit.putString("bml_${loginId}_enc_token", it) }
prefs.getString("bml_enc_device_id", null)?.let { edit.putString("bml_${loginId}_enc_device_id", it) }
prefs.getString("bml_enc_profile", null)?.let { edit.putString("bml_${loginId}_enc_profile", it) }
edit.putString("bml_${loginId}_enc_username", oldEncUsername)
edit.remove("bml_enc_username").remove("bml_enc_password").remove("bml_enc_otp_seed")
.remove("bml_enc_token").remove("bml_enc_device_id")
.remove("bml_enc_profile").remove("bml_enc_full_name")
val ids = org.json.JSONArray(listOf(loginId)).toString()
edit.putString("bml_login_ids", ids)
edit.apply()
listOf(loginId)
} catch (_: Exception) { emptyList() }
}
fun hasBmlCredentials(): Boolean = getBmlLoginIds().isNotEmpty()
private fun addBmlLoginId(loginId: String) {
val ids = getBmlLoginIds().toMutableList()
if (loginId !in ids) {
ids.add(loginId)
prefs.edit().putString("bml_login_ids", org.json.JSONArray(ids).toString()).apply()
}
}
private fun removeBmlLoginId(loginId: String) {
val ids = getBmlLoginIds().toMutableList()
if (ids.remove(loginId))
prefs.edit().putString("bml_login_ids", org.json.JSONArray(ids).toString()).apply()
}
fun saveBmlCredentials(loginId: String, username: String, password: String, otpSeed: String) {
addBmlLoginId(loginId)
val key = getOrCreateKey()
prefs.edit()
.putString("bml_enc_username", encrypt(username, key))
.putString("bml_enc_password", encrypt(password, key))
.putString("bml_enc_otp_seed", encrypt(otpSeed, key))
.putString("bml_${loginId}_enc_username", encrypt(username, key))
.putString("bml_${loginId}_enc_password", encrypt(password, key))
.putString("bml_${loginId}_enc_otp_seed", encrypt(otpSeed, key))
.apply()
}
fun loadBmlCredentials(): BmlCredentials? {
fun loadBmlCredentials(loginId: String): BmlCredentials? {
val key = getOrCreateKey()
val encUsername = prefs.getString("bml_enc_username", null) ?: return null
val encPassword = prefs.getString("bml_enc_password", null) ?: return null
val encSeed = prefs.getString("bml_enc_otp_seed", null) ?: return null
val encUsername = prefs.getString("bml_${loginId}_enc_username", null) ?: return null
val encPassword = prefs.getString("bml_${loginId}_enc_password", null) ?: return null
val encSeed = prefs.getString("bml_${loginId}_enc_otp_seed", null) ?: return null
return try {
BmlCredentials(decrypt(encUsername, key), decrypt(encPassword, key), decrypt(encSeed, key))
} catch (_: Exception) { null }
}
fun clearBmlCredentials() {
fun clearBmlCredentials(loginId: String) {
removeBmlLoginId(loginId)
prefs.edit()
.remove("bml_enc_username")
.remove("bml_enc_password")
.remove("bml_enc_otp_seed")
.remove("bml_${loginId}_enc_username")
.remove("bml_${loginId}_enc_password")
.remove("bml_${loginId}_enc_otp_seed")
.apply()
}
// ── BML session token ─────────────────────────────────────────────────────
// ── BML session token (per loginId) ───────────────────────────────────────
fun saveBmlSession(accessToken: String, deviceId: String) {
fun saveBmlSession(loginId: String, accessToken: String, deviceId: String) {
val key = getOrCreateKey()
prefs.edit()
.putString("bml_enc_token", encrypt(accessToken, key))
.putString("bml_enc_device_id", encrypt(deviceId, key))
.putString("bml_${loginId}_enc_token", encrypt(accessToken, key))
.putString("bml_${loginId}_enc_device_id", encrypt(deviceId, key))
.apply()
}
fun loadBmlSession(): Pair<String, String>? {
fun loadBmlSession(loginId: String): Pair<String, String>? {
val key = getOrCreateKey()
val encToken = prefs.getString("bml_enc_token", null) ?: return null
val encDeviceId = prefs.getString("bml_enc_device_id", null) ?: return null
val encToken = prefs.getString("bml_${loginId}_enc_token", null) ?: return null
val encDeviceId = prefs.getString("bml_${loginId}_enc_device_id", null) ?: return null
return try {
Pair(decrypt(encToken, key), decrypt(encDeviceId, key))
} catch (_: Exception) { null }
}
fun clearBmlSession() {
fun clearBmlSession(loginId: String) {
prefs.edit()
.remove("bml_enc_token")
.remove("bml_enc_device_id")
.remove("bml_${loginId}_enc_token")
.remove("bml_${loginId}_enc_device_id")
.apply()
}
@@ -315,16 +362,6 @@ class CredentialStore(context: Context) {
return try { decrypt(enc, key) } catch (_: Exception) { null }
}
fun saveBmlFullName(name: String) {
val key = getOrCreateKey()
prefs.edit().putString("bml_enc_full_name", encrypt(name, key)).apply()
}
fun loadBmlFullName(): String? {
val key = getOrCreateKey()
val enc = prefs.getString("bml_enc_full_name", null) ?: return null
return try { decrypt(enc, key) } catch (_: Exception) { null }
}
fun saveMibUserProfile(p: MibUserProfile) {
val json = JSONObject().apply {
@@ -355,7 +392,7 @@ class CredentialStore(context: Context) {
} catch (_: Exception) { null }
}
fun saveBmlUserProfile(p: BmlUserProfile) {
fun saveBmlUserProfile(loginId: String, p: BmlUserProfile) {
val json = JSONObject().apply {
put("fullName", p.fullName)
put("email", p.email)
@@ -365,13 +402,12 @@ class CredentialStore(context: Context) {
put("birthdate", p.birthdate)
}.toString()
val key = getOrCreateKey()
prefs.edit().putString("bml_enc_profile", encrypt(json, key)).apply()
prefs.edit().putString("bml_enc_full_name", encrypt(p.fullName, key)).apply()
prefs.edit().putString("bml_${loginId}_enc_profile", encrypt(json, key)).apply()
}
fun loadBmlUserProfile(): BmlUserProfile? {
fun loadBmlUserProfile(loginId: String): BmlUserProfile? {
val key = getOrCreateKey()
val enc = prefs.getString("bml_enc_profile", null) ?: return null
val enc = prefs.getString("bml_${loginId}_enc_profile", null) ?: return null
return try {
val o = JSONObject(decrypt(enc, key))
BmlUserProfile(