Compare commits
5
Commits
04af4e1bbd
..
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c428d1f6
|
||
|
|
7b4f650f4e
|
||
|
|
00e109562b
|
||
|
|
cd4b3fef8b
|
||
|
|
389344a192
|
@@ -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
|
||||
|
||||
@@ -34,6 +34,7 @@ class AccountHistoryAdapter(
|
||||
private val iconUrlCache = mutableMapOf<String, Bitmap>()
|
||||
var onImageNeeded: ((counterpartyName: String) -> Unit)? = null
|
||||
var onIconUrlNeeded: ((url: String) -> Unit)? = null
|
||||
var onTransferClick: ((MibAccount) -> Unit)? = null
|
||||
|
||||
fun updateImage(counterpartyName: String, bitmap: Bitmap) {
|
||||
imageCache[counterpartyName] = bitmap
|
||||
@@ -164,6 +165,7 @@ class AccountHistoryAdapter(
|
||||
} else {
|
||||
b.llHeaderBlocked.visibility = View.GONE
|
||||
}
|
||||
b.btnHeaderTransfer.setOnClickListener { onTransferClick?.invoke(acc) }
|
||||
}
|
||||
|
||||
private fun friendlyType(raw: String): String {
|
||||
|
||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
@@ -82,6 +83,9 @@ class AccountHistoryFragment : Fragment() {
|
||||
adapter = AccountHistoryAdapter(account)
|
||||
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
||||
adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) }
|
||||
adapter.onTransferClick = { acc ->
|
||||
(activity as? HomeActivity)?.navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFrom(acc))
|
||||
}
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
@@ -130,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 {
|
||||
@@ -185,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)
|
||||
@@ -199,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,
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.databinding.ItemAccountBinding
|
||||
import sh.sar.basedbank.databinding.ItemCardBinding
|
||||
import sh.sar.basedbank.databinding.ItemProfileHeaderBinding
|
||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||
|
||||
class AccountsAdapter(
|
||||
accounts: List<MibAccount>,
|
||||
@@ -21,7 +21,7 @@ class AccountsAdapter(
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private sealed class Item {
|
||||
data class SectionTitle(val label: String, val chip: String) : Item()
|
||||
data class SectionTitle(val label: String) : Item()
|
||||
data class Account(val account: MibAccount) : Item()
|
||||
data class Card(val account: MibAccount) : Item()
|
||||
}
|
||||
@@ -35,19 +35,40 @@ class AccountsAdapter(
|
||||
}
|
||||
|
||||
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
||||
val regular = 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" }
|
||||
|
||||
if (regular.isNotEmpty()) {
|
||||
add(Item.SectionTitle("Accounts", ""))
|
||||
regular.forEach { add(Item.Account(it)) }
|
||||
// Group non-prepaid accounts by their derived section title, preserving order
|
||||
val groups = LinkedHashMap<String, MutableList<MibAccount>>()
|
||||
for (acc in nonPrepaid) {
|
||||
val title = sectionTitle(acc)
|
||||
groups.getOrPut(title) { mutableListOf() }.add(acc)
|
||||
}
|
||||
for ((title, group) in groups) {
|
||||
add(Item.SectionTitle(title))
|
||||
group.forEach { add(Item.Account(it)) }
|
||||
}
|
||||
|
||||
if (prepaid.isNotEmpty()) {
|
||||
add(Item.SectionTitle("Cards", "BML"))
|
||||
add(Item.SectionTitle("Cards · Bank of Maldives"))
|
||||
prepaid.forEach { add(Item.Card(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun sectionTitle(account: MibAccount): String {
|
||||
val profileLabel = when (account.profileType) {
|
||||
"0" -> "Personal"
|
||||
"1" -> "Business"
|
||||
else -> account.profileName
|
||||
}
|
||||
val bank = when {
|
||||
account.profileType.startsWith("BML") -> "Bank of Maldives"
|
||||
account.profileType == "FAHIPAY" -> "Fahipay"
|
||||
else -> "Maldives Islamic Bank"
|
||||
}
|
||||
return if (profileLabel.isNotBlank()) "$profileLabel · $bank" else bank
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int) = when (items[position]) {
|
||||
is Item.SectionTitle -> TYPE_HEADER
|
||||
is Item.Account -> TYPE_ACCOUNT
|
||||
@@ -57,7 +78,7 @@ class AccountsAdapter(
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
TYPE_HEADER -> SectionViewHolder(ItemProfileHeaderBinding.inflate(inflater, parent, false))
|
||||
TYPE_HEADER -> SectionViewHolder(ItemDateHeaderBinding.inflate(inflater, parent, false))
|
||||
TYPE_CARD -> CardViewHolder(ItemCardBinding.inflate(inflater, parent, false))
|
||||
else -> AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
@@ -73,16 +94,10 @@ class AccountsAdapter(
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
private inner class SectionViewHolder(private val binding: ItemProfileHeaderBinding) :
|
||||
private inner class SectionViewHolder(private val binding: ItemDateHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: Item.SectionTitle) {
|
||||
binding.tvProfileName.text = item.label
|
||||
if (item.chip.isNotEmpty()) {
|
||||
binding.tvProfileType.text = item.chip
|
||||
binding.tvProfileType.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.tvProfileType.visibility = View.GONE
|
||||
}
|
||||
binding.tvDateHeader.text = item.label
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,18 +106,8 @@ class AccountsAdapter(
|
||||
fun bind(account: MibAccount) {
|
||||
binding.tvAccountName.text = account.accountBriefName
|
||||
binding.tvAccountNumber.text = account.accountNumber
|
||||
binding.tvPillBank.text = when {
|
||||
account.profileType.startsWith("BML") -> "BML"
|
||||
account.profileType == "FAHIPAY" -> "FP"
|
||||
else -> null
|
||||
}
|
||||
binding.tvPillType.text = friendlyAccountType(account.accountTypeName)
|
||||
binding.tvPillProfile.text = when (account.profileType) {
|
||||
"0" -> "Personal"
|
||||
"1" -> "Business"
|
||||
else -> account.profileName
|
||||
}
|
||||
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||
binding.tvPillType.text = friendlyAccountType(account.accountTypeName)
|
||||
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||
binding.root.setOnClickListener { onAccountClick(account) }
|
||||
binding.root.setOnLongClickListener {
|
||||
copyToClipboard(it.context, account.accountNumber)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -122,6 +122,12 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
attachMediator(initialPages)
|
||||
|
||||
binding.etSheetSearch.addTextChangedListener { pagerAdapter.rebuildAll() }
|
||||
binding.etSheetSearch.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
val allIndex = pagerAdapter.pages.indexOfFirst { it.tag == null }
|
||||
if (allIndex >= 0) binding.viewPager.setCurrentItem(allIndex, true)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||
val pages = buildList {
|
||||
@@ -203,11 +209,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)
|
||||
|
||||
@@ -158,7 +158,7 @@ class ContactsFragment : Fragment() {
|
||||
colorHex = contact.bankColor,
|
||||
imageHash = contact.customerImgHash
|
||||
)
|
||||
(requireActivity() as HomeActivity).showWithBackStack(fragment)
|
||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, fragment)
|
||||
}
|
||||
|
||||
private fun confirmDelete(contact: MibBeneficiary) {
|
||||
@@ -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
|
||||
@@ -199,18 +202,19 @@ class HomeActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateTo(itemId: Int) {
|
||||
when (itemId) {
|
||||
R.id.nav_dashboard -> show(DashboardFragment())
|
||||
R.id.nav_accounts -> show(AccountsFragment())
|
||||
R.id.nav_contacts -> show(ContactsFragment())
|
||||
R.id.nav_transfer -> show(TransferFragment())
|
||||
R.id.nav_transfer_history -> show(TransferHistoryFragment())
|
||||
R.id.nav_finances -> show(FinancingFragment())
|
||||
R.id.nav_otp -> show(OtpFragment())
|
||||
R.id.nav_settings -> show(SettingsFragment())
|
||||
else -> Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
fun navigateTo(itemId: Int, fragment: Fragment? = null) {
|
||||
val dest = fragment ?: when (itemId) {
|
||||
R.id.nav_dashboard -> DashboardFragment()
|
||||
R.id.nav_accounts -> AccountsFragment()
|
||||
R.id.nav_contacts -> ContactsFragment()
|
||||
R.id.nav_transfer -> TransferFragment()
|
||||
R.id.nav_transfer_history -> TransferHistoryFragment()
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
|
||||
}
|
||||
show(dest)
|
||||
binding.navigationView.setCheckedItem(itemId)
|
||||
val bottomNavIds = setOf(R.id.nav_dashboard, R.id.nav_accounts, R.id.nav_contacts, R.id.nav_transfer, R.id.nav_more)
|
||||
if (binding.bottomNavigation.visibility == View.VISIBLE && itemId in bottomNavIds) {
|
||||
@@ -323,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
|
||||
@@ -334,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 {
|
||||
@@ -367,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,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 {
|
||||
@@ -425,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))
|
||||
@@ -456,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)
|
||||
}
|
||||
}
|
||||
@@ -486,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 */ }
|
||||
}
|
||||
@@ -503,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
|
||||
@@ -534,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)
|
||||
@@ -580,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))
|
||||
@@ -612,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 = ""
|
||||
@@ -93,6 +95,11 @@ class TransferFragment : Fragment() {
|
||||
private const val ARG_SUBTITLE = "contact_subtitle"
|
||||
private const val ARG_COLOR = "contact_color"
|
||||
private const val ARG_IMAGE_HASH = "contact_image_hash"
|
||||
private const val ARG_FROM_ACCOUNT = "from_account"
|
||||
|
||||
fun newInstanceFrom(account: MibAccount) = TransferFragment().apply {
|
||||
arguments = Bundle().apply { putString(ARG_FROM_ACCOUNT, account.accountNumber) }
|
||||
}
|
||||
|
||||
fun newInstance(
|
||||
accountNumber: String,
|
||||
@@ -171,6 +178,16 @@ class TransferFragment : Fragment() {
|
||||
updateAmountPrefix(picked)
|
||||
showFromCard(picked)
|
||||
}
|
||||
|
||||
val fromNumber = arguments?.getString(ARG_FROM_ACCOUNT)
|
||||
if (fromNumber != null && selectedAccount == null) {
|
||||
val match = accounts.firstOrNull { it.accountNumber == fromNumber }
|
||||
if (match != null) {
|
||||
selectedAccount = match
|
||||
updateAmountPrefix(match)
|
||||
showFromCard(match)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,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
|
||||
}
|
||||
@@ -256,7 +274,7 @@ class TransferFragment : Fragment() {
|
||||
Toast.makeText(requireContext(), R.string.transfer_select_source_first, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
val accountNumber = binding.etTo.text?.toString()?.trim() ?: ""
|
||||
val accountNumber = AccountInputParser.normalize(binding.etTo.text?.toString()?.trim() ?: "")
|
||||
if (accountNumber.isBlank()) {
|
||||
Toast.makeText(requireContext(), R.string.transfer_enter_account_first, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
@@ -287,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
|
||||
@@ -533,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()
|
||||
@@ -562,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}"
|
||||
|
||||
@@ -592,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 {
|
||||
@@ -600,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)
|
||||
@@ -726,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 {
|
||||
@@ -735,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
|
||||
@@ -744,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")
|
||||
@@ -767,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 {
|
||||
@@ -917,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))
|
||||
@@ -927,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]
|
||||
@@ -958,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) {
|
||||
|
||||
@@ -11,6 +11,21 @@ object AccountInputParser {
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip spaces and remove a 960/+960 country code prefix, but only when
|
||||
* the stripped result is exactly 7 digits (so "9603456" is left intact).
|
||||
*/
|
||||
fun normalize(input: String): String {
|
||||
var s = input.replace(" ", "")
|
||||
val stripped = when {
|
||||
s.startsWith("+960") -> s.removePrefix("+960")
|
||||
s.startsWith("960") -> s.removePrefix("960")
|
||||
else -> null
|
||||
}
|
||||
if (stripped != null && stripped.matches(Regex("^\\d{7}$"))) s = stripped
|
||||
return s
|
||||
}
|
||||
|
||||
fun detect(input: String): InputType {
|
||||
val s = input.trim()
|
||||
return when {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="14dp"
|
||||
android:gravity="center_vertical"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
|
||||
@@ -43,7 +39,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Right: segmented pill (bank | type | profile) + balance -->
|
||||
<!-- Right: segmented pill (bank | type) + balance -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -58,41 +54,11 @@
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/pill_segment_bg">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPillBank"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingVertical="6dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="16dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPillType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="6dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="16dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPillProfile"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="6dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
@@ -111,4 +77,10 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<LinearLayout
|
||||
@@ -165,6 +166,20 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnHeaderTransfer"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
app:icon="@drawable/ic_send"
|
||||
app:iconSize="20dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="14dp"
|
||||
android:gravity="center_vertical"
|
||||
android:foreground="?attr/selectableItemBackground">
|
||||
|
||||
@@ -89,4 +85,10 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
||||
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
||||
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
||||
<exclude domain="cache" path="."/>
|
||||
<exclude domain="root" path="cache/"/>
|
||||
</full-backup-content>
|
||||
@@ -14,7 +14,7 @@
|
||||
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
||||
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
||||
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
||||
<exclude domain="cache" path="."/>
|
||||
<exclude domain="root" path="cache/"/>
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="sharedpref" path="credential_store.xml"/>
|
||||
@@ -26,6 +26,6 @@
|
||||
<exclude domain="sharedpref" path="foreign_limits_cache.xml"/>
|
||||
<exclude domain="sharedpref" path="recents_cache.xml"/>
|
||||
<exclude domain="sharedpref" path="lock_attempts.xml"/>
|
||||
<exclude domain="cache" path="."/>
|
||||
<exclude domain="root" path="cache/"/>
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
Reference in New Issue
Block a user