diff --git a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt index 28bb402..acbcfb6 100644 --- a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt +++ b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt @@ -19,11 +19,19 @@ class BasedBankApp : Application() { var fullName: String = "" var mibSession: MibSession? = null var mibProfiles: List = emptyList() - var bmlSession: BmlSession? = null + /** Active BML sessions keyed by loginId (= BML username). */ + val bmlSessions: MutableMap = mutableMapOf() var bmlAccounts: List = emptyList() var fahipaySession: FahipaySession? = null var fahipayAccounts: List = 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() diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt index 8b68d49..4f3ba9a 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt @@ -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 { + fun fetchAccounts(session: BmlSession, loginTag: String): List { 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 { @@ -324,11 +324,11 @@ class BmlLoginFlow { return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false } } - fun fetchContacts(session: BmlSession): List { + fun fetchContacts(session: BmlSession, loginId: String): List { 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 { + private fun parseContacts(json: String, loginId: String = ""): List { 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 diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt index 91fcbbd..d9fa6b4 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt @@ -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, diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt index 5c597cf..0cdb475 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountsAdapter.kt @@ -35,8 +35,8 @@ class AccountsAdapter( } private fun buildItems(accounts: List): List = 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>() diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt index 527036b..2e0c35a 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AddContactSheetFragment.kt @@ -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 diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt index 74d7107..82ef191 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt @@ -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) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt index 36fda74..b514cfe 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt @@ -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(), diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index f50ebb4..b604603 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -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 diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt index cfff421..d7e4103 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt @@ -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 } } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt index f147d57..b49692c 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt @@ -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() diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt index ff4a37e..d884228 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt @@ -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, allContacts: List ): Triple { - 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 = 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}" diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt index 84a407d..8774ed0 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferHistoryFragment.kt @@ -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, diff --git a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt index 37ffa53..9daf33c 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt @@ -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 diff --git a/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt b/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt index 222153c..7172194 100644 --- a/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt +++ b/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt @@ -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) { 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) { + fun saveBml(context: Context, loginId: String, accounts: List) { 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 { + fun loadBml(context: Context, loginId: String): List { 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): List = + loginIds.flatMap { loadBml(context, it) } + fun saveFahipay(context: Context, accounts: List) { val arr = JSONArray() for (acc in accounts) { diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt index aeec856..621bc54 100644 --- a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt +++ b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt @@ -84,7 +84,9 @@ object ContactsCache { } } - fun saveBml(context: Context, contacts: List) { + private fun bmlKey(loginId: String) = "bml_contacts_$loginId" + + fun saveBml(context: Context, loginId: String, contacts: List) { 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 { + fun loadBml(context: Context, loginId: String): List { 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): List = + loginIds.flatMap { loadBml(context, it) } + fun saveFahipay(context: Context, contacts: List, categories: List) { val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() val arr = JSONArray() diff --git a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt index 8457c69..f9565e7 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -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 { + 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? { + fun loadBmlSession(loginId: String): Pair? { 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(