diff --git a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt index acbcfb6..acbc409 100644 --- a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt +++ b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt @@ -17,14 +17,42 @@ class BasedBankApp : Application() { // Held in memory after successful login; cleared on logout var accounts: List = emptyList() var fullName: String = "" - var mibSession: MibSession? = null - var mibProfiles: List = emptyList() + /** Active MIB sessions keyed by loginId (= MIB username). */ + val mibSessions: MutableMap = mutableMapOf() + val mibProfilesMap: MutableMap> = mutableMapOf() + val mibLoginFlows: MutableMap = mutableMapOf() + var mibAccounts: List = emptyList() /** Active BML sessions keyed by loginId (= BML username). */ val bmlSessions: MutableMap = mutableMapOf() var bmlAccounts: List = emptyList() - var fahipaySession: FahipaySession? = null + /** Active Fahipay sessions keyed by loginId (= profileId). */ + val fahipaySessions: MutableMap = mutableMapOf() var fahipayAccounts: List = emptyList() + /** Returns the MIB session for the given account (matched via loginTag). */ + fun mibSessionFor(account: MibAccount): MibSession? = + mibSessions[account.loginTag.removePrefix("mib_")] + + /** Returns any available MIB session. */ + fun anyMibSession(): MibSession? = mibSessions.values.firstOrNull() + + /** Returns all MIB profiles across all logins. */ + fun allMibProfiles(): List = mibProfilesMap.values.flatten() + + /** Returns the MibLoginFlow for a given loginId, creating and caching it if needed. */ + fun mibFlowFor(loginId: String): MibLoginFlow = + mibLoginFlows.getOrPut(loginId) { + MibLoginFlow(CredentialStore(this)).also { flow -> + flow.onSessionRefreshed = { session, profiles -> + mibSessions[loginId] = session + mibProfilesMap[loginId] = profiles + } + } + } + + /** Returns any available MibLoginFlow. */ + fun anyMibFlow(): MibLoginFlow? = mibLoginFlows.values.firstOrNull() + /** Returns the BML session for the given account (matched via loginTag). */ fun bmlSessionFor(account: MibAccount): BmlSession? = bmlSessions[account.loginTag.removePrefix("bml_")] @@ -32,18 +60,13 @@ class BasedBankApp : Application() { /** Returns any available BML session (for non-account-specific operations). */ fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull() + /** Returns the Fahipay session for the given account (matched via loginTag = "fahipay_${profileId}"). */ + fun fahipaySessionFor(account: MibAccount): FahipaySession? = + fahipaySessions[account.loginTag.removePrefix("fahipay_")] + /** Serialises all MIB profile-switch + request sequences to prevent session corruption. */ val mibMutex = Mutex() - val mibLoginFlow by lazy { - MibLoginFlow(CredentialStore(this)).also { flow -> - flow.onSessionRefreshed = { session, profiles -> - mibSession = session - mibProfiles = profiles - } - } - } - override fun onCreate() { super.onCreate() DynamicColors.applyToActivitiesIfAvailable(this) diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt index 0d94302..202fa0b 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt @@ -29,6 +29,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { var onSessionRefreshed: ((MibSession, List) -> Unit)? = null // Stored after login so the session can be silently recovered on 419 + @Volatile private var loginId: String = "" @Volatile private var storedUsername: String? = null @Volatile private var storedPasswordHash: String? = null @Volatile private var storedOtpSeed: String? = null @@ -58,11 +59,12 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { * Returns list of accounts from all profiles on success. */ fun login(username: String, passwordHash: String, otpSeed: String): List { + loginId = username storedUsername = username storedPasswordHash = passwordHash storedOtpSeed = otpSeed val appId = getOrCreateAppId() - val keys = credentialStore.loadMibKeys() + val keys = credentialStore.loadMibKeys(loginId) return if (keys != null) { regularLogin(username, passwordHash, appId, keys.first, keys.second) @@ -106,7 +108,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { val keyData = otpResp.getJSONArray("data").getJSONObject(0) val key1 = keyData.getString("key1") val key2 = keyData.getString("key2") - credentialStore.saveMibKeys(key1, key2) + credentialStore.saveMibKeys(loginId, key1, key2) return regularLogin(username, passwordHash, appId, key1, key2) } @@ -139,7 +141,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { lastSession = session2 lastProfiles = profiles // keep ALL profiles so settings can show them all - val hidden = credentialStore.getHiddenMibProfileIds() + val hidden = credentialStore.getHiddenMibProfileIds(loginId) // When the server already selected the profile and returned balances in A41 // (single-profile case: profileSelected=true), use those accounts directly @@ -433,11 +435,11 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { private fun generateOtp(seed: String): String = Totp.generate(seed) private fun getOrCreateAppId(): String { - var id = credentialStore.loadMibAppId() + var id = credentialStore.loadMibAppId(loginId) if (id == null) { val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" id = "IOS17.2-" + (1..15).map { chars[Random.nextInt(chars.length)] }.joinToString("") - credentialStore.saveMibAppId(id) + credentialStore.saveMibAppId(loginId, id) } return id } 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 e832c87..2dcc068 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 @@ -176,7 +176,7 @@ class AccountHistoryFragment : Fragment() { return } val app = requireActivity().application as BasedBankApp - val sess = app.mibSession ?: return + val sess = app.anyMibSession() ?: return viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { try { val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch 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 d983ae4..3713f25 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 @@ -50,6 +50,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { val label: String, val isBml: Boolean, val mibProfile: MibProfile? = null, + val mibLoginId: String? = null, val bmlLoginId: String? = null, val subtitle: String = "" ) @@ -91,8 +92,10 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { private fun buildDestinations(): List { val list = mutableListOf() - for (profile in app.mibProfiles) { - list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile, subtitle = profile.cifType)) + for ((loginId, profiles) in app.mibProfilesMap) { + for (profile in profiles) { + list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile, mibLoginId = loginId, subtitle = profile.cifType)) + } } val store = CredentialStore(requireContext()) for ((loginId, _) in app.bmlSessions) { @@ -249,7 +252,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { if (mibVerified != null) return mibVerified // 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML) - val mibSess = app.mibSession ?: return null + val mibSess = app.anyMibSession() ?: return null return try { val info = MibTransferClient().lookup(mibSess, input) BmlAccountValidation( @@ -266,11 +269,12 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { } private fun lookupForMib(dest: DestinationOption, input: String): BmlAccountValidation? { - val mibSess = app.mibSession ?: return null + val loginId = dest.mibLoginId ?: return null + val mibSess = app.mibSessions[loginId] ?: return null val profile = dest.mibProfile ?: return null val mibResult = try { - app.mibLoginFlow.switchProfile(mibSess, profile) + app.mibFlowFor(loginId).switchProfile(mibSess, profile) val info = MibTransferClient().lookup(mibSess, input) BmlAccountValidation( trnType = if (info.bankId == "MADVMVMV") "MIB_INTERNAL" else "MIB_LOCAL", @@ -429,8 +433,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { } private fun saveToMib(alias: String): Boolean { - val mibSess = app.mibSession ?: return false val dest = selectedDest ?: return false + val loginId = dest.mibLoginId ?: return false + val mibSess = app.mibSessions[loginId] ?: return false val profile = dest.mibProfile ?: return false val account = mibLookupAccount ?: return false val currency = binding.etCurrency.text?.toString()?.trim() ?: "MVR" @@ -442,7 +447,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { val name = bmlLookup?.name ?: "" return try { - app.mibLoginFlow.switchProfile(mibSess, profile) + app.mibFlowFor(loginId).switchProfile(mibSess, profile) MibContactsClient().createContact( session = mibSess, benefType = benefType, @@ -488,8 +493,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() { if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, fresh) } else { val profile = dest.mibProfile ?: return@launch - val mibSess = app.mibSession ?: return@launch - app.mibLoginFlow.switchProfile(mibSess, profile) + val mibLoginId = dest.mibLoginId ?: return@launch + val mibSess = app.mibSessions[mibLoginId] ?: return@launch + app.mibFlowFor(mibLoginId).switchProfile(mibSess, profile) val fresh = MibContactsClient().fetchContacts(mibSess) .map { it.copy(profileId = profile.profileId) } val existing = viewModel.contacts.value ?: emptyList() 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 031c28a..41be3ba 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 @@ -36,7 +36,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() { private val sharedImageCache = mutableMapOf() private val profileImageHashes = mutableSetOf() private val app get() = requireActivity().application as BasedBankApp - private val session get() = app.mibSession + private val session get() = app.anyMibSession() private var fromAccountNumber: String = "" private var mediator: TabLayoutMediator? = null @@ -291,7 +291,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() { lifecycleScope.launch(Dispatchers.IO) { try { val base64 = if (hash in profileImageHashes) { - app.mibLoginFlow.fetchProfileImage(sess, hash) + app.anyMibFlow()?.fetchProfileImage(sess, hash) } else { MibContactsClient().fetchProfileImageBase64(sess, hash) } ?: return@launch 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 3bfe121..ec95350 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 @@ -40,7 +40,7 @@ class ContactsFragment : Fragment() { private val pendingHashes = mutableSetOf() private val sharedImageCache = mutableMapOf() private val app get() = requireActivity().application as BasedBankApp - private val session get() = app.mibSession + private val session get() = app.anyMibSession() private var allContacts: List = emptyList() private var currentSearch: String = "" 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 324223a..aef216e 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 @@ -128,31 +128,36 @@ class HomeActivity : AppCompatActivity() { // Load data val app = application as BasedBankApp - if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) { + if (app.mibAccounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) { // Came from fresh manual login — accounts ready, rest fetched in background - val mibAccounts = app.accounts.filter { it.bank == "MIB" } - val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts + val merged = app.mibAccounts + app.bmlAccounts + app.fahipayAccounts viewModel.accounts.value = merged.filterVisibleAccounts() - if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts) + if (app.mibAccounts.isNotEmpty()) AccountCache.save(this, app.mibAccounts) 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) + if (app.fahipayAccounts.isNotEmpty()) { + val byLoginId = app.fahipayAccounts.groupBy { it.loginTag.removePrefix("fahipay_") } + byLoginId.forEach { (loginId, accs) -> AccountCache.saveFahipay(this, loginId, accs) } + } val cachedFinancing = FinancingCache.load(this) if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing val cachedLimits = ForeignLimitsCache.load(this) if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits - refreshFinancing(app.mibSession, app.mibProfiles.filterVisibleProfiles()) + for ((loginId, session) in app.mibSessions) { + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId)) + } 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, store.getBmlLoginIds()) - val cachedFahipay = AccountCache.loadFahipay(this) + val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds()) val merged = cachedMib + cachedBml + cachedFahipay if (merged.isNotEmpty()) viewModel.accounts.value = merged val cachedFinancing = FinancingCache.load(this) @@ -160,7 +165,7 @@ class HomeActivity : AppCompatActivity() { val cachedLimits = ForeignLimitsCache.load(this) if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits - autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store) + autoRefresh(store) } // Show dashboard on first create @@ -169,27 +174,29 @@ class HomeActivity : AppCompatActivity() { binding.navigationView.setCheckedItem(R.id.nav_dashboard) } - // Keep MIB session alive every 25 seconds while the app is in the foreground + // Keep all MIB sessions alive every 25 seconds while the app is in the foreground lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { while (true) { delay(25_000) - val session = (application as BasedBankApp).mibSession ?: continue - withContext(Dispatchers.IO) { - try { - val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " + - "IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597" - val request = Request.Builder() - .url("https://faisamobilex-wv.mib.com.mv/aProfile/keepAlive") - .post(ByteArray(0).toRequestBody()) - .header("Cookie", cookieHeader) - .header("User-Agent", "okhttp/4.11.0") - .header("Accept", "application/json, text/plain, */*") - .header("Accept-Encoding", "gzip") - .header("Connection", "Keep-Alive") - .build() - OkHttpClient().newCall(request).execute().close() - } catch (_: Exception) {} + val sessions = (application as BasedBankApp).mibSessions.values.toList() + for (session in sessions) { + withContext(Dispatchers.IO) { + try { + val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " + + "IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597" + val request = Request.Builder() + .url("https://faisamobilex-wv.mib.com.mv/aProfile/keepAlive") + .post(ByteArray(0).toRequestBody()) + .header("Cookie", cookieHeader) + .header("User-Agent", "okhttp/4.11.0") + .header("Accept", "application/json, text/plain, */*") + .header("Accept-Encoding", "gzip") + .header("Connection", "Keep-Alive") + .build() + OkHttpClient().newCall(request).execute().close() + } catch (_: Exception) {} + } } } } @@ -353,51 +360,47 @@ class HomeActivity : AppCompatActivity() { fun relogin() { val store = CredentialStore(this) - val hasMib = store.hasMibCredentials() + val mibLoginIds = store.getMibLoginIds() val bmlLoginIds = store.getBmlLoginIds() - val hasFahipay = store.hasFahipayCredentials() - if (!hasMib && bmlLoginIds.isEmpty() && !hasFahipay) { + val fahipayLoginIds = store.getFahipayLoginIds() + if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) { startActivity(Intent(this, LoginActivity::class.java)) finish() return } - // Immediately drop accounts for logged-out banks from the displayed list + // Immediately drop accounts for logged-out logins from the displayed list val current = viewModel.accounts.value ?: emptyList() viewModel.accounts.value = current.filter { acc -> - if (!hasMib && acc.bank == "MIB") return@filter false - if (acc.bank == "BML") { - val loginId = acc.loginTag.removePrefix("bml_") - return@filter loginId in bmlLoginIds - } - if (!hasFahipay && acc.bank == "FAHIPAY") return@filter false + if (acc.bank == "MIB") return@filter acc.loginTag.removePrefix("mib_") in mibLoginIds + if (acc.bank == "BML") return@filter acc.loginTag.removePrefix("bml_") in bmlLoginIds + if (acc.bank == "FAHIPAY") return@filter acc.loginTag.removePrefix("fahipay_") in fahipayLoginIds true } - autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store) + autoRefresh(store) } - private fun autoRefresh( - mibCreds: CredentialStore.MibCredentials?, - fahipayCreds: CredentialStore.FahipayCredentials?, - store: CredentialStore - ) { + private fun autoRefresh(store: CredentialStore) { + val mibLoginIds = store.getMibLoginIds() val bmlLoginIds = store.getBmlLoginIds() - if (mibCreds == null && bmlLoginIds.isEmpty() && fahipayCreds == null) return + val fahipayLoginIds = store.getFahipayLoginIds() + if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) return binding.refreshIndicator.visibility = View.VISIBLE lifecycleScope.launch { - val mibJob = mibCreds?.let { - async(Dispatchers.IO) { + // One async job per MIB login, all run in parallel + val mibJobs = mibLoginIds.mapNotNull { loginId -> + val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null + loginId to async(Dispatchers.IO) { try { val flow = MibLoginFlow(CredentialStore(this@HomeActivity)) - val accounts = flow.login(it.username, it.passwordHash, it.otpSeed) + val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed) val app = application as BasedBankApp - app.accounts = accounts - app.mibSession = flow.lastSession - app.mibProfiles = flow.lastProfiles - AccountCache.save(this@HomeActivity, accounts) - CredentialStore(this@HomeActivity).saveMibProfiles(flow.lastProfiles) + app.mibSessions[loginId] = flow.lastSession!! + app.mibProfilesMap[loginId] = flow.lastProfiles + app.mibLoginFlows[loginId] = flow + store.saveMibProfiles(loginId, flow.lastProfiles) accounts - } catch (_: Exception) { AccountCache.load(this@HomeActivity) } + } catch (_: Exception) { AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" } } } } @@ -435,78 +438,87 @@ class HomeActivity : AppCompatActivity() { } } - val fahipayJob = fahipayCreds?.let { creds -> - async(Dispatchers.IO) { + // One async job per Fahipay login, all run in parallel + val fahipayJobs = fahipayLoginIds.mapNotNull { loginId -> + val creds = store.loadFahipayCredentials(loginId) ?: return@mapNotNull null + loginId to async(Dispatchers.IO) { val fahipayFlow = FahipayLoginFlow() val deviceUuid = store.getOrCreateFahipayDeviceUuid() + val loginTag = "fahipay_$loginId" - val savedSession = store.loadFahipaySession() + val savedSession = store.loadFahipaySession(loginId) if (savedSession != null) { try { val session = FahipaySession(savedSession.first, savedSession.second) fahipayFlow.setSessionCookie(session.sessionCookie) val balance = fahipayFlow.fetchBalance(session) val profile = fahipayFlow.fetchProfile(session) - val loginTag = "fahipay_${profile.profileId}" val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag)) val app = application as BasedBankApp - app.fahipaySession = session - app.fahipayAccounts = accounts - AccountCache.saveFahipay(this@HomeActivity, accounts) - return@async Pair(session, accounts) - } catch (_: Exception) { - } + app.fahipaySessions[loginId] = session + AccountCache.saveFahipay(this@HomeActivity, loginId, accounts) + return@async accounts + } catch (_: Exception) { } } try { val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid) if (step.twoFactorRequired) { - return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity)) + return@async AccountCache.loadFahipay(this@HomeActivity, loginId) } - val authId = step.authId ?: return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity)) + val authId = step.authId ?: return@async AccountCache.loadFahipay(this@HomeActivity, loginId) val cookieValue = fahipayFlow.getSessionCookieValue() ?: "" val session = FahipaySession(authId, cookieValue) - store.saveFahipaySession(authId, cookieValue) + store.saveFahipaySession(loginId, authId, cookieValue) val profile = fahipayFlow.fetchProfile(session) val balance = fahipayFlow.fetchBalance(session) - val loginTag = "fahipay_${profile.profileId}" val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag)) val app = application as BasedBankApp - app.fahipaySession = session - app.fahipayAccounts = accounts - AccountCache.saveFahipay(this@HomeActivity, accounts) - Pair(session, accounts) + app.fahipaySessions[loginId] = session + AccountCache.saveFahipay(this@HomeActivity, loginId, accounts) + accounts } catch (_: Exception) { - Pair(null, AccountCache.loadFahipay(this@HomeActivity)) + AccountCache.loadFahipay(this@HomeActivity, loginId) } } } - val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity) + val mibResults = mibJobs.map { (loginId, job) -> loginId to job.await() } + val mibAccounts = mibResults.flatMap { it.second } val bmlResults = bmlJobs.map { (_, job) -> job.await() } val bmlAccounts = bmlResults.flatMap { it.second } - val (_, fahipayAccounts) = fahipayJob?.await() ?: Pair(null, AccountCache.loadFahipay(this@HomeActivity)) + val fahipayAccounts = fahipayJobs.flatMap { (_, job) -> job.await() } val app = application as BasedBankApp + app.mibAccounts = mibAccounts + AccountCache.save(this@HomeActivity, mibAccounts) app.bmlAccounts = bmlAccounts + app.fahipayAccounts = fahipayAccounts viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts()) binding.refreshIndicator.visibility = View.GONE for ((_, session) in app.bmlSessions) refreshBmlLimits(session) - refreshFinancing(app.mibSession, app.mibProfiles.filterVisibleProfiles()) + for ((loginId, session) in app.mibSessions) { + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId)) + } } } /** Filters MIB accounts whose profileId the user has hidden in settings. */ private fun List.filterVisibleAccounts(): List { - val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds() - if (hidden.isEmpty()) return this - return filter { it.bank != "MIB" || it.profileId !in hidden } + val store = CredentialStore(this@HomeActivity) + return filter { acc -> + if (acc.bank != "MIB") return@filter true + val loginId = acc.loginTag.removePrefix("mib_") + val hidden = store.getHiddenMibProfileIds(loginId) + hidden.isEmpty() || acc.profileId !in hidden + } } - /** Filters MIB profiles the user has hidden — prevents API requests for hidden profiles. */ - private fun List.filterVisibleProfiles(): List { - val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds() + /** Filters MIB profiles the user has hidden for a given loginId. */ + private fun List.filterVisibleProfiles(loginId: String): List { + val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds(loginId) if (hidden.isEmpty()) return this return filter { it.profileId !in hidden } } @@ -571,9 +583,12 @@ class HomeActivity : AppCompatActivity() { if (cats.isNotEmpty()) viewModel.contactCategories.value = cats } // Refresh all banks in background - refreshContacts(app.mibSession, app.mibProfiles.filterVisibleProfiles()) + for ((loginId, session) in app.mibSessions) { + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + refreshContacts(loginId, session, profiles.filterVisibleProfiles(loginId)) + } refreshBmlContacts(app) - if (app.fahipaySession != null) refreshFahipayContacts(app.fahipaySession!!) + for ((_, session) in app.fahipaySessions) refreshFahipayContacts(session) } private fun refreshFahipayContacts(session: FahipaySession) { @@ -609,9 +624,9 @@ class HomeActivity : AppCompatActivity() { return result } - private fun refreshContacts(session: MibSession?, profiles: List) { - if (session == null || profiles.isEmpty()) return - val flow = MibLoginFlow(CredentialStore(this)) + private fun refreshContacts(loginId: String, session: MibSession, profiles: List) { + if (profiles.isEmpty()) return + val flow = (application as BasedBankApp).mibFlowFor(loginId) val contactsClient = MibContactsClient() lifecycleScope.launch { try { @@ -636,8 +651,8 @@ class HomeActivity : AppCompatActivity() { } if (allContacts.isNotEmpty()) { ContactsCache.save(this@HomeActivity, allContacts, allCategories) - val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds() - val bmlContacts = ContactsCache.loadBml(this@HomeActivity, bmlLoginIds) + val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity) + val bmlContacts = ContactsCache.loadBml(this@HomeActivity, store.getBmlLoginIds()) val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity) val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity) viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts)) @@ -653,7 +668,7 @@ class HomeActivity : AppCompatActivity() { val current = viewModel.accounts.value ?: emptyList() if (src.bank == "FAHIPAY") { val fresh = withContext(Dispatchers.IO) { - val sess = app.fahipaySession ?: return@withContext null + val sess = app.fahipaySessionFor(src) ?: return@withContext null try { val flow = FahipayLoginFlow() flow.setSessionCookie(sess.sessionCookie) @@ -661,8 +676,9 @@ class HomeActivity : AppCompatActivity() { val profile = flow.fetchProfile(sess) val loginTag = "fahipay_${profile.profileId}" val accounts = listOf(flow.buildAccount(profile, balance, loginTag)) - AccountCache.saveFahipay(this@HomeActivity, accounts) - app.fahipayAccounts = accounts + val loginId = src.loginTag.removePrefix("fahipay_") + AccountCache.saveFahipay(this@HomeActivity, loginId, accounts) + app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != src.loginTag } + accounts accounts } catch (_: Exception) { null } } ?: return@launch @@ -683,40 +699,43 @@ class HomeActivity : AppCompatActivity() { val otherAccounts = current.filter { it.bank != "BML" || it.loginTag != src.loginTag } viewModel.accounts.postValue(otherAccounts + fresh) } else { + val loginId = src.loginTag.removePrefix("mib_") val fresh = withContext(Dispatchers.IO) { val store = CredentialStore(this@HomeActivity) - val hidden = store.getHiddenMibProfileIds() - val allVisible = app.mibProfiles.filter { hidden.isEmpty() || it.profileId !in hidden } - // Try P47 for all visible profiles (fast path, works for multi-profile) - val sess = app.mibSession + val hidden = store.getHiddenMibProfileIds(loginId) + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + val allVisible = profiles.filter { hidden.isEmpty() || it.profileId !in hidden } + val sess = app.mibSessions[loginId] if (sess != null && allVisible.isNotEmpty()) { try { - val accounts = MibLoginFlow(store).fetchAllProfiles(sess, allVisible, src.loginTag) + val accounts = app.mibFlowFor(loginId).fetchAllProfiles(sess, allVisible, src.loginTag) if (accounts.isNotEmpty()) return@withContext accounts } catch (_: Exception) { } } - // P47 returned nothing — re-login to get fresh balances (handles single-profile A41 path) - val creds = store.loadMibCredentials() ?: return@withContext null + val creds = store.loadMibCredentials(loginId) ?: return@withContext null try { val flow = MibLoginFlow(store) val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed) - app.mibSession = flow.lastSession - app.mibProfiles = flow.lastProfiles - store.saveMibProfiles(flow.lastProfiles) + app.mibSessions[loginId] = flow.lastSession!! + app.mibProfilesMap[loginId] = flow.lastProfiles + app.mibLoginFlows[loginId] = flow + store.saveMibProfiles(loginId, flow.lastProfiles) accounts.takeIf { it.isNotEmpty() } } catch (_: Exception) { null } } ?: return@launch - // Replace all MIB accounts with fresh data - val others = current.filter { it.bank != "MIB" } - AccountCache.save(this@HomeActivity, fresh) + // Replace accounts for this MIB login + val others = current.filter { it.loginTag != src.loginTag } + val newMibAccounts = app.mibAccounts.filter { it.loginTag != src.loginTag } + fresh + app.mibAccounts = newMibAccounts + AccountCache.save(this@HomeActivity, newMibAccounts) viewModel.accounts.postValue(others + fresh) } } } - private fun refreshFinancing(session: MibSession?, profiles: List) { - if (session == null || profiles.isEmpty()) return - val flow = MibLoginFlow(CredentialStore(this)) + private fun refreshFinancing(loginId: String, session: MibSession, profiles: List) { + if (profiles.isEmpty()) return + val flow = (application as BasedBankApp).mibFlowFor(loginId) val client = MibFinancingClient() lifecycleScope.launch { try { 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 d7e4103..b3fef88 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 @@ -71,8 +71,9 @@ class OtpFragment : Fragment() { val app = requireActivity().application as BasedBankApp val entries = mutableListOf() - store.loadMibCredentials()?.let { creds -> - val name = store.loadMibFullName() + for (loginId in store.getMibLoginIds()) { + val creds = store.loadMibCredentials(loginId) ?: continue + val name = store.loadMibFullName(loginId) entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed)) } for (loginId in store.getBmlLoginIds()) { @@ -88,20 +89,23 @@ class OtpFragment : Fragment() { // Fetch real names in background if not yet cached, then refresh labels viewLifecycleOwner.lifecycleScope.launch { var changed = false - if (store.loadMibFullName() == null) { - app.mibSession?.let { session -> + for (loginId in store.getMibLoginIds()) { + if (store.loadMibFullName(loginId) == null) { + val session = app.mibSessions[loginId] ?: continue + val flow = app.mibFlowFor(loginId) val profile = withContext(Dispatchers.IO) { - try { app.mibLoginFlow.fetchPersonalProfile(session) } catch (_: Exception) { null } + try { flow.fetchPersonalProfile(session) } catch (_: Exception) { null } } if (profile != null) { - store.saveMibUserProfile(CredentialStore.MibUserProfile( + store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile( fullName = profile.fullName, username = profile.username, email = profile.email, mobile = profile.mobile, enrolled = profile.enrolled )) - val idx = entries.indexOfFirst { it.seed == store.loadMibCredentials()?.otpSeed } + val seed = store.loadMibCredentials(loginId)?.otpSeed + val idx = entries.indexOfFirst { it.seed == seed } if (idx >= 0) { entries[idx] = entries[idx].copy(label = "MIB · ${profile.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 121cd0b..f55147f 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 @@ -60,18 +60,18 @@ class SettingsLoginsFragment : Fragment() { val container = binding.loginsContainer container.removeAllViews() - val hasMib = store.hasMibCredentials() + val mibLoginIds = store.getMibLoginIds() val bmlLoginIds = store.getBmlLoginIds() - val hasFahipay = store.hasFahipayCredentials() + val fahipayLoginIds = store.getFahipayLoginIds() - binding.tvLoginsTitle.visibility = if (hasMib || bmlLoginIds.isNotEmpty() || hasFahipay) View.VISIBLE else View.GONE + binding.tvLoginsTitle.visibility = if (mibLoginIds.isNotEmpty() || bmlLoginIds.isNotEmpty() || fahipayLoginIds.isNotEmpty()) View.VISIBLE else View.GONE - if (hasMib) { - val profile = store.loadMibUserProfile() + for (loginId in mibLoginIds) { + val profile = store.loadMibUserProfile(loginId) val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name) - val mibProfiles = store.loadMibProfiles() + val mibProfiles = store.loadMibProfiles(loginId) addLoginRow(container, R.drawable.mib_logo, displayName) { - showMibLoginDetails(store, profile, mibProfiles) + showMibLoginDetails(store, loginId, profile, mibProfiles) } } @@ -99,8 +99,8 @@ class SettingsLoginsFragment : Fragment() { } } - if (hasFahipay) { - val profile = store.loadFahipayUserProfile() + for (loginId in fahipayLoginIds) { + val profile = store.loadFahipayUserProfile(loginId) val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name) addLoginRow(container, R.drawable.fahipay_logo, displayName) { showLoginDetails( @@ -111,7 +111,7 @@ class SettingsLoginsFragment : Fragment() { if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}") }.trim(), - onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store) } } + onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } } ) } } @@ -145,12 +145,13 @@ class SettingsLoginsFragment : Fragment() { private fun showMibLoginDetails( store: CredentialStore, + loginId: String, profile: CredentialStore.MibUserProfile?, mibProfiles: List ) { val ctx = requireContext() val dp = ctx.resources.displayMetrics.density - val originalHidden = store.getHiddenMibProfileIds() + val originalHidden = store.getHiddenMibProfileIds(loginId) val hidden = originalHidden.toMutableSet() val scroll = android.widget.ScrollView(ctx) @@ -231,7 +232,7 @@ class SettingsLoginsFragment : Fragment() { .setPositiveButton(R.string.save, null) // null — set manually after show() .setNeutralButton(R.string.close, null) .setNegativeButton(R.string.settings_logout) { _, _ -> - confirmLogout(getString(R.string.mib_name)) { logoutMib(store) } + confirmLogout(getString(R.string.mib_name)) { logoutMib(store, loginId) } } .show() @@ -247,7 +248,7 @@ class SettingsLoginsFragment : Fragment() { } saveBtn.setOnClickListener { - store.setHiddenMibProfileIds(hidden) + store.setHiddenMibProfileIds(loginId, hidden) clearAllCaches(ctx) dialog.dismiss() (activity as? HomeActivity)?.relogin() @@ -272,12 +273,16 @@ class SettingsLoginsFragment : Fragment() { .show() } - private fun logoutMib(store: CredentialStore) { + private fun logoutMib(store: CredentialStore, loginId: String) { val ctx = requireContext() - store.clearMibCredentials() + store.clearMibCredentials(loginId) ctx.getSharedPreferences("mib_prefs", Context.MODE_PRIVATE).edit().clear().apply() val app = requireActivity().application as BasedBankApp - app.accounts = emptyList(); app.mibSession = null; app.mibProfiles = emptyList() + app.mibSessions.remove(loginId) + app.mibProfilesMap.remove(loginId) + app.mibLoginFlows.remove(loginId) + app.mibAccounts = app.mibAccounts.filter { it.loginTag != "mib_$loginId" } + app.accounts = app.accounts.filter { it.loginTag != "mib_$loginId" } clearAllCaches(ctx) (activity as HomeActivity).relogin() buildLoginsSection() @@ -294,11 +299,12 @@ class SettingsLoginsFragment : Fragment() { buildLoginsSection() } - private fun logoutFahipay(store: CredentialStore) { + private fun logoutFahipay(store: CredentialStore, loginId: String) { val ctx = requireContext() - store.clearFahipayCredentials(); store.clearFahipaySession() + store.clearFahipayCredentials(loginId) val app = requireActivity().application as BasedBankApp - app.fahipaySession = null; app.fahipayAccounts = emptyList() + app.fahipaySessions.remove(loginId) + app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != "fahipay_$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 368971a..f0cf6d4 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 @@ -64,7 +64,9 @@ class TransferFragment : Fragment() { private val viewModel: HomeViewModel by activityViewModels() private var selectedAccount: MibAccount? = null - private val session get() = (requireActivity().application as BasedBankApp).mibSession + private val session get() = selectedAccount + ?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) } + ?: (requireActivity().application as BasedBankApp).anyMibSession() private fun bmlSessionFor(account: MibAccount?) = account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) } ?: (requireActivity().application as BasedBankApp).anyBmlSession() @@ -244,7 +246,7 @@ class TransferFragment : Fragment() { val app = requireActivity().application as BasedBankApp viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { try { - val base64 = app.mibLoginFlow.fetchProfileImage(sess, hash) ?: return@launch + val base64 = app.anyMibFlow()?.fetchProfileImage(sess, hash) ?: return@launch val bytes = Base64.decode(base64, Base64.DEFAULT) val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch withContext(Dispatchers.Main) { @@ -706,12 +708,14 @@ class TransferFragment : Fragment() { ): Triple { val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null) val app = requireActivity().application as BasedBankApp + val loginId = src.loginTag.removePrefix("mib_") // Switch to the profile that owns the source account if (src.profileId.isNotBlank()) { - val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId } - if (profile != null) app.mibLoginFlow.switchProfile(sess, profile) + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + val profile = profiles.firstOrNull { it.profileId == src.profileId } + if (profile != null) app.mibFlowFor(loginId).switchProfile(sess, profile) } - val otp = CredentialStore(requireContext()).loadMibCredentials()?.otpSeed + val otp = CredentialStore(requireContext()).loadMibCredentials(loginId)?.otpSeed ?.let { Totp.generate(it) } ?: return Triple(false, "OTP unavailable", null) val currencyCode = if (src.currencyName == "USD") "840" else "462" @@ -878,7 +882,7 @@ class TransferFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { try { val base64 = if (isProfile) { - app.mibLoginFlow.fetchProfileImage(sess, hash) + app.anyMibFlow()?.fetchProfileImage(sess, hash) } else { MibContactsClient().fetchProfileImageBase64(sess, hash) } ?: return@launch 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 1384b24..0b8e13f 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 @@ -142,7 +142,6 @@ class TransferHistoryFragment : Fragment() { } val app = requireActivity().application as BasedBankApp - val mibSession = app.mibSession lifecycleScope.launch { val newTransactions = withContext(Dispatchers.IO) { @@ -189,7 +188,7 @@ class TransferHistoryFragment : Fragment() { // Fahipay accounts val fahipayStates = activeStates.filter { it.account.bank == "FAHIPAY" } for (state in fahipayStates) { - val session = app.fahipaySession ?: continue + val session = app.fahipaySessionFor(state.account) ?: continue try { val flow = FahipayLoginFlow() flow.setSessionCookie(session.sessionCookie) @@ -207,24 +206,27 @@ class TransferHistoryFragment : Fragment() { // MIB accounts: serialized per profile, protected by mutex to prevent session race val mibStates = activeStates.filter { it.account.bank == "MIB" } - for ((profileId, states) in mibStates.groupBy { it.account.profileId }) { - val session = mibSession ?: break - app.mibMutex.withLock { - val profile = app.mibProfiles.firstOrNull { it.profileId == profileId } - if (profile != null) app.mibLoginFlow.switchProfile(session, profile) - for (state in states) { - try { - val (list, total) = MibHistoryClient().fetchHistory( - session = session, - accountNo = state.account.accountNumber, - accountDisplayName = state.account.accountBriefName, - start = state.mibNextStart, - pageSize = pageSize - ) - if (total > 0) state.mibTotalCount = total - state.mibNextStart += list.size.coerceAtLeast(pageSize) - results.addAll(list) - } catch (_: Exception) {} + for ((loginId, loginStates) in mibStates.groupBy { it.account.loginTag.removePrefix("mib_") }) { + val session = app.mibSessions[loginId] ?: continue + for ((profileId, states) in loginStates.groupBy { it.account.profileId }) { + app.mibMutex.withLock { + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + val profile = profiles.firstOrNull { it.profileId == profileId } + if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile) + for (state in states) { + try { + val (list, total) = MibHistoryClient().fetchHistory( + session = session, + accountNo = state.account.accountNumber, + accountDisplayName = state.account.accountBriefName, + start = state.mibNextStart, + pageSize = pageSize + ) + if (total > 0) state.mibTotalCount = total + state.mibNextStart += list.size.coerceAtLeast(pageSize) + results.addAll(list) + } catch (_: Exception) {} + } } } } @@ -278,7 +280,7 @@ class TransferHistoryFragment : Fragment() { return } val app = requireActivity().application as BasedBankApp - val sess = app.mibSession ?: return + val sess = app.anyMibSession() ?: return viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { try { val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptFragment.kt index cee2e23..3616651 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferReceiptFragment.kt @@ -154,11 +154,11 @@ class TransferReceiptFragment : Fragment() { private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) { val app = requireActivity().application as BasedBankApp - val sess = app.mibSession ?: return + val sess = app.anyMibSession() ?: return viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { try { val base64 = if (isProfile) { - app.mibLoginFlow.fetchProfileImage(sess, hash) + app.anyMibFlow()?.fetchProfileImage(sess, hash) } else { MibContactsClient().fetchProfileImageBase64(sess, hash) } ?: return@launch 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 b27ba07..f0c8161 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 @@ -170,6 +170,7 @@ class CredentialsFragment : Fragment() { binding.btnLogin.isEnabled = false val passwordHash = MibLoginFlow.hashPassword(password) + val loginId = username val flow = MibLoginFlow(CredentialStore(requireContext())) viewLifecycleOwner.lifecycleScope.launch { @@ -178,11 +179,12 @@ class CredentialsFragment : Fragment() { flow.login(username, passwordHash, otpSeed) } val store = CredentialStore(requireContext()) - store.saveMibCredentials(username, passwordHash, otpSeed) + store.saveMibCredentials(loginId, username, passwordHash, otpSeed) withContext(Dispatchers.IO) { flow.lastSession?.let { s -> val profile = flow.fetchPersonalProfile(s) if (profile != null) store.saveMibUserProfile( + loginId, CredentialStore.MibUserProfile( fullName = profile.fullName, username = profile.username, @@ -193,12 +195,15 @@ class CredentialsFragment : Fragment() { ) } } - AccountCache.save(requireContext(), accounts) - CredentialStore(requireContext()).saveMibProfiles(flow.lastProfiles) + store.saveMibProfiles(loginId, flow.lastProfiles) val app = requireActivity().application as BasedBankApp - app.accounts = accounts - app.mibSession = flow.lastSession - app.mibProfiles = flow.lastProfiles + // Merge with any existing MIB accounts from other logins + app.mibAccounts = app.mibAccounts.filter { it.loginTag != "mib_$loginId" } + accounts + app.accounts = app.accounts.filter { it.loginTag != "mib_$loginId" } + accounts + AccountCache.save(requireContext(), app.mibAccounts) + app.mibSessions[loginId] = flow.lastSession!! + app.mibProfilesMap[loginId] = flow.lastProfiles + app.mibLoginFlows[loginId] = flow val intent = Intent(requireContext(), HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK startActivity(intent) @@ -375,11 +380,13 @@ class CredentialsFragment : Fragment() { val b = flow.fetchBalance(session) Pair(p, b) } - val loginTag = "fahipay_${profile.profileId}" + val loginId = profile.profileId + val loginTag = "fahipay_$loginId" val account = flow.buildAccount(profile, balance, loginTag) - store.saveFahipayCredentials(idCard, password) - store.saveFahipaySession(session.authId, session.sessionCookie) + store.saveFahipayCredentials(loginId, idCard, password) + store.saveFahipaySession(loginId, session.authId, session.sessionCookie) store.saveFahipayUserProfile( + loginId, CredentialStore.FahipayUserProfile( fullName = profile.fullName, email = profile.email, @@ -390,11 +397,11 @@ class CredentialsFragment : Fragment() { linkedAccounts = profile.linkedAccounts ) ) - AccountCache.saveFahipay(requireContext(), listOf(account)) + AccountCache.saveFahipay(requireContext(), loginId, listOf(account)) val app = requireActivity().application as BasedBankApp - app.fahipaySession = session - app.fahipayAccounts = listOf(account) - app.accounts = app.accounts + listOf(account) + app.fahipaySessions[loginId] = session + app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != loginTag } + listOf(account) + app.accounts = app.accounts.filter { it.loginTag != loginTag } + listOf(account) val intent = Intent(requireContext(), HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK startActivity(intent) 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 3ab454d..789eb24 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,8 @@ object AccountCache { private const val PREFS = "account_cache" private const val KEY_MIB = "mib_accounts" - private const val KEY_FAHIPAY = "fahipay_accounts" - private fun bmlKey(loginId: String) = "bml_accounts_$loginId" + private fun fahipayKey(loginId: String) = "fahipay_accounts_$loginId" fun save(context: Context, accounts: List) { val arr = JSONArray() @@ -93,7 +92,7 @@ object AccountCache { fun loadBml(context: Context, loginIds: List): List = loginIds.flatMap { loadBml(context, it) } - fun saveFahipay(context: Context, accounts: List) { + fun saveFahipay(context: Context, loginId: String, accounts: List) { val arr = JSONArray() for (acc in accounts) { arr.put(JSONObject().apply { @@ -113,12 +112,12 @@ object AccountCache { }) } context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .edit().putString(KEY_FAHIPAY, CacheEncryption.encrypt(arr.toString())).apply() + .edit().putString(fahipayKey(loginId), CacheEncryption.encrypt(arr.toString())).apply() } - fun loadFahipay(context: Context): List { + fun loadFahipay(context: Context, loginId: String): List { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - .getString(KEY_FAHIPAY, null) ?: return emptyList() + .getString(fahipayKey(loginId), null) ?: return emptyList() return try { val arr = JSONArray(CacheEncryption.decrypt(raw)) (0 until arr.length()).map { i -> @@ -144,6 +143,9 @@ object AccountCache { } catch (_: Exception) { emptyList() } } + fun loadFahipay(context: Context, loginIds: List): List = + loginIds.flatMap { loadFahipay(context, it) } + fun clear(context: Context) { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply() } diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt b/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt index 6bdd5a2..0392069 100644 --- a/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt +++ b/app/src/main/java/sh/sar/basedbank/util/ContactManager.kt @@ -25,11 +25,14 @@ object ContactManager { } private fun deleteMib(contact: ContactDisplay, app: BasedBankApp): Boolean { - val sess = app.mibSession ?: return false + val sess = app.anyMibSession() ?: return false return try { if (contact.profileId.isNotBlank()) { - val profile = app.mibProfiles.firstOrNull { it.profileId == contact.profileId } - if (profile != null) app.mibLoginFlow.switchProfile(sess, profile) + val (loginId, profile) = app.mibProfilesMap.entries + .firstNotNullOfOrNull { (id, profiles) -> + profiles.firstOrNull { it.profileId == contact.profileId }?.let { id to it } + } ?: (null to null) + if (profile != null && loginId != null) app.mibFlowFor(loginId).switchProfile(sess, profile) } MibContactsClient().deleteContact(sess, contact.id) } catch (_: Exception) { false } 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 db74c15..dfea43e 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -21,75 +21,131 @@ class CredentialStore(context: Context) { data class BmlCredentials(val username: String, val password: String, val otpSeed: String) data class FahipayCredentials(val idCard: String, val password: String) - // ── MIB login credentials ───────────────────────────────────────────────── + // ── MIB login credentials (multi-login, keyed by loginId = username) ───── - fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username") - fun hasFahipayCredentials(): Boolean = prefs.contains("fahipay_enc_id_card") + fun getMibLoginIds(): List { + maybeMigrateLegacyMib() + val json = prefs.getString("mib_login_ids", null) ?: return emptyList() + return try { + val arr = org.json.JSONArray(json) + (0 until arr.length()).map { arr.getString(it) } + } catch (_: Exception) { emptyList() } + } - fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) { + fun hasMibCredentials(): Boolean = getMibLoginIds().isNotEmpty() + + private fun addMibLoginId(loginId: String) { + val ids = getMibLoginIds().toMutableList() + if (loginId !in ids) { + ids.add(loginId) + prefs.edit().putString("mib_login_ids", org.json.JSONArray(ids).toString()).apply() + } + } + + private fun removeMibLoginId(loginId: String) { + val ids = getMibLoginIds().toMutableList() + if (ids.remove(loginId)) + prefs.edit().putString("mib_login_ids", org.json.JSONArray(ids).toString()).apply() + } + + fun saveMibCredentials(loginId: String, username: String, passwordHash: String, otpSeed: String) { + addMibLoginId(loginId) val key = getOrCreateKey() prefs.edit() - .putString("mib_enc_username", encrypt(username, key)) - .putString("mib_enc_password_hash", encrypt(passwordHash, key)) - .putString("mib_enc_otp_seed", encrypt(otpSeed, key)) + .putString("mib_${loginId}_enc_password_hash", encrypt(passwordHash, key)) + .putString("mib_${loginId}_enc_otp_seed", encrypt(otpSeed, key)) .apply() } - fun loadMibCredentials(): MibCredentials? { + fun loadMibCredentials(loginId: String): MibCredentials? { val key = getOrCreateKey() - val encUsername = prefs.getString("mib_enc_username", null) ?: return null - val encHash = prefs.getString("mib_enc_password_hash", null) ?: return null - val encSeed = prefs.getString("mib_enc_otp_seed", null) ?: return null + val encHash = prefs.getString("mib_${loginId}_enc_password_hash", null) ?: return null + val encSeed = prefs.getString("mib_${loginId}_enc_otp_seed", null) ?: return null return try { - MibCredentials( - decrypt(encUsername, key), - decrypt(encHash, key), - decrypt(encSeed, key) - ) + MibCredentials(loginId, decrypt(encHash, key), decrypt(encSeed, key)) } catch (_: Exception) { null } } - fun clearMibCredentials() { + fun clearMibCredentials(loginId: String) { + removeMibLoginId(loginId) prefs.edit() - .remove("mib_enc_username") - .remove("mib_enc_password_hash") - .remove("mib_enc_otp_seed") - .remove("mib_enc_key1") - .remove("mib_enc_key2") - .remove("mib_enc_app_id") + .remove("mib_${loginId}_enc_password_hash") + .remove("mib_${loginId}_enc_otp_seed") + .remove("mib_${loginId}_enc_key1") + .remove("mib_${loginId}_enc_key2") + .remove("mib_${loginId}_enc_app_id") + .remove("mib_${loginId}_all_profiles") + .remove("mib_${loginId}_enc_profile") + .remove("mib_${loginId}_enc_full_name") + .remove("mib_${loginId}_hidden_profile_ids") .apply() } - // ── MIB session keys (key1/key2) and app ID ─────────────────────────────── + // ── MIB session keys (key1/key2) and app ID (per loginId) ──────────────── - fun saveMibKeys(key1: String, key2: String) { + fun saveMibKeys(loginId: String, key1: String, key2: String) { val key = getOrCreateKey() prefs.edit() - .putString("mib_enc_key1", encrypt(key1, key)) - .putString("mib_enc_key2", encrypt(key2, key)) + .putString("mib_${loginId}_enc_key1", encrypt(key1, key)) + .putString("mib_${loginId}_enc_key2", encrypt(key2, key)) .apply() } - fun loadMibKeys(): Pair? { + fun loadMibKeys(loginId: String): Pair? { val key = getOrCreateKey() - val encKey1 = prefs.getString("mib_enc_key1", null) ?: return null - val encKey2 = prefs.getString("mib_enc_key2", null) ?: return null + val encKey1 = prefs.getString("mib_${loginId}_enc_key1", null) ?: return null + val encKey2 = prefs.getString("mib_${loginId}_enc_key2", null) ?: return null return try { Pair(decrypt(encKey1, key), decrypt(encKey2, key)) } catch (_: Exception) { null } } - fun saveMibAppId(id: String) { + fun saveMibAppId(loginId: String, id: String) { val key = getOrCreateKey() - prefs.edit().putString("mib_enc_app_id", encrypt(id, key)).apply() + prefs.edit().putString("mib_${loginId}_enc_app_id", encrypt(id, key)).apply() } - fun loadMibAppId(): String? { + fun loadMibAppId(loginId: String): String? { val key = getOrCreateKey() - val enc = prefs.getString("mib_enc_app_id", null) ?: return null + val enc = prefs.getString("mib_${loginId}_enc_app_id", null) ?: return null return try { decrypt(enc, key) } catch (_: Exception) { null } } + /** One-time migration: if old single-login MIB data exists, move it to per-loginId storage. */ + private var migrationChecked = false + private fun maybeMigrateLegacyMib() { + if (migrationChecked) return + migrationChecked = true + if (prefs.contains("mib_login_ids")) return // already migrated + val encUsername = prefs.getString("mib_enc_username", null) ?: return + val key = try { getOrCreateKey() } catch (_: Exception) { return } + val loginId = try { decrypt(encUsername, key) } catch (_: Exception) { return } + val editor = prefs.edit() + // Migrate credentials + prefs.getString("mib_enc_password_hash", null)?.let { editor.putString("mib_${loginId}_enc_password_hash", it) } + prefs.getString("mib_enc_otp_seed", null)?.let { editor.putString("mib_${loginId}_enc_otp_seed", it) } + prefs.getString("mib_enc_key1", null)?.let { editor.putString("mib_${loginId}_enc_key1", it) } + prefs.getString("mib_enc_key2", null)?.let { editor.putString("mib_${loginId}_enc_key2", it) } + prefs.getString("mib_enc_app_id", null)?.let { editor.putString("mib_${loginId}_enc_app_id", it) } + prefs.getString("mib_all_profiles", null)?.let { editor.putString("mib_${loginId}_all_profiles", it) } + prefs.getString("mib_enc_profile", null)?.let { editor.putString("mib_${loginId}_enc_profile", it) } + prefs.getString("mib_enc_full_name", null)?.let { editor.putString("mib_${loginId}_enc_full_name", it) } + prefs.getStringSet("mib_hidden_profile_ids", null)?.let { editor.putStringSet("mib_${loginId}_hidden_profile_ids", it) } + // Register the login ID and clear legacy keys + editor.putString("mib_login_ids", org.json.JSONArray(listOf(loginId)).toString()) + editor.remove("mib_enc_username") + editor.remove("mib_enc_password_hash") + editor.remove("mib_enc_otp_seed") + editor.remove("mib_enc_key1") + editor.remove("mib_enc_key2") + editor.remove("mib_enc_app_id") + editor.remove("mib_all_profiles") + editor.remove("mib_enc_profile") + editor.remove("mib_enc_full_name") + editor.remove("mib_hidden_profile_ids") + editor.apply() + } + // ── BML login credentials (multi-login, keyed by loginId = username) ──────── fun getBmlLoginIds(): List { @@ -171,58 +227,81 @@ class CredentialStore(context: Context) { .apply() } - // ── Fahipay login credentials ───────────────────────────────────────────── + // ── Fahipay login credentials (multi-login, keyed by loginId = profileId) ── - fun saveFahipayCredentials(idCard: String, password: String) { + fun getFahipayLoginIds(): List { + maybeMigrateLegacyFahipay() + val json = prefs.getString("fahipay_login_ids", null) ?: return emptyList() + return try { + val arr = org.json.JSONArray(json) + (0 until arr.length()).map { arr.getString(it) } + } catch (_: Exception) { emptyList() } + } + + fun hasFahipayCredentials(): Boolean = getFahipayLoginIds().isNotEmpty() + + private fun addFahipayLoginId(loginId: String) { + val ids = getFahipayLoginIds().toMutableList() + if (loginId !in ids) { + ids.add(loginId) + prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(ids).toString()).apply() + } + } + + private fun removeFahipayLoginId(loginId: String) { + val ids = getFahipayLoginIds().toMutableList() + if (ids.remove(loginId)) + prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(ids).toString()).apply() + } + + fun saveFahipayCredentials(loginId: String, idCard: String, password: String) { + addFahipayLoginId(loginId) val key = getOrCreateKey() prefs.edit() - .putString("fahipay_enc_id_card", encrypt(idCard, key)) - .putString("fahipay_enc_password", encrypt(password, key)) + .putString("fahipay_${loginId}_enc_id_card", encrypt(idCard, key)) + .putString("fahipay_${loginId}_enc_password", encrypt(password, key)) .apply() } - fun loadFahipayCredentials(): FahipayCredentials? { + fun loadFahipayCredentials(loginId: String): FahipayCredentials? { val key = getOrCreateKey() - val encId = prefs.getString("fahipay_enc_id_card", null) ?: return null - val encPw = prefs.getString("fahipay_enc_password", null) ?: return null + val encId = prefs.getString("fahipay_${loginId}_enc_id_card", null) ?: return null + val encPw = prefs.getString("fahipay_${loginId}_enc_password", null) ?: return null return try { FahipayCredentials(decrypt(encId, key), decrypt(encPw, key)) } catch (_: Exception) { null } } - fun clearFahipayCredentials() { + fun clearFahipayCredentials(loginId: String) { + removeFahipayLoginId(loginId) prefs.edit() - .remove("fahipay_enc_id_card") - .remove("fahipay_enc_password") + .remove("fahipay_${loginId}_enc_id_card") + .remove("fahipay_${loginId}_enc_password") + .remove("fahipay_${loginId}_enc_auth_id") + .remove("fahipay_${loginId}_enc_session_cookie") + .remove("fahipay_${loginId}_enc_profile") .apply() } - // ── Fahipay session (authId + __Secure-sess cookie) ─────────────────────── + // ── Fahipay session (authId + __Secure-sess cookie) (per loginId) ───────── - fun saveFahipaySession(authId: String, sessionCookie: String) { + fun saveFahipaySession(loginId: String, authId: String, sessionCookie: String) { val key = getOrCreateKey() prefs.edit() - .putString("fahipay_enc_auth_id", encrypt(authId, key)) - .putString("fahipay_enc_session_cookie", encrypt(sessionCookie, key)) + .putString("fahipay_${loginId}_enc_auth_id", encrypt(authId, key)) + .putString("fahipay_${loginId}_enc_session_cookie", encrypt(sessionCookie, key)) .apply() } - fun loadFahipaySession(): Pair? { + fun loadFahipaySession(loginId: String): Pair? { val key = getOrCreateKey() - val encAuth = prefs.getString("fahipay_enc_auth_id", null) ?: return null - val encCookie = prefs.getString("fahipay_enc_session_cookie", null) ?: return null + val encAuth = prefs.getString("fahipay_${loginId}_enc_auth_id", null) ?: return null + val encCookie = prefs.getString("fahipay_${loginId}_enc_session_cookie", null) ?: return null return try { Pair(decrypt(encAuth, key), decrypt(encCookie, key)) } catch (_: Exception) { null } } - fun clearFahipaySession() { - prefs.edit() - .remove("fahipay_enc_auth_id") - .remove("fahipay_enc_session_cookie") - .apply() - } - // ── Fahipay device UUID (generated once, shared across all Fahipay accounts) ─ fun getOrCreateFahipayDeviceUuid(): String { @@ -236,7 +315,7 @@ class CredentialStore(context: Context) { return uuid } - // ── Fahipay user profile ────────────────────────────────────────────────── + // ── Fahipay user profile (per loginId) ──────────────────────────────────── data class FahipayUserProfile( val fullName: String, @@ -248,7 +327,7 @@ class CredentialStore(context: Context) { val linkedAccounts: String ) - fun saveFahipayUserProfile(p: FahipayUserProfile) { + fun saveFahipayUserProfile(loginId: String, p: FahipayUserProfile) { val json = org.json.JSONObject().apply { put("fullName", p.fullName) put("email", p.email) @@ -259,12 +338,12 @@ class CredentialStore(context: Context) { put("linkedAccounts", p.linkedAccounts) }.toString() val key = getOrCreateKey() - prefs.edit().putString("fahipay_enc_profile", encrypt(json, key)).apply() + prefs.edit().putString("fahipay_${loginId}_enc_profile", encrypt(json, key)).apply() } - fun loadFahipayUserProfile(): FahipayUserProfile? { + fun loadFahipayUserProfile(loginId: String): FahipayUserProfile? { val key = getOrCreateKey() - val enc = prefs.getString("fahipay_enc_profile", null) ?: return null + val enc = prefs.getString("fahipay_${loginId}_enc_profile", null) ?: return null return try { val o = org.json.JSONObject(decrypt(enc, key)) FahipayUserProfile( @@ -279,6 +358,33 @@ class CredentialStore(context: Context) { } catch (_: Exception) { null } } + /** One-time migration: if old single-login Fahipay data exists, move it to per-loginId storage. */ + private var fahipayMigrationChecked = false + private fun maybeMigrateLegacyFahipay() { + if (fahipayMigrationChecked) return + fahipayMigrationChecked = true + if (prefs.contains("fahipay_login_ids")) return // already migrated + val encProfile = prefs.getString("fahipay_enc_profile", null) ?: return + val key = try { getOrCreateKey() } catch (_: Exception) { return } + val loginId = try { + val o = org.json.JSONObject(decrypt(encProfile, key)) + o.optString("profileId").takeIf { it.isNotBlank() } + } catch (_: Exception) { null } ?: return + val editor = prefs.edit() + prefs.getString("fahipay_enc_id_card", null)?.let { editor.putString("fahipay_${loginId}_enc_id_card", it) } + prefs.getString("fahipay_enc_password", null)?.let { editor.putString("fahipay_${loginId}_enc_password", it) } + prefs.getString("fahipay_enc_auth_id", null)?.let { editor.putString("fahipay_${loginId}_enc_auth_id", it) } + prefs.getString("fahipay_enc_session_cookie", null)?.let { editor.putString("fahipay_${loginId}_enc_session_cookie", it) } + editor.putString("fahipay_${loginId}_enc_profile", encProfile) + editor.putString("fahipay_login_ids", org.json.JSONArray(listOf(loginId)).toString()) + editor.remove("fahipay_enc_id_card") + editor.remove("fahipay_enc_password") + editor.remove("fahipay_enc_auth_id") + editor.remove("fahipay_enc_session_cookie") + editor.remove("fahipay_enc_profile") + editor.apply() + } + // ── Security credential (PIN / pattern hash) ────────────────────────────── /** @@ -329,9 +435,9 @@ class CredentialStore(context: Context) { val birthdate: String ) - // ── MIB operating profiles (all profiles, regardless of visibility) ─────── + // ── MIB operating profiles (per loginId) ───────────────────────────────── - fun saveMibProfiles(profiles: List) { + fun saveMibProfiles(loginId: String, profiles: List) { val arr = org.json.JSONArray() for (p in profiles) { arr.put(org.json.JSONObject().apply { @@ -342,11 +448,11 @@ class CredentialStore(context: Context) { put("color", p.color) }) } - prefs.edit().putString("mib_all_profiles", arr.toString()).apply() + prefs.edit().putString("mib_${loginId}_all_profiles", arr.toString()).apply() } - fun loadMibProfiles(): List { - val raw = prefs.getString("mib_all_profiles", null) ?: return emptyList() + fun loadMibProfiles(loginId: String): List { + val raw = prefs.getString("mib_${loginId}_all_profiles", null) ?: return emptyList() return try { val arr = org.json.JSONArray(raw) (0 until arr.length()).map { i -> @@ -366,19 +472,18 @@ class CredentialStore(context: Context) { } catch (_: Exception) { emptyList() } } - fun saveMibFullName(name: String) { + fun saveMibFullName(loginId: String, name: String) { val key = getOrCreateKey() - prefs.edit().putString("mib_enc_full_name", encrypt(name, key)).apply() + prefs.edit().putString("mib_${loginId}_enc_full_name", encrypt(name, key)).apply() } - fun loadMibFullName(): String? { + fun loadMibFullName(loginId: String): String? { val key = getOrCreateKey() - val enc = prefs.getString("mib_enc_full_name", null) ?: return null + val enc = prefs.getString("mib_${loginId}_enc_full_name", null) ?: return null return try { decrypt(enc, key) } catch (_: Exception) { null } } - - fun saveMibUserProfile(p: MibUserProfile) { + fun saveMibUserProfile(loginId: String, p: MibUserProfile) { val json = JSONObject().apply { put("fullName", p.fullName) put("username", p.username) @@ -387,14 +492,13 @@ class CredentialStore(context: Context) { put("enrolled", p.enrolled) }.toString() val key = getOrCreateKey() - prefs.edit().putString("mib_enc_profile", encrypt(json, key)).apply() - // Keep the name in sync with the fast-path field - prefs.edit().putString("mib_enc_full_name", encrypt(p.fullName, key)).apply() + prefs.edit().putString("mib_${loginId}_enc_profile", encrypt(json, key)).apply() + prefs.edit().putString("mib_${loginId}_enc_full_name", encrypt(p.fullName, key)).apply() } - fun loadMibUserProfile(): MibUserProfile? { + fun loadMibUserProfile(loginId: String): MibUserProfile? { val key = getOrCreateKey() - val enc = prefs.getString("mib_enc_profile", null) ?: return null + val enc = prefs.getString("mib_${loginId}_enc_profile", null) ?: return null return try { val o = JSONObject(decrypt(enc, key)) MibUserProfile( @@ -436,14 +540,14 @@ class CredentialStore(context: Context) { } catch (_: Exception) { null } } - // ── MIB profile visibility ──────────────────────────────────────────────── + // ── MIB profile visibility (per loginId) ───────────────────────────────── - /** Returns the set of MIB profile IDs the user has chosen to hide from the app. */ - fun getHiddenMibProfileIds(): Set = - prefs.getStringSet("mib_hidden_profile_ids", emptySet()) ?: emptySet() + /** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */ + fun getHiddenMibProfileIds(loginId: String): Set = + prefs.getStringSet("mib_${loginId}_hidden_profile_ids", emptySet()) ?: emptySet() - fun setHiddenMibProfileIds(ids: Set) = - prefs.edit().putStringSet("mib_hidden_profile_ids", ids).apply() + fun setHiddenMibProfileIds(loginId: String, ids: Set) = + prefs.edit().putStringSet("mib_${loginId}_hidden_profile_ids", ids).apply() // ── Crypto primitives ───────────────────────────────────────────────────── diff --git a/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt b/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt index 82fd128..4a94748 100644 --- a/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt +++ b/app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt @@ -54,7 +54,7 @@ class HistoryFetcher(private val account: MibAccount) { } private fun fetchFahipay(app: BasedBankApp): List { - val session = app.fahipaySession ?: return emptyList() + val session = app.fahipaySessionFor(account) ?: return emptyList() val flow = FahipayLoginFlow() flow.setSessionCookie(session.sessionCookie) val (list, total) = flow.fetchHistory( @@ -69,9 +69,11 @@ class HistoryFetcher(private val account: MibAccount) { } private fun fetchMib(app: BasedBankApp, pageSize: Int): List { - val session = app.mibSession ?: return emptyList() - val profile = app.mibProfiles.firstOrNull { it.profileId == account.profileId } - if (profile != null) app.mibLoginFlow.switchProfile(session, profile) + val loginId = account.loginTag.removePrefix("mib_") + val session = app.mibSessions[loginId] ?: return emptyList() + val profiles = app.mibProfilesMap[loginId] ?: emptyList() + val profile = profiles.firstOrNull { it.profileId == account.profileId } + if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile) val (list, total) = MibHistoryClient().fetchHistory( session = session, accountNo = account.accountNumber, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d0c3038..b8d6c61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,7 +76,7 @@ Dashboard - Add Account + Add Login Accounts Contacts Activities diff --git a/bml_logo_long.png b/bml_logo_long.png new file mode 100644 index 0000000..83bacca Binary files /dev/null and b/bml_logo_long.png differ