add support for multiple BML accounts, and BML credit cards
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s
This commit is contained in:
@@ -19,11 +19,19 @@ class BasedBankApp : Application() {
|
||||
var fullName: String = ""
|
||||
var mibSession: MibSession? = null
|
||||
var mibProfiles: List<MibProfile> = emptyList()
|
||||
var bmlSession: BmlSession? = null
|
||||
/** Active BML sessions keyed by loginId (= BML username). */
|
||||
val bmlSessions: MutableMap<String, BmlSession> = mutableMapOf()
|
||||
var bmlAccounts: List<MibAccount> = emptyList()
|
||||
var fahipaySession: FahipaySession? = null
|
||||
var fahipayAccounts: List<MibAccount> = emptyList()
|
||||
|
||||
/** Returns the BML session for the given account (matched via loginTag). */
|
||||
fun bmlSessionFor(account: MibAccount): BmlSession? =
|
||||
bmlSessions[account.loginTag.removePrefix("bml_")]
|
||||
|
||||
/** Returns any available BML session (for non-account-specific operations). */
|
||||
fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull()
|
||||
|
||||
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
||||
val mibMutex = Mutex()
|
||||
|
||||
|
||||
@@ -166,17 +166,17 @@ class BmlLoginFlow {
|
||||
.takeIf { it.isNotBlank() } ?: throw Exception("Token exchange failed")
|
||||
|
||||
val session = BmlSession(accessToken = accessToken, deviceId = deviceId)
|
||||
val accounts = fetchAccounts(session)
|
||||
val accounts = fetchAccounts(session, "bml_$username")
|
||||
return Pair(session, accounts)
|
||||
}
|
||||
|
||||
fun fetchAccounts(session: BmlSession): List<MibAccount> {
|
||||
fun fetchAccounts(session: BmlSession, loginTag: String): List<MibAccount> {
|
||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
||||
val code = resp.code
|
||||
val json = resp.body?.string()
|
||||
resp.close()
|
||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||
return parseDashboard(json ?: return emptyList(), "bml_${session.deviceId}")
|
||||
return parseDashboard(json ?: return emptyList(), loginTag)
|
||||
}
|
||||
|
||||
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
|
||||
@@ -324,11 +324,11 @@ class BmlLoginFlow {
|
||||
return try { JSONObject(json).optBoolean("success") } catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
||||
fun fetchContacts(session: BmlSession, loginId: String): List<MibBeneficiary> {
|
||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/contacts")).execute()
|
||||
val json = resp.body?.string() ?: return emptyList()
|
||||
resp.close()
|
||||
return parseContacts(json)
|
||||
return parseContacts(json, loginId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -634,29 +634,28 @@ class BmlLoginFlow {
|
||||
internalId = internalId
|
||||
))
|
||||
} else if (accountType == "Card") {
|
||||
val isVisible = item.optBoolean("account_visible", false)
|
||||
if (!isVisible) continue // debit cards and other hidden cards — skip
|
||||
val isPrepaid = item.optBoolean("prepaid_card", false)
|
||||
if (isPrepaid) {
|
||||
val cardBalance = item.optJSONObject("cardBalance")
|
||||
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
||||
prepaidCards.add(MibAccount(
|
||||
profileName = "Personal",
|
||||
profileType = "BML_PREPAID",
|
||||
accountNumber = accountNumber,
|
||||
accountBriefName = product,
|
||||
currencyName = currency,
|
||||
accountTypeName = product,
|
||||
availableBalance = "%.2f".format(available),
|
||||
currentBalance = "%.2f".format(cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0),
|
||||
blockedAmount = "0.00",
|
||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||
statusDesc = status,
|
||||
profileImageHash = null,
|
||||
loginTag = loginTag,
|
||||
internalId = internalId
|
||||
))
|
||||
} else {
|
||||
// Linked debit cards have no independent balance or account link — skip
|
||||
}
|
||||
val cardBalance = item.optJSONObject("cardBalance")
|
||||
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
||||
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
||||
prepaidCards.add(MibAccount(
|
||||
profileName = "Personal",
|
||||
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
||||
accountNumber = accountNumber,
|
||||
accountBriefName = item.optString("alias").ifBlank { product },
|
||||
currencyName = currency,
|
||||
accountTypeName = product,
|
||||
availableBalance = "%.2f".format(available),
|
||||
currentBalance = "%.2f".format(current),
|
||||
blockedAmount = "0.00",
|
||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||
statusDesc = status,
|
||||
profileImageHash = null,
|
||||
loginTag = loginTag,
|
||||
internalId = internalId
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +693,7 @@ class BmlLoginFlow {
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
private fun parseContacts(json: String): List<MibBeneficiary> {
|
||||
private fun parseContacts(json: String, loginId: String = ""): List<MibBeneficiary> {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return emptyList()
|
||||
val payload: JSONArray = root.optJSONArray("payload") ?: return emptyList()
|
||||
@@ -715,7 +714,8 @@ class BmlLoginFlow {
|
||||
benefStatus = item.optString("status", "S"),
|
||||
transferCyDesc = item.optString("currency", "MVR"),
|
||||
customerImgHash = null,
|
||||
benefCategoryId = "BML"
|
||||
benefCategoryId = "BML",
|
||||
profileId = loginId
|
||||
))
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -35,8 +35,8 @@ class AccountsAdapter(
|
||||
}
|
||||
|
||||
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
||||
val nonPrepaid = accounts.filter { it.profileType != "BML_PREPAID" }
|
||||
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" }
|
||||
val nonPrepaid = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||
|
||||
// Group non-prepaid accounts by their derived section title, preserving order
|
||||
val groups = LinkedHashMap<String, MutableList<MibAccount>>()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -63,7 +63,9 @@ class TransferFragment : Fragment() {
|
||||
|
||||
private var selectedAccount: MibAccount? = null
|
||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||
private val bmlSession get() = (requireActivity().application as BasedBankApp).bmlSession
|
||||
private fun bmlSessionFor(account: MibAccount?) =
|
||||
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
||||
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
||||
|
||||
// Resolved recipient info — set after successful lookup or prefill
|
||||
private var resolvedAccountNumber = ""
|
||||
@@ -203,6 +205,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
val typeLabel = when {
|
||||
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
||||
account.profileType == "BML_CREDIT" -> "Credit Card"
|
||||
account.accountTypeName.isNotBlank() -> account.accountTypeName
|
||||
else -> account.profileType
|
||||
}
|
||||
@@ -302,7 +305,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
val mibSess = session
|
||||
val bmlSess = bmlSession
|
||||
val bmlSess = bmlSessionFor(selectedAccount)
|
||||
if (mibSess == null && bmlSess == null) {
|
||||
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
@@ -548,7 +551,7 @@ class TransferFragment : Fragment() {
|
||||
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
||||
|
||||
val isSrcBml = src.profileType.startsWith("BML")
|
||||
val isSrcCard = src.profileType == "BML_PREPAID"
|
||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||
val currency = src.currencyName.ifBlank { "MVR" }
|
||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
||||
@@ -577,6 +580,7 @@ class TransferFragment : Fragment() {
|
||||
?.transferCyDesc?.ifBlank { "MVR" }
|
||||
?: if (isDestMib) "MVR" else "MVR"
|
||||
val isUsdToMvr = currency.equals("USD", ignoreCase = true) && destCurrency.equals("MVR", ignoreCase = true)
|
||||
val isSrcCredit = src.profileType == "BML_CREDIT"
|
||||
|
||||
val mainMsg = "Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}"
|
||||
|
||||
@@ -607,7 +611,7 @@ class TransferFragment : Fragment() {
|
||||
.setPositiveButton(R.string.transfer_confirm) { _, _ -> doTransfer() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
|
||||
if (isUsdToMvr) {
|
||||
if (isUsdToMvr || isSrcCredit) {
|
||||
val ctx = requireContext()
|
||||
val dp = resources.displayMetrics.density
|
||||
val container = LinearLayout(ctx).apply {
|
||||
@@ -615,13 +619,24 @@ class TransferFragment : Fragment() {
|
||||
setPadding((24 * dp).toInt(), (16 * dp).toInt(), (24 * dp).toInt(), 0)
|
||||
}
|
||||
container.addView(TextView(ctx).apply { text = mainMsg })
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = "⚠ You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
|
||||
setTextColor(Color.RED)
|
||||
textSize = 16f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setPadding(0, (16 * dp).toInt(), 0, 0)
|
||||
})
|
||||
if (isUsdToMvr) {
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = "⚠ You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
|
||||
setTextColor(Color.RED)
|
||||
textSize = 16f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setPadding(0, (16 * dp).toInt(), 0, 0)
|
||||
})
|
||||
}
|
||||
if (isSrcCredit) {
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = "⚠ Transferring from a credit card is treated as a cash advance. Cash advance fees will be charged on the 10th of the month."
|
||||
setTextColor(Color.RED)
|
||||
textSize = 16f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setPadding(0, (16 * dp).toInt(), 0, 0)
|
||||
})
|
||||
}
|
||||
dialogBuilder.setView(container)
|
||||
} else {
|
||||
dialogBuilder.setMessage(mainMsg)
|
||||
@@ -741,8 +756,9 @@ class TransferFragment : Fragment() {
|
||||
allAccounts: List<MibAccount>,
|
||||
allContacts: List<MibBeneficiary>
|
||||
): Triple<Boolean, String, TransferReceiptData?> {
|
||||
val sess = bmlSession ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||
val otp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
||||
val loginId = src.loginTag.removePrefix("bml_")
|
||||
val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||
val otp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
||||
?.let { Totp.generate(it) }
|
||||
?: return Triple(false, "OTP unavailable", null)
|
||||
val debitAccount = src.internalId.ifBlank {
|
||||
@@ -750,7 +766,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
// Determine type + credit account
|
||||
val isDestMyCard = allAccounts.any { it.profileType == "BML_PREPAID" && it.accountNumber == destAccount }
|
||||
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
||||
val (transferType, creditAccount, bank) = when {
|
||||
isSrcCard -> {
|
||||
// CAD: card → own BML account
|
||||
@@ -759,7 +775,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
isDestMyCard -> {
|
||||
// CPA: BML CASA → own card top-up
|
||||
val card = allAccounts.first { it.profileType == "BML_PREPAID" && it.accountNumber == destAccount }
|
||||
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
||||
}
|
||||
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
||||
@@ -782,7 +798,7 @@ class TransferFragment : Fragment() {
|
||||
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
||||
|
||||
// Step 2: confirm with fresh OTP
|
||||
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
||||
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
||||
?.let { Totp.generate(it) } ?: otp
|
||||
|
||||
return try {
|
||||
@@ -932,8 +948,8 @@ class TransferFragment : Fragment() {
|
||||
) : BaseAdapter(), Filterable {
|
||||
|
||||
private val items: List<Any> = buildList {
|
||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
|
||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||
addAll(regular)
|
||||
if (cards.isNotEmpty()) {
|
||||
add(getString(R.string.cards))
|
||||
@@ -942,7 +958,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
|
||||
?.takeUnless { it.profileType == "BML_PREPAID" && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
|
||||
override fun getCount() = items.size
|
||||
override fun getItem(position: Int) = items[position]
|
||||
@@ -973,7 +989,7 @@ class TransferFragment : Fragment() {
|
||||
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
.also { it.root.tag = it }
|
||||
}
|
||||
val inactive = acc.profileType == "BML_PREPAID" && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
b.tvDropdownAccountName.text = acc.accountBriefName
|
||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,9 +9,10 @@ object AccountCache {
|
||||
|
||||
private const val PREFS = "account_cache"
|
||||
private const val KEY_MIB = "mib_accounts"
|
||||
private const val KEY_BML = "bml_accounts"
|
||||
private const val KEY_FAHIPAY = "fahipay_accounts"
|
||||
|
||||
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
||||
|
||||
fun save(context: Context, accounts: List<MibAccount>) {
|
||||
val arr = JSONArray()
|
||||
for (acc in accounts) {
|
||||
@@ -36,7 +37,7 @@ object AccountCache {
|
||||
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
|
||||
}
|
||||
|
||||
fun saveBml(context: Context, accounts: List<MibAccount>) {
|
||||
fun saveBml(context: Context, loginId: String, accounts: List<MibAccount>) {
|
||||
val arr = JSONArray()
|
||||
for (acc in accounts) {
|
||||
arr.put(JSONObject().apply {
|
||||
@@ -56,15 +57,14 @@ object AccountCache {
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.edit().putString(KEY_BML, CacheEncryption.encrypt(arr.toString())).apply()
|
||||
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||
}
|
||||
|
||||
fun loadBml(context: Context): List<MibAccount> {
|
||||
fun loadBml(context: Context, loginId: String): List<MibAccount> {
|
||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getString(KEY_BML, null) ?: return emptyList()
|
||||
.getString(bmlKey(loginId), null) ?: return emptyList()
|
||||
return try {
|
||||
val json = CacheEncryption.decrypt(raw)
|
||||
val arr = JSONArray(json)
|
||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||
(0 until arr.length()).map { i ->
|
||||
val o = arr.getJSONObject(i)
|
||||
MibAccount(
|
||||
@@ -84,9 +84,12 @@ object AccountCache {
|
||||
internalId = o.optString("internalId", "")
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) { emptyList() }
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
|
||||
loginIds.flatMap { loadBml(context, it) }
|
||||
|
||||
fun saveFahipay(context: Context, accounts: List<MibAccount>) {
|
||||
val arr = JSONArray()
|
||||
for (acc in accounts) {
|
||||
|
||||
@@ -84,7 +84,9 @@ object ContactsCache {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveBml(context: Context, contacts: List<MibBeneficiary>) {
|
||||
private fun bmlKey(loginId: String) = "bml_contacts_$loginId"
|
||||
|
||||
fun saveBml(context: Context, loginId: String, contacts: List<MibBeneficiary>) {
|
||||
val arr = JSONArray()
|
||||
for (c in contacts) {
|
||||
arr.put(JSONObject().apply {
|
||||
@@ -99,18 +101,18 @@ object ContactsCache {
|
||||
put("benefStatus", c.benefStatus)
|
||||
put("transferCyDesc", c.transferCyDesc)
|
||||
put("benefCategoryId", c.benefCategoryId)
|
||||
put("profileId", c.profileId)
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.edit().putString("bml_contacts", CacheEncryption.encrypt(arr.toString())).apply()
|
||||
.edit().putString(bmlKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||
}
|
||||
|
||||
fun loadBml(context: Context): List<MibBeneficiary> {
|
||||
fun loadBml(context: Context, loginId: String): List<MibBeneficiary> {
|
||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getString("bml_contacts", null) ?: return emptyList()
|
||||
.getString(bmlKey(loginId), null) ?: return emptyList()
|
||||
return try {
|
||||
val json = CacheEncryption.decrypt(raw)
|
||||
val arr = JSONArray(json)
|
||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||
(0 until arr.length()).map { i ->
|
||||
val o = arr.getJSONObject(i)
|
||||
MibBeneficiary(
|
||||
@@ -125,12 +127,16 @@ object ContactsCache {
|
||||
benefStatus = o.optString("benefStatus"),
|
||||
transferCyDesc = o.optString("transferCyDesc", "MVR"),
|
||||
customerImgHash = null,
|
||||
benefCategoryId = o.optString("benefCategoryId", "BML")
|
||||
benefCategoryId = o.optString("benefCategoryId", "BML"),
|
||||
profileId = o.optString("profileId", "")
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) { emptyList() }
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
fun loadBml(context: Context, loginIds: List<String>): List<MibBeneficiary> =
|
||||
loginIds.flatMap { loadBml(context, it) }
|
||||
|
||||
fun saveFahipay(context: Context, contacts: List<MibBeneficiary>, categories: List<MibBeneficiaryCategory>) {
|
||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||
val arr = JSONArray()
|
||||
|
||||
@@ -24,7 +24,6 @@ class CredentialStore(context: Context) {
|
||||
// ── MIB login credentials ─────────────────────────────────────────────────
|
||||
|
||||
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
|
||||
fun hasBmlCredentials(): Boolean = prefs.contains("bml_enc_username")
|
||||
fun hasFahipayCredentials(): Boolean = prefs.contains("fahipay_enc_id_card")
|
||||
|
||||
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
|
||||
@@ -91,58 +90,106 @@ class CredentialStore(context: Context) {
|
||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
// ── BML login credentials ─────────────────────────────────────────────────
|
||||
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
|
||||
|
||||
fun saveBmlCredentials(username: String, password: String, otpSeed: String) {
|
||||
fun getBmlLoginIds(): List<String> {
|
||||
val json = prefs.getString("bml_login_ids", null)
|
||||
if (json != null) {
|
||||
return try {
|
||||
val arr = org.json.JSONArray(json)
|
||||
(0 until arr.length()).map { arr.getString(it) }
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
// One-time migration from single-slot BML storage
|
||||
val oldEncUsername = prefs.getString("bml_enc_username", null) ?: return emptyList()
|
||||
return try {
|
||||
val key = getOrCreateKey()
|
||||
val loginId = decrypt(oldEncUsername, key)
|
||||
val edit = prefs.edit()
|
||||
prefs.getString("bml_enc_password", null)?.let { edit.putString("bml_${loginId}_enc_password", it) }
|
||||
prefs.getString("bml_enc_otp_seed", null)?.let { edit.putString("bml_${loginId}_enc_otp_seed", it) }
|
||||
prefs.getString("bml_enc_token", null)?.let { edit.putString("bml_${loginId}_enc_token", it) }
|
||||
prefs.getString("bml_enc_device_id", null)?.let { edit.putString("bml_${loginId}_enc_device_id", it) }
|
||||
prefs.getString("bml_enc_profile", null)?.let { edit.putString("bml_${loginId}_enc_profile", it) }
|
||||
edit.putString("bml_${loginId}_enc_username", oldEncUsername)
|
||||
edit.remove("bml_enc_username").remove("bml_enc_password").remove("bml_enc_otp_seed")
|
||||
.remove("bml_enc_token").remove("bml_enc_device_id")
|
||||
.remove("bml_enc_profile").remove("bml_enc_full_name")
|
||||
val ids = org.json.JSONArray(listOf(loginId)).toString()
|
||||
edit.putString("bml_login_ids", ids)
|
||||
edit.apply()
|
||||
listOf(loginId)
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
fun hasBmlCredentials(): Boolean = getBmlLoginIds().isNotEmpty()
|
||||
|
||||
private fun addBmlLoginId(loginId: String) {
|
||||
val ids = getBmlLoginIds().toMutableList()
|
||||
if (loginId !in ids) {
|
||||
ids.add(loginId)
|
||||
prefs.edit().putString("bml_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeBmlLoginId(loginId: String) {
|
||||
val ids = getBmlLoginIds().toMutableList()
|
||||
if (ids.remove(loginId))
|
||||
prefs.edit().putString("bml_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||
}
|
||||
|
||||
fun saveBmlCredentials(loginId: String, username: String, password: String, otpSeed: String) {
|
||||
addBmlLoginId(loginId)
|
||||
val key = getOrCreateKey()
|
||||
prefs.edit()
|
||||
.putString("bml_enc_username", encrypt(username, key))
|
||||
.putString("bml_enc_password", encrypt(password, key))
|
||||
.putString("bml_enc_otp_seed", encrypt(otpSeed, key))
|
||||
.putString("bml_${loginId}_enc_username", encrypt(username, key))
|
||||
.putString("bml_${loginId}_enc_password", encrypt(password, key))
|
||||
.putString("bml_${loginId}_enc_otp_seed", encrypt(otpSeed, key))
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun loadBmlCredentials(): BmlCredentials? {
|
||||
fun loadBmlCredentials(loginId: String): BmlCredentials? {
|
||||
val key = getOrCreateKey()
|
||||
val encUsername = prefs.getString("bml_enc_username", null) ?: return null
|
||||
val encPassword = prefs.getString("bml_enc_password", null) ?: return null
|
||||
val encSeed = prefs.getString("bml_enc_otp_seed", null) ?: return null
|
||||
val encUsername = prefs.getString("bml_${loginId}_enc_username", null) ?: return null
|
||||
val encPassword = prefs.getString("bml_${loginId}_enc_password", null) ?: return null
|
||||
val encSeed = prefs.getString("bml_${loginId}_enc_otp_seed", null) ?: return null
|
||||
return try {
|
||||
BmlCredentials(decrypt(encUsername, key), decrypt(encPassword, key), decrypt(encSeed, key))
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun clearBmlCredentials() {
|
||||
fun clearBmlCredentials(loginId: String) {
|
||||
removeBmlLoginId(loginId)
|
||||
prefs.edit()
|
||||
.remove("bml_enc_username")
|
||||
.remove("bml_enc_password")
|
||||
.remove("bml_enc_otp_seed")
|
||||
.remove("bml_${loginId}_enc_username")
|
||||
.remove("bml_${loginId}_enc_password")
|
||||
.remove("bml_${loginId}_enc_otp_seed")
|
||||
.apply()
|
||||
}
|
||||
|
||||
// ── BML session token ─────────────────────────────────────────────────────
|
||||
// ── BML session token (per loginId) ───────────────────────────────────────
|
||||
|
||||
fun saveBmlSession(accessToken: String, deviceId: String) {
|
||||
fun saveBmlSession(loginId: String, accessToken: String, deviceId: String) {
|
||||
val key = getOrCreateKey()
|
||||
prefs.edit()
|
||||
.putString("bml_enc_token", encrypt(accessToken, key))
|
||||
.putString("bml_enc_device_id", encrypt(deviceId, key))
|
||||
.putString("bml_${loginId}_enc_token", encrypt(accessToken, key))
|
||||
.putString("bml_${loginId}_enc_device_id", encrypt(deviceId, key))
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun loadBmlSession(): Pair<String, String>? {
|
||||
fun loadBmlSession(loginId: String): Pair<String, String>? {
|
||||
val key = getOrCreateKey()
|
||||
val encToken = prefs.getString("bml_enc_token", null) ?: return null
|
||||
val encDeviceId = prefs.getString("bml_enc_device_id", null) ?: return null
|
||||
val encToken = prefs.getString("bml_${loginId}_enc_token", null) ?: return null
|
||||
val encDeviceId = prefs.getString("bml_${loginId}_enc_device_id", null) ?: return null
|
||||
return try {
|
||||
Pair(decrypt(encToken, key), decrypt(encDeviceId, key))
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun clearBmlSession() {
|
||||
fun clearBmlSession(loginId: String) {
|
||||
prefs.edit()
|
||||
.remove("bml_enc_token")
|
||||
.remove("bml_enc_device_id")
|
||||
.remove("bml_${loginId}_enc_token")
|
||||
.remove("bml_${loginId}_enc_device_id")
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -315,16 +362,6 @@ class CredentialStore(context: Context) {
|
||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun saveBmlFullName(name: String) {
|
||||
val key = getOrCreateKey()
|
||||
prefs.edit().putString("bml_enc_full_name", encrypt(name, key)).apply()
|
||||
}
|
||||
|
||||
fun loadBmlFullName(): String? {
|
||||
val key = getOrCreateKey()
|
||||
val enc = prefs.getString("bml_enc_full_name", null) ?: return null
|
||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun saveMibUserProfile(p: MibUserProfile) {
|
||||
val json = JSONObject().apply {
|
||||
@@ -355,7 +392,7 @@ class CredentialStore(context: Context) {
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun saveBmlUserProfile(p: BmlUserProfile) {
|
||||
fun saveBmlUserProfile(loginId: String, p: BmlUserProfile) {
|
||||
val json = JSONObject().apply {
|
||||
put("fullName", p.fullName)
|
||||
put("email", p.email)
|
||||
@@ -365,13 +402,12 @@ class CredentialStore(context: Context) {
|
||||
put("birthdate", p.birthdate)
|
||||
}.toString()
|
||||
val key = getOrCreateKey()
|
||||
prefs.edit().putString("bml_enc_profile", encrypt(json, key)).apply()
|
||||
prefs.edit().putString("bml_enc_full_name", encrypt(p.fullName, key)).apply()
|
||||
prefs.edit().putString("bml_${loginId}_enc_profile", encrypt(json, key)).apply()
|
||||
}
|
||||
|
||||
fun loadBmlUserProfile(): BmlUserProfile? {
|
||||
fun loadBmlUserProfile(loginId: String): BmlUserProfile? {
|
||||
val key = getOrCreateKey()
|
||||
val enc = prefs.getString("bml_enc_profile", null) ?: return null
|
||||
val enc = prefs.getString("bml_${loginId}_enc_profile", null) ?: return null
|
||||
return try {
|
||||
val o = JSONObject(decrypt(enc, key))
|
||||
BmlUserProfile(
|
||||
|
||||
Reference in New Issue
Block a user