Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b1e73533f6
|
|||
|
3a5b9459a9
|
|||
|
9c9729e268
|
|||
|
399cfbf108
|
|||
|
19f4d01015
|
|||
|
8c40322ff0
|
|||
|
782e2e7674
|
6
.idea/deploymentTargetSelector.xml
generated
6
.idea/deploymentTargetSelector.xml
generated
@@ -3,11 +3,11 @@
|
|||||||
<component name="deploymentTargetSelector">
|
<component name="deploymentTargetSelector">
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DIALOG" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-05-15T13:54:16.798188666Z">
|
<DropdownSelection timestamp="2026-05-18T20:24:18.550107339Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=a703e092" />
|
<DeviceId pluginId="Default" identifier="serial=10.0.1.239:5555;connection=ce61d84c" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "sh.sar.basedbank"
|
applicationId = "sh.sar.basedbank"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 2
|
versionCode = 3
|
||||||
versionName = "1.0.3"
|
versionName = "1.0.4"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,42 @@ class BasedBankApp : Application() {
|
|||||||
// Held in memory after successful login; cleared on logout
|
// Held in memory after successful login; cleared on logout
|
||||||
var accounts: List<MibAccount> = emptyList()
|
var accounts: List<MibAccount> = emptyList()
|
||||||
var fullName: String = ""
|
var fullName: String = ""
|
||||||
var mibSession: MibSession? = null
|
/** Active MIB sessions keyed by loginId (= MIB username). */
|
||||||
var mibProfiles: List<MibProfile> = emptyList()
|
val mibSessions: MutableMap<String, MibSession> = mutableMapOf()
|
||||||
|
val mibProfilesMap: MutableMap<String, List<MibProfile>> = mutableMapOf()
|
||||||
|
val mibLoginFlows: MutableMap<String, MibLoginFlow> = mutableMapOf()
|
||||||
|
var mibAccounts: List<MibAccount> = emptyList()
|
||||||
/** Active BML sessions keyed by loginId (= BML username). */
|
/** Active BML sessions keyed by loginId (= BML username). */
|
||||||
val bmlSessions: MutableMap<String, BmlSession> = mutableMapOf()
|
val bmlSessions: MutableMap<String, BmlSession> = mutableMapOf()
|
||||||
var bmlAccounts: List<MibAccount> = emptyList()
|
var bmlAccounts: List<MibAccount> = emptyList()
|
||||||
var fahipaySession: FahipaySession? = null
|
/** Active Fahipay sessions keyed by loginId (= profileId). */
|
||||||
|
val fahipaySessions: MutableMap<String, FahipaySession> = mutableMapOf()
|
||||||
var fahipayAccounts: List<MibAccount> = emptyList()
|
var fahipayAccounts: List<MibAccount> = emptyList()
|
||||||
|
|
||||||
|
/** Returns the MIB session for the given account (matched via loginTag). */
|
||||||
|
fun mibSessionFor(account: MibAccount): MibSession? =
|
||||||
|
mibSessions[account.loginTag.removePrefix("mib_")]
|
||||||
|
|
||||||
|
/** Returns any available MIB session. */
|
||||||
|
fun anyMibSession(): MibSession? = mibSessions.values.firstOrNull()
|
||||||
|
|
||||||
|
/** Returns all MIB profiles across all logins. */
|
||||||
|
fun allMibProfiles(): List<MibProfile> = mibProfilesMap.values.flatten()
|
||||||
|
|
||||||
|
/** Returns the MibLoginFlow for a given loginId, creating and caching it if needed. */
|
||||||
|
fun mibFlowFor(loginId: String): MibLoginFlow =
|
||||||
|
mibLoginFlows.getOrPut(loginId) {
|
||||||
|
MibLoginFlow(CredentialStore(this)).also { flow ->
|
||||||
|
flow.onSessionRefreshed = { session, profiles ->
|
||||||
|
mibSessions[loginId] = session
|
||||||
|
mibProfilesMap[loginId] = profiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns any available MibLoginFlow. */
|
||||||
|
fun anyMibFlow(): MibLoginFlow? = mibLoginFlows.values.firstOrNull()
|
||||||
|
|
||||||
/** Returns the BML session for the given account (matched via loginTag). */
|
/** Returns the BML session for the given account (matched via loginTag). */
|
||||||
fun bmlSessionFor(account: MibAccount): BmlSession? =
|
fun bmlSessionFor(account: MibAccount): BmlSession? =
|
||||||
bmlSessions[account.loginTag.removePrefix("bml_")]
|
bmlSessions[account.loginTag.removePrefix("bml_")]
|
||||||
@@ -32,18 +60,13 @@ class BasedBankApp : Application() {
|
|||||||
/** Returns any available BML session (for non-account-specific operations). */
|
/** Returns any available BML session (for non-account-specific operations). */
|
||||||
fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull()
|
fun anyBmlSession(): BmlSession? = bmlSessions.values.firstOrNull()
|
||||||
|
|
||||||
|
/** Returns the Fahipay session for the given account (matched via loginTag = "fahipay_${profileId}"). */
|
||||||
|
fun fahipaySessionFor(account: MibAccount): FahipaySession? =
|
||||||
|
fahipaySessions[account.loginTag.removePrefix("fahipay_")]
|
||||||
|
|
||||||
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
/** Serialises all MIB profile-switch + request sequences to prevent session corruption. */
|
||||||
val mibMutex = Mutex()
|
val mibMutex = Mutex()
|
||||||
|
|
||||||
val mibLoginFlow by lazy {
|
|
||||||
MibLoginFlow(CredentialStore(this)).also { flow ->
|
|
||||||
flow.onSessionRefreshed = { session, profiles ->
|
|
||||||
mibSession = session
|
|
||||||
mibProfiles = profiles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
|||||||
@@ -618,6 +618,7 @@ class BmlLoginFlow {
|
|||||||
if (accountType == "CASA") {
|
if (accountType == "CASA") {
|
||||||
val available = item.optDouble("availableBalance", 0.0)
|
val available = item.optDouble("availableBalance", 0.0)
|
||||||
casaAccounts.add(MibAccount(
|
casaAccounts.add(MibAccount(
|
||||||
|
bank = "BML",
|
||||||
profileName = "Personal",
|
profileName = "Personal",
|
||||||
profileType = "BML",
|
profileType = "BML",
|
||||||
accountNumber = accountNumber,
|
accountNumber = accountNumber,
|
||||||
@@ -641,6 +642,7 @@ class BmlLoginFlow {
|
|||||||
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
||||||
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
||||||
prepaidCards.add(MibAccount(
|
prepaidCards.add(MibAccount(
|
||||||
|
bank = "BML",
|
||||||
profileName = "Personal",
|
profileName = "Personal",
|
||||||
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
||||||
accountNumber = accountNumber,
|
accountNumber = accountNumber,
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ class FahipayLoginFlow {
|
|||||||
|
|
||||||
fun buildAccount(profile: FahipayUserProfile, balance: Double, loginTag: String): MibAccount =
|
fun buildAccount(profile: FahipayUserProfile, balance: Double, loginTag: String): MibAccount =
|
||||||
MibAccount(
|
MibAccount(
|
||||||
|
bank = "FAHIPAY",
|
||||||
profileName = profile.fullName.ifBlank { "Fahipay" },
|
profileName = profile.fullName.ifBlank { "Fahipay" },
|
||||||
profileType = "FAHIPAY",
|
profileType = "FAHIPAY",
|
||||||
accountNumber = profile.walletAccount,
|
accountNumber = profile.walletAccount,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
var onSessionRefreshed: ((MibSession, List<MibProfile>) -> Unit)? = null
|
var onSessionRefreshed: ((MibSession, List<MibProfile>) -> Unit)? = null
|
||||||
|
|
||||||
// Stored after login so the session can be silently recovered on 419
|
// Stored after login so the session can be silently recovered on 419
|
||||||
|
@Volatile private var loginId: String = ""
|
||||||
@Volatile private var storedUsername: String? = null
|
@Volatile private var storedUsername: String? = null
|
||||||
@Volatile private var storedPasswordHash: String? = null
|
@Volatile private var storedPasswordHash: String? = null
|
||||||
@Volatile private var storedOtpSeed: String? = null
|
@Volatile private var storedOtpSeed: String? = null
|
||||||
@@ -58,11 +59,12 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
* Returns list of accounts from all profiles on success.
|
* Returns list of accounts from all profiles on success.
|
||||||
*/
|
*/
|
||||||
fun login(username: String, passwordHash: String, otpSeed: String): List<MibAccount> {
|
fun login(username: String, passwordHash: String, otpSeed: String): List<MibAccount> {
|
||||||
|
loginId = username
|
||||||
storedUsername = username
|
storedUsername = username
|
||||||
storedPasswordHash = passwordHash
|
storedPasswordHash = passwordHash
|
||||||
storedOtpSeed = otpSeed
|
storedOtpSeed = otpSeed
|
||||||
val appId = getOrCreateAppId()
|
val appId = getOrCreateAppId()
|
||||||
val keys = credentialStore.loadMibKeys()
|
val keys = credentialStore.loadMibKeys(loginId)
|
||||||
|
|
||||||
return if (keys != null) {
|
return if (keys != null) {
|
||||||
regularLogin(username, passwordHash, appId, keys.first, keys.second)
|
regularLogin(username, passwordHash, appId, keys.first, keys.second)
|
||||||
@@ -106,7 +108,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
val keyData = otpResp.getJSONArray("data").getJSONObject(0)
|
val keyData = otpResp.getJSONArray("data").getJSONObject(0)
|
||||||
val key1 = keyData.getString("key1")
|
val key1 = keyData.getString("key1")
|
||||||
val key2 = keyData.getString("key2")
|
val key2 = keyData.getString("key2")
|
||||||
credentialStore.saveMibKeys(key1, key2)
|
credentialStore.saveMibKeys(loginId, key1, key2)
|
||||||
|
|
||||||
return regularLogin(username, passwordHash, appId, key1, key2)
|
return regularLogin(username, passwordHash, appId, key1, key2)
|
||||||
}
|
}
|
||||||
@@ -136,10 +138,52 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val profiles = parseProfiles(loginResp)
|
val profiles = parseProfiles(loginResp)
|
||||||
|
|
||||||
lastSession = session2
|
lastSession = session2
|
||||||
lastProfiles = profiles
|
lastProfiles = profiles // keep ALL profiles so settings can show them all
|
||||||
return fetchAllProfiles(session2, profiles, "mib_$username")
|
|
||||||
|
val hidden = credentialStore.getHiddenMibProfileIds(loginId)
|
||||||
|
|
||||||
|
// When the server already selected the profile and returned balances in A41
|
||||||
|
// (single-profile case: profileSelected=true), use those accounts directly
|
||||||
|
// without making an extra P47 call (which the server ignores or rejects).
|
||||||
|
if (loginResp.optBoolean("profileSelected", false)) {
|
||||||
|
val a41Balances = loginResp.optJSONArray("accountBalance")
|
||||||
|
if (a41Balances != null && a41Balances.length() > 0) {
|
||||||
|
val selectedId = loginResp.optString("selectedProfileId")
|
||||||
|
val profile = profiles.firstOrNull { it.profileId == selectedId }
|
||||||
|
?: profiles.firstOrNull()
|
||||||
|
if (profile != null && (hidden.isEmpty() || profile.profileId !in hidden)) {
|
||||||
|
val allAccounts = mutableListOf<MibAccount>()
|
||||||
|
for (i in 0 until a41Balances.length()) {
|
||||||
|
val a = a41Balances.getJSONObject(i)
|
||||||
|
allAccounts.add(
|
||||||
|
MibAccount(
|
||||||
|
bank = "MIB",
|
||||||
|
profileName = profile.name,
|
||||||
|
profileType = profile.profileType,
|
||||||
|
cifType = profile.cifType,
|
||||||
|
accountNumber = a.optString("accountNumber"),
|
||||||
|
accountBriefName = a.optString("accountBriefName"),
|
||||||
|
currencyName = a.optString("currencyName"),
|
||||||
|
accountTypeName = a.optString("accountTypeName"),
|
||||||
|
availableBalance = a.optString("availableBalance"),
|
||||||
|
currentBalance = a.optString("currentBalance"),
|
||||||
|
blockedAmount = a.optString("blockedAmount"),
|
||||||
|
mvrBalance = a.optString("mvrBalance"),
|
||||||
|
statusDesc = a.optString("statusDesc"),
|
||||||
|
profileImageHash = profile.customerImage,
|
||||||
|
loginTag = "mib_$username",
|
||||||
|
profileId = profile.profileId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return allAccounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val visibleProfiles = if (hidden.isEmpty()) profiles else profiles.filter { it.profileId !in hidden }
|
||||||
|
return fetchAllProfiles(session2, visibleProfiles, "mib_$username")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||||
@@ -271,8 +315,10 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
val a = accountBalances.getJSONObject(i)
|
val a = accountBalances.getJSONObject(i)
|
||||||
allAccounts.add(
|
allAccounts.add(
|
||||||
MibAccount(
|
MibAccount(
|
||||||
|
bank = "MIB",
|
||||||
profileName = profile.name,
|
profileName = profile.name,
|
||||||
profileType = profile.profileType,
|
profileType = profile.profileType,
|
||||||
|
cifType = profile.cifType,
|
||||||
accountNumber = a.optString("accountNumber"),
|
accountNumber = a.optString("accountNumber"),
|
||||||
accountBriefName = a.optString("accountBriefName"),
|
accountBriefName = a.optString("accountBriefName"),
|
||||||
currencyName = a.optString("currencyName"),
|
currencyName = a.optString("currencyName"),
|
||||||
@@ -389,11 +435,11 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
private fun generateOtp(seed: String): String = Totp.generate(seed)
|
private fun generateOtp(seed: String): String = Totp.generate(seed)
|
||||||
|
|
||||||
private fun getOrCreateAppId(): String {
|
private fun getOrCreateAppId(): String {
|
||||||
var id = credentialStore.loadMibAppId()
|
var id = credentialStore.loadMibAppId(loginId)
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
id = "IOS17.2-" + (1..15).map { chars[Random.nextInt(chars.length)] }.joinToString("")
|
id = "IOS17.2-" + (1..15).map { chars[Random.nextInt(chars.length)] }.joinToString("")
|
||||||
credentialStore.saveMibAppId(id)
|
credentialStore.saveMibAppId(loginId, id)
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ data class MibProfile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class MibAccount(
|
data class MibAccount(
|
||||||
|
val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow
|
||||||
val profileName: String,
|
val profileName: String,
|
||||||
val profileType: String,
|
val profileType: String,
|
||||||
|
val cifType: String = "", // MIB: human-readable profile category (e.g. "Individual", "Sole Propr"); empty for other banks
|
||||||
val accountNumber: String,
|
val accountNumber: String,
|
||||||
val accountBriefName: String,
|
val accountBriefName: String,
|
||||||
val currencyName: String,
|
val currencyName: String,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
import sh.sar.basedbank.api.mib.Transaction
|
||||||
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
import sh.sar.basedbank.databinding.ItemAccountHistoryHeaderBinding
|
import sh.sar.basedbank.databinding.ItemAccountHistoryHeaderBinding
|
||||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||||
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
|
import sh.sar.basedbank.databinding.ItemLoadingFooterBinding
|
||||||
@@ -20,7 +21,8 @@ import java.util.Date
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class AccountHistoryAdapter(
|
class AccountHistoryAdapter(
|
||||||
private val account: MibAccount
|
private val account: MibAccount,
|
||||||
|
private val display: AccountHistoryDisplay
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
private sealed class Item {
|
private sealed class Item {
|
||||||
@@ -138,7 +140,7 @@ class AccountHistoryAdapter(
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (holder) {
|
when (holder) {
|
||||||
is HeaderVH -> holder.bind(account)
|
is HeaderVH -> holder.bind(display)
|
||||||
is DateHeaderVH -> holder.bind((displayItems[position - 1] as Item.DateHeader).label)
|
is DateHeaderVH -> holder.bind((displayItems[position - 1] as Item.DateHeader).label)
|
||||||
is TransactionVH -> holder.bind((displayItems[position - 1] as Item.Trx).transaction)
|
is TransactionVH -> holder.bind((displayItems[position - 1] as Item.Trx).transaction)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
@@ -147,37 +149,20 @@ class AccountHistoryAdapter(
|
|||||||
|
|
||||||
inner class HeaderVH(private val b: ItemAccountHistoryHeaderBinding) :
|
inner class HeaderVH(private val b: ItemAccountHistoryHeaderBinding) :
|
||||||
RecyclerView.ViewHolder(b.root) {
|
RecyclerView.ViewHolder(b.root) {
|
||||||
fun bind(acc: MibAccount) {
|
fun bind(d: AccountHistoryDisplay) {
|
||||||
b.tvHeaderAccountName.text = acc.accountBriefName
|
b.tvHeaderAccountName.text = d.name
|
||||||
b.tvHeaderAccountNumber.text = acc.accountNumber
|
b.tvHeaderAccountNumber.text = d.number
|
||||||
b.tvHeaderPillBank.text = when {
|
b.tvHeaderPillBank.text = d.bankPill
|
||||||
acc.profileType.startsWith("BML") -> "BML"
|
b.tvHeaderPillType.text = d.typeLabel
|
||||||
acc.profileType == "FAHIPAY" -> "FP"
|
b.tvHeaderAvailable.text = d.availableBalance
|
||||||
else -> null
|
b.tvHeaderBalance.text = d.workingBalance
|
||||||
}
|
if (d.blockedBalance != null) {
|
||||||
b.tvHeaderPillType.text = friendlyType(acc.accountTypeName)
|
b.tvHeaderBlocked.text = d.blockedBalance
|
||||||
b.tvHeaderAvailable.text = "${acc.currencyName} ${acc.availableBalance}"
|
|
||||||
b.tvHeaderBalance.text = "${acc.currencyName} ${acc.currentBalance}"
|
|
||||||
val blocked = acc.blockedAmount.toDoubleOrNull() ?: 0.0
|
|
||||||
if (blocked > 0.0) {
|
|
||||||
b.tvHeaderBlocked.text = "${acc.currencyName} ${acc.blockedAmount}"
|
|
||||||
b.llHeaderBlocked.visibility = View.VISIBLE
|
b.llHeaderBlocked.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
b.llHeaderBlocked.visibility = View.GONE
|
b.llHeaderBlocked.visibility = View.GONE
|
||||||
}
|
}
|
||||||
b.btnHeaderTransfer.setOnClickListener { onTransferClick?.invoke(acc) }
|
b.btnHeaderTransfer.setOnClickListener { onTransferClick?.invoke(account) }
|
||||||
}
|
|
||||||
|
|
||||||
private fun friendlyType(raw: String): String {
|
|
||||||
val u = raw.trim().uppercase()
|
|
||||||
return when {
|
|
||||||
u.contains("SAVING") -> "Savings"
|
|
||||||
u.contains("CURRENT") -> "Current"
|
|
||||||
u.contains("WADIAH") -> "Islamic"
|
|
||||||
u.contains("VISA") || u.contains("MASTERCARD") || u.contains("AMEX") -> "Card"
|
|
||||||
u.contains("PREPAID") -> "Prepaid"
|
|
||||||
else -> raw.trim().take(12)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import android.util.Base64
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -17,23 +19,18 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
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
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibHistoryClient
|
|
||||||
import sh.sar.basedbank.api.mib.Transaction
|
import sh.sar.basedbank.api.mib.Transaction
|
||||||
import sh.sar.basedbank.api.mib.TransactionCache
|
import sh.sar.basedbank.api.mib.TransactionCache
|
||||||
import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding
|
import sh.sar.basedbank.databinding.FragmentAccountHistoryBinding
|
||||||
|
import sh.sar.basedbank.util.AccountHistoryParser
|
||||||
import sh.sar.basedbank.util.ContactImageCache
|
import sh.sar.basedbank.util.ContactImageCache
|
||||||
|
import sh.sar.basedbank.util.HistoryFetcher
|
||||||
import sh.sar.basedbank.util.MerchantIconCache
|
import sh.sar.basedbank.util.MerchantIconCache
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class AccountHistoryFragment : Fragment() {
|
class AccountHistoryFragment : Fragment() {
|
||||||
|
|
||||||
@@ -43,21 +40,13 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
|
|
||||||
private lateinit var adapter: AccountHistoryAdapter
|
private lateinit var adapter: AccountHistoryAdapter
|
||||||
private lateinit var account: MibAccount
|
private lateinit var account: MibAccount
|
||||||
|
private lateinit var fetcher: HistoryFetcher
|
||||||
|
|
||||||
private val allTransactions = mutableListOf<Transaction>()
|
private val allTransactions = mutableListOf<Transaction>()
|
||||||
private var searchQuery = ""
|
private var searchQuery = ""
|
||||||
private var firstPageDone = false
|
private var firstPageDone = false
|
||||||
private val pendingImageNames = mutableSetOf<String>()
|
private val pendingImageNames = mutableSetOf<String>()
|
||||||
private val pendingIconUrls = mutableSetOf<String>()
|
private val pendingIconUrls = mutableSetOf<String>()
|
||||||
|
|
||||||
// Pagination state
|
|
||||||
private var mibNextStart = 1
|
|
||||||
private var mibTotalCount = -1 // -1 = unknown; loaded on first fetch
|
|
||||||
private var bmlNextPage = 1
|
|
||||||
private var bmlTotalPages = -1
|
|
||||||
private var cardMonthOffset = 0 // 0 = current month, 1 = prev, etc.
|
|
||||||
private var fahipayNextStart = 0
|
|
||||||
private var fahipayTotal = -1
|
|
||||||
private var isLoading = false
|
private var isLoading = false
|
||||||
private val pageSize = 10
|
private val pageSize = 10
|
||||||
|
|
||||||
@@ -79,8 +68,10 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val accountNumber = requireArguments().getString(ARG_ACCOUNT_NUMBER) ?: return
|
val accountNumber = requireArguments().getString(ARG_ACCOUNT_NUMBER) ?: return
|
||||||
account = viewModel.accounts.value?.find { it.accountNumber == accountNumber } ?: return
|
account = viewModel.accounts.value?.find { it.accountNumber == accountNumber } ?: return
|
||||||
|
fetcher = HistoryFetcher(account)
|
||||||
|
|
||||||
adapter = AccountHistoryAdapter(account)
|
val historyDisplay = AccountHistoryParser.from(account) ?: return
|
||||||
|
adapter = AccountHistoryAdapter(account, historyDisplay)
|
||||||
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
adapter.onImageNeeded = { name -> loadContactImage(name) }
|
||||||
adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) }
|
adapter.onIconUrlNeeded = { url -> loadMerchantIcon(url) }
|
||||||
adapter.onTransferClick = { acc ->
|
adapter.onTransferClick = { acc ->
|
||||||
@@ -88,6 +79,16 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||||
|
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||||
|
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
|
||||||
if (dy <= 0 || isLoading) return
|
if (dy <= 0 || isLoading) return
|
||||||
@@ -104,11 +105,10 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
searchQuery = s?.toString()?.trim() ?: ""
|
searchQuery = s?.toString()?.trim() ?: ""
|
||||||
filterAndDisplay()
|
filterAndDisplay()
|
||||||
if (searchQuery.isNotBlank() && hasMore() && !isLoading) loadNextPage()
|
if (searchQuery.isNotBlank() && fetcher.hasMore() && !isLoading) loadNextPage()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Load cache immediately, then fetch fresh data in background
|
|
||||||
val cached = TransactionCache.load(requireContext(), account.accountNumber)
|
val cached = TransactionCache.load(requireContext(), account.accountNumber)
|
||||||
if (cached.isNotEmpty()) {
|
if (cached.isNotEmpty()) {
|
||||||
allTransactions.addAll(cached)
|
allTransactions.addAll(cached)
|
||||||
@@ -133,19 +133,8 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE
|
binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isMib() = !account.profileType.startsWith("BML") && account.profileType != "FAHIPAY"
|
|
||||||
private fun isBmlCard() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
|
||||||
private fun isFahipay() = account.profileType == "FAHIPAY"
|
|
||||||
|
|
||||||
private fun hasMore(): Boolean = when {
|
|
||||||
isFahipay() -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
|
||||||
isMib() -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
|
||||||
isBmlCard() -> cardMonthOffset < 3 // load up to 3 months
|
|
||||||
else -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadNextPage() {
|
private fun loadNextPage() {
|
||||||
if (isLoading || !hasMore()) return
|
if (isLoading || !fetcher.hasMore()) return
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
if (firstPageDone && allTransactions.isNotEmpty()) {
|
if (firstPageDone && allTransactions.isNotEmpty()) {
|
||||||
@@ -155,68 +144,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val transactions: List<Transaction> = withContext(Dispatchers.IO) {
|
val transactions = fetcher.fetchNextPage(app, pageSize)
|
||||||
when {
|
|
||||||
isFahipay() -> {
|
|
||||||
val session = app.fahipaySession ?: return@withContext emptyList()
|
|
||||||
val flow = FahipayLoginFlow()
|
|
||||||
flow.setSessionCookie(session.sessionCookie)
|
|
||||||
val (list, total) = flow.fetchHistory(
|
|
||||||
session = session,
|
|
||||||
accountDisplayName = account.accountBriefName,
|
|
||||||
accountNumber = account.accountNumber,
|
|
||||||
start = fahipayNextStart
|
|
||||||
)
|
|
||||||
if (total > 0) fahipayTotal = total
|
|
||||||
fahipayNextStart += list.size
|
|
||||||
list
|
|
||||||
}
|
|
||||||
isMib() -> {
|
|
||||||
val session = app.mibSession ?: return@withContext emptyList()
|
|
||||||
app.mibMutex.withLock {
|
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == account.profileId }
|
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(session, profile)
|
|
||||||
val (list, total) = MibHistoryClient().fetchHistory(
|
|
||||||
session = session,
|
|
||||||
accountNo = account.accountNumber,
|
|
||||||
accountDisplayName = account.accountBriefName,
|
|
||||||
start = mibNextStart,
|
|
||||||
pageSize = pageSize
|
|
||||||
)
|
|
||||||
if (total > 0) mibTotalCount = total
|
|
||||||
mibNextStart += list.size.coerceAtLeast(pageSize)
|
|
||||||
list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isBmlCard() -> {
|
|
||||||
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)
|
|
||||||
cardMonthOffset++
|
|
||||||
BmlLoginFlow().fetchCardHistory(
|
|
||||||
session = session,
|
|
||||||
cardId = account.internalId,
|
|
||||||
accountDisplayName = account.accountBriefName,
|
|
||||||
accountNumber = account.accountNumber,
|
|
||||||
month = month
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val session = app.bmlSessionFor(account) ?: return@withContext emptyList()
|
|
||||||
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
|
||||||
session = session,
|
|
||||||
accountId = account.internalId,
|
|
||||||
accountDisplayName = account.accountBriefName,
|
|
||||||
accountNumber = account.accountNumber,
|
|
||||||
page = bmlNextPage
|
|
||||||
)
|
|
||||||
if (totalPages > 0) bmlTotalPages = totalPages
|
|
||||||
bmlNextPage++
|
|
||||||
list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
||||||
@@ -233,7 +161,6 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
allTransactions.sortByDescending { AccountHistoryAdapter.parseDateMillis(it.date) }
|
allTransactions.sortByDescending { AccountHistoryAdapter.parseDateMillis(it.date) }
|
||||||
TransactionCache.save(requireContext(), account.accountNumber, allTransactions)
|
TransactionCache.save(requireContext(), account.accountNumber, allTransactions)
|
||||||
if (searchQuery.isBlank()) {
|
if (searchQuery.isBlank()) {
|
||||||
// Append incrementally to preserve scroll position
|
|
||||||
val sorted = newOnes.sortedByDescending { AccountHistoryAdapter.parseDateMillis(it.date) }
|
val sorted = newOnes.sortedByDescending { AccountHistoryAdapter.parseDateMillis(it.date) }
|
||||||
adapter.appendTransactions(sorted)
|
adapter.appendTransactions(sorted)
|
||||||
binding.emptyView.visibility = View.GONE
|
binding.emptyView.visibility = View.GONE
|
||||||
@@ -243,7 +170,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
} else {
|
} else {
|
||||||
adapter.showLoadingFooter = false
|
adapter.showLoadingFooter = false
|
||||||
}
|
}
|
||||||
if (searchQuery.isNotBlank() && hasMore()) loadNextPage()
|
if (searchQuery.isNotBlank() && fetcher.hasMore()) loadNextPage()
|
||||||
} else {
|
} else {
|
||||||
adapter.showLoadingFooter = false
|
adapter.showLoadingFooter = false
|
||||||
if (allTransactions.isEmpty()) binding.emptyView.visibility = View.VISIBLE
|
if (allTransactions.isEmpty()) binding.emptyView.visibility = View.VISIBLE
|
||||||
@@ -261,7 +188,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val sess = app.mibSession ?: return
|
val sess = app.anyMibSession() ?: return
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
||||||
@@ -288,8 +215,7 @@ class AccountHistoryFragment : Fragment() {
|
|||||||
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
||||||
val bytes = response.body?.bytes() ?: return@launch
|
val bytes = response.body?.bytes() ?: return@launch
|
||||||
response.close()
|
response.close()
|
||||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||||
?: return@launch
|
|
||||||
MerchantIconCache.save(requireContext(), url, bitmap)
|
MerchantIconCache.save(requireContext(), url, bitmap)
|
||||||
withContext(Dispatchers.Main) { adapter.updateIconUrl(url, bitmap) }
|
withContext(Dispatchers.Main) { adapter.updateIconUrl(url, bitmap) }
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|||||||
@@ -8,23 +8,24 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import sh.sar.basedbank.R
|
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.databinding.ItemAccountBinding
|
import sh.sar.basedbank.databinding.ItemAccountBinding
|
||||||
import sh.sar.basedbank.databinding.ItemCardBinding
|
import sh.sar.basedbank.databinding.ItemCardBinding
|
||||||
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
|
||||||
import sh.sar.basedbank.util.BmlDashboardParser
|
import sh.sar.basedbank.util.AccountListDisplay
|
||||||
import sh.sar.basedbank.util.MibAccountParser
|
import sh.sar.basedbank.util.AccountListParser
|
||||||
|
|
||||||
class AccountsAdapter(
|
class AccountsAdapter(
|
||||||
accounts: List<MibAccount>,
|
accounts: List<MibAccount>,
|
||||||
private val onAccountClick: (MibAccount) -> Unit = {}
|
private val onAccountClick: (MibAccount) -> Unit = {}
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
var onTransferClick: ((MibAccount) -> Unit)? = null
|
||||||
|
|
||||||
private sealed class Item {
|
private sealed class Item {
|
||||||
data class SectionTitle(val label: String) : Item()
|
data class SectionTitle(val label: String) : Item()
|
||||||
data class Account(val account: MibAccount) : Item()
|
data class Account(val account: MibAccount, val display: AccountListDisplay) : Item()
|
||||||
data class Card(val account: MibAccount) : Item()
|
data class Card(val account: MibAccount, val display: AccountListDisplay) : Item()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val items: MutableList<Item> = buildItems(accounts).toMutableList()
|
private val items: MutableList<Item> = buildItems(accounts).toMutableList()
|
||||||
@@ -36,38 +37,38 @@ class AccountsAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
private fun buildItems(accounts: List<MibAccount>): List<Item> = buildList {
|
||||||
val nonPrepaid = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
val displayed = accounts.mapNotNull { acc -> AccountListParser.from(acc)?.let { acc to it } }
|
||||||
val prepaid = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
val nonCards = displayed.filter { !it.second.isCard }
|
||||||
|
val cards = displayed.filter { it.second.isCard }
|
||||||
|
|
||||||
// Group non-prepaid accounts by their derived section title, preserving order
|
val groups = LinkedHashMap<String, MutableList<Pair<MibAccount, AccountListDisplay>>>()
|
||||||
val groups = LinkedHashMap<String, MutableList<MibAccount>>()
|
for ((acc, display) in nonCards) {
|
||||||
for (acc in nonPrepaid) {
|
|
||||||
val title = sectionTitle(acc)
|
val title = sectionTitle(acc)
|
||||||
groups.getOrPut(title) { mutableListOf() }.add(acc)
|
groups.getOrPut(title) { mutableListOf() }.add(acc to display)
|
||||||
}
|
}
|
||||||
for ((title, group) in groups) {
|
for ((title, group) in groups) {
|
||||||
add(Item.SectionTitle(title))
|
add(Item.SectionTitle(title))
|
||||||
group.forEach { add(Item.Account(it)) }
|
group.forEach { (acc, display) -> add(Item.Account(acc, display)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prepaid.isNotEmpty()) {
|
if (cards.isNotEmpty()) {
|
||||||
add(Item.SectionTitle("Cards · Bank of Maldives"))
|
add(Item.SectionTitle("Cards · Bank of Maldives"))
|
||||||
prepaid.forEach { add(Item.Card(it)) }
|
cards.forEach { (acc, display) -> add(Item.Card(acc, display)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sectionTitle(account: MibAccount): String {
|
private fun sectionTitle(account: MibAccount): String {
|
||||||
val profileLabel = when (account.profileType) {
|
val bankName = when (account.bank) {
|
||||||
"0" -> "Personal"
|
"BML" -> "Bank of Maldives"
|
||||||
"1" -> "Business"
|
"FAHIPAY" -> "Fahipay"
|
||||||
else -> account.profileName
|
"MIB" -> "Maldives Islamic Bank"
|
||||||
|
else -> account.bank
|
||||||
}
|
}
|
||||||
val bank = when {
|
val profileLabel = when (account.bank) {
|
||||||
account.profileType.startsWith("BML") -> "Bank of Maldives"
|
"MIB" -> account.cifType.ifBlank { account.profileName }
|
||||||
account.profileType == "FAHIPAY" -> "Fahipay"
|
else -> account.profileName
|
||||||
else -> "Maldives Islamic Bank"
|
|
||||||
}
|
}
|
||||||
return if (profileLabel.isNotBlank()) "$profileLabel · $bank" else bank
|
return if (profileLabel.isNotBlank()) "$profileLabel · $bankName" else bankName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = when (items[position]) {
|
override fun getItemViewType(position: Int) = when (items[position]) {
|
||||||
@@ -79,17 +80,17 @@ class AccountsAdapter(
|
|||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
TYPE_HEADER -> SectionViewHolder(ItemDateHeaderBinding.inflate(inflater, parent, false))
|
TYPE_HEADER -> SectionViewHolder(ItemDateHeaderBinding.inflate(inflater, parent, false))
|
||||||
TYPE_CARD -> CardViewHolder(ItemCardBinding.inflate(inflater, parent, false))
|
TYPE_CARD -> CardViewHolder(ItemCardBinding.inflate(inflater, parent, false))
|
||||||
else -> AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
|
else -> AccountViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
when (val item = items[position]) {
|
when (val item = items[position]) {
|
||||||
is Item.SectionTitle -> (holder as SectionViewHolder).bind(item)
|
is Item.SectionTitle -> (holder as SectionViewHolder).bind(item)
|
||||||
is Item.Account -> (holder as AccountViewHolder).bind(item.account)
|
is Item.Account -> (holder as AccountViewHolder).bind(item.account, item.display)
|
||||||
is Item.Card -> (holder as CardViewHolder).bind(item.account)
|
is Item.Card -> (holder as CardViewHolder).bind(item.account, item.display)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,18 +105,15 @@ class AccountsAdapter(
|
|||||||
|
|
||||||
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
|
private inner class AccountViewHolder(private val binding: ItemAccountBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(account: MibAccount) {
|
fun bind(account: MibAccount, display: AccountListDisplay) {
|
||||||
binding.tvAccountName.text = account.accountBriefName
|
binding.tvAccountName.text = display.name
|
||||||
binding.tvAccountNumber.text = account.accountNumber
|
binding.tvAccountNumber.text = display.number
|
||||||
val label = if (account.profileType.startsWith("BML"))
|
binding.tvAccountType.text = display.typeLabel
|
||||||
BmlDashboardParser.productLabel(account.accountTypeName)
|
binding.tvBalance.text = display.balance
|
||||||
else
|
binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) }
|
||||||
MibAccountParser.productLabel(account.accountTypeName)
|
|
||||||
binding.tvPillType.text = label
|
|
||||||
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
|
|
||||||
binding.root.setOnClickListener { onAccountClick(account) }
|
binding.root.setOnClickListener { onAccountClick(account) }
|
||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
copyToClipboard(it.context, account.accountNumber)
|
copyToClipboard(it.context, display.number)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,23 +121,22 @@ class AccountsAdapter(
|
|||||||
|
|
||||||
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
private inner class CardViewHolder(private val binding: ItemCardBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(account: MibAccount) {
|
fun bind(account: MibAccount, display: AccountListDisplay) {
|
||||||
binding.ivCardBrand.setImageResource(cardBrandIcon(account.accountTypeName))
|
binding.ivCardBrand.setImageResource(display.cardBrandIcon)
|
||||||
binding.tvCardName.text = account.accountBriefName
|
binding.tvCardName.text = display.name
|
||||||
binding.tvCardNumber.text = account.accountNumber
|
binding.tvCardNumber.text = display.number
|
||||||
binding.tvCardProduct.text = BmlDashboardParser.productLabel(account.accountTypeName)
|
binding.tvCardProduct.text = display.typeLabel
|
||||||
binding.layoutCardBalance.visibility = View.VISIBLE
|
binding.layoutCardBalance.visibility = View.VISIBLE
|
||||||
binding.tvCardBalance.text = "${account.currencyName} ${account.availableBalance}"
|
binding.tvCardBalance.text = display.balance
|
||||||
|
if (display.statusLabel != null) {
|
||||||
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
binding.tvCardStatus.text = display.statusLabel
|
||||||
if (isActive) {
|
|
||||||
binding.tvCardStatus.visibility = View.GONE
|
|
||||||
binding.root.alpha = 1f
|
|
||||||
} else {
|
|
||||||
binding.tvCardStatus.text = account.statusDesc
|
|
||||||
binding.tvCardStatus.visibility = View.VISIBLE
|
binding.tvCardStatus.visibility = View.VISIBLE
|
||||||
binding.root.alpha = 0.45f
|
binding.root.alpha = 0.45f
|
||||||
|
} else {
|
||||||
|
binding.tvCardStatus.visibility = View.GONE
|
||||||
|
binding.root.alpha = 1f
|
||||||
}
|
}
|
||||||
|
binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) }
|
||||||
binding.root.setOnClickListener { onAccountClick(account) }
|
binding.root.setOnClickListener { onAccountClick(account) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,13 +151,5 @@ class AccountsAdapter(
|
|||||||
cm.setPrimaryClip(ClipData.newPlainText("Account Number", accountNumber))
|
cm.setPrimaryClip(ClipData.newPlainText("Account Number", accountNumber))
|
||||||
Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cardBrandIcon(productName: String): Int = when {
|
|
||||||
productName.contains("AMEX", ignoreCase = true) ||
|
|
||||||
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> R.drawable.americanexpress
|
|
||||||
productName.contains("VISA", ignoreCase = true) -> R.drawable.visa
|
|
||||||
productName.contains("MASTERCARD", ignoreCase = true) -> R.drawable.mastercard
|
|
||||||
else -> R.drawable.ic_nav_card
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -26,9 +28,21 @@ class AccountsFragment : Fragment() {
|
|||||||
adapter = AccountsAdapter(emptyList()) { account ->
|
adapter = AccountsAdapter(emptyList()) { account ->
|
||||||
(activity as? HomeActivity)?.showWithBackStack(AccountHistoryFragment.newInstance(account))
|
(activity as? HomeActivity)?.showWithBackStack(AccountHistoryFragment.newInstance(account))
|
||||||
}
|
}
|
||||||
|
adapter.onTransferClick = { account ->
|
||||||
|
(activity as? HomeActivity)?.navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFrom(account))
|
||||||
|
}
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||||
|
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||||
|
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { adapter.updateAccounts(it) }
|
viewModel.accounts.observe(viewLifecycleOwner) { adapter.updateAccounts(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val label: String,
|
val label: String,
|
||||||
val isBml: Boolean,
|
val isBml: Boolean,
|
||||||
val mibProfile: MibProfile? = null,
|
val mibProfile: MibProfile? = null,
|
||||||
|
val mibLoginId: String? = null,
|
||||||
val bmlLoginId: String? = null,
|
val bmlLoginId: String? = null,
|
||||||
val subtitle: String = ""
|
val subtitle: String = ""
|
||||||
)
|
)
|
||||||
@@ -91,8 +92,10 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
private fun buildDestinations(): List<DestinationOption> {
|
private fun buildDestinations(): List<DestinationOption> {
|
||||||
val list = mutableListOf<DestinationOption>()
|
val list = mutableListOf<DestinationOption>()
|
||||||
for (profile in app.mibProfiles) {
|
for ((loginId, profiles) in app.mibProfilesMap) {
|
||||||
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile))
|
for (profile in profiles) {
|
||||||
|
list.add(DestinationOption("MIB · ${profile.name}", isBml = false, mibProfile = profile, mibLoginId = loginId, subtitle = profile.cifType))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val store = CredentialStore(requireContext())
|
val store = CredentialStore(requireContext())
|
||||||
for ((loginId, _) in app.bmlSessions) {
|
for ((loginId, _) in app.bmlSessions) {
|
||||||
@@ -249,7 +252,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
if (mibVerified != null) return mibVerified
|
if (mibVerified != null) return mibVerified
|
||||||
|
|
||||||
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
|
// 3) Fall back to MIB IPS lookup (for USD MIB accounts not reachable via BML)
|
||||||
val mibSess = app.mibSession ?: return null
|
val mibSess = app.anyMibSession() ?: return null
|
||||||
return try {
|
return try {
|
||||||
val info = MibTransferClient().lookup(mibSess, input)
|
val info = MibTransferClient().lookup(mibSess, input)
|
||||||
BmlAccountValidation(
|
BmlAccountValidation(
|
||||||
@@ -266,11 +269,12 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupForMib(dest: DestinationOption, input: String): BmlAccountValidation? {
|
private fun lookupForMib(dest: DestinationOption, input: String): BmlAccountValidation? {
|
||||||
val mibSess = app.mibSession ?: return null
|
val loginId = dest.mibLoginId ?: return null
|
||||||
|
val mibSess = app.mibSessions[loginId] ?: return null
|
||||||
val profile = dest.mibProfile ?: return null
|
val profile = dest.mibProfile ?: return null
|
||||||
|
|
||||||
val mibResult = try {
|
val mibResult = try {
|
||||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
app.mibFlowFor(loginId).switchProfile(mibSess, profile)
|
||||||
val info = MibTransferClient().lookup(mibSess, input)
|
val info = MibTransferClient().lookup(mibSess, input)
|
||||||
BmlAccountValidation(
|
BmlAccountValidation(
|
||||||
trnType = if (info.bankId == "MADVMVMV") "MIB_INTERNAL" else "MIB_LOCAL",
|
trnType = if (info.bankId == "MADVMVMV") "MIB_INTERNAL" else "MIB_LOCAL",
|
||||||
@@ -429,8 +433,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveToMib(alias: String): Boolean {
|
private fun saveToMib(alias: String): Boolean {
|
||||||
val mibSess = app.mibSession ?: return false
|
|
||||||
val dest = selectedDest ?: return false
|
val dest = selectedDest ?: return false
|
||||||
|
val loginId = dest.mibLoginId ?: return false
|
||||||
|
val mibSess = app.mibSessions[loginId] ?: return false
|
||||||
val profile = dest.mibProfile ?: return false
|
val profile = dest.mibProfile ?: return false
|
||||||
val account = mibLookupAccount ?: return false
|
val account = mibLookupAccount ?: return false
|
||||||
val currency = binding.etCurrency.text?.toString()?.trim() ?: "MVR"
|
val currency = binding.etCurrency.text?.toString()?.trim() ?: "MVR"
|
||||||
@@ -442,7 +447,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
val name = bmlLookup?.name ?: ""
|
val name = bmlLookup?.name ?: ""
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
app.mibFlowFor(loginId).switchProfile(mibSess, profile)
|
||||||
MibContactsClient().createContact(
|
MibContactsClient().createContact(
|
||||||
session = mibSess,
|
session = mibSess,
|
||||||
benefType = benefType,
|
benefType = benefType,
|
||||||
@@ -488,8 +493,9 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
|
|||||||
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, fresh)
|
if (loginId.isNotBlank()) ContactsCache.saveBml(requireContext(), loginId, fresh)
|
||||||
} else {
|
} else {
|
||||||
val profile = dest.mibProfile ?: return@launch
|
val profile = dest.mibProfile ?: return@launch
|
||||||
val mibSess = app.mibSession ?: return@launch
|
val mibLoginId = dest.mibLoginId ?: return@launch
|
||||||
app.mibLoginFlow.switchProfile(mibSess, profile)
|
val mibSess = app.mibSessions[mibLoginId] ?: return@launch
|
||||||
|
app.mibFlowFor(mibLoginId).switchProfile(mibSess, profile)
|
||||||
val fresh = MibContactsClient().fetchContacts(mibSess)
|
val fresh = MibContactsClient().fetchContacts(mibSess)
|
||||||
.map { it.copy(profileId = profile.profileId) }
|
.map { it.copy(profileId = profile.profileId) }
|
||||||
val existing = viewModel.contacts.value ?: emptyList()
|
val existing = viewModel.contacts.value ?: emptyList()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
||||||
private val profileImageHashes = mutableSetOf<String>()
|
private val profileImageHashes = mutableSetOf<String>()
|
||||||
private val app get() = requireActivity().application as BasedBankApp
|
private val app get() = requireActivity().application as BasedBankApp
|
||||||
private val session get() = app.mibSession
|
private val session get() = app.anyMibSession()
|
||||||
|
|
||||||
private var fromAccountNumber: String = ""
|
private var fromAccountNumber: String = ""
|
||||||
private var mediator: TabLayoutMediator? = null
|
private var mediator: TabLayoutMediator? = null
|
||||||
@@ -291,7 +291,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = if (hash in profileImageHashes) {
|
val base64 = if (hash in profileImageHashes) {
|
||||||
app.mibLoginFlow.fetchProfileImage(sess, hash)
|
app.anyMibFlow()?.fetchProfileImage(sess, hash)
|
||||||
} else {
|
} else {
|
||||||
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ import android.view.ViewGroup
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
|
||||||
import sh.sar.basedbank.databinding.ItemContactBinding
|
import sh.sar.basedbank.databinding.ItemContactBinding
|
||||||
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
|
|
||||||
class ContactsAdapter(
|
class ContactsAdapter(
|
||||||
private val imageCache: MutableMap<String, Bitmap>,
|
private val imageCache: MutableMap<String, Bitmap>,
|
||||||
private val onImageNeeded: (hash: String) -> Unit,
|
private val onImageNeeded: (hash: String) -> Unit,
|
||||||
private val onDeleteClick: (MibBeneficiary) -> Unit,
|
private val onDeleteClick: (ContactDisplay) -> Unit,
|
||||||
private val onTransferClick: (MibBeneficiary) -> Unit
|
private val onTransferClick: (ContactDisplay) -> Unit
|
||||||
) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private var allContacts: List<MibBeneficiary> = emptyList()
|
private var allContacts: List<ContactDisplay> = emptyList()
|
||||||
private var displayed: List<MibBeneficiary> = emptyList()
|
private var displayed: List<ContactDisplay> = emptyList()
|
||||||
|
|
||||||
private var activeCategoryId: String? = null
|
private var activeCategoryId: String? = null
|
||||||
private var searchQuery: String = ""
|
private var searchQuery: String = ""
|
||||||
|
|
||||||
fun updateContacts(contacts: List<MibBeneficiary>) {
|
fun updateContacts(contacts: List<ContactDisplay>) {
|
||||||
allContacts = contacts
|
allContacts = contacts
|
||||||
applyFilter()
|
applyFilter()
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ class ContactsAdapter(
|
|||||||
fun updateImage(hash: String, bitmap: Bitmap) {
|
fun updateImage(hash: String, bitmap: Bitmap) {
|
||||||
imageCache[hash] = bitmap
|
imageCache[hash] = bitmap
|
||||||
displayed.forEachIndexed { index, contact ->
|
displayed.forEachIndexed { index, contact ->
|
||||||
if (contact.customerImgHash == hash) notifyItemChanged(index)
|
if (contact.imageHash == hash) notifyItemChanged(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +48,11 @@ class ContactsAdapter(
|
|||||||
|
|
||||||
private fun applyFilter() {
|
private fun applyFilter() {
|
||||||
displayed = allContacts.filter { contact ->
|
displayed = allContacts.filter { contact ->
|
||||||
val matchesCategory = activeCategoryId == null || contact.benefCategoryId == activeCategoryId
|
val matchesCategory = activeCategoryId == null || contact.categoryId == activeCategoryId
|
||||||
val matchesSearch = searchQuery.isBlank() ||
|
val matchesSearch = searchQuery.isBlank() ||
|
||||||
contact.benefNickName.contains(searchQuery, ignoreCase = true) ||
|
contact.name.contains(searchQuery, ignoreCase = true) ||
|
||||||
contact.benefName.contains(searchQuery, ignoreCase = true) ||
|
contact.realName.contains(searchQuery, ignoreCase = true) ||
|
||||||
contact.benefAccount.contains(searchQuery)
|
contact.accountNumber.contains(searchQuery)
|
||||||
matchesCategory && matchesSearch
|
matchesCategory && matchesSearch
|
||||||
}
|
}
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
@@ -76,7 +76,7 @@ class ContactsAdapter(
|
|||||||
binding.root.setOnLongClickListener {
|
binding.root.setOnLongClickListener {
|
||||||
val pos = holder.bindingAdapterPosition
|
val pos = holder.bindingAdapterPosition
|
||||||
if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
||||||
val account = displayed[pos].benefAccount
|
val account = displayed[pos].accountNumber
|
||||||
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
|
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
|
||||||
Toast.makeText(it.context, account, Toast.LENGTH_SHORT).show()
|
Toast.makeText(it.context, account, Toast.LENGTH_SHORT).show()
|
||||||
@@ -88,7 +88,7 @@ class ContactsAdapter(
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val contact = displayed[position]
|
val contact = displayed[position]
|
||||||
val cachedImage = contact.customerImgHash?.let { hash ->
|
val cachedImage = contact.imageHash?.let { hash ->
|
||||||
imageCache[hash] ?: run { onImageNeeded(hash); null }
|
imageCache[hash] ?: run { onImageNeeded(hash); null }
|
||||||
}
|
}
|
||||||
holder.bind(contact, cachedImage)
|
holder.bind(contact, cachedImage)
|
||||||
@@ -99,21 +99,24 @@ class ContactsAdapter(
|
|||||||
inner class ViewHolder(val binding: ItemContactBinding) :
|
inner class ViewHolder(val binding: ItemContactBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun bind(contact: MibBeneficiary, photo: Bitmap?) {
|
fun bind(contact: ContactDisplay, photo: Bitmap?) {
|
||||||
val isFahipay = contact.benefType == "FAHIPAY"
|
binding.tvContactName.text = contact.name
|
||||||
binding.tvContactName.text = contact.benefNickName
|
binding.tvContactAccount.text = contact.accountNumber
|
||||||
binding.tvContactAccount.text = contact.benefAccount
|
binding.tvRealName.text = contact.detail ?: ""
|
||||||
binding.tvRealName.text = if (isFahipay) "" else "${contact.benefName} · ${contact.transferCyDesc} · ${contact.benefBankName}"
|
binding.tvRealName.visibility =
|
||||||
binding.tvRealName.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
if (contact.detail != null) android.view.View.VISIBLE else android.view.View.GONE
|
||||||
binding.btnTransferContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
binding.btnTransferContact.visibility =
|
||||||
binding.btnEditContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
if (contact.canTransfer) android.view.View.VISIBLE else android.view.View.GONE
|
||||||
binding.btnDeleteContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
|
binding.btnEditContact.visibility =
|
||||||
|
if (contact.canEdit) android.view.View.VISIBLE else android.view.View.GONE
|
||||||
|
binding.btnDeleteContact.visibility =
|
||||||
|
if (contact.canDelete) android.view.View.VISIBLE else android.view.View.GONE
|
||||||
|
|
||||||
if (photo != null) {
|
if (photo != null) {
|
||||||
binding.ivContactPhoto.setImageBitmap(photo)
|
binding.ivContactPhoto.setImageBitmap(photo)
|
||||||
} else {
|
} else {
|
||||||
binding.ivContactPhoto.setImageBitmap(
|
binding.ivContactPhoto.setImageBitmap(
|
||||||
makeInitialsBitmap(contact.benefNickName, contact.bankColor)
|
makeInitialsBitmap(contact.name, contact.bankColor)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
@@ -21,13 +23,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.databinding.FragmentContactsBinding
|
import sh.sar.basedbank.databinding.FragmentContactsBinding
|
||||||
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
import sh.sar.basedbank.util.ContactImageCache
|
import sh.sar.basedbank.util.ContactImageCache
|
||||||
|
import sh.sar.basedbank.util.ContactListParser
|
||||||
|
import sh.sar.basedbank.util.ContactManager
|
||||||
import sh.sar.basedbank.util.ContactsCache
|
import sh.sar.basedbank.util.ContactsCache
|
||||||
|
import sh.sar.basedbank.util.TransferNetwork
|
||||||
|
|
||||||
class ContactsFragment : Fragment() {
|
class ContactsFragment : Fragment() {
|
||||||
|
|
||||||
@@ -38,9 +42,9 @@ class ContactsFragment : Fragment() {
|
|||||||
private val pendingHashes = mutableSetOf<String>()
|
private val pendingHashes = mutableSetOf<String>()
|
||||||
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
private val sharedImageCache = mutableMapOf<String, Bitmap>()
|
||||||
private val app get() = requireActivity().application as BasedBankApp
|
private val app get() = requireActivity().application as BasedBankApp
|
||||||
private val session get() = app.mibSession
|
private val session get() = app.anyMibSession()
|
||||||
|
|
||||||
private var allContacts: List<MibBeneficiary> = emptyList()
|
private var allContacts: List<ContactDisplay> = emptyList()
|
||||||
private var currentSearch: String = ""
|
private var currentSearch: String = ""
|
||||||
private var mediator: TabLayoutMediator? = null
|
private var mediator: TabLayoutMediator? = null
|
||||||
private lateinit var pagerAdapter: ContactsPagerAdapter
|
private lateinit var pagerAdapter: ContactsPagerAdapter
|
||||||
@@ -53,9 +57,9 @@ class ContactsFragment : Fragment() {
|
|||||||
private val density get() = resources.displayMetrics.density
|
private val density get() = resources.displayMetrics.density
|
||||||
val contactAdapters: List<ContactsAdapter> = pages.map { page ->
|
val contactAdapters: List<ContactsAdapter> = pages.map { page ->
|
||||||
ContactsAdapter(
|
ContactsAdapter(
|
||||||
imageCache = sharedImageCache,
|
imageCache = sharedImageCache,
|
||||||
onImageNeeded = { hash -> fetchImage(hash) },
|
onImageNeeded = { hash -> fetchImage(hash) },
|
||||||
onDeleteClick = { contact -> confirmDelete(contact) },
|
onDeleteClick = { contact -> confirmDelete(contact) },
|
||||||
onTransferClick = { contact -> openTransfer(contact) }
|
onTransferClick = { contact -> openTransfer(contact) }
|
||||||
).also { a ->
|
).also { a ->
|
||||||
a.setFilter(page.categoryId, currentSearch)
|
a.setFilter(page.categoryId, currentSearch)
|
||||||
@@ -63,7 +67,7 @@ class ContactsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateContacts(contacts: List<MibBeneficiary>) =
|
fun updateContacts(contacts: List<ContactDisplay>) =
|
||||||
contactAdapters.forEach { it.updateContacts(contacts) }
|
contactAdapters.forEach { it.updateContacts(contacts) }
|
||||||
|
|
||||||
fun updateSearch(query: String) =
|
fun updateSearch(query: String) =
|
||||||
@@ -86,7 +90,7 @@ class ContactsFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
clipToPadding = false
|
clipToPadding = false
|
||||||
val p4 = (4 * density).toInt()
|
val p4 = (4 * density).toInt()
|
||||||
val p80 = (80 * density).toInt()
|
val p80 = (65 * density).toInt()
|
||||||
setPadding(0, p4, 0, p80)
|
setPadding(0, p4, 0, p80)
|
||||||
adapter = contactAdapters[viewType]
|
adapter = contactAdapters[viewType]
|
||||||
}
|
}
|
||||||
@@ -113,6 +117,17 @@ class ContactsFragment : Fragment() {
|
|||||||
pagerAdapter.updateSearch(currentSearch)
|
pagerAdapter.updateSearch(currentSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val fabMarginBase = (16 * resources.displayMetrics.density).toInt()
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.fabAddContact) { v, insets ->
|
||||||
|
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||||
|
val lp = v.layoutParams as androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams
|
||||||
|
lp.bottomMargin = fabMarginBase + extraBottom
|
||||||
|
v.layoutParams = lp
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
binding.fabAddContact.setOnClickListener {
|
binding.fabAddContact.setOnClickListener {
|
||||||
AddContactSheetFragment().show(childFragmentManager, "add_contact")
|
AddContactSheetFragment().show(childFragmentManager, "add_contact")
|
||||||
}
|
}
|
||||||
@@ -124,8 +139,8 @@ class ContactsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.contacts.observe(viewLifecycleOwner) { contacts ->
|
viewModel.contacts.observe(viewLifecycleOwner) { contacts ->
|
||||||
allContacts = contacts
|
allContacts = ContactListParser.fromList(contacts)
|
||||||
pagerAdapter.updateContacts(contacts)
|
pagerAdapter.updateContacts(allContacts)
|
||||||
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
|
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
|
||||||
binding.loadingView.visibility = View.GONE
|
binding.loadingView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -150,31 +165,29 @@ class ContactsFragment : Fragment() {
|
|||||||
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
|
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openTransfer(contact: MibBeneficiary) {
|
private fun openTransfer(contact: ContactDisplay) {
|
||||||
val fragment = TransferFragment.newInstance(
|
val fragment = TransferFragment.newInstance(
|
||||||
accountNumber = contact.benefAccount,
|
accountNumber = contact.accountNumber,
|
||||||
displayName = contact.benefNickName,
|
displayName = contact.name,
|
||||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
subtitle = contact.transferSubtitle,
|
||||||
colorHex = contact.bankColor,
|
colorHex = contact.bankColor,
|
||||||
imageHash = contact.customerImgHash
|
imageHash = contact.imageHash
|
||||||
)
|
)
|
||||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, fragment)
|
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun confirmDelete(contact: MibBeneficiary) {
|
private fun confirmDelete(contact: ContactDisplay) {
|
||||||
AlertDialog.Builder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.contact_delete_title)
|
.setTitle(R.string.contact_delete_title)
|
||||||
.setMessage(getString(R.string.contact_delete_message, contact.benefNickName))
|
.setMessage(getString(R.string.contact_delete_message, contact.name))
|
||||||
.setPositiveButton(R.string.contact_delete) { _, _ -> deleteContact(contact) }
|
.setPositiveButton(R.string.contact_delete) { _, _ -> deleteContact(contact) }
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteContact(contact: MibBeneficiary) {
|
private fun deleteContact(contact: ContactDisplay) {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) { ContactManager.delete(contact, app) }
|
||||||
if (contact.benefCategoryId == "BML") deleteBml(contact) else deleteMib(contact)
|
|
||||||
}
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Toast.makeText(requireContext(), R.string.contact_deleted, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.contact_deleted, Toast.LENGTH_SHORT).show()
|
||||||
removeFromViewModel(contact)
|
removeFromViewModel(contact)
|
||||||
@@ -184,27 +197,10 @@ class ContactsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteBml(contact: MibBeneficiary): Boolean {
|
private fun removeFromViewModel(contact: ContactDisplay) {
|
||||||
val sess = app.bmlSessions[contact.profileId] ?: app.anyBmlSession() ?: return false
|
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.id } ?: return
|
||||||
val contactId = contact.benefNo.removePrefix("bml_")
|
|
||||||
return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteMib(contact: MibBeneficiary): Boolean {
|
|
||||||
val sess = session ?: return false
|
|
||||||
return try {
|
|
||||||
if (contact.profileId.isNotBlank()) {
|
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == contact.profileId }
|
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(sess, profile)
|
|
||||||
}
|
|
||||||
MibContactsClient().deleteContact(sess, contact.benefNo)
|
|
||||||
} catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeFromViewModel(contact: MibBeneficiary) {
|
|
||||||
val updated = viewModel.contacts.value?.filter { it.benefNo != contact.benefNo } ?: return
|
|
||||||
viewModel.contacts.value = updated
|
viewModel.contacts.value = updated
|
||||||
if (contact.benefCategoryId == "BML") {
|
if (contact.network == TransferNetwork.BML) {
|
||||||
updated.filter { it.benefCategoryId == "BML" }
|
updated.filter { it.benefCategoryId == "BML" }
|
||||||
.groupBy { it.profileId }
|
.groupBy { it.profileId }
|
||||||
.forEach { (loginId, contacts) ->
|
.forEach { (loginId, contacts) ->
|
||||||
@@ -221,7 +217,6 @@ class ContactsFragment : Fragment() {
|
|||||||
|
|
||||||
private fun fetchImage(hash: String) {
|
private fun fetchImage(hash: String) {
|
||||||
if (!pendingHashes.add(hash)) return
|
if (!pendingHashes.add(hash)) return
|
||||||
// Check disk cache first — if hash matches we already have the image
|
|
||||||
val cached = ContactImageCache.load(requireContext(), hash)
|
val cached = ContactImageCache.load(requireContext(), hash)
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
view?.post { pagerAdapter.updateImage(hash, cached) }
|
view?.post { pagerAdapter.updateImage(hash, cached) }
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
@@ -30,17 +33,34 @@ class DashboardFragment : Fragment() {
|
|||||||
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
|
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
|
||||||
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
||||||
|
|
||||||
binding.btnTransfer.setOnClickListener {
|
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer)
|
ViewCompat.setOnApplyWindowInsetsListener(binding.buttonBar) { v, insets ->
|
||||||
}
|
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
binding.btnPayMvQr.setOnClickListener {
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||||
|
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||||
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
requireActivity().title = getString(R.string.nav_dashboard)
|
requireActivity().title = getString(R.string.nav_dashboard)
|
||||||
|
refreshQuickActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshQuickActions() {
|
||||||
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
val ids = NavCustomization.getQuickActions(prefs)
|
||||||
|
listOf(binding.btnQuickAction1, binding.btnQuickAction2).forEachIndexed { i, btn ->
|
||||||
|
val def = NavCustomization.ALL_SWAPPABLE.find { it.id == ids[i] }
|
||||||
|
if (def != null) {
|
||||||
|
btn.setText(def.titleRes)
|
||||||
|
btn.icon = ContextCompat.getDrawable(requireContext(), def.iconRes)
|
||||||
|
}
|
||||||
|
btn.setOnClickListener { (requireActivity() as HomeActivity).navigateTo(ids[i]) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBalances(accounts: List<MibAccount>) {
|
private fun updateBalances(accounts: List<MibAccount>) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -27,6 +29,15 @@ class FinancingFragment : Fragment() {
|
|||||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||||
|
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||||
|
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.financing.observe(viewLifecycleOwner) { deals ->
|
viewModel.financing.observe(viewLifecycleOwner) { deals ->
|
||||||
adapter.updateDeals(deals)
|
adapter.updateDeals(deals)
|
||||||
binding.recyclerView.visibility = if (deals.isEmpty()) View.GONE else View.VISIBLE
|
binding.recyclerView.visibility = if (deals.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import android.os.Looper
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import sh.sar.basedbank.ui.home.NavCustomization
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -108,14 +110,21 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
||||||
if (suppressBottomNavCallback) return@setOnItemSelectedListener true
|
if (suppressBottomNavCallback) return@setOnItemSelectedListener true
|
||||||
when (item.itemId) {
|
val frag = when (item.itemId) {
|
||||||
R.id.nav_dashboard -> { show(DashboardFragment()); true }
|
R.id.nav_dashboard -> DashboardFragment()
|
||||||
R.id.nav_accounts -> { show(AccountsFragment()); true }
|
R.id.nav_accounts -> AccountsFragment()
|
||||||
R.id.nav_contacts -> { show(ContactsFragment()); true }
|
R.id.nav_contacts -> ContactsFragment()
|
||||||
R.id.nav_transfer -> { show(TransferFragment()); true }
|
R.id.nav_transfer -> TransferFragment()
|
||||||
R.id.nav_more -> { show(MoreFragment()); true }
|
R.id.nav_more -> MoreFragment()
|
||||||
else -> false
|
R.id.nav_transfer_history -> TransferHistoryFragment()
|
||||||
|
R.id.nav_finances -> FinancingFragment()
|
||||||
|
R.id.nav_otp -> OtpFragment()
|
||||||
|
R.id.nav_settings -> SettingsFragment()
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
if (frag != null) show(frag)
|
||||||
|
else Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
applyNavMode()
|
applyNavMode()
|
||||||
@@ -128,31 +137,36 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
if (app.accounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
|
if (app.mibAccounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
|
||||||
// Came from fresh manual login — accounts ready, rest fetched in background
|
// Came from fresh manual login — accounts ready, rest fetched in background
|
||||||
val mibAccounts = app.accounts.filter { !it.profileType.startsWith("BML") && it.profileType != "FAHIPAY" }
|
val merged = app.mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
||||||
val merged = mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
viewModel.accounts.value = merged.filterVisibleAccounts()
|
||||||
viewModel.accounts.value = merged
|
if (app.mibAccounts.isNotEmpty()) AccountCache.save(this, app.mibAccounts)
|
||||||
if (mibAccounts.isNotEmpty()) AccountCache.save(this, mibAccounts)
|
|
||||||
if (app.bmlAccounts.isNotEmpty()) {
|
if (app.bmlAccounts.isNotEmpty()) {
|
||||||
val byLoginId = app.bmlAccounts.groupBy { it.loginTag.removePrefix("bml_") }
|
val byLoginId = app.bmlAccounts.groupBy { it.loginTag.removePrefix("bml_") }
|
||||||
byLoginId.forEach { (loginId, accounts) -> AccountCache.saveBml(this, loginId, accounts) }
|
byLoginId.forEach { (loginId, accounts) -> AccountCache.saveBml(this, loginId, accounts) }
|
||||||
}
|
}
|
||||||
if (app.fahipayAccounts.isNotEmpty()) AccountCache.saveFahipay(this, app.fahipayAccounts)
|
if (app.fahipayAccounts.isNotEmpty()) {
|
||||||
|
val byLoginId = app.fahipayAccounts.groupBy { it.loginTag.removePrefix("fahipay_") }
|
||||||
|
byLoginId.forEach { (loginId, accs) -> AccountCache.saveFahipay(this, loginId, accs) }
|
||||||
|
}
|
||||||
|
|
||||||
val cachedFinancing = FinancingCache.load(this)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||||
val cachedLimits = ForeignLimitsCache.load(this)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
for ((loginId, session) in app.mibSessions) {
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
|
}
|
||||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
} else {
|
} else {
|
||||||
// Came from lock screen — show caches immediately, refresh everything in background
|
// Came from lock screen — show caches immediately, refresh everything in background
|
||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
val cachedMib = AccountCache.load(this)
|
val cachedMib = AccountCache.load(this)
|
||||||
val cachedBml = AccountCache.loadBml(this, store.getBmlLoginIds())
|
val cachedBml = AccountCache.loadBml(this, store.getBmlLoginIds())
|
||||||
val cachedFahipay = AccountCache.loadFahipay(this)
|
val cachedFahipay = AccountCache.loadFahipay(this, store.getFahipayLoginIds())
|
||||||
val merged = cachedMib + cachedBml + cachedFahipay
|
val merged = cachedMib + cachedBml + cachedFahipay
|
||||||
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
||||||
val cachedFinancing = FinancingCache.load(this)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
@@ -160,7 +174,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val cachedLimits = ForeignLimitsCache.load(this)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
|
autoRefresh(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show dashboard on first create
|
// Show dashboard on first create
|
||||||
@@ -169,27 +183,29 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
|
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep MIB session alive every 25 seconds while the app is in the foreground
|
// Keep all MIB sessions alive every 25 seconds while the app is in the foreground
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(25_000)
|
delay(25_000)
|
||||||
val session = (application as BasedBankApp).mibSession ?: continue
|
val sessions = (application as BasedBankApp).mibSessions.values.toList()
|
||||||
withContext(Dispatchers.IO) {
|
for (session in sessions) {
|
||||||
try {
|
withContext(Dispatchers.IO) {
|
||||||
val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " +
|
try {
|
||||||
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597"
|
val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " +
|
||||||
val request = Request.Builder()
|
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597"
|
||||||
.url("https://faisamobilex-wv.mib.com.mv/aProfile/keepAlive")
|
val request = Request.Builder()
|
||||||
.post(ByteArray(0).toRequestBody())
|
.url("https://faisamobilex-wv.mib.com.mv/aProfile/keepAlive")
|
||||||
.header("Cookie", cookieHeader)
|
.post(ByteArray(0).toRequestBody())
|
||||||
.header("User-Agent", "okhttp/4.11.0")
|
.header("Cookie", cookieHeader)
|
||||||
.header("Accept", "application/json, text/plain, */*")
|
.header("User-Agent", "okhttp/4.11.0")
|
||||||
.header("Accept-Encoding", "gzip")
|
.header("Accept", "application/json, text/plain, */*")
|
||||||
.header("Connection", "Keep-Alive")
|
.header("Accept-Encoding", "gzip")
|
||||||
.build()
|
.header("Connection", "Keep-Alive")
|
||||||
OkHttpClient().newCall(request).execute().close()
|
.build()
|
||||||
} catch (_: Exception) {}
|
OkHttpClient().newCall(request).execute().close()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,12 +219,15 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun applyNavMode() {
|
fun applyNavMode() {
|
||||||
val isBottom = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
|
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||||
|
val isBottom = prefs.getBoolean("bottom_nav", false)
|
||||||
if (isBottom) {
|
if (isBottom) {
|
||||||
binding.drawerLayout.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
binding.drawerLayout.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||||
toggle.isDrawerIndicatorEnabled = false
|
toggle.isDrawerIndicatorEnabled = false
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
binding.bottomNavigation.visibility = View.VISIBLE
|
binding.bottomNavigation.visibility = View.VISIBLE
|
||||||
|
rebuildBottomNav(prefs)
|
||||||
|
applyNavLabelVisibility()
|
||||||
} else {
|
} else {
|
||||||
binding.drawerLayout.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED)
|
binding.drawerLayout.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||||
toggle.isDrawerIndicatorEnabled = true
|
toggle.isDrawerIndicatorEnabled = true
|
||||||
@@ -217,6 +236,27 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun rebuildBottomNav(prefs: android.content.SharedPreferences = getSharedPreferences("prefs", MODE_PRIVATE)) {
|
||||||
|
val slots = NavCustomization.getSlots(prefs)
|
||||||
|
val menu = binding.bottomNavigation.menu
|
||||||
|
menu.clear()
|
||||||
|
menu.add(Menu.NONE, R.id.nav_dashboard, 0, R.string.nav_dashboard)
|
||||||
|
.setIcon(R.drawable.ic_nav_dashboard)
|
||||||
|
slots.forEachIndexed { i, id ->
|
||||||
|
val item = NavCustomization.ALL_SWAPPABLE.find { it.id == id } ?: return@forEachIndexed
|
||||||
|
menu.add(Menu.NONE, item.id, i + 1, item.titleRes).setIcon(item.iconRes)
|
||||||
|
}
|
||||||
|
menu.add(Menu.NONE, R.id.nav_more, 4, R.string.nav_more)
|
||||||
|
.setIcon(R.drawable.ic_nav_more)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyNavLabelVisibility() {
|
||||||
|
val showLabels = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav_show_labels", true)
|
||||||
|
binding.bottomNavigation.labelVisibilityMode =
|
||||||
|
if (showLabels) NavigationBarView.LABEL_VISIBILITY_LABELED
|
||||||
|
else NavigationBarView.LABEL_VISIBILITY_AUTO
|
||||||
|
}
|
||||||
|
|
||||||
fun navigateTo(itemId: Int, fragment: Fragment? = null) {
|
fun navigateTo(itemId: Int, fragment: Fragment? = null) {
|
||||||
val dest = fragment ?: when (itemId) {
|
val dest = fragment ?: when (itemId) {
|
||||||
R.id.nav_dashboard -> DashboardFragment()
|
R.id.nav_dashboard -> DashboardFragment()
|
||||||
@@ -231,11 +271,15 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
show(dest)
|
show(dest)
|
||||||
binding.navigationView.setCheckedItem(itemId)
|
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) {
|
||||||
if (binding.bottomNavigation.visibility == View.VISIBLE && itemId in bottomNavIds) {
|
val bottomNavIds = (0 until binding.bottomNavigation.menu.size())
|
||||||
suppressBottomNavCallback = true
|
.map { binding.bottomNavigation.menu.getItem(it).itemId }.toSet()
|
||||||
binding.bottomNavigation.selectedItemId = itemId
|
val selectId = if (itemId in bottomNavIds) itemId else if (R.id.nav_more in bottomNavIds) R.id.nav_more else null
|
||||||
suppressBottomNavCallback = false
|
if (selectId != null) {
|
||||||
|
suppressBottomNavCallback = true
|
||||||
|
binding.bottomNavigation.selectedItemId = selectId
|
||||||
|
suppressBottomNavCallback = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,50 +397,47 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
fun relogin() {
|
fun relogin() {
|
||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
val hasMib = store.hasMibCredentials()
|
val mibLoginIds = store.getMibLoginIds()
|
||||||
val bmlLoginIds = store.getBmlLoginIds()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
val hasFahipay = store.hasFahipayCredentials()
|
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||||
if (!hasMib && bmlLoginIds.isEmpty() && !hasFahipay) {
|
if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) {
|
||||||
startActivity(Intent(this, LoginActivity::class.java))
|
startActivity(Intent(this, LoginActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Immediately drop accounts for logged-out banks from the displayed list
|
// Immediately drop accounts for logged-out logins from the displayed list
|
||||||
val current = viewModel.accounts.value ?: emptyList()
|
val current = viewModel.accounts.value ?: emptyList()
|
||||||
viewModel.accounts.value = current.filter { acc ->
|
viewModel.accounts.value = current.filter { acc ->
|
||||||
if (!hasMib && !acc.profileType.startsWith("BML") && acc.profileType != "FAHIPAY") return@filter false
|
if (acc.bank == "MIB") return@filter acc.loginTag.removePrefix("mib_") in mibLoginIds
|
||||||
if (acc.profileType.startsWith("BML")) {
|
if (acc.bank == "BML") return@filter acc.loginTag.removePrefix("bml_") in bmlLoginIds
|
||||||
val loginId = acc.loginTag.removePrefix("bml_")
|
if (acc.bank == "FAHIPAY") return@filter acc.loginTag.removePrefix("fahipay_") in fahipayLoginIds
|
||||||
return@filter loginId in bmlLoginIds
|
|
||||||
}
|
|
||||||
if (!hasFahipay && acc.profileType == "FAHIPAY") return@filter false
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
autoRefresh(store.loadMibCredentials(), store.loadFahipayCredentials(), store)
|
autoRefresh(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoRefresh(
|
private fun autoRefresh(store: CredentialStore) {
|
||||||
mibCreds: CredentialStore.MibCredentials?,
|
val mibLoginIds = store.getMibLoginIds()
|
||||||
fahipayCreds: CredentialStore.FahipayCredentials?,
|
|
||||||
store: CredentialStore
|
|
||||||
) {
|
|
||||||
val bmlLoginIds = store.getBmlLoginIds()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
if (mibCreds == null && bmlLoginIds.isEmpty() && fahipayCreds == null) return
|
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||||
|
if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) return
|
||||||
binding.refreshIndicator.visibility = View.VISIBLE
|
binding.refreshIndicator.visibility = View.VISIBLE
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val mibJob = mibCreds?.let {
|
// One async job per MIB login, all run in parallel
|
||||||
async(Dispatchers.IO) {
|
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
||||||
|
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
||||||
|
loginId to async(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
|
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
|
||||||
val accounts = flow.login(it.username, it.passwordHash, it.otpSeed)
|
val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.accounts = accounts
|
app.mibSessions[loginId] = flow.lastSession!!
|
||||||
app.mibSession = flow.lastSession
|
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||||
app.mibProfiles = flow.lastProfiles
|
app.mibLoginFlows[loginId] = flow
|
||||||
AccountCache.save(this@HomeActivity, accounts)
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
accounts
|
accounts
|
||||||
} catch (_: Exception) { AccountCache.load(this@HomeActivity) }
|
} catch (_: Exception) { AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,68 +475,97 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val fahipayJob = fahipayCreds?.let { creds ->
|
// One async job per Fahipay login, all run in parallel
|
||||||
async(Dispatchers.IO) {
|
val fahipayJobs = fahipayLoginIds.mapNotNull { loginId ->
|
||||||
|
val creds = store.loadFahipayCredentials(loginId) ?: return@mapNotNull null
|
||||||
|
loginId to async(Dispatchers.IO) {
|
||||||
val fahipayFlow = FahipayLoginFlow()
|
val fahipayFlow = FahipayLoginFlow()
|
||||||
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
|
val deviceUuid = store.getOrCreateFahipayDeviceUuid()
|
||||||
|
val loginTag = "fahipay_$loginId"
|
||||||
|
|
||||||
val savedSession = store.loadFahipaySession()
|
val savedSession = store.loadFahipaySession(loginId)
|
||||||
if (savedSession != null) {
|
if (savedSession != null) {
|
||||||
try {
|
try {
|
||||||
val session = FahipaySession(savedSession.first, savedSession.second)
|
val session = FahipaySession(savedSession.first, savedSession.second)
|
||||||
fahipayFlow.setSessionCookie(session.sessionCookie)
|
fahipayFlow.setSessionCookie(session.sessionCookie)
|
||||||
val balance = fahipayFlow.fetchBalance(session)
|
val balance = fahipayFlow.fetchBalance(session)
|
||||||
val profile = fahipayFlow.fetchProfile(session)
|
val profile = fahipayFlow.fetchProfile(session)
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
|
||||||
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.fahipaySession = session
|
app.fahipaySessions[loginId] = session
|
||||||
app.fahipayAccounts = accounts
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
return@async accounts
|
||||||
return@async Pair(session, accounts)
|
} catch (_: Exception) { }
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
|
val step = fahipayFlow.login(creds.idCard, creds.password, deviceUuid)
|
||||||
if (step.twoFactorRequired) {
|
if (step.twoFactorRequired) {
|
||||||
return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
return@async AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
}
|
}
|
||||||
val authId = step.authId ?: return@async Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
val authId = step.authId ?: return@async AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
|
val cookieValue = fahipayFlow.getSessionCookieValue() ?: ""
|
||||||
val session = FahipaySession(authId, cookieValue)
|
val session = FahipaySession(authId, cookieValue)
|
||||||
store.saveFahipaySession(authId, cookieValue)
|
store.saveFahipaySession(loginId, authId, cookieValue)
|
||||||
val profile = fahipayFlow.fetchProfile(session)
|
val profile = fahipayFlow.fetchProfile(session)
|
||||||
val balance = fahipayFlow.fetchBalance(session)
|
val balance = fahipayFlow.fetchBalance(session)
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
|
||||||
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(fahipayFlow.buildAccount(profile, balance, loginTag))
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
app.fahipaySession = session
|
app.fahipaySessions[loginId] = session
|
||||||
app.fahipayAccounts = accounts
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
accounts
|
||||||
Pair(session, accounts)
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
|
val mibResults = mibJobs.map { (loginId, job) -> loginId to job.await() }
|
||||||
|
val mibAccounts = mibResults.flatMap { it.second }
|
||||||
val bmlResults = bmlJobs.map { (_, job) -> job.await() }
|
val bmlResults = bmlJobs.map { (_, job) -> job.await() }
|
||||||
val bmlAccounts = bmlResults.flatMap { it.second }
|
val bmlAccounts = bmlResults.flatMap { it.second }
|
||||||
val (_, fahipayAccounts) = fahipayJob?.await() ?: Pair(null, AccountCache.loadFahipay(this@HomeActivity))
|
val fahipayAccounts = fahipayJobs.flatMap { (_, job) -> job.await() }
|
||||||
|
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
|
app.mibAccounts = mibAccounts
|
||||||
|
AccountCache.save(this@HomeActivity, mibAccounts)
|
||||||
app.bmlAccounts = bmlAccounts
|
app.bmlAccounts = bmlAccounts
|
||||||
viewModel.accounts.postValue(mibAccounts + bmlAccounts + fahipayAccounts)
|
app.fahipayAccounts = fahipayAccounts
|
||||||
|
viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts())
|
||||||
binding.refreshIndicator.visibility = View.GONE
|
binding.refreshIndicator.visibility = View.GONE
|
||||||
|
|
||||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
for ((loginId, session) in app.mibSessions) {
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Filters MIB accounts whose profileId the user has hidden in settings. */
|
||||||
|
private fun List<MibAccount>.filterVisibleAccounts(): List<MibAccount> {
|
||||||
|
val store = CredentialStore(this@HomeActivity)
|
||||||
|
return filter { acc ->
|
||||||
|
if (acc.bank != "MIB") return@filter true
|
||||||
|
val loginId = acc.loginTag.removePrefix("mib_")
|
||||||
|
val hidden = store.getHiddenMibProfileIds(loginId)
|
||||||
|
hidden.isEmpty() || acc.profileId !in hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filters MIB profiles the user has hidden for a given loginId. */
|
||||||
|
private fun List<MibProfile>.filterVisibleProfiles(loginId: String): List<MibProfile> {
|
||||||
|
val hidden = CredentialStore(this@HomeActivity).getHiddenMibProfileIds(loginId)
|
||||||
|
if (hidden.isEmpty()) return this
|
||||||
|
return filter { it.profileId !in hidden }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by SettingsLoginsFragment after the user changes profile visibility. */
|
||||||
|
fun applyProfileVisibility() {
|
||||||
|
val current = viewModel.accounts.value ?: return
|
||||||
|
viewModel.accounts.value = current.filterVisibleAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshBmlLimits(session: BmlSession) {
|
private fun refreshBmlLimits(session: BmlSession) {
|
||||||
val bmlFlow = BmlLoginFlow()
|
val bmlFlow = BmlLoginFlow()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -550,9 +620,12 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
if (cats.isNotEmpty()) viewModel.contactCategories.value = cats
|
if (cats.isNotEmpty()) viewModel.contactCategories.value = cats
|
||||||
}
|
}
|
||||||
// Refresh all banks in background
|
// Refresh all banks in background
|
||||||
refreshContacts(app.mibSession, app.mibProfiles)
|
for ((loginId, session) in app.mibSessions) {
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
refreshContacts(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
|
}
|
||||||
refreshBmlContacts(app)
|
refreshBmlContacts(app)
|
||||||
if (app.fahipaySession != null) refreshFahipayContacts(app.fahipaySession!!)
|
for ((_, session) in app.fahipaySessions) refreshFahipayContacts(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFahipayContacts(session: FahipaySession) {
|
private fun refreshFahipayContacts(session: FahipaySession) {
|
||||||
@@ -588,9 +661,9 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshContacts(session: MibSession?, profiles: List<MibProfile>) {
|
private fun refreshContacts(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||||
if (session == null || profiles.isEmpty()) return
|
if (profiles.isEmpty()) return
|
||||||
val flow = MibLoginFlow(CredentialStore(this))
|
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||||
val contactsClient = MibContactsClient()
|
val contactsClient = MibContactsClient()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -615,8 +688,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
if (allContacts.isNotEmpty()) {
|
if (allContacts.isNotEmpty()) {
|
||||||
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
||||||
val bmlLoginIds = sh.sar.basedbank.util.CredentialStore(this@HomeActivity).getBmlLoginIds()
|
val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity)
|
||||||
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, bmlLoginIds)
|
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, store.getBmlLoginIds())
|
||||||
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
|
||||||
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
|
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
|
||||||
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
|
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
|
||||||
@@ -630,9 +703,9 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val current = viewModel.accounts.value ?: emptyList()
|
val current = viewModel.accounts.value ?: emptyList()
|
||||||
if (src.profileType == "FAHIPAY") {
|
if (src.bank == "FAHIPAY") {
|
||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.fahipaySession ?: return@withContext null
|
val sess = app.fahipaySessionFor(src) ?: return@withContext null
|
||||||
try {
|
try {
|
||||||
val flow = FahipayLoginFlow()
|
val flow = FahipayLoginFlow()
|
||||||
flow.setSessionCookie(sess.sessionCookie)
|
flow.setSessionCookie(sess.sessionCookie)
|
||||||
@@ -640,14 +713,15 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val profile = flow.fetchProfile(sess)
|
val profile = flow.fetchProfile(sess)
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
val loginTag = "fahipay_${profile.profileId}"
|
||||||
val accounts = listOf(flow.buildAccount(profile, balance, loginTag))
|
val accounts = listOf(flow.buildAccount(profile, balance, loginTag))
|
||||||
AccountCache.saveFahipay(this@HomeActivity, accounts)
|
val loginId = src.loginTag.removePrefix("fahipay_")
|
||||||
app.fahipayAccounts = accounts
|
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||||
|
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != src.loginTag } + accounts
|
||||||
accounts
|
accounts
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
val others = current.filter { it.profileType != "FAHIPAY" }
|
val others = current.filter { it.bank != "FAHIPAY" }
|
||||||
viewModel.accounts.postValue(others + fresh)
|
viewModel.accounts.postValue(others + fresh)
|
||||||
} else if (src.profileType.startsWith("BML")) {
|
} else if (src.bank == "BML") {
|
||||||
val loginId = src.loginTag.removePrefix("bml_")
|
val loginId = src.loginTag.removePrefix("bml_")
|
||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.bmlSessionFor(src) ?: return@withContext null
|
val sess = app.bmlSessionFor(src) ?: return@withContext null
|
||||||
@@ -659,27 +733,46 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
accounts
|
accounts
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
val otherAccounts = current.filter { !it.profileType.startsWith("BML") || it.loginTag != src.loginTag }
|
val otherAccounts = current.filter { it.bank != "BML" || it.loginTag != src.loginTag }
|
||||||
viewModel.accounts.postValue(otherAccounts + fresh)
|
viewModel.accounts.postValue(otherAccounts + fresh)
|
||||||
} else {
|
} else {
|
||||||
|
val loginId = src.loginTag.removePrefix("mib_")
|
||||||
val fresh = withContext(Dispatchers.IO) {
|
val fresh = withContext(Dispatchers.IO) {
|
||||||
val sess = app.mibSession ?: return@withContext null
|
val store = CredentialStore(this@HomeActivity)
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId } ?: return@withContext null
|
val hidden = store.getHiddenMibProfileIds(loginId)
|
||||||
try { MibLoginFlow(CredentialStore(this@HomeActivity)).fetchAllProfiles(sess, listOf(profile), src.loginTag) }
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
catch (_: Exception) { null }
|
val allVisible = profiles.filter { hidden.isEmpty() || it.profileId !in hidden }
|
||||||
|
val sess = app.mibSessions[loginId]
|
||||||
|
if (sess != null && allVisible.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val accounts = app.mibFlowFor(loginId).fetchAllProfiles(sess, allVisible, src.loginTag)
|
||||||
|
if (accounts.isNotEmpty()) return@withContext accounts
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
val creds = store.loadMibCredentials(loginId) ?: return@withContext null
|
||||||
|
try {
|
||||||
|
val flow = MibLoginFlow(store)
|
||||||
|
val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||||
|
app.mibSessions[loginId] = flow.lastSession!!
|
||||||
|
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||||
|
app.mibLoginFlows[loginId] = flow
|
||||||
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
|
accounts.takeIf { it.isNotEmpty() }
|
||||||
|
} catch (_: Exception) { null }
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
// Replace accounts from this profile only, keep everything else
|
// Replace accounts for this MIB login
|
||||||
val others = current.filter { it.profileId != src.profileId || it.profileType.startsWith("BML") }
|
val others = current.filter { it.loginTag != src.loginTag }
|
||||||
val merged = others + fresh
|
val newMibAccounts = app.mibAccounts.filter { it.loginTag != src.loginTag } + fresh
|
||||||
AccountCache.save(this@HomeActivity, merged.filter { !it.profileType.startsWith("BML") })
|
app.mibAccounts = newMibAccounts
|
||||||
viewModel.accounts.postValue(merged)
|
AccountCache.save(this@HomeActivity, newMibAccounts)
|
||||||
|
viewModel.accounts.postValue(others + fresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFinancing(session: MibSession?, profiles: List<MibProfile>) {
|
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||||
if (session == null || profiles.isEmpty()) return
|
if (profiles.isEmpty()) return
|
||||||
val flow = MibLoginFlow(CredentialStore(this))
|
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||||
val client = MibFinancingClient()
|
val client = MibFinancingClient()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -7,35 +8,23 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
|
|
||||||
class MoreFragment : Fragment() {
|
class MoreFragment : Fragment() {
|
||||||
|
|
||||||
private data class NavItem(val id: Int, @DrawableRes val icon: Int, @StringRes val title: Int)
|
|
||||||
|
|
||||||
private val items = listOf(
|
|
||||||
NavItem(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr),
|
|
||||||
NavItem(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities),
|
|
||||||
NavItem(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history),
|
|
||||||
NavItem(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances),
|
|
||||||
NavItem(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings),
|
|
||||||
NavItem(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp),
|
|
||||||
NavItem(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||||
inflater.inflate(R.layout.fragment_more, container, false)
|
inflater.inflate(R.layout.fragment_more, container, false)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
val items = NavCustomization.getMoreItems(prefs)
|
||||||
val list = view.findViewById<LinearLayout>(R.id.moreList)
|
val list = view.findViewById<LinearLayout>(R.id.moreList)
|
||||||
val inflater = LayoutInflater.from(requireContext())
|
val inflater = LayoutInflater.from(requireContext())
|
||||||
for (item in items) {
|
for (item in items) {
|
||||||
val row = inflater.inflate(R.layout.item_more_nav, list, false)
|
val row = inflater.inflate(R.layout.item_more_nav, list, false)
|
||||||
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.icon)
|
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
|
||||||
row.findViewById<TextView>(R.id.tvLabel).setText(item.title)
|
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
|
||||||
row.setOnClickListener { (requireActivity() as HomeActivity).navigateTo(item.id) }
|
row.setOnClickListener { (requireActivity() as HomeActivity).navigateTo(item.id) }
|
||||||
list.addView(row)
|
list.addView(row)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
|
||||||
|
object NavCustomization {
|
||||||
|
|
||||||
|
data class NavItemDef(
|
||||||
|
val id: Int,
|
||||||
|
@DrawableRes val iconRes: Int,
|
||||||
|
@StringRes val titleRes: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
/** All items that can occupy either a bottom nav slot or the "More" screen. */
|
||||||
|
val ALL_SWAPPABLE = listOf(
|
||||||
|
NavItemDef(R.id.nav_accounts, R.drawable.ic_nav_accounts, R.string.nav_accounts),
|
||||||
|
NavItemDef(R.id.nav_contacts, R.drawable.ic_contacts, R.string.nav_contacts),
|
||||||
|
NavItemDef(R.id.nav_transfer, R.drawable.ic_send, R.string.transfer),
|
||||||
|
NavItemDef(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr),
|
||||||
|
NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities),
|
||||||
|
NavItemDef(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history),
|
||||||
|
NavItemDef(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances),
|
||||||
|
NavItemDef(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings),
|
||||||
|
NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp),
|
||||||
|
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getSlots(prefs: SharedPreferences): List<Int> = listOf(
|
||||||
|
prefs.getInt("bottom_nav_slot_1", R.id.nav_accounts),
|
||||||
|
prefs.getInt("bottom_nav_slot_2", R.id.nav_contacts),
|
||||||
|
prefs.getInt("bottom_nav_slot_3", R.id.nav_transfer),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun saveSlots(prefs: SharedPreferences, slots: List<Int>) {
|
||||||
|
prefs.edit()
|
||||||
|
.putInt("bottom_nav_slot_1", slots[0])
|
||||||
|
.putInt("bottom_nav_slot_2", slots[1])
|
||||||
|
.putInt("bottom_nav_slot_3", slots[2])
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getQuickActions(prefs: SharedPreferences): List<Int> = listOf(
|
||||||
|
prefs.getInt("quick_action_1", R.id.nav_transfer),
|
||||||
|
prefs.getInt("quick_action_2", R.id.nav_pay_mv_qr),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun saveQuickActions(prefs: SharedPreferences, ids: List<Int>) {
|
||||||
|
prefs.edit()
|
||||||
|
.putInt("quick_action_1", ids[0])
|
||||||
|
.putInt("quick_action_2", ids[1])
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Items that belong in the "More" screen — those not occupying a bottom nav slot. */
|
||||||
|
fun getMoreItems(prefs: SharedPreferences): List<NavItemDef> {
|
||||||
|
val slots = getSlots(prefs).toSet()
|
||||||
|
return ALL_SWAPPABLE.filter { it.id !in slots }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
@@ -16,7 +18,14 @@ class NavMoreSheetFragment : BottomSheetDialogFragment() {
|
|||||||
inflater.inflate(R.layout.sheet_nav_more, container, false)
|
inflater.inflate(R.layout.sheet_nav_more, container, false)
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
view.findViewById<NavigationView>(R.id.navMoreView).setNavigationItemSelectedListener { item ->
|
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
val items = NavCustomization.getMoreItems(prefs)
|
||||||
|
val navView = view.findViewById<NavigationView>(R.id.navMoreView)
|
||||||
|
navView.menu.clear()
|
||||||
|
items.forEachIndexed { i, item ->
|
||||||
|
navView.menu.add(Menu.NONE, item.id, i, item.titleRes).setIcon(item.iconRes)
|
||||||
|
}
|
||||||
|
navView.setNavigationItemSelectedListener { item ->
|
||||||
dismiss()
|
dismiss()
|
||||||
onNavigate?.invoke(item.itemId)
|
onNavigate?.invoke(item.itemId)
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ class OtpFragment : Fragment() {
|
|||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
|
||||||
val entries = mutableListOf<OtpEntry>()
|
val entries = mutableListOf<OtpEntry>()
|
||||||
store.loadMibCredentials()?.let { creds ->
|
for (loginId in store.getMibLoginIds()) {
|
||||||
val name = store.loadMibFullName()
|
val creds = store.loadMibCredentials(loginId) ?: continue
|
||||||
|
val name = store.loadMibFullName(loginId)
|
||||||
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
|
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
|
||||||
}
|
}
|
||||||
for (loginId in store.getBmlLoginIds()) {
|
for (loginId in store.getBmlLoginIds()) {
|
||||||
@@ -88,20 +89,23 @@ class OtpFragment : Fragment() {
|
|||||||
// Fetch real names in background if not yet cached, then refresh labels
|
// Fetch real names in background if not yet cached, then refresh labels
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
var changed = false
|
var changed = false
|
||||||
if (store.loadMibFullName() == null) {
|
for (loginId in store.getMibLoginIds()) {
|
||||||
app.mibSession?.let { session ->
|
if (store.loadMibFullName(loginId) == null) {
|
||||||
|
val session = app.mibSessions[loginId] ?: continue
|
||||||
|
val flow = app.mibFlowFor(loginId)
|
||||||
val profile = withContext(Dispatchers.IO) {
|
val profile = withContext(Dispatchers.IO) {
|
||||||
try { app.mibLoginFlow.fetchPersonalProfile(session) } catch (_: Exception) { null }
|
try { flow.fetchPersonalProfile(session) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
store.saveMibUserProfile(CredentialStore.MibUserProfile(
|
store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile(
|
||||||
fullName = profile.fullName,
|
fullName = profile.fullName,
|
||||||
username = profile.username,
|
username = profile.username,
|
||||||
email = profile.email,
|
email = profile.email,
|
||||||
mobile = profile.mobile,
|
mobile = profile.mobile,
|
||||||
enrolled = profile.enrolled
|
enrolled = profile.enrolled
|
||||||
))
|
))
|
||||||
val idx = entries.indexOfFirst { it.seed == store.loadMibCredentials()?.otpSeed }
|
val seed = store.loadMibCredentials(loginId)?.otpSeed
|
||||||
|
val idx = entries.indexOfFirst { it.seed == seed }
|
||||||
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "MIB · ${profile.fullName}"); changed = true }
|
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "MIB · ${profile.fullName}"); changed = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,45 @@
|
|||||||
package sh.sar.basedbank.ui.home
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales
|
import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding
|
import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
class SettingsAppearanceFragment : Fragment() {
|
class SettingsAppearanceFragment : Fragment() {
|
||||||
|
|
||||||
private var _binding: FragmentSettingsAppearanceBinding? = null
|
private var _binding: FragmentSettingsAppearanceBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private lateinit var prefs: SharedPreferences
|
||||||
|
private val slots = mutableListOf<Int>()
|
||||||
|
private val quickActions = mutableListOf<Int>()
|
||||||
|
private lateinit var slotAdapter: NavItemAdapter
|
||||||
|
private lateinit var quickActionAdapter: NavItemAdapter
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentSettingsAppearanceBinding.inflate(inflater, container, false)
|
_binding = FragmentSettingsAppearanceBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
// Navigation mode
|
// Navigation mode
|
||||||
val isBottom = prefs.getBoolean("bottom_nav", false)
|
val isBottom = prefs.getBoolean("bottom_nav", false)
|
||||||
@@ -32,8 +48,35 @@ class SettingsAppearanceFragment : Fragment() {
|
|||||||
if (!isChecked) return@addOnButtonCheckedListener
|
if (!isChecked) return@addOnButtonCheckedListener
|
||||||
prefs.edit().putBoolean("bottom_nav", checkedId == R.id.btnNavBottom).apply()
|
prefs.edit().putBoolean("bottom_nav", checkedId == R.id.btnNavBottom).apply()
|
||||||
(activity as? HomeActivity)?.applyNavMode()
|
(activity as? HomeActivity)?.applyNavMode()
|
||||||
|
updateShortcutsVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quick actions
|
||||||
|
quickActions.clear()
|
||||||
|
quickActions.addAll(NavCustomization.getQuickActions(prefs))
|
||||||
|
quickActionAdapter = NavItemAdapter(quickActions) {
|
||||||
|
NavCustomization.saveQuickActions(prefs, quickActions)
|
||||||
|
}
|
||||||
|
setupNavItemRecyclerView(binding.rvQuickActions, quickActionAdapter, quickActions)
|
||||||
|
|
||||||
|
// Bottom bar shortcuts
|
||||||
|
slots.clear()
|
||||||
|
slots.addAll(NavCustomization.getSlots(prefs))
|
||||||
|
slotAdapter = NavItemAdapter(slots) {
|
||||||
|
NavCustomization.saveSlots(prefs, slots)
|
||||||
|
(activity as? HomeActivity)?.rebuildBottomNav(prefs)
|
||||||
|
}
|
||||||
|
setupNavItemRecyclerView(binding.rvNavSlots, slotAdapter, slots)
|
||||||
|
// Show labels toggle
|
||||||
|
val showLabels = prefs.getBoolean("bottom_nav_show_labels", true)
|
||||||
|
binding.switchShowLabels.isChecked = showLabels
|
||||||
|
binding.switchShowLabels.setOnCheckedChangeListener { _, checked ->
|
||||||
|
prefs.edit().putBoolean("bottom_nav_show_labels", checked).apply()
|
||||||
|
(activity as? HomeActivity)?.applyNavLabelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateShortcutsVisibility()
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
val saved = prefs.getString("theme", "system")
|
val saved = prefs.getString("theme", "system")
|
||||||
binding.themeToggle.check(when (saved) {
|
binding.themeToggle.check(when (saved) {
|
||||||
@@ -63,6 +106,95 @@ class SettingsAppearanceFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupNavItemRecyclerView(
|
||||||
|
rv: RecyclerView,
|
||||||
|
adapter: NavItemAdapter,
|
||||||
|
items: MutableList<Int>
|
||||||
|
) {
|
||||||
|
rv.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
rv.adapter = adapter
|
||||||
|
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||||
|
ItemTouchHelper.START or ItemTouchHelper.END, 0
|
||||||
|
) {
|
||||||
|
override fun onMove(
|
||||||
|
rv: RecyclerView,
|
||||||
|
from: RecyclerView.ViewHolder,
|
||||||
|
to: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
val fromPos = from.adapterPosition
|
||||||
|
val toPos = to.adapterPosition
|
||||||
|
Collections.swap(items, fromPos, toPos)
|
||||||
|
adapter.notifyItemMoved(fromPos, toPos)
|
||||||
|
adapter.onSave()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||||
|
}).attachToRecyclerView(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateShortcutsVisibility() {
|
||||||
|
val isBottom = prefs.getBoolean("bottom_nav", false)
|
||||||
|
binding.sectionBottomBarShortcuts.alpha = if (isBottom) 1f else 0.38f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showItemPicker(items: MutableList<Int>, slotIndex: Int, adapter: NavItemAdapter) {
|
||||||
|
if (items === slots && !prefs.getBoolean("bottom_nav", false)) return
|
||||||
|
val ctx = requireContext()
|
||||||
|
val otherIds = items.filterIndexed { i, _ -> i != slotIndex }.toSet()
|
||||||
|
val available = NavCustomization.ALL_SWAPPABLE.filter { it.id !in otherIds }
|
||||||
|
val listLayout = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL }
|
||||||
|
val rows = available.map { item ->
|
||||||
|
LayoutInflater.from(ctx).inflate(R.layout.item_more_nav, listLayout, false).also { row ->
|
||||||
|
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
|
||||||
|
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
|
||||||
|
listLayout.addView(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scroll = ScrollView(ctx).apply { addView(listLayout) }
|
||||||
|
var dialog: androidx.appcompat.app.AlertDialog? = null
|
||||||
|
dialog = MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(R.string.settings_bottom_bar_select)
|
||||||
|
.setView(scroll)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
available.forEachIndexed { i, item ->
|
||||||
|
rows[i].setOnClickListener {
|
||||||
|
items[slotIndex] = item.id
|
||||||
|
adapter.onSave()
|
||||||
|
adapter.notifyItemChanged(slotIndex)
|
||||||
|
dialog?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class NavItemAdapter(
|
||||||
|
val items: MutableList<Int>,
|
||||||
|
val onSave: () -> Unit
|
||||||
|
) : RecyclerView.Adapter<NavItemAdapter.VH>() {
|
||||||
|
|
||||||
|
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val ivNavIcon: ImageView = view.findViewById(R.id.ivNavIcon)
|
||||||
|
val tvNavLabel: TextView = view.findViewById(R.id.tvNavLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_nav_slot, parent, false)
|
||||||
|
val itemWidth = if (parent.measuredWidth > 0) parent.measuredWidth / items.size
|
||||||
|
else RecyclerView.LayoutParams.WRAP_CONTENT
|
||||||
|
view.layoutParams = RecyclerView.LayoutParams(itemWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
|
return VH(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||||
|
val def = NavCustomization.ALL_SWAPPABLE.find { it.id == items[position] } ?: return
|
||||||
|
holder.ivNavIcon.setImageResource(def.iconRes)
|
||||||
|
holder.tvNavLabel.setText(def.titleRes)
|
||||||
|
holder.itemView.setOnClickListener { showItemPicker(items, holder.adapterPosition, this) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
requireActivity().title = getString(R.string.settings_appearance)
|
requireActivity().title = getString(R.string.settings_appearance)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.Gravity
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckBox
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -14,6 +15,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
import sh.sar.basedbank.api.mib.TransactionCache
|
import sh.sar.basedbank.api.mib.TransactionCache
|
||||||
import sh.sar.basedbank.databinding.FragmentSettingsLoginsBinding
|
import sh.sar.basedbank.databinding.FragmentSettingsLoginsBinding
|
||||||
import sh.sar.basedbank.ui.login.LoginActivity
|
import sh.sar.basedbank.ui.login.LoginActivity
|
||||||
@@ -58,31 +60,18 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
val container = binding.loginsContainer
|
val container = binding.loginsContainer
|
||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
|
||||||
val hasMib = store.hasMibCredentials()
|
val mibLoginIds = store.getMibLoginIds()
|
||||||
val bmlLoginIds = store.getBmlLoginIds()
|
val bmlLoginIds = store.getBmlLoginIds()
|
||||||
val hasFahipay = store.hasFahipayCredentials()
|
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||||
|
|
||||||
binding.tvLoginsTitle.visibility = if (hasMib || bmlLoginIds.isNotEmpty() || hasFahipay) View.VISIBLE else View.GONE
|
binding.tvLoginsTitle.visibility = if (mibLoginIds.isNotEmpty() || bmlLoginIds.isNotEmpty() || fahipayLoginIds.isNotEmpty()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
if (hasMib) {
|
for (loginId in mibLoginIds) {
|
||||||
val profile = store.loadMibUserProfile()
|
val profile = store.loadMibUserProfile(loginId)
|
||||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name)
|
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name)
|
||||||
val profileNames = AccountCache.load(ctx).map { it.profileName }.filter { it.isNotBlank() }.distinct()
|
val mibProfiles = store.loadMibProfiles(loginId)
|
||||||
addLoginRow(container, R.drawable.mib_logo, displayName) {
|
addLoginRow(container, R.drawable.mib_logo, displayName) {
|
||||||
showLoginDetails(
|
showMibLoginDetails(store, loginId, profile, mibProfiles)
|
||||||
title = getString(R.string.mib_name),
|
|
||||||
details = buildString {
|
|
||||||
if (!profile?.fullName.isNullOrBlank()) appendLine("${getString(R.string.login_detail_name)}: ${profile!!.fullName}")
|
|
||||||
if (!profile?.email.isNullOrBlank()) appendLine("${getString(R.string.login_detail_email)}: ${profile!!.email}")
|
|
||||||
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
|
|
||||||
if (profileNames.isNotEmpty()) {
|
|
||||||
appendLine()
|
|
||||||
appendLine(getString(R.string.login_detail_profiles))
|
|
||||||
profileNames.forEach { appendLine(" • $it") }
|
|
||||||
}
|
|
||||||
}.trim(),
|
|
||||||
onLogout = { confirmLogout(getString(R.string.mib_name)) { logoutMib(store) } }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +99,8 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFahipay) {
|
for (loginId in fahipayLoginIds) {
|
||||||
val profile = store.loadFahipayUserProfile()
|
val profile = store.loadFahipayUserProfile(loginId)
|
||||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
||||||
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
||||||
showLoginDetails(
|
showLoginDetails(
|
||||||
@@ -122,7 +111,7 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
|
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
|
||||||
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}")
|
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}")
|
||||||
}.trim(),
|
}.trim(),
|
||||||
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store) } }
|
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +143,118 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
container.addView(row)
|
container.addView(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showMibLoginDetails(
|
||||||
|
store: CredentialStore,
|
||||||
|
loginId: String,
|
||||||
|
profile: CredentialStore.MibUserProfile?,
|
||||||
|
mibProfiles: List<MibProfile>
|
||||||
|
) {
|
||||||
|
val ctx = requireContext()
|
||||||
|
val dp = ctx.resources.displayMetrics.density
|
||||||
|
val originalHidden = store.getHiddenMibProfileIds(loginId)
|
||||||
|
val hidden = originalHidden.toMutableSet()
|
||||||
|
|
||||||
|
val scroll = android.widget.ScrollView(ctx)
|
||||||
|
val container = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
val pad = (16 * dp).toInt()
|
||||||
|
setPadding(pad, (8 * dp).toInt(), pad, pad)
|
||||||
|
}
|
||||||
|
scroll.addView(container)
|
||||||
|
|
||||||
|
// Account info lines
|
||||||
|
listOfNotNull(
|
||||||
|
profile?.fullName?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_name)}: $it" },
|
||||||
|
profile?.email?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_email)}: $it" },
|
||||||
|
profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: $it" }
|
||||||
|
).forEach { line ->
|
||||||
|
container.addView(TextView(ctx).apply {
|
||||||
|
text = line
|
||||||
|
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||||
|
it.bottomMargin = (4 * dp).toInt()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mibProfiles.isNotEmpty()) {
|
||||||
|
if (profile != null) {
|
||||||
|
container.addView(View(ctx).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (1 * dp).toInt()).also {
|
||||||
|
it.topMargin = (12 * dp).toInt(); it.bottomMargin = (12 * dp).toInt()
|
||||||
|
}
|
||||||
|
setBackgroundColor(0x1F000000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
container.addView(TextView(ctx).apply {
|
||||||
|
text = getString(R.string.login_detail_profiles)
|
||||||
|
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelMedium)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||||
|
it.bottomMargin = (8 * dp).toInt()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build checkbox rows — wired up after dialog.show() so we can reference the Save button
|
||||||
|
val checkboxRows = mibProfiles.map { p ->
|
||||||
|
val row = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||||
|
it.bottomMargin = (4 * dp).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val textCol = LinearLayout(ctx).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||||
|
}
|
||||||
|
textCol.addView(TextView(ctx).apply {
|
||||||
|
text = p.name
|
||||||
|
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
|
||||||
|
})
|
||||||
|
if (p.cifType.isNotBlank()) {
|
||||||
|
textCol.addView(TextView(ctx).apply {
|
||||||
|
text = p.cifType
|
||||||
|
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall)
|
||||||
|
alpha = 0.6f
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val cb = CheckBox(ctx).apply { isChecked = p.profileId !in hidden }
|
||||||
|
row.addView(textCol)
|
||||||
|
row.addView(cb)
|
||||||
|
container.addView(row)
|
||||||
|
p to cb
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(ctx)
|
||||||
|
.setTitle(getString(R.string.mib_name))
|
||||||
|
.setView(scroll)
|
||||||
|
.setPositiveButton(R.string.save, null) // null — set manually after show()
|
||||||
|
.setNeutralButton(R.string.close, null)
|
||||||
|
.setNegativeButton(R.string.settings_logout) { _, _ ->
|
||||||
|
confirmLogout(getString(R.string.mib_name)) { logoutMib(store, loginId) }
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
val saveBtn = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
|
||||||
|
saveBtn.isEnabled = false
|
||||||
|
|
||||||
|
checkboxRows.forEach { (p, cb) ->
|
||||||
|
cb.setOnCheckedChangeListener { _, checked ->
|
||||||
|
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId)
|
||||||
|
val atLeastOneVisible = mibProfiles.any { it.profileId !in hidden }
|
||||||
|
saveBtn.isEnabled = hidden != originalHidden && atLeastOneVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.setOnClickListener {
|
||||||
|
store.setHiddenMibProfileIds(loginId, hidden)
|
||||||
|
clearAllCaches(ctx)
|
||||||
|
dialog.dismiss()
|
||||||
|
(activity as? HomeActivity)?.relogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) {
|
private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
@@ -172,12 +273,16 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logoutMib(store: CredentialStore) {
|
private fun logoutMib(store: CredentialStore, loginId: String) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
store.clearMibCredentials()
|
store.clearMibCredentials(loginId)
|
||||||
ctx.getSharedPreferences("mib_prefs", Context.MODE_PRIVATE).edit().clear().apply()
|
ctx.getSharedPreferences("mib_prefs", Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.accounts = emptyList(); app.mibSession = null; app.mibProfiles = emptyList()
|
app.mibSessions.remove(loginId)
|
||||||
|
app.mibProfilesMap.remove(loginId)
|
||||||
|
app.mibLoginFlows.remove(loginId)
|
||||||
|
app.mibAccounts = app.mibAccounts.filter { it.loginTag != "mib_$loginId" }
|
||||||
|
app.accounts = app.accounts.filter { it.loginTag != "mib_$loginId" }
|
||||||
clearAllCaches(ctx)
|
clearAllCaches(ctx)
|
||||||
(activity as HomeActivity).relogin()
|
(activity as HomeActivity).relogin()
|
||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
@@ -194,11 +299,12 @@ class SettingsLoginsFragment : Fragment() {
|
|||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun logoutFahipay(store: CredentialStore) {
|
private fun logoutFahipay(store: CredentialStore, loginId: String) {
|
||||||
val ctx = requireContext()
|
val ctx = requireContext()
|
||||||
store.clearFahipayCredentials(); store.clearFahipaySession()
|
store.clearFahipayCredentials(loginId)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.fahipaySession = null; app.fahipayAccounts = emptyList()
|
app.fahipaySessions.remove(loginId)
|
||||||
|
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != "fahipay_$loginId" }
|
||||||
clearAllCaches(ctx)
|
clearAllCaches(ctx)
|
||||||
(activity as HomeActivity).relogin()
|
(activity as HomeActivity).relogin()
|
||||||
buildLoginsSection()
|
buildLoginsSection()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -49,6 +50,7 @@ import sh.sar.basedbank.api.mib.MibTransferResult
|
|||||||
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||||
|
import sh.sar.basedbank.util.AccountListParser
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.util.AccountInputParser
|
import sh.sar.basedbank.util.AccountInputParser
|
||||||
import sh.sar.basedbank.util.PaymvQrParser
|
import sh.sar.basedbank.util.PaymvQrParser
|
||||||
@@ -63,7 +65,9 @@ class TransferFragment : Fragment() {
|
|||||||
private val viewModel: HomeViewModel by activityViewModels()
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private var selectedAccount: MibAccount? = null
|
private var selectedAccount: MibAccount? = null
|
||||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
private val session get() = selectedAccount
|
||||||
|
?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) }
|
||||||
|
?: (requireActivity().application as BasedBankApp).anyMibSession()
|
||||||
private fun bmlSessionFor(account: MibAccount?) =
|
private fun bmlSessionFor(account: MibAccount?) =
|
||||||
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
||||||
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
||||||
@@ -146,8 +150,11 @@ class TransferFragment : Fragment() {
|
|||||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnTransfer.isEnabled = false
|
||||||
binding.btnTransfer.setOnClickListener { initiateTransfer() }
|
binding.btnTransfer.setOnClickListener { initiateTransfer() }
|
||||||
|
|
||||||
|
binding.etAmount.addTextChangedListener { updateTransferButton() }
|
||||||
|
|
||||||
// Pre-select contact if navigated from contacts page
|
// Pre-select contact if navigated from contacts page
|
||||||
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
||||||
prefillToDirectly(
|
prefillToDirectly(
|
||||||
@@ -183,6 +190,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.cardFromInfo.visibility = View.GONE
|
binding.cardFromInfo.visibility = View.GONE
|
||||||
binding.tilFrom.visibility = View.VISIBLE
|
binding.tilFrom.visibility = View.VISIBLE
|
||||||
binding.actvFrom.setText("", false)
|
binding.actvFrom.setText("", false)
|
||||||
|
updateTransferButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||||
@@ -194,6 +202,7 @@ class TransferFragment : Fragment() {
|
|||||||
selectedAccount = picked
|
selectedAccount = picked
|
||||||
updateAmountPrefix(picked)
|
updateAmountPrefix(picked)
|
||||||
showFromCard(picked)
|
showFromCard(picked)
|
||||||
|
updateTransferButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
val fromNumber = arguments?.getString(ARG_FROM_ACCOUNT)
|
val fromNumber = arguments?.getString(ARG_FROM_ACCOUNT)
|
||||||
@@ -203,22 +212,22 @@ class TransferFragment : Fragment() {
|
|||||||
selectedAccount = match
|
selectedAccount = match
|
||||||
updateAmountPrefix(match)
|
updateAmountPrefix(match)
|
||||||
showFromCard(match)
|
showFromCard(match)
|
||||||
|
updateTransferButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFromCard(account: MibAccount) {
|
private fun showFromCard(account: MibAccount) {
|
||||||
val isBml = account.profileType.startsWith("BML")
|
val colorHex = when (account.bank) {
|
||||||
val colorHex = when {
|
"BML" -> "#0066A1"
|
||||||
isBml -> "#0066A1"
|
"FAHIPAY" -> "#15BEA7"
|
||||||
account.profileType == "FAHIPAY" -> "#15BEA7"
|
else -> "#FE860E"
|
||||||
else -> "#FE860E"
|
|
||||||
}
|
}
|
||||||
val bankLabel = when {
|
val bankLabel = when (account.bank) {
|
||||||
isBml -> "BML"
|
"BML" -> "BML"
|
||||||
account.profileType == "FAHIPAY" -> "FP"
|
"FAHIPAY" -> "FP"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
val typeLabel = when {
|
val typeLabel = when {
|
||||||
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
||||||
@@ -234,7 +243,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tilFrom.visibility = View.GONE
|
binding.tilFrom.visibility = View.GONE
|
||||||
binding.cardFromInfo.visibility = View.VISIBLE
|
binding.cardFromInfo.visibility = View.VISIBLE
|
||||||
|
|
||||||
if (!isBml && account.profileImageHash != null) {
|
if (account.bank != "BML" && account.profileImageHash != null) {
|
||||||
loadFromPhoto(account.profileImageHash)
|
loadFromPhoto(account.profileImageHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +253,7 @@ class TransferFragment : Fragment() {
|
|||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = app.mibLoginFlow.fetchProfileImage(sess, hash) ?: return@launch
|
val base64 = app.anyMibFlow()?.fetchProfileImage(sess, hash) ?: return@launch
|
||||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -271,6 +280,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
binding.btnScanQr.visibility = View.VISIBLE
|
binding.btnScanQr.visibility = View.VISIBLE
|
||||||
binding.tilTo.error = null
|
binding.tilTo.error = null
|
||||||
|
updateTransferButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.etTo.addTextChangedListener {
|
binding.etTo.addTextChangedListener {
|
||||||
@@ -282,6 +292,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
binding.btnScanQr.visibility = View.VISIBLE
|
binding.btnScanQr.visibility = View.VISIBLE
|
||||||
|
updateTransferButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +323,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fahipay source: only phone numbers are supported
|
// Fahipay source: only phone numbers are supported
|
||||||
if (selectedAccount?.profileType == "FAHIPAY") {
|
if (selectedAccount?.bank == "FAHIPAY") {
|
||||||
if (AccountInputParser.detect(accountNumber) == AccountInputParser.InputType.PHONE) {
|
if (AccountInputParser.detect(accountNumber) == AccountInputParser.InputType.PHONE) {
|
||||||
lookupFahipayTarget(accountNumber)
|
lookupFahipayTarget(accountNumber)
|
||||||
} else {
|
} else {
|
||||||
@@ -328,7 +339,7 @@ class TransferFragment : Fragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true
|
val isBmlSource = selectedAccount?.bank == "BML"
|
||||||
|
|
||||||
startLookupLoading()
|
startLookupLoading()
|
||||||
|
|
||||||
@@ -394,6 +405,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.btnPickContact.visibility = View.GONE
|
binding.btnPickContact.visibility = View.GONE
|
||||||
binding.btnScanQr.visibility = View.GONE
|
binding.btnScanQr.visibility = View.GONE
|
||||||
binding.cardToInfo.visibility = View.VISIBLE
|
binding.cardToInfo.visibility = View.VISIBLE
|
||||||
|
updateTransferButton()
|
||||||
saveToRecents(info)
|
saveToRecents(info)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
@@ -520,6 +532,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.btnPickContact.visibility = View.GONE
|
binding.btnPickContact.visibility = View.GONE
|
||||||
binding.btnScanQr.visibility = View.GONE
|
binding.btnScanQr.visibility = View.GONE
|
||||||
binding.cardToInfo.visibility = View.VISIBLE
|
binding.cardToInfo.visibility = View.VISIBLE
|
||||||
|
updateTransferButton()
|
||||||
|
|
||||||
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
@@ -567,7 +580,7 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tilAmount.error = null
|
binding.tilAmount.error = null
|
||||||
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
||||||
|
|
||||||
val isSrcBml = src.profileType.startsWith("BML")
|
val isSrcBml = src.bank == "BML"
|
||||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
||||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||||
val currency = src.currencyName.ifBlank { "MVR" }
|
val currency = src.currencyName.ifBlank { "MVR" }
|
||||||
@@ -578,10 +591,10 @@ class TransferFragment : Fragment() {
|
|||||||
if (isSrcBml && isDestMib && currency == "USD") {
|
if (isSrcBml && isDestMib && currency == "USD") {
|
||||||
val hasBmlContact = allContacts.any { it.benefCategoryId == "BML" && it.benefAccount == resolvedAccountNumber }
|
val hasBmlContact = allContacts.any { it.benefCategoryId == "BML" && it.benefAccount == resolvedAccountNumber }
|
||||||
if (!hasBmlContact) {
|
if (!hasBmlContact) {
|
||||||
AlertDialog.Builder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.transfer_bml_contact_required_title)
|
.setTitle(R.string.transfer_bml_contact_required_title)
|
||||||
.setMessage(R.string.transfer_bml_contact_required_msg)
|
.setMessage(R.string.transfer_bml_contact_required_msg)
|
||||||
.setPositiveButton("OK", null)
|
.setPositiveButton(R.string.close, null)
|
||||||
.show()
|
.show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -625,7 +638,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialogBuilder = AlertDialog.Builder(requireContext())
|
val dialogBuilder = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.transfer)
|
.setTitle(R.string.transfer)
|
||||||
.setPositiveButton(R.string.transfer_confirm) { _, _ -> doTransfer() }
|
.setPositiveButton(R.string.transfer_confirm) { _, _ -> doTransfer() }
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
@@ -706,12 +719,14 @@ class TransferFragment : Fragment() {
|
|||||||
): Triple<Boolean, String, TransferReceiptData?> {
|
): Triple<Boolean, String, TransferReceiptData?> {
|
||||||
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
val loginId = src.loginTag.removePrefix("mib_")
|
||||||
// Switch to the profile that owns the source account
|
// Switch to the profile that owns the source account
|
||||||
if (src.profileId.isNotBlank()) {
|
if (src.profileId.isNotBlank()) {
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId }
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(sess, profile)
|
val profile = profiles.firstOrNull { it.profileId == src.profileId }
|
||||||
|
if (profile != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
||||||
}
|
}
|
||||||
val otp = CredentialStore(requireContext()).loadMibCredentials()?.otpSeed
|
val otp = CredentialStore(requireContext()).loadMibCredentials(loginId)?.otpSeed
|
||||||
?.let { Totp.generate(it) }
|
?.let { Totp.generate(it) }
|
||||||
?: return Triple(false, "OTP unavailable", null)
|
?: return Triple(false, "OTP unavailable", null)
|
||||||
val currencyCode = if (src.currencyName == "USD") "840" else "462"
|
val currencyCode = if (src.currencyName == "USD") "840" else "462"
|
||||||
@@ -848,6 +863,11 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateTransferButton() {
|
||||||
|
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
||||||
|
binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
|
||||||
|
}
|
||||||
|
|
||||||
private fun clearForm() {
|
private fun clearForm() {
|
||||||
selectedAccount = null
|
selectedAccount = null
|
||||||
binding.actvFrom.setText("", false)
|
binding.actvFrom.setText("", false)
|
||||||
@@ -878,7 +898,7 @@ class TransferFragment : Fragment() {
|
|||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = if (isProfile) {
|
val base64 = if (isProfile) {
|
||||||
app.mibLoginFlow.fetchProfileImage(sess, hash)
|
app.anyMibFlow()?.fetchProfileImage(sess, hash)
|
||||||
} else {
|
} else {
|
||||||
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
@@ -1009,11 +1029,11 @@ class TransferFragment : Fragment() {
|
|||||||
.also { it.root.tag = it }
|
.also { it.root.tag = it }
|
||||||
}
|
}
|
||||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||||
val isBmlAccount = acc.profileType.startsWith("BML")
|
val isBmlAccount = acc.bank == "BML"
|
||||||
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||||
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
||||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||||
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"
|
b.tvDropdownBalance.text = AccountListParser.from(acc)?.balance ?: ""
|
||||||
b.root.alpha = if (inactive) 0.4f else 1f
|
b.root.alpha = if (inactive) 0.4f else 1f
|
||||||
b.root
|
b.root
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import android.util.Base64
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -61,10 +63,10 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
var fahipayTotal: Int = -1
|
var fahipayTotal: Int = -1
|
||||||
) {
|
) {
|
||||||
fun hasMore(): Boolean = when {
|
fun hasMore(): Boolean = when {
|
||||||
account.profileType == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
account.bank == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||||
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
|
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
|
||||||
account.profileType.startsWith("BML") -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
account.bank == "BML" -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||||
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +89,15 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||||
|
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||||
|
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||||
|
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
|
||||||
if (dy <= 0 || isLoading) return
|
if (dy <= 0 || isLoading) return
|
||||||
@@ -142,14 +153,13 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val mibSession = app.mibSession
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val newTransactions = withContext(Dispatchers.IO) {
|
val newTransactions = withContext(Dispatchers.IO) {
|
||||||
val results = mutableListOf<Transaction>()
|
val results = mutableListOf<Transaction>()
|
||||||
|
|
||||||
// BML accounts: fetch in parallel
|
// BML accounts: fetch in parallel
|
||||||
val bmlStates = activeStates.filter { it.account.profileType.startsWith("BML") }
|
val bmlStates = activeStates.filter { it.account.bank == "BML" }
|
||||||
results.addAll(bmlStates.map { state ->
|
results.addAll(bmlStates.map { state ->
|
||||||
async {
|
async {
|
||||||
try {
|
try {
|
||||||
@@ -187,9 +197,9 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
}.awaitAll().flatten())
|
}.awaitAll().flatten())
|
||||||
|
|
||||||
// Fahipay accounts
|
// Fahipay accounts
|
||||||
val fahipayStates = activeStates.filter { it.account.profileType == "FAHIPAY" }
|
val fahipayStates = activeStates.filter { it.account.bank == "FAHIPAY" }
|
||||||
for (state in fahipayStates) {
|
for (state in fahipayStates) {
|
||||||
val session = app.fahipaySession ?: continue
|
val session = app.fahipaySessionFor(state.account) ?: continue
|
||||||
try {
|
try {
|
||||||
val flow = FahipayLoginFlow()
|
val flow = FahipayLoginFlow()
|
||||||
flow.setSessionCookie(session.sessionCookie)
|
flow.setSessionCookie(session.sessionCookie)
|
||||||
@@ -206,27 +216,28 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MIB accounts: serialized per profile, protected by mutex to prevent session race
|
// MIB accounts: serialized per profile, protected by mutex to prevent session race
|
||||||
val mibStates = activeStates.filter {
|
val mibStates = activeStates.filter { it.account.bank == "MIB" }
|
||||||
!it.account.profileType.startsWith("BML") && it.account.profileType != "FAHIPAY"
|
for ((loginId, loginStates) in mibStates.groupBy { it.account.loginTag.removePrefix("mib_") }) {
|
||||||
}
|
val session = app.mibSessions[loginId] ?: continue
|
||||||
for ((profileId, states) in mibStates.groupBy { it.account.profileId }) {
|
for ((profileId, states) in loginStates.groupBy { it.account.profileId }) {
|
||||||
val session = mibSession ?: break
|
app.mibMutex.withLock {
|
||||||
app.mibMutex.withLock {
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
val profile = app.mibProfiles.firstOrNull { it.profileId == profileId }
|
val profile = profiles.firstOrNull { it.profileId == profileId }
|
||||||
if (profile != null) app.mibLoginFlow.switchProfile(session, profile)
|
if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile)
|
||||||
for (state in states) {
|
for (state in states) {
|
||||||
try {
|
try {
|
||||||
val (list, total) = MibHistoryClient().fetchHistory(
|
val (list, total) = MibHistoryClient().fetchHistory(
|
||||||
session = session,
|
session = session,
|
||||||
accountNo = state.account.accountNumber,
|
accountNo = state.account.accountNumber,
|
||||||
accountDisplayName = state.account.accountBriefName,
|
accountDisplayName = state.account.accountBriefName,
|
||||||
start = state.mibNextStart,
|
start = state.mibNextStart,
|
||||||
pageSize = pageSize
|
pageSize = pageSize
|
||||||
)
|
)
|
||||||
if (total > 0) state.mibTotalCount = total
|
if (total > 0) state.mibTotalCount = total
|
||||||
state.mibNextStart += list.size.coerceAtLeast(pageSize)
|
state.mibNextStart += list.size.coerceAtLeast(pageSize)
|
||||||
results.addAll(list)
|
results.addAll(list)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +291,7 @@ class TransferHistoryFragment : Fragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val sess = app.mibSession ?: return
|
val sess = app.anyMibSession() ?: return
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
val base64 = MibContactsClient().fetchProfileImageBase64(sess, hash) ?: return@launch
|
||||||
|
|||||||
@@ -154,11 +154,11 @@ class TransferReceiptFragment : Fragment() {
|
|||||||
|
|
||||||
private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) {
|
private fun loadProfileImage(hash: String, isProfile: Boolean, onLoaded: (Bitmap) -> Unit) {
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val sess = app.mibSession ?: return
|
val sess = app.anyMibSession() ?: return
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val base64 = if (isProfile) {
|
val base64 = if (isProfile) {
|
||||||
app.mibLoginFlow.fetchProfileImage(sess, hash)
|
app.anyMibFlow()?.fetchProfileImage(sess, hash)
|
||||||
} else {
|
} else {
|
||||||
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
MibContactsClient().fetchProfileImageBase64(sess, hash)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
when (bankType) {
|
when (bankType) {
|
||||||
"BML" -> {
|
"BML" -> {
|
||||||
binding.ivBankLogo.setImageResource(R.drawable.bml_logo_vector)
|
binding.ivBankLogo.setImageResource(R.drawable.bml_logo_long)
|
||||||
binding.tvSignInDesc.setText(R.string.bml_sign_in_desc)
|
binding.tvSignInDesc.setText(R.string.bml_sign_in_desc)
|
||||||
}
|
}
|
||||||
"FAHIPAY" -> {
|
"FAHIPAY" -> {
|
||||||
@@ -170,6 +170,7 @@ class CredentialsFragment : Fragment() {
|
|||||||
binding.btnLogin.isEnabled = false
|
binding.btnLogin.isEnabled = false
|
||||||
|
|
||||||
val passwordHash = MibLoginFlow.hashPassword(password)
|
val passwordHash = MibLoginFlow.hashPassword(password)
|
||||||
|
val loginId = username
|
||||||
val flow = MibLoginFlow(CredentialStore(requireContext()))
|
val flow = MibLoginFlow(CredentialStore(requireContext()))
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
@@ -178,11 +179,12 @@ class CredentialsFragment : Fragment() {
|
|||||||
flow.login(username, passwordHash, otpSeed)
|
flow.login(username, passwordHash, otpSeed)
|
||||||
}
|
}
|
||||||
val store = CredentialStore(requireContext())
|
val store = CredentialStore(requireContext())
|
||||||
store.saveMibCredentials(username, passwordHash, otpSeed)
|
store.saveMibCredentials(loginId, username, passwordHash, otpSeed)
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
flow.lastSession?.let { s ->
|
flow.lastSession?.let { s ->
|
||||||
val profile = flow.fetchPersonalProfile(s)
|
val profile = flow.fetchPersonalProfile(s)
|
||||||
if (profile != null) store.saveMibUserProfile(
|
if (profile != null) store.saveMibUserProfile(
|
||||||
|
loginId,
|
||||||
CredentialStore.MibUserProfile(
|
CredentialStore.MibUserProfile(
|
||||||
fullName = profile.fullName,
|
fullName = profile.fullName,
|
||||||
username = profile.username,
|
username = profile.username,
|
||||||
@@ -193,11 +195,15 @@ class CredentialsFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AccountCache.save(requireContext(), accounts)
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.accounts = accounts
|
// Merge with any existing MIB accounts from other logins
|
||||||
app.mibSession = flow.lastSession
|
app.mibAccounts = app.mibAccounts.filter { it.loginTag != "mib_$loginId" } + accounts
|
||||||
app.mibProfiles = flow.lastProfiles
|
app.accounts = app.accounts.filter { it.loginTag != "mib_$loginId" } + accounts
|
||||||
|
AccountCache.save(requireContext(), app.mibAccounts)
|
||||||
|
app.mibSessions[loginId] = flow.lastSession!!
|
||||||
|
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||||
|
app.mibLoginFlows[loginId] = flow
|
||||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@@ -374,11 +380,13 @@ class CredentialsFragment : Fragment() {
|
|||||||
val b = flow.fetchBalance(session)
|
val b = flow.fetchBalance(session)
|
||||||
Pair(p, b)
|
Pair(p, b)
|
||||||
}
|
}
|
||||||
val loginTag = "fahipay_${profile.profileId}"
|
val loginId = profile.profileId
|
||||||
|
val loginTag = "fahipay_$loginId"
|
||||||
val account = flow.buildAccount(profile, balance, loginTag)
|
val account = flow.buildAccount(profile, balance, loginTag)
|
||||||
store.saveFahipayCredentials(idCard, password)
|
store.saveFahipayCredentials(loginId, idCard, password)
|
||||||
store.saveFahipaySession(session.authId, session.sessionCookie)
|
store.saveFahipaySession(loginId, session.authId, session.sessionCookie)
|
||||||
store.saveFahipayUserProfile(
|
store.saveFahipayUserProfile(
|
||||||
|
loginId,
|
||||||
CredentialStore.FahipayUserProfile(
|
CredentialStore.FahipayUserProfile(
|
||||||
fullName = profile.fullName,
|
fullName = profile.fullName,
|
||||||
email = profile.email,
|
email = profile.email,
|
||||||
@@ -389,11 +397,11 @@ class CredentialsFragment : Fragment() {
|
|||||||
linkedAccounts = profile.linkedAccounts
|
linkedAccounts = profile.linkedAccounts
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AccountCache.saveFahipay(requireContext(), listOf(account))
|
AccountCache.saveFahipay(requireContext(), loginId, listOf(account))
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.fahipaySession = session
|
app.fahipaySessions[loginId] = session
|
||||||
app.fahipayAccounts = listOf(account)
|
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != loginTag } + listOf(account)
|
||||||
app.accounts = app.accounts + listOf(account)
|
app.accounts = app.accounts.filter { it.loginTag != loginTag } + listOf(account)
|
||||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
|||||||
view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, originalBottomPadding + navBar.bottom)
|
view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, originalBottomPadding + navBar.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.viewPager) { view, insets ->
|
||||||
|
val statusBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
view.setPadding(view.paddingLeft, statusBar.top, view.paddingRight, view.paddingBottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = OnboardingPagerAdapter(this)
|
val adapter = OnboardingPagerAdapter(this)
|
||||||
binding.viewPager.adapter = adapter
|
binding.viewPager.adapter = adapter
|
||||||
@@ -56,7 +61,7 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pre-select language button without triggering the listener
|
// Pre-select language button without triggering the listener
|
||||||
val savedLang = prefs.getString("language", null)
|
val savedLang = prefs.getString("language", null) ?: "en".also { selectLanguage(it) }
|
||||||
binding.languageToggle.clearOnButtonCheckedListeners()
|
binding.languageToggle.clearOnButtonCheckedListeners()
|
||||||
when (savedLang) {
|
when (savedLang) {
|
||||||
"en" -> binding.btnLangEnglish.isChecked = true
|
"en" -> binding.btnLangEnglish.isChecked = true
|
||||||
|
|||||||
@@ -9,16 +9,17 @@ object AccountCache {
|
|||||||
|
|
||||||
private const val PREFS = "account_cache"
|
private const val PREFS = "account_cache"
|
||||||
private const val KEY_MIB = "mib_accounts"
|
private const val KEY_MIB = "mib_accounts"
|
||||||
private const val KEY_FAHIPAY = "fahipay_accounts"
|
|
||||||
|
|
||||||
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
private fun bmlKey(loginId: String) = "bml_accounts_$loginId"
|
||||||
|
private fun fahipayKey(loginId: String) = "fahipay_accounts_$loginId"
|
||||||
|
|
||||||
fun save(context: Context, accounts: List<MibAccount>) {
|
fun save(context: Context, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
|
put("bank", acc.bank)
|
||||||
put("profileName", acc.profileName)
|
put("profileName", acc.profileName)
|
||||||
put("profileType", acc.profileType)
|
put("profileType", acc.profileType)
|
||||||
|
put("cifType", acc.cifType)
|
||||||
put("accountNumber", acc.accountNumber)
|
put("accountNumber", acc.accountNumber)
|
||||||
put("accountBriefName", acc.accountBriefName)
|
put("accountBriefName", acc.accountBriefName)
|
||||||
put("currencyName", acc.currencyName)
|
put("currencyName", acc.currencyName)
|
||||||
@@ -68,20 +69,21 @@ object AccountCache {
|
|||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibAccount(
|
MibAccount(
|
||||||
profileName = o.optString("profileName"),
|
bank = "BML",
|
||||||
profileType = o.optString("profileType"),
|
profileName = o.optString("profileName"),
|
||||||
accountNumber = o.optString("accountNumber"),
|
profileType = o.optString("profileType"),
|
||||||
|
accountNumber = o.optString("accountNumber"),
|
||||||
accountBriefName = o.optString("accountBriefName"),
|
accountBriefName = o.optString("accountBriefName"),
|
||||||
currencyName = o.optString("currencyName"),
|
currencyName = o.optString("currencyName"),
|
||||||
accountTypeName = o.optString("accountTypeName"),
|
accountTypeName = o.optString("accountTypeName"),
|
||||||
availableBalance = o.optString("availableBalance"),
|
availableBalance = o.optString("availableBalance"),
|
||||||
currentBalance = o.optString("currentBalance"),
|
currentBalance = o.optString("currentBalance"),
|
||||||
blockedAmount = o.optString("blockedAmount"),
|
blockedAmount = o.optString("blockedAmount"),
|
||||||
mvrBalance = o.optString("mvrBalance"),
|
mvrBalance = o.optString("mvrBalance"),
|
||||||
statusDesc = o.optString("statusDesc"),
|
statusDesc = o.optString("statusDesc"),
|
||||||
profileImageHash = null,
|
profileImageHash = null,
|
||||||
loginTag = o.optString("loginTag"),
|
loginTag = o.optString("loginTag"),
|
||||||
internalId = o.optString("internalId", "")
|
internalId = o.optString("internalId", "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
@@ -90,7 +92,7 @@ object AccountCache {
|
|||||||
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
|
fun loadBml(context: Context, loginIds: List<String>): List<MibAccount> =
|
||||||
loginIds.flatMap { loadBml(context, it) }
|
loginIds.flatMap { loadBml(context, it) }
|
||||||
|
|
||||||
fun saveFahipay(context: Context, accounts: List<MibAccount>) {
|
fun saveFahipay(context: Context, loginId: String, accounts: List<MibAccount>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
for (acc in accounts) {
|
for (acc in accounts) {
|
||||||
arr.put(JSONObject().apply {
|
arr.put(JSONObject().apply {
|
||||||
@@ -110,36 +112,40 @@ object AccountCache {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.edit().putString(KEY_FAHIPAY, CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(fahipayKey(loginId), CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipay(context: Context): List<MibAccount> {
|
fun loadFahipay(context: Context, loginId: String): List<MibAccount> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_FAHIPAY, null) ?: return emptyList()
|
.getString(fahipayKey(loginId), null) ?: return emptyList()
|
||||||
return try {
|
return try {
|
||||||
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
val arr = JSONArray(CacheEncryption.decrypt(raw))
|
||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibAccount(
|
MibAccount(
|
||||||
profileName = o.optString("profileName"),
|
bank = "FAHIPAY",
|
||||||
profileType = o.optString("profileType"),
|
profileName = o.optString("profileName"),
|
||||||
accountNumber = o.optString("accountNumber"),
|
profileType = o.optString("profileType"),
|
||||||
|
accountNumber = o.optString("accountNumber"),
|
||||||
accountBriefName = o.optString("accountBriefName"),
|
accountBriefName = o.optString("accountBriefName"),
|
||||||
currencyName = o.optString("currencyName"),
|
currencyName = o.optString("currencyName"),
|
||||||
accountTypeName = o.optString("accountTypeName"),
|
accountTypeName = o.optString("accountTypeName"),
|
||||||
availableBalance = o.optString("availableBalance"),
|
availableBalance = o.optString("availableBalance"),
|
||||||
currentBalance = o.optString("currentBalance"),
|
currentBalance = o.optString("currentBalance"),
|
||||||
blockedAmount = o.optString("blockedAmount"),
|
blockedAmount = o.optString("blockedAmount"),
|
||||||
mvrBalance = o.optString("mvrBalance"),
|
mvrBalance = o.optString("mvrBalance"),
|
||||||
statusDesc = o.optString("statusDesc"),
|
statusDesc = o.optString("statusDesc"),
|
||||||
profileImageHash = null,
|
profileImageHash = null,
|
||||||
loginTag = o.optString("loginTag"),
|
loginTag = o.optString("loginTag"),
|
||||||
internalId = o.optString("internalId", "")
|
internalId = o.optString("internalId", "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { emptyList() }
|
} catch (_: Exception) { emptyList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadFahipay(context: Context, loginIds: List<String>): List<MibAccount> =
|
||||||
|
loginIds.flatMap { loadFahipay(context, it) }
|
||||||
|
|
||||||
fun clear(context: Context) {
|
fun clear(context: Context) {
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
}
|
}
|
||||||
@@ -153,20 +159,22 @@ object AccountCache {
|
|||||||
(0 until arr.length()).map { i ->
|
(0 until arr.length()).map { i ->
|
||||||
val o = arr.getJSONObject(i)
|
val o = arr.getJSONObject(i)
|
||||||
MibAccount(
|
MibAccount(
|
||||||
profileName = o.optString("profileName"),
|
bank = o.optString("bank", "MIB"),
|
||||||
profileType = o.optString("profileType"),
|
profileName = o.optString("profileName"),
|
||||||
accountNumber = o.optString("accountNumber"),
|
profileType = o.optString("profileType"),
|
||||||
|
cifType = o.optString("cifType", ""),
|
||||||
|
accountNumber = o.optString("accountNumber"),
|
||||||
accountBriefName = o.optString("accountBriefName"),
|
accountBriefName = o.optString("accountBriefName"),
|
||||||
currencyName = o.optString("currencyName"),
|
currencyName = o.optString("currencyName"),
|
||||||
accountTypeName = o.optString("accountTypeName"),
|
accountTypeName = o.optString("accountTypeName"),
|
||||||
availableBalance = o.optString("availableBalance"),
|
availableBalance = o.optString("availableBalance"),
|
||||||
currentBalance = o.optString("currentBalance"),
|
currentBalance = o.optString("currentBalance"),
|
||||||
blockedAmount = o.optString("blockedAmount"),
|
blockedAmount = o.optString("blockedAmount"),
|
||||||
mvrBalance = o.optString("mvrBalance"),
|
mvrBalance = o.optString("mvrBalance"),
|
||||||
statusDesc = o.optString("statusDesc"),
|
statusDesc = o.optString("statusDesc"),
|
||||||
profileImageHash = o.optString("profileImageHash").takeIf { it.isNotBlank() },
|
profileImageHash = o.optString("profileImageHash").takeIf { it.isNotBlank() },
|
||||||
loginTag = o.optString("loginTag"),
|
loginTag = o.optString("loginTag"),
|
||||||
profileId = o.optString("profileId", "")
|
profileId = o.optString("profileId", "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard display model for the account history header card — produced by
|
||||||
|
* per-bank parsers, consumed by AccountHistoryAdapter with no bank logic.
|
||||||
|
*/
|
||||||
|
data class AccountHistoryDisplay(
|
||||||
|
val name: String,
|
||||||
|
val number: String,
|
||||||
|
val bankPill: String?, // "BML", "FP", null for MIB (no pill)
|
||||||
|
val typeLabel: String, // e.g. "Savings", "Current", "Visa Platinum"
|
||||||
|
val availableBalance: String, // formatted "CCY amount"
|
||||||
|
val workingBalance: String, // ledger/working balance — formatted "CCY amount"
|
||||||
|
val blockedBalance: String? // null if zero or not applicable
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.bmlapi.BmlHistoryParser
|
||||||
|
import sh.sar.basedbank.util.fahipayapi.FahipayHistoryParser
|
||||||
|
import sh.sar.basedbank.util.mibapi.MibHistoryParser
|
||||||
|
|
||||||
|
object AccountHistoryParser {
|
||||||
|
|
||||||
|
fun from(account: MibAccount): AccountHistoryDisplay? = when (account.bank) {
|
||||||
|
"BML" -> BmlHistoryParser.displayData(account)
|
||||||
|
"FAHIPAY" -> FahipayHistoryParser.displayData(account)
|
||||||
|
"MIB" -> MibHistoryParser.displayData(account)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
data class AccountListDisplay(
|
||||||
|
val name: String,
|
||||||
|
val number: String,
|
||||||
|
val typeLabel: String,
|
||||||
|
val balance: String,
|
||||||
|
val isCard: Boolean = false,
|
||||||
|
val cardBrandIcon: Int = 0, // drawable res, only meaningful if isCard
|
||||||
|
val statusLabel: String? = null // null = active; shown as status pill if set
|
||||||
|
)
|
||||||
16
app/src/main/java/sh/sar/basedbank/util/AccountListParser.kt
Normal file
16
app/src/main/java/sh/sar/basedbank/util/AccountListParser.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.bmlapi.BmlDashboardParser
|
||||||
|
import sh.sar.basedbank.util.fahipayapi.FahipayAccountParser
|
||||||
|
import sh.sar.basedbank.util.mibapi.MibAccountParser
|
||||||
|
|
||||||
|
object AccountListParser {
|
||||||
|
|
||||||
|
fun from(account: MibAccount): AccountListDisplay? = when (account.bank) {
|
||||||
|
"BML" -> BmlDashboardParser.displayData(account)
|
||||||
|
"FAHIPAY" -> FahipayAccountParser.displayData(account)
|
||||||
|
"MIB" -> MibAccountParser.displayData(account)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package sh.sar.basedbank.util
|
|
||||||
|
|
||||||
object BmlDashboardParser {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a display-ready product label for a BML dashboard account or card.
|
|
||||||
* Known BML product names are mapped to short friendly labels.
|
|
||||||
* Everything else is title-cased (first letter of each word capitalised).
|
|
||||||
*/
|
|
||||||
fun productLabel(raw: String): String {
|
|
||||||
val u = raw.trim().uppercase()
|
|
||||||
return when {
|
|
||||||
u == "SAVINGS ACCOUNT" -> "Savings"
|
|
||||||
u == "CURRENT ACCOUNT" ||
|
|
||||||
u == "CURRENT ACCOUNT(PERSONAL)" ||
|
|
||||||
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
|
|
||||||
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
|
|
||||||
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
|
|
||||||
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
|
|
||||||
else -> toTitleCase(raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toTitleCase(input: String): String =
|
|
||||||
input.trim().lowercase().split(" ").joinToString(" ") { word ->
|
|
||||||
word.replaceFirstChar { it.uppercaseChar() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
app/src/main/java/sh/sar/basedbank/util/ContactDisplay.kt
Normal file
22
app/src/main/java/sh/sar/basedbank/util/ContactDisplay.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard display model for a contact row — produced by per-bank parsers,
|
||||||
|
* consumed by UI. No bank-specific logic in adapters or fragments.
|
||||||
|
*/
|
||||||
|
data class ContactDisplay(
|
||||||
|
val id: String, // internal contact ID (benefNo)
|
||||||
|
val name: String, // display nickname
|
||||||
|
val realName: String, // legal name — used for search
|
||||||
|
val accountNumber: String,
|
||||||
|
val categoryId: String, // for tab filtering
|
||||||
|
val network: TransferNetwork,
|
||||||
|
val bankColor: String,
|
||||||
|
val detail: String?, // pre-formatted "Name · CCY · Bank" line; null = hide row
|
||||||
|
val imageHash: String?,
|
||||||
|
val profileId: String, // MIB profile ID or BML loginTag (needed by ContactManager)
|
||||||
|
val transferSubtitle: String, // "Bank · accountNumber" shown in transfer screen
|
||||||
|
val canTransfer: Boolean,
|
||||||
|
val canEdit: Boolean,
|
||||||
|
val canDelete: Boolean
|
||||||
|
)
|
||||||
18
app/src/main/java/sh/sar/basedbank/util/ContactListParser.kt
Normal file
18
app/src/main/java/sh/sar/basedbank/util/ContactListParser.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.util.bmlapi.BmlContactParser
|
||||||
|
import sh.sar.basedbank.util.fahipayapi.FahipayContactParser
|
||||||
|
import sh.sar.basedbank.util.mibapi.MibContactParser
|
||||||
|
|
||||||
|
object ContactListParser {
|
||||||
|
|
||||||
|
fun from(contact: MibBeneficiary): ContactDisplay? = when {
|
||||||
|
contact.benefCategoryId == "BML" -> BmlContactParser.displayData(contact)
|
||||||
|
contact.benefType == "FAHIPAY" -> FahipayContactParser.displayData(contact)
|
||||||
|
contact.benefType in setOf("I", "L", "S") -> MibContactParser.displayData(contact)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromList(contacts: List<MibBeneficiary>): List<ContactDisplay> = contacts.mapNotNull { from(it) }
|
||||||
|
}
|
||||||
40
app/src/main/java/sh/sar/basedbank/util/ContactManager.kt
Normal file
40
app/src/main/java/sh/sar/basedbank/util/ContactManager.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import sh.sar.basedbank.BasedBankApp
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behaviour dispatcher for contact operations.
|
||||||
|
* Routes add/delete to the correct bank API based on TransferNetwork.
|
||||||
|
* UI code never inspects the network or bank type directly.
|
||||||
|
*/
|
||||||
|
object ContactManager {
|
||||||
|
|
||||||
|
/** Deletes [contact] via the appropriate bank API. Returns true on success. */
|
||||||
|
suspend fun delete(contact: ContactDisplay, app: BasedBankApp): Boolean = when (contact.network) {
|
||||||
|
TransferNetwork.BML -> deleteBml(contact, app)
|
||||||
|
TransferNetwork.FAHIPAY -> false // Fahipay contacts are read-only
|
||||||
|
TransferNetwork.MIB, TransferNetwork.LOCAL, TransferNetwork.SWIFT -> deleteMib(contact, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteBml(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
||||||
|
val sess = app.bmlSessions[contact.profileId] ?: app.anyBmlSession() ?: return false
|
||||||
|
val contactId = contact.id.removePrefix("bml_")
|
||||||
|
return try { BmlLoginFlow().deleteContact(sess, contactId) } catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteMib(contact: ContactDisplay, app: BasedBankApp): Boolean {
|
||||||
|
val sess = app.anyMibSession() ?: return false
|
||||||
|
return try {
|
||||||
|
if (contact.profileId.isNotBlank()) {
|
||||||
|
val (loginId, profile) = app.mibProfilesMap.entries
|
||||||
|
.firstNotNullOfOrNull { (id, profiles) ->
|
||||||
|
profiles.firstOrNull { it.profileId == contact.profileId }?.let { id to it }
|
||||||
|
} ?: (null to null)
|
||||||
|
if (profile != null && loginId != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
||||||
|
}
|
||||||
|
MibContactsClient().deleteContact(sess, contact.id)
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,75 +21,131 @@ class CredentialStore(context: Context) {
|
|||||||
data class BmlCredentials(val username: String, val password: String, val otpSeed: String)
|
data class BmlCredentials(val username: String, val password: String, val otpSeed: String)
|
||||||
data class FahipayCredentials(val idCard: String, val password: String)
|
data class FahipayCredentials(val idCard: String, val password: String)
|
||||||
|
|
||||||
// ── MIB login credentials ─────────────────────────────────────────────────
|
// ── MIB login credentials (multi-login, keyed by loginId = username) ─────
|
||||||
|
|
||||||
fun hasMibCredentials(): Boolean = prefs.contains("mib_enc_username")
|
fun getMibLoginIds(): List<String> {
|
||||||
fun hasFahipayCredentials(): Boolean = prefs.contains("fahipay_enc_id_card")
|
maybeMigrateLegacyMib()
|
||||||
|
val json = prefs.getString("mib_login_ids", null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = org.json.JSONArray(json)
|
||||||
|
(0 until arr.length()).map { arr.getString(it) }
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
fun saveMibCredentials(username: String, passwordHash: String, otpSeed: String) {
|
fun hasMibCredentials(): Boolean = getMibLoginIds().isNotEmpty()
|
||||||
|
|
||||||
|
private fun addMibLoginId(loginId: String) {
|
||||||
|
val ids = getMibLoginIds().toMutableList()
|
||||||
|
if (loginId !in ids) {
|
||||||
|
ids.add(loginId)
|
||||||
|
prefs.edit().putString("mib_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeMibLoginId(loginId: String) {
|
||||||
|
val ids = getMibLoginIds().toMutableList()
|
||||||
|
if (ids.remove(loginId))
|
||||||
|
prefs.edit().putString("mib_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMibCredentials(loginId: String, username: String, passwordHash: String, otpSeed: String) {
|
||||||
|
addMibLoginId(loginId)
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("mib_enc_username", encrypt(username, key))
|
.putString("mib_${loginId}_enc_password_hash", encrypt(passwordHash, key))
|
||||||
.putString("mib_enc_password_hash", encrypt(passwordHash, key))
|
.putString("mib_${loginId}_enc_otp_seed", encrypt(otpSeed, key))
|
||||||
.putString("mib_enc_otp_seed", encrypt(otpSeed, key))
|
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibCredentials(): MibCredentials? {
|
fun loadMibCredentials(loginId: String): MibCredentials? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encUsername = prefs.getString("mib_enc_username", null) ?: return null
|
val encHash = prefs.getString("mib_${loginId}_enc_password_hash", null) ?: return null
|
||||||
val encHash = prefs.getString("mib_enc_password_hash", null) ?: return null
|
val encSeed = prefs.getString("mib_${loginId}_enc_otp_seed", null) ?: return null
|
||||||
val encSeed = prefs.getString("mib_enc_otp_seed", null) ?: return null
|
|
||||||
return try {
|
return try {
|
||||||
MibCredentials(
|
MibCredentials(loginId, decrypt(encHash, key), decrypt(encSeed, key))
|
||||||
decrypt(encUsername, key),
|
|
||||||
decrypt(encHash, key),
|
|
||||||
decrypt(encSeed, key)
|
|
||||||
)
|
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearMibCredentials() {
|
fun clearMibCredentials(loginId: String) {
|
||||||
|
removeMibLoginId(loginId)
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove("mib_enc_username")
|
.remove("mib_${loginId}_enc_password_hash")
|
||||||
.remove("mib_enc_password_hash")
|
.remove("mib_${loginId}_enc_otp_seed")
|
||||||
.remove("mib_enc_otp_seed")
|
.remove("mib_${loginId}_enc_key1")
|
||||||
.remove("mib_enc_key1")
|
.remove("mib_${loginId}_enc_key2")
|
||||||
.remove("mib_enc_key2")
|
.remove("mib_${loginId}_enc_app_id")
|
||||||
.remove("mib_enc_app_id")
|
.remove("mib_${loginId}_all_profiles")
|
||||||
|
.remove("mib_${loginId}_enc_profile")
|
||||||
|
.remove("mib_${loginId}_enc_full_name")
|
||||||
|
.remove("mib_${loginId}_hidden_profile_ids")
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── MIB session keys (key1/key2) and app ID ───────────────────────────────
|
// ── MIB session keys (key1/key2) and app ID (per loginId) ────────────────
|
||||||
|
|
||||||
fun saveMibKeys(key1: String, key2: String) {
|
fun saveMibKeys(loginId: String, key1: String, key2: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("mib_enc_key1", encrypt(key1, key))
|
.putString("mib_${loginId}_enc_key1", encrypt(key1, key))
|
||||||
.putString("mib_enc_key2", encrypt(key2, key))
|
.putString("mib_${loginId}_enc_key2", encrypt(key2, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibKeys(): Pair<String, String>? {
|
fun loadMibKeys(loginId: String): Pair<String, String>? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encKey1 = prefs.getString("mib_enc_key1", null) ?: return null
|
val encKey1 = prefs.getString("mib_${loginId}_enc_key1", null) ?: return null
|
||||||
val encKey2 = prefs.getString("mib_enc_key2", null) ?: return null
|
val encKey2 = prefs.getString("mib_${loginId}_enc_key2", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
Pair(decrypt(encKey1, key), decrypt(encKey2, key))
|
Pair(decrypt(encKey1, key), decrypt(encKey2, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveMibAppId(id: String) {
|
fun saveMibAppId(loginId: String, id: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("mib_enc_app_id", encrypt(id, key)).apply()
|
prefs.edit().putString("mib_${loginId}_enc_app_id", encrypt(id, key)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibAppId(): String? {
|
fun loadMibAppId(loginId: String): String? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("mib_enc_app_id", null) ?: return null
|
val enc = prefs.getString("mib_${loginId}_enc_app_id", null) ?: return null
|
||||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One-time migration: if old single-login MIB data exists, move it to per-loginId storage. */
|
||||||
|
private var migrationChecked = false
|
||||||
|
private fun maybeMigrateLegacyMib() {
|
||||||
|
if (migrationChecked) return
|
||||||
|
migrationChecked = true
|
||||||
|
if (prefs.contains("mib_login_ids")) return // already migrated
|
||||||
|
val encUsername = prefs.getString("mib_enc_username", null) ?: return
|
||||||
|
val key = try { getOrCreateKey() } catch (_: Exception) { return }
|
||||||
|
val loginId = try { decrypt(encUsername, key) } catch (_: Exception) { return }
|
||||||
|
val editor = prefs.edit()
|
||||||
|
// Migrate credentials
|
||||||
|
prefs.getString("mib_enc_password_hash", null)?.let { editor.putString("mib_${loginId}_enc_password_hash", it) }
|
||||||
|
prefs.getString("mib_enc_otp_seed", null)?.let { editor.putString("mib_${loginId}_enc_otp_seed", it) }
|
||||||
|
prefs.getString("mib_enc_key1", null)?.let { editor.putString("mib_${loginId}_enc_key1", it) }
|
||||||
|
prefs.getString("mib_enc_key2", null)?.let { editor.putString("mib_${loginId}_enc_key2", it) }
|
||||||
|
prefs.getString("mib_enc_app_id", null)?.let { editor.putString("mib_${loginId}_enc_app_id", it) }
|
||||||
|
prefs.getString("mib_all_profiles", null)?.let { editor.putString("mib_${loginId}_all_profiles", it) }
|
||||||
|
prefs.getString("mib_enc_profile", null)?.let { editor.putString("mib_${loginId}_enc_profile", it) }
|
||||||
|
prefs.getString("mib_enc_full_name", null)?.let { editor.putString("mib_${loginId}_enc_full_name", it) }
|
||||||
|
prefs.getStringSet("mib_hidden_profile_ids", null)?.let { editor.putStringSet("mib_${loginId}_hidden_profile_ids", it) }
|
||||||
|
// Register the login ID and clear legacy keys
|
||||||
|
editor.putString("mib_login_ids", org.json.JSONArray(listOf(loginId)).toString())
|
||||||
|
editor.remove("mib_enc_username")
|
||||||
|
editor.remove("mib_enc_password_hash")
|
||||||
|
editor.remove("mib_enc_otp_seed")
|
||||||
|
editor.remove("mib_enc_key1")
|
||||||
|
editor.remove("mib_enc_key2")
|
||||||
|
editor.remove("mib_enc_app_id")
|
||||||
|
editor.remove("mib_all_profiles")
|
||||||
|
editor.remove("mib_enc_profile")
|
||||||
|
editor.remove("mib_enc_full_name")
|
||||||
|
editor.remove("mib_hidden_profile_ids")
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
|
// ── BML login credentials (multi-login, keyed by loginId = username) ────────
|
||||||
|
|
||||||
fun getBmlLoginIds(): List<String> {
|
fun getBmlLoginIds(): List<String> {
|
||||||
@@ -171,58 +227,81 @@ class CredentialStore(context: Context) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fahipay login credentials ─────────────────────────────────────────────
|
// ── Fahipay login credentials (multi-login, keyed by loginId = profileId) ──
|
||||||
|
|
||||||
fun saveFahipayCredentials(idCard: String, password: String) {
|
fun getFahipayLoginIds(): List<String> {
|
||||||
|
maybeMigrateLegacyFahipay()
|
||||||
|
val json = prefs.getString("fahipay_login_ids", null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = org.json.JSONArray(json)
|
||||||
|
(0 until arr.length()).map { arr.getString(it) }
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasFahipayCredentials(): Boolean = getFahipayLoginIds().isNotEmpty()
|
||||||
|
|
||||||
|
private fun addFahipayLoginId(loginId: String) {
|
||||||
|
val ids = getFahipayLoginIds().toMutableList()
|
||||||
|
if (loginId !in ids) {
|
||||||
|
ids.add(loginId)
|
||||||
|
prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeFahipayLoginId(loginId: String) {
|
||||||
|
val ids = getFahipayLoginIds().toMutableList()
|
||||||
|
if (ids.remove(loginId))
|
||||||
|
prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(ids).toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveFahipayCredentials(loginId: String, idCard: String, password: String) {
|
||||||
|
addFahipayLoginId(loginId)
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("fahipay_enc_id_card", encrypt(idCard, key))
|
.putString("fahipay_${loginId}_enc_id_card", encrypt(idCard, key))
|
||||||
.putString("fahipay_enc_password", encrypt(password, key))
|
.putString("fahipay_${loginId}_enc_password", encrypt(password, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipayCredentials(): FahipayCredentials? {
|
fun loadFahipayCredentials(loginId: String): FahipayCredentials? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encId = prefs.getString("fahipay_enc_id_card", null) ?: return null
|
val encId = prefs.getString("fahipay_${loginId}_enc_id_card", null) ?: return null
|
||||||
val encPw = prefs.getString("fahipay_enc_password", null) ?: return null
|
val encPw = prefs.getString("fahipay_${loginId}_enc_password", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
FahipayCredentials(decrypt(encId, key), decrypt(encPw, key))
|
FahipayCredentials(decrypt(encId, key), decrypt(encPw, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFahipayCredentials() {
|
fun clearFahipayCredentials(loginId: String) {
|
||||||
|
removeFahipayLoginId(loginId)
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove("fahipay_enc_id_card")
|
.remove("fahipay_${loginId}_enc_id_card")
|
||||||
.remove("fahipay_enc_password")
|
.remove("fahipay_${loginId}_enc_password")
|
||||||
|
.remove("fahipay_${loginId}_enc_auth_id")
|
||||||
|
.remove("fahipay_${loginId}_enc_session_cookie")
|
||||||
|
.remove("fahipay_${loginId}_enc_profile")
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fahipay session (authId + __Secure-sess cookie) ───────────────────────
|
// ── Fahipay session (authId + __Secure-sess cookie) (per loginId) ─────────
|
||||||
|
|
||||||
fun saveFahipaySession(authId: String, sessionCookie: String) {
|
fun saveFahipaySession(loginId: String, authId: String, sessionCookie: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("fahipay_enc_auth_id", encrypt(authId, key))
|
.putString("fahipay_${loginId}_enc_auth_id", encrypt(authId, key))
|
||||||
.putString("fahipay_enc_session_cookie", encrypt(sessionCookie, key))
|
.putString("fahipay_${loginId}_enc_session_cookie", encrypt(sessionCookie, key))
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipaySession(): Pair<String, String>? {
|
fun loadFahipaySession(loginId: String): Pair<String, String>? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val encAuth = prefs.getString("fahipay_enc_auth_id", null) ?: return null
|
val encAuth = prefs.getString("fahipay_${loginId}_enc_auth_id", null) ?: return null
|
||||||
val encCookie = prefs.getString("fahipay_enc_session_cookie", null) ?: return null
|
val encCookie = prefs.getString("fahipay_${loginId}_enc_session_cookie", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
Pair(decrypt(encAuth, key), decrypt(encCookie, key))
|
Pair(decrypt(encAuth, key), decrypt(encCookie, key))
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFahipaySession() {
|
|
||||||
prefs.edit()
|
|
||||||
.remove("fahipay_enc_auth_id")
|
|
||||||
.remove("fahipay_enc_session_cookie")
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Fahipay device UUID (generated once, shared across all Fahipay accounts) ─
|
// ── Fahipay device UUID (generated once, shared across all Fahipay accounts) ─
|
||||||
|
|
||||||
fun getOrCreateFahipayDeviceUuid(): String {
|
fun getOrCreateFahipayDeviceUuid(): String {
|
||||||
@@ -236,7 +315,7 @@ class CredentialStore(context: Context) {
|
|||||||
return uuid
|
return uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fahipay user profile ──────────────────────────────────────────────────
|
// ── Fahipay user profile (per loginId) ────────────────────────────────────
|
||||||
|
|
||||||
data class FahipayUserProfile(
|
data class FahipayUserProfile(
|
||||||
val fullName: String,
|
val fullName: String,
|
||||||
@@ -248,7 +327,7 @@ class CredentialStore(context: Context) {
|
|||||||
val linkedAccounts: String
|
val linkedAccounts: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun saveFahipayUserProfile(p: FahipayUserProfile) {
|
fun saveFahipayUserProfile(loginId: String, p: FahipayUserProfile) {
|
||||||
val json = org.json.JSONObject().apply {
|
val json = org.json.JSONObject().apply {
|
||||||
put("fullName", p.fullName)
|
put("fullName", p.fullName)
|
||||||
put("email", p.email)
|
put("email", p.email)
|
||||||
@@ -259,12 +338,12 @@ class CredentialStore(context: Context) {
|
|||||||
put("linkedAccounts", p.linkedAccounts)
|
put("linkedAccounts", p.linkedAccounts)
|
||||||
}.toString()
|
}.toString()
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("fahipay_enc_profile", encrypt(json, key)).apply()
|
prefs.edit().putString("fahipay_${loginId}_enc_profile", encrypt(json, key)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFahipayUserProfile(): FahipayUserProfile? {
|
fun loadFahipayUserProfile(loginId: String): FahipayUserProfile? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("fahipay_enc_profile", null) ?: return null
|
val enc = prefs.getString("fahipay_${loginId}_enc_profile", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
val o = org.json.JSONObject(decrypt(enc, key))
|
val o = org.json.JSONObject(decrypt(enc, key))
|
||||||
FahipayUserProfile(
|
FahipayUserProfile(
|
||||||
@@ -279,6 +358,33 @@ class CredentialStore(context: Context) {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One-time migration: if old single-login Fahipay data exists, move it to per-loginId storage. */
|
||||||
|
private var fahipayMigrationChecked = false
|
||||||
|
private fun maybeMigrateLegacyFahipay() {
|
||||||
|
if (fahipayMigrationChecked) return
|
||||||
|
fahipayMigrationChecked = true
|
||||||
|
if (prefs.contains("fahipay_login_ids")) return // already migrated
|
||||||
|
val encProfile = prefs.getString("fahipay_enc_profile", null) ?: return
|
||||||
|
val key = try { getOrCreateKey() } catch (_: Exception) { return }
|
||||||
|
val loginId = try {
|
||||||
|
val o = org.json.JSONObject(decrypt(encProfile, key))
|
||||||
|
o.optString("profileId").takeIf { it.isNotBlank() }
|
||||||
|
} catch (_: Exception) { null } ?: return
|
||||||
|
val editor = prefs.edit()
|
||||||
|
prefs.getString("fahipay_enc_id_card", null)?.let { editor.putString("fahipay_${loginId}_enc_id_card", it) }
|
||||||
|
prefs.getString("fahipay_enc_password", null)?.let { editor.putString("fahipay_${loginId}_enc_password", it) }
|
||||||
|
prefs.getString("fahipay_enc_auth_id", null)?.let { editor.putString("fahipay_${loginId}_enc_auth_id", it) }
|
||||||
|
prefs.getString("fahipay_enc_session_cookie", null)?.let { editor.putString("fahipay_${loginId}_enc_session_cookie", it) }
|
||||||
|
editor.putString("fahipay_${loginId}_enc_profile", encProfile)
|
||||||
|
editor.putString("fahipay_login_ids", org.json.JSONArray(listOf(loginId)).toString())
|
||||||
|
editor.remove("fahipay_enc_id_card")
|
||||||
|
editor.remove("fahipay_enc_password")
|
||||||
|
editor.remove("fahipay_enc_auth_id")
|
||||||
|
editor.remove("fahipay_enc_session_cookie")
|
||||||
|
editor.remove("fahipay_enc_profile")
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Security credential (PIN / pattern hash) ──────────────────────────────
|
// ── Security credential (PIN / pattern hash) ──────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -329,19 +435,55 @@ class CredentialStore(context: Context) {
|
|||||||
val birthdate: String
|
val birthdate: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun saveMibFullName(name: String) {
|
// ── MIB operating profiles (per loginId) ─────────────────────────────────
|
||||||
val key = getOrCreateKey()
|
|
||||||
prefs.edit().putString("mib_enc_full_name", encrypt(name, key)).apply()
|
fun saveMibProfiles(loginId: String, profiles: List<sh.sar.basedbank.api.mib.MibProfile>) {
|
||||||
|
val arr = org.json.JSONArray()
|
||||||
|
for (p in profiles) {
|
||||||
|
arr.put(org.json.JSONObject().apply {
|
||||||
|
put("profileId", p.profileId)
|
||||||
|
put("name", p.name)
|
||||||
|
put("cifType", p.cifType)
|
||||||
|
put("profileType", p.profileType)
|
||||||
|
put("color", p.color)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
prefs.edit().putString("mib_${loginId}_all_profiles", arr.toString()).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibFullName(): String? {
|
fun loadMibProfiles(loginId: String): List<sh.sar.basedbank.api.mib.MibProfile> {
|
||||||
|
val raw = prefs.getString("mib_${loginId}_all_profiles", null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = org.json.JSONArray(raw)
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
sh.sar.basedbank.api.mib.MibProfile(
|
||||||
|
profileId = o.optString("profileId"),
|
||||||
|
customerProfileId = o.optString("profileId"),
|
||||||
|
annexId = "",
|
||||||
|
customerId = "",
|
||||||
|
name = o.optString("name"),
|
||||||
|
cifType = o.optString("cifType"),
|
||||||
|
profileType = o.optString("profileType"),
|
||||||
|
color = o.optString("color"),
|
||||||
|
customerImage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMibFullName(loginId: String, name: String) {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("mib_enc_full_name", null) ?: return null
|
prefs.edit().putString("mib_${loginId}_enc_full_name", encrypt(name, key)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMibFullName(loginId: String): String? {
|
||||||
|
val key = getOrCreateKey()
|
||||||
|
val enc = prefs.getString("mib_${loginId}_enc_full_name", null) ?: return null
|
||||||
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
return try { decrypt(enc, key) } catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveMibUserProfile(loginId: String, p: MibUserProfile) {
|
||||||
fun saveMibUserProfile(p: MibUserProfile) {
|
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("fullName", p.fullName)
|
put("fullName", p.fullName)
|
||||||
put("username", p.username)
|
put("username", p.username)
|
||||||
@@ -350,14 +492,13 @@ class CredentialStore(context: Context) {
|
|||||||
put("enrolled", p.enrolled)
|
put("enrolled", p.enrolled)
|
||||||
}.toString()
|
}.toString()
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
prefs.edit().putString("mib_enc_profile", encrypt(json, key)).apply()
|
prefs.edit().putString("mib_${loginId}_enc_profile", encrypt(json, key)).apply()
|
||||||
// Keep the name in sync with the fast-path field
|
prefs.edit().putString("mib_${loginId}_enc_full_name", encrypt(p.fullName, key)).apply()
|
||||||
prefs.edit().putString("mib_enc_full_name", encrypt(p.fullName, key)).apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMibUserProfile(): MibUserProfile? {
|
fun loadMibUserProfile(loginId: String): MibUserProfile? {
|
||||||
val key = getOrCreateKey()
|
val key = getOrCreateKey()
|
||||||
val enc = prefs.getString("mib_enc_profile", null) ?: return null
|
val enc = prefs.getString("mib_${loginId}_enc_profile", null) ?: return null
|
||||||
return try {
|
return try {
|
||||||
val o = JSONObject(decrypt(enc, key))
|
val o = JSONObject(decrypt(enc, key))
|
||||||
MibUserProfile(
|
MibUserProfile(
|
||||||
@@ -399,6 +540,15 @@ class CredentialStore(context: Context) {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── MIB profile visibility (per loginId) ─────────────────────────────────
|
||||||
|
|
||||||
|
/** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */
|
||||||
|
fun getHiddenMibProfileIds(loginId: String): Set<String> =
|
||||||
|
prefs.getStringSet("mib_${loginId}_hidden_profile_ids", emptySet()) ?: emptySet()
|
||||||
|
|
||||||
|
fun setHiddenMibProfileIds(loginId: String, ids: Set<String>) =
|
||||||
|
prefs.edit().putStringSet("mib_${loginId}_hidden_profile_ids", ids).apply()
|
||||||
|
|
||||||
// ── Crypto primitives ─────────────────────────────────────────────────────
|
// ── Crypto primitives ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun getOrCreateKey(): SecretKey {
|
private fun getOrCreateKey(): SecretKey {
|
||||||
|
|||||||
117
app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt
Normal file
117
app/src/main/java/sh/sar/basedbank/util/HistoryFetcher.kt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import sh.sar.basedbank.BasedBankApp
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
|
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.api.mib.MibHistoryClient
|
||||||
|
import sh.sar.basedbank.api.mib.Transaction
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates all bank-specific pagination state and fetch logic for account history.
|
||||||
|
* The fragment holds one instance per account and calls [hasMore] / [fetchNextPage]
|
||||||
|
* without knowing which bank it is talking to.
|
||||||
|
*/
|
||||||
|
class HistoryFetcher(private val account: MibAccount) {
|
||||||
|
|
||||||
|
private val isMib get() = account.bank == "MIB"
|
||||||
|
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
|
private val isFahipay get() = account.bank == "FAHIPAY"
|
||||||
|
|
||||||
|
// MIB pagination
|
||||||
|
private var mibNextStart = 1
|
||||||
|
private var mibTotalCount = -1
|
||||||
|
|
||||||
|
// BML CASA pagination
|
||||||
|
private var bmlNextPage = 1
|
||||||
|
private var bmlTotalPages = -1
|
||||||
|
|
||||||
|
// BML card pagination (month-based)
|
||||||
|
private var cardMonthOffset = 0
|
||||||
|
|
||||||
|
// Fahipay pagination
|
||||||
|
private var fahipayNextStart = 0
|
||||||
|
private var fahipayTotal = -1
|
||||||
|
|
||||||
|
fun hasMore(): Boolean = when {
|
||||||
|
isFahipay -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||||
|
isMib -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||||
|
isBmlCard -> cardMonthOffset < 3
|
||||||
|
else -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List<Transaction> = when {
|
||||||
|
isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) }
|
||||||
|
isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } }
|
||||||
|
isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) }
|
||||||
|
else -> withContext(Dispatchers.IO) { fetchBmlCasa(app) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchFahipay(app: BasedBankApp): List<Transaction> {
|
||||||
|
val session = app.fahipaySessionFor(account) ?: return emptyList()
|
||||||
|
val flow = FahipayLoginFlow()
|
||||||
|
flow.setSessionCookie(session.sessionCookie)
|
||||||
|
val (list, total) = flow.fetchHistory(
|
||||||
|
session = session,
|
||||||
|
accountDisplayName = account.accountBriefName,
|
||||||
|
accountNumber = account.accountNumber,
|
||||||
|
start = fahipayNextStart
|
||||||
|
)
|
||||||
|
if (total > 0) fahipayTotal = total
|
||||||
|
fahipayNextStart += list.size
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchMib(app: BasedBankApp, pageSize: Int): List<Transaction> {
|
||||||
|
val loginId = account.loginTag.removePrefix("mib_")
|
||||||
|
val session = app.mibSessions[loginId] ?: return emptyList()
|
||||||
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
|
val profile = profiles.firstOrNull { it.profileId == account.profileId }
|
||||||
|
if (profile != null) app.mibFlowFor(loginId).switchProfile(session, profile)
|
||||||
|
val (list, total) = MibHistoryClient().fetchHistory(
|
||||||
|
session = session,
|
||||||
|
accountNo = account.accountNumber,
|
||||||
|
accountDisplayName = account.accountBriefName,
|
||||||
|
start = mibNextStart,
|
||||||
|
pageSize = pageSize
|
||||||
|
)
|
||||||
|
if (total > 0) mibTotalCount = total
|
||||||
|
mibNextStart += list.size.coerceAtLeast(pageSize)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchBmlCard(app: BasedBankApp): List<Transaction> {
|
||||||
|
val session = app.bmlSessionFor(account) ?: return emptyList()
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
cal.add(Calendar.MONTH, -cardMonthOffset)
|
||||||
|
val month = SimpleDateFormat("yyyyMM", Locale.US).format(cal.time)
|
||||||
|
cardMonthOffset++
|
||||||
|
return BmlLoginFlow().fetchCardHistory(
|
||||||
|
session = session,
|
||||||
|
cardId = account.internalId,
|
||||||
|
accountDisplayName = account.accountBriefName,
|
||||||
|
accountNumber = account.accountNumber,
|
||||||
|
month = month
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchBmlCasa(app: BasedBankApp): List<Transaction> {
|
||||||
|
val session = app.bmlSessionFor(account) ?: return emptyList()
|
||||||
|
val (list, totalPages) = BmlLoginFlow().fetchAccountHistory(
|
||||||
|
session = session,
|
||||||
|
accountId = account.internalId,
|
||||||
|
accountDisplayName = account.accountBriefName,
|
||||||
|
accountNumber = account.accountNumber,
|
||||||
|
page = bmlNextPage
|
||||||
|
)
|
||||||
|
if (totalPages > 0) bmlTotalPages = totalPages
|
||||||
|
bmlNextPage++
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package sh.sar.basedbank.util
|
|
||||||
|
|
||||||
object MibAccountParser {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a display-ready product label for a MIB (Faisanet) account type name.
|
|
||||||
* Known MIB accountTypeName values are mapped to short friendly labels.
|
|
||||||
* Everything else is returned trimmed as-is.
|
|
||||||
*/
|
|
||||||
fun productLabel(raw: String): String {
|
|
||||||
val u = raw.trim().uppercase()
|
|
||||||
return when {
|
|
||||||
u == "SAVING ACCOUNT" -> "Savings"
|
|
||||||
u == "CURRENT ACCOUNT" -> "Current"
|
|
||||||
else -> raw.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
app/src/main/java/sh/sar/basedbank/util/TransferNetwork.kt
Normal file
10
app/src/main/java/sh/sar/basedbank/util/TransferNetwork.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
/** App-unified term for the transfer routing network a contact uses. */
|
||||||
|
enum class TransferNetwork {
|
||||||
|
MIB, // MIB internal (both parties on MIB)
|
||||||
|
LOCAL, // local inter-bank via IPS (e.g. BML from MIB's side)
|
||||||
|
SWIFT, // international SWIFT
|
||||||
|
BML, // Bank of Maldives
|
||||||
|
FAHIPAY // Fahipay wallet
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package sh.sar.basedbank.util.bmlapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
|
import sh.sar.basedbank.util.TransferNetwork
|
||||||
|
|
||||||
|
object BmlContactParser {
|
||||||
|
|
||||||
|
fun displayData(contact: MibBeneficiary) = ContactDisplay(
|
||||||
|
id = contact.benefNo,
|
||||||
|
name = contact.benefNickName,
|
||||||
|
realName = contact.benefName,
|
||||||
|
accountNumber = contact.benefAccount,
|
||||||
|
categoryId = contact.benefCategoryId,
|
||||||
|
network = TransferNetwork.BML,
|
||||||
|
bankColor = contact.bankColor,
|
||||||
|
detail = "${contact.benefName} · ${contact.transferCyDesc} · ${contact.benefBankName}",
|
||||||
|
imageHash = contact.customerImgHash,
|
||||||
|
profileId = contact.profileId,
|
||||||
|
transferSubtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||||
|
canTransfer = true,
|
||||||
|
canEdit = true,
|
||||||
|
canDelete = true
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package sh.sar.basedbank.util.bmlapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.AccountListDisplay
|
||||||
|
|
||||||
|
object BmlDashboardParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all display fields for an account/card row in the accounts list.
|
||||||
|
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
||||||
|
*/
|
||||||
|
fun displayData(account: MibAccount): AccountListDisplay {
|
||||||
|
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
|
return if (isCard) {
|
||||||
|
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
||||||
|
AccountListDisplay(
|
||||||
|
name = account.accountBriefName,
|
||||||
|
number = account.accountNumber,
|
||||||
|
typeLabel = productLabel(account.accountTypeName),
|
||||||
|
balance = "${account.currencyName} ${account.availableBalance}",
|
||||||
|
isCard = true,
|
||||||
|
cardBrandIcon = cardBrandIcon(account.accountTypeName),
|
||||||
|
statusLabel = if (isActive) null else account.statusDesc
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AccountListDisplay(
|
||||||
|
name = account.accountBriefName,
|
||||||
|
number = account.accountNumber,
|
||||||
|
typeLabel = productLabel(account.accountTypeName),
|
||||||
|
balance = listBalance(account)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a display-ready product label for a BML dashboard account or card.
|
||||||
|
*/
|
||||||
|
fun productLabel(raw: String): String {
|
||||||
|
val u = raw.trim().uppercase()
|
||||||
|
return when {
|
||||||
|
u == "SAVINGS ACCOUNT" -> "Savings"
|
||||||
|
u == "CURRENT ACCOUNT" ||
|
||||||
|
u == "CURRENT ACCOUNT(PERSONAL)" ||
|
||||||
|
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
|
||||||
|
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
|
||||||
|
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
|
||||||
|
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
|
||||||
|
else -> toTitleCase(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Balance shown in the accounts list — ledger (working) balance for BML CASA. */
|
||||||
|
fun listBalance(account: MibAccount): String =
|
||||||
|
"${account.currencyName} ${account.currentBalance}"
|
||||||
|
|
||||||
|
fun cardBrandIcon(productName: String): Int = when {
|
||||||
|
productName.contains("AMEX", ignoreCase = true) ||
|
||||||
|
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> R.drawable.americanexpress
|
||||||
|
productName.contains("VISA", ignoreCase = true) -> R.drawable.visa
|
||||||
|
productName.contains("MASTERCARD", ignoreCase = true) -> R.drawable.mastercard
|
||||||
|
else -> R.drawable.ic_nav_card
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toTitleCase(input: String): String =
|
||||||
|
input.trim().lowercase().split(" ").joinToString(" ") { word ->
|
||||||
|
word.replaceFirstChar { it.uppercaseChar() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package sh.sar.basedbank.util.bmlapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
|
|
||||||
|
object BmlHistoryParser {
|
||||||
|
|
||||||
|
fun displayData(account: MibAccount): AccountHistoryDisplay {
|
||||||
|
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||||
|
return AccountHistoryDisplay(
|
||||||
|
name = account.accountBriefName,
|
||||||
|
number = account.accountNumber,
|
||||||
|
bankPill = "BML",
|
||||||
|
typeLabel = BmlDashboardParser.productLabel(account.accountTypeName),
|
||||||
|
availableBalance = "${account.currencyName} ${account.availableBalance}",
|
||||||
|
workingBalance = "${account.currencyName} ${account.currentBalance}",
|
||||||
|
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package sh.sar.basedbank.util.fahipayapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.AccountListDisplay
|
||||||
|
|
||||||
|
object FahipayAccountParser {
|
||||||
|
|
||||||
|
fun displayData(account: MibAccount) = AccountListDisplay(
|
||||||
|
name = account.accountBriefName,
|
||||||
|
number = account.accountNumber,
|
||||||
|
typeLabel = account.accountTypeName,
|
||||||
|
balance = "${account.currencyName} ${account.availableBalance}"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package sh.sar.basedbank.util.fahipayapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
|
import sh.sar.basedbank.util.TransferNetwork
|
||||||
|
|
||||||
|
object FahipayContactParser {
|
||||||
|
|
||||||
|
fun displayData(contact: MibBeneficiary) = ContactDisplay(
|
||||||
|
id = contact.benefNo,
|
||||||
|
name = contact.benefNickName,
|
||||||
|
realName = contact.benefName,
|
||||||
|
accountNumber = contact.benefAccount,
|
||||||
|
categoryId = contact.benefCategoryId,
|
||||||
|
network = TransferNetwork.FAHIPAY,
|
||||||
|
bankColor = contact.bankColor,
|
||||||
|
detail = null, // Fahipay contacts show no detail line
|
||||||
|
imageHash = contact.customerImgHash,
|
||||||
|
profileId = contact.profileId,
|
||||||
|
transferSubtitle = contact.benefAccount,
|
||||||
|
canTransfer = false,
|
||||||
|
canEdit = false,
|
||||||
|
canDelete = false
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package sh.sar.basedbank.util.fahipayapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
|
|
||||||
|
object FahipayHistoryParser {
|
||||||
|
|
||||||
|
fun displayData(account: MibAccount) = AccountHistoryDisplay(
|
||||||
|
name = account.accountBriefName,
|
||||||
|
number = account.accountNumber,
|
||||||
|
bankPill = "FP",
|
||||||
|
typeLabel = account.accountTypeName,
|
||||||
|
availableBalance = "${account.currencyName} ${account.availableBalance}",
|
||||||
|
workingBalance = "${account.currencyName} ${account.currentBalance}",
|
||||||
|
blockedBalance = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package sh.sar.basedbank.util.mibapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.AccountListDisplay
|
||||||
|
|
||||||
|
object MibAccountParser {
|
||||||
|
|
||||||
|
fun displayData(account: MibAccount) = AccountListDisplay(
|
||||||
|
name = account.accountBriefName,
|
||||||
|
number = account.accountNumber,
|
||||||
|
typeLabel = productLabel(account.accountTypeName),
|
||||||
|
balance = "${account.currencyName} ${account.availableBalance}"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a display-ready product label for a MIB (Faisanet) account type name.
|
||||||
|
*/
|
||||||
|
fun productLabel(raw: String): String {
|
||||||
|
val u = raw.trim().uppercase()
|
||||||
|
return when {
|
||||||
|
u == "SAVING ACCOUNT" -> "Savings"
|
||||||
|
u == "CURRENT ACCOUNT" -> "Current"
|
||||||
|
else -> raw.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package sh.sar.basedbank.util.mibapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.util.ContactDisplay
|
||||||
|
import sh.sar.basedbank.util.TransferNetwork
|
||||||
|
|
||||||
|
object MibContactParser {
|
||||||
|
|
||||||
|
fun displayData(contact: MibBeneficiary): ContactDisplay {
|
||||||
|
val network = when (contact.benefType) {
|
||||||
|
"I" -> TransferNetwork.MIB
|
||||||
|
"S" -> TransferNetwork.SWIFT
|
||||||
|
else -> TransferNetwork.LOCAL // "L" and anything else
|
||||||
|
}
|
||||||
|
return ContactDisplay(
|
||||||
|
id = contact.benefNo,
|
||||||
|
name = contact.benefNickName,
|
||||||
|
realName = contact.benefName,
|
||||||
|
accountNumber = contact.benefAccount,
|
||||||
|
categoryId = contact.benefCategoryId,
|
||||||
|
network = network,
|
||||||
|
bankColor = contact.bankColor,
|
||||||
|
detail = "${contact.benefName} · ${contact.transferCyDesc} · ${contact.benefBankName}",
|
||||||
|
imageHash = contact.customerImgHash,
|
||||||
|
profileId = contact.profileId,
|
||||||
|
transferSubtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||||
|
canTransfer = true,
|
||||||
|
canEdit = true,
|
||||||
|
canDelete = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package sh.sar.basedbank.util.mibapi
|
||||||
|
|
||||||
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||||
|
|
||||||
|
object MibHistoryParser {
|
||||||
|
|
||||||
|
fun displayData(account: MibAccount) = AccountHistoryDisplay(
|
||||||
|
name = account.accountBriefName,
|
||||||
|
number = account.accountNumber,
|
||||||
|
bankPill = null, // MIB has no bank pill
|
||||||
|
typeLabel = MibAccountParser.productLabel(account.accountTypeName),
|
||||||
|
availableBalance = "${account.currencyName} ${account.availableBalance}",
|
||||||
|
workingBalance = "${account.currencyName} ${account.currentBalance}",
|
||||||
|
blockedBalance = null
|
||||||
|
)
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable/bml_logo_long.png
Normal file
BIN
app/src/main/res/drawable/bml_logo_long.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
10
app/src/main/res/drawable/ic_drag_handle.xml
Normal file
10
app/src/main/res/drawable/ic_drag_handle.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:pathData="M9,3h2v2H9V3zm4,0h2v2h-2V3zM9,7h2v2H9V7zm4,0h2v2h-2V7zM9,11h2v2H9v-2zm4,0h2v2h-2v-2zM9,15h2v2H9v-2zm4,0h2v2h-2v-2zM9,19h2v2H9v-2zm4,0h2v2h-2v-2z"/>
|
||||||
|
</vector>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="138dp"
|
android:layout_width="138dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:src="@drawable/bml_logo_vector"
|
android:src="@drawable/bml_logo_long"
|
||||||
android:contentDescription="@string/bml_name"
|
android:contentDescription="@string/bml_name"
|
||||||
android:scaleType="fitStart"
|
android:scaleType="fitStart"
|
||||||
android:layout_marginBottom="12dp" />
|
android:layout_marginBottom="12dp" />
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.core.widget.NestedScrollView
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
android:background="?attr/colorSurface">
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -162,33 +168,57 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<!-- Quick actions fixed at bottom -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/buttonBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/dashboard_quick_actions"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnTransfer"
|
android:id="@+id/btnQuickAction1"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:text="@string/transfer" />
|
app:iconGravity="textStart"
|
||||||
|
app:iconSize="18dp"
|
||||||
|
app:iconPadding="8dp" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnPayMvQr"
|
android:id="@+id/btnQuickAction2"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:text="@string/pay_mv_qr" />
|
app:iconGravity="textStart"
|
||||||
|
app:iconSize="18dp"
|
||||||
|
app:iconPadding="8dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingHorizontal="24dp"
|
android:paddingHorizontal="24dp"
|
||||||
android:paddingTop="40dp"
|
android:paddingTop="64dp"
|
||||||
android:paddingBottom="24dp"
|
android:paddingBottom="24dp"
|
||||||
android:gravity="center_horizontal">
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,69 @@
|
|||||||
|
|
||||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
|
<!-- Quick actions (always active) -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/dashboard_quick_actions"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvQuickActions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:nestedScrollingEnabled="false"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Bottom bar shortcuts — shown only when bottom nav is active -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/sectionBottomBarShortcuts"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/settings_bottom_bar_shortcuts"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvNavSlots"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:nestedScrollingEnabled="false"
|
||||||
|
android:overScrollMode="never" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/settings_bottom_bar_show_labels"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/switchShowLabels"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -37,9 +37,17 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:layout_marginTop="2dp" />
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAccountType"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Right: segmented pill (bank | type) + balance -->
|
<!-- Right: balance + transfer button -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -47,31 +55,22 @@
|
|||||||
android:gravity="end"
|
android:gravity="end"
|
||||||
android:layout_marginStart="16dp">
|
android:layout_marginStart="16dp">
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:background="@drawable/pill_segment_bg">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvPillType"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingHorizontal="12dp"
|
|
||||||
android:paddingVertical="6dp"
|
|
||||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
|
||||||
android:textColor="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvBalance"
|
android:id="@+id/tvBalance"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
android:textColor="?attr/colorOnSurface"
|
android:textColor="?attr/colorOnSurface" />
|
||||||
android:layout_marginTop="6dp" />
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnTransfer"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:tint="?attr/colorPrimary"
|
||||||
|
android:contentDescription="Transfer" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Status pill + balance (prepaid only) -->
|
<!-- Transfer button + balance -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layoutCardBalance"
|
android:id="@+id/layoutCardBalance"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -85,6 +85,16 @@
|
|||||||
android:textColor="?attr/colorOnSurface"
|
android:textColor="?attr/colorOnSurface"
|
||||||
android:layout_marginTop="6dp" />
|
android:layout_marginTop="6dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnTransfer"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:tint="?attr/colorPrimary"
|
||||||
|
android:contentDescription="Transfer" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
28
app/src/main/res/layout/item_nav_slot.xml
Normal file
28
app/src/main/res/layout/item_nav_slot.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivNavIcon"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_marginBottom="6dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvNavLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:gravity="center"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.navigation.NavigationView
|
<com.google.android.material.navigation.NavigationView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/navMoreView"
|
android:id="@+id/navMoreView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
app:menu="@menu/more_nav_menu" />
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
<string name="work_in_progress">ތައްޔާރުވަމުން ދަނީ</string>
|
<string name="work_in_progress">ތައްޔާރުވަމުން ދަނީ</string>
|
||||||
|
|
||||||
<!-- Dashboard -->
|
<!-- Dashboard -->
|
||||||
|
<string name="dashboard_quick_actions">ހަލުވި ހަރަކާތްތައް</string>
|
||||||
<string name="balance_mvr">ޖުމްލަ MVR</string>
|
<string name="balance_mvr">ޖުމްލަ MVR</string>
|
||||||
<string name="balance_usd">ޖުމްލަ USD</string>
|
<string name="balance_usd">ޖުމްލަ USD</string>
|
||||||
<string name="card_support_wip">ކާޑް ސަޕޯޓް</string>
|
<string name="card_support_wip">ކާޑް ސަޕޯޓް</string>
|
||||||
@@ -86,6 +87,11 @@
|
|||||||
<string name="pay_mv_qr">PayMV QR</string>
|
<string name="pay_mv_qr">PayMV QR</string>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
|
<string name="settings_bottom_bar_shortcuts">ތިރި ބާ ޝޯޓްކަޓްތައް</string>
|
||||||
|
<string name="settings_bottom_bar_select">ބަޓަން ހިޔާރު ކުރޭ</string>
|
||||||
|
<string name="settings_bottom_bar_slot_1">ތަން 1</string>
|
||||||
|
<string name="settings_bottom_bar_slot_2">ތަން 2</string>
|
||||||
|
<string name="settings_bottom_bar_slot_3">ތަން 3</string>
|
||||||
<string name="theme">ތީމް</string>
|
<string name="theme">ތީމް</string>
|
||||||
<string name="theme_system">ސިސްޓަމް</string>
|
<string name="theme_system">ސިސްޓަމް</string>
|
||||||
<string name="theme_light">ލައިޓް</string>
|
<string name="theme_light">ލައިޓް</string>
|
||||||
@@ -113,6 +119,8 @@
|
|||||||
<string name="close">ބަންދު</string>
|
<string name="close">ބަންދު</string>
|
||||||
<string name="cancel">ކެންސަލް</string>
|
<string name="cancel">ކެންސަލް</string>
|
||||||
|
|
||||||
|
<string name="settings_bottom_bar_show_labels">ބޮޓަމް ބާ ލޭބަލް އަބަދުވެސް ދެއްކުން</string>
|
||||||
|
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<string name="accounts">އެކައުންޓްތައް</string>
|
<string name="accounts">އެކައުންޓްތައް</string>
|
||||||
<string name="available_balance">ލިބެން ހުރި ބެލެންސް</string>
|
<string name="available_balance">ލިބެން ހުރި ބެލެންސް</string>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<string name="nav_dashboard">Dashboard</string>
|
<string name="nav_dashboard">Dashboard</string>
|
||||||
<string name="nav_add_account">Add Account</string>
|
<string name="nav_add_account">Add Login</string>
|
||||||
<string name="nav_accounts">Accounts</string>
|
<string name="nav_accounts">Accounts</string>
|
||||||
<string name="nav_contacts">Contacts</string>
|
<string name="nav_contacts">Contacts</string>
|
||||||
<string name="nav_activities">Activities</string>
|
<string name="nav_activities">Activities</string>
|
||||||
@@ -92,6 +92,7 @@
|
|||||||
|
|
||||||
<!-- Dashboard -->
|
<!-- Dashboard -->
|
||||||
<string name="dashboard_pending_finances">Pending Finances</string>
|
<string name="dashboard_pending_finances">Pending Finances</string>
|
||||||
|
<string name="dashboard_quick_actions">Quick Actions</string>
|
||||||
<string name="balance_mvr">MVR Total</string>
|
<string name="balance_mvr">MVR Total</string>
|
||||||
<string name="balance_usd">USD Total</string>
|
<string name="balance_usd">USD Total</string>
|
||||||
<string name="card_support_wip">Card Support</string>
|
<string name="card_support_wip">Card Support</string>
|
||||||
@@ -136,6 +137,12 @@
|
|||||||
<string name="settings_nav_drawer">Drawer</string>
|
<string name="settings_nav_drawer">Drawer</string>
|
||||||
<string name="settings_nav_bottom">Bottom Bar</string>
|
<string name="settings_nav_bottom">Bottom Bar</string>
|
||||||
<string name="settings_appearance">Appearance</string>
|
<string name="settings_appearance">Appearance</string>
|
||||||
|
<string name="settings_bottom_bar_shortcuts">Bottom Bar Shortcuts</string>
|
||||||
|
<string name="settings_bottom_bar_show_labels">Always show bottom bar labels</string>
|
||||||
|
<string name="settings_bottom_bar_select">Choose button</string>
|
||||||
|
<string name="settings_bottom_bar_slot_1">Slot 1</string>
|
||||||
|
<string name="settings_bottom_bar_slot_2">Slot 2</string>
|
||||||
|
<string name="settings_bottom_bar_slot_3">Slot 3</string>
|
||||||
<string name="settings_privacy_security">Privacy & Security</string>
|
<string name="settings_privacy_security">Privacy & Security</string>
|
||||||
<string name="settings_storage">Storage</string>
|
<string name="settings_storage">Storage</string>
|
||||||
<string name="settings_logins">Logins</string>
|
<string name="settings_logins">Logins</string>
|
||||||
@@ -150,6 +157,7 @@
|
|||||||
<string name="login_detail_id_card">ID Card</string>
|
<string name="login_detail_id_card">ID Card</string>
|
||||||
<string name="login_detail_profiles">Profiles</string>
|
<string name="login_detail_profiles">Profiles</string>
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
|
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
|
|||||||
BIN
bml_logo_long.png
Normal file
BIN
bml_logo_long.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
85
docs/PARSERS.md
Normal file
85
docs/PARSERS.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Account Display Parser Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Each bank's API returns account data in different formats and uses different field names for balances, product types, and status. To keep screens bank-agnostic, each bank has a dedicated parser that translates raw `MibAccount` model data into a standard `AccountListDisplay` object. Screens consume only `AccountListDisplay` — they never inspect `bank` or `profileType` or apply bank-specific logic.
|
||||||
|
|
||||||
|
## Bank Discriminator — `MibAccount.bank`
|
||||||
|
|
||||||
|
All dispatchers route by `account.bank`, a string set explicitly by each login flow at account creation time:
|
||||||
|
|
||||||
|
| `bank` value | Set by |
|
||||||
|
|--------------|-------------------|
|
||||||
|
| `"MIB"` | `MibLoginFlow` |
|
||||||
|
| `"BML"` | `BmlLoginFlow` |
|
||||||
|
| `"FAHIPAY"` | `FahipayLoginFlow`|
|
||||||
|
|
||||||
|
`profileType` is a bank-internal value (e.g. MIB's numeric profile ID, or BML's `"BML_PREPAID"`) and is **never** used for bank routing. Card-type checks within BML still use `profileType` (`"BML_PREPAID"` / `"BML_CREDIT"`).
|
||||||
|
|
||||||
|
`cifType` (MIB only) is the human-readable profile category name returned by the `operatingProfiles` API (e.g. `"Individual"`, `"Sole Propr"`). It is stored on `MibAccount` and surfaced in the accounts list section header and settings. It is **never hardcoded** in the app.
|
||||||
|
|
||||||
|
## Standard Output Model
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// util/AccountListDisplay.kt
|
||||||
|
data class AccountListDisplay(
|
||||||
|
val name: String, // account or card display name
|
||||||
|
val number: String, // account/card number
|
||||||
|
val typeLabel: String, // human-friendly product type (e.g. "Savings", "Visa Platinum")
|
||||||
|
val balance: String, // formatted balance string (e.g. "MVR 1,234.56")
|
||||||
|
val isCard: Boolean, // true → use card layout; false → use account layout
|
||||||
|
val cardBrandIcon: Int, // drawable res for card brand logo (Visa / Mastercard / Amex)
|
||||||
|
val statusLabel: String? // null = active; non-null = shown as status label (e.g. "Inactive")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dispatcher
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// util/AccountListParser.kt
|
||||||
|
AccountListParser.from(account: MibAccount): AccountListDisplay?
|
||||||
|
```
|
||||||
|
|
||||||
|
Routes to the correct parser based on `account.bank`. Returns `null` for unknown banks — never falls back to a specific bank.
|
||||||
|
|
||||||
|
| `account.bank` | Parser |
|
||||||
|
|----------------|-------------------------|
|
||||||
|
| `"BML"` | `BmlDashboardParser` |
|
||||||
|
| `"FAHIPAY"` | `FahipayAccountParser` |
|
||||||
|
| `"MIB"` | `MibAccountParser` |
|
||||||
|
| anything else | `null` |
|
||||||
|
|
||||||
|
## Bank Parsers
|
||||||
|
|
||||||
|
### BML — `util/bmlapi/BmlDashboardParser`
|
||||||
|
|
||||||
|
Handles both CASA accounts and prepaid/credit cards.
|
||||||
|
|
||||||
|
- **CASA balance**: uses `ledgerBalance` (working balance) — mapped to `account.currentBalance`
|
||||||
|
- **Card balance**: uses `availableBalance` (available limit from `cardBalance.AvailableLimit`)
|
||||||
|
- **Card brand**: resolved from product name (`VISA` / `MASTERCARD` / `AMEX`)
|
||||||
|
- **Status**: cards with `statusDesc != "Active"` surface `statusLabel`; active cards return `null`
|
||||||
|
|
||||||
|
### MIB — `util/mibapi/MibAccountParser`
|
||||||
|
|
||||||
|
- **Balance**: `availableBalance` from the MIB API directly
|
||||||
|
- Known product names (`SAVING ACCOUNT`, `CURRENT ACCOUNT`) mapped to short labels
|
||||||
|
- `cifType` (e.g. `"Individual"`, `"Sole Propr"`) comes from `MibProfile.cifType`, stored on `MibAccount`, displayed in section headers
|
||||||
|
|
||||||
|
### Fahipay — `util/fahipayapi/FahipayAccountParser`
|
||||||
|
|
||||||
|
- Single wallet account per user; `accountTypeName` is always `"Digital Wallet"`
|
||||||
|
- **Balance**: `availableBalance`
|
||||||
|
|
||||||
|
## Adding a New Bank
|
||||||
|
|
||||||
|
1. Create `util/<bankname>api/<Bank>AccountParser.kt` with a `displayData(account: MibAccount): AccountListDisplay` function
|
||||||
|
2. Set `bank = "<BANKNAME>"` in the new login flow when creating `MibAccount` objects
|
||||||
|
3. Add a `when` branch in `AccountListParser.from()` (and other dispatchers) for the new bank value
|
||||||
|
4. No changes needed in any screen or adapter
|
||||||
|
|
||||||
|
## Usage in Screens
|
||||||
|
|
||||||
|
`AccountsAdapter` calls `AccountListParser.from(account)` once per item (skipping `null` results) and binds the resulting `AccountListDisplay` directly. The adapter has zero bank-specific logic.
|
||||||
|
|
||||||
|
The transfer screen dropdown (`TransferFragment`) also uses `AccountListParser.from(acc)?.balance` for the source account balance display.
|
||||||
Reference in New Issue
Block a user