add BML usd limits to dashboard and optmize login flow/session expirey
This commit is contained in:
@@ -19,6 +19,8 @@ import java.security.SecureRandom
|
||||
import java.util.Base64
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AuthExpiredException : Exception("Session expired")
|
||||
|
||||
class BmlLoginFlow {
|
||||
|
||||
private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
|
||||
@@ -162,9 +164,43 @@ class BmlLoginFlow {
|
||||
|
||||
fun fetchAccounts(session: BmlSession): List<MibAccount> {
|
||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
||||
val json = resp.body?.string() ?: return emptyList()
|
||||
val code = resp.code
|
||||
val json = resp.body?.string()
|
||||
resp.close()
|
||||
return parseDashboard(json, "bml_${session.deviceId}")
|
||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||
return parseDashboard(json ?: return emptyList(), "bml_${session.deviceId}")
|
||||
}
|
||||
|
||||
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
|
||||
val resp = apiClient.newCall(
|
||||
Request.Builder().url("https://app.bankofmaldives.com.mv/api/v2/foreign-limits")
|
||||
.header("Authorization", "Bearer ${session.accessToken}")
|
||||
.header("User-Agent", APP_USER_AGENT)
|
||||
.header("x-app-version", APP_VERSION)
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
).execute()
|
||||
val code = resp.code
|
||||
val json = resp.body?.string()
|
||||
resp.close()
|
||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||
return parseForeignLimits(json ?: return emptyList())
|
||||
}
|
||||
|
||||
fun fetchUserInfo(session: BmlSession): String {
|
||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/userinfo")).execute()
|
||||
val json = resp.body?.string() ?: return ""
|
||||
resp.close()
|
||||
return try {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return ""
|
||||
val payload = root.optJSONObject("payload") ?: return ""
|
||||
payload.optString("name").ifBlank {
|
||||
payload.optString("fullName").ifBlank {
|
||||
payload.optString("customer_name")
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) { "" }
|
||||
}
|
||||
|
||||
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
||||
@@ -243,6 +279,37 @@ class BmlLoginFlow {
|
||||
return casaAccounts + prepaidCards
|
||||
}
|
||||
|
||||
private fun parseForeignLimits(json: String): List<BmlForeignLimit> {
|
||||
return try {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return emptyList()
|
||||
val payload = root.optJSONArray("payload") ?: return emptyList()
|
||||
(0 until payload.length()).map { i ->
|
||||
val item = payload.getJSONObject(i)
|
||||
val usage = item.optJSONObject("usageByCategory") ?: JSONObject()
|
||||
val atm = usage.optJSONObject("ATM") ?: JSONObject()
|
||||
val ecom = usage.optJSONObject("ECOM") ?: JSONObject()
|
||||
val pos = usage.optJSONObject("POS") ?: JSONObject()
|
||||
BmlForeignLimit(
|
||||
type = item.optString("type", "Debit"),
|
||||
used = item.optDouble("used", 0.0),
|
||||
totalLimit = item.optDouble("totalLimit", 0.0),
|
||||
generalCap = item.optDouble("generalCap", 0.0),
|
||||
generalRemaining = item.optDouble("generalRemaining", 0.0),
|
||||
medicalRemaining = item.optDouble("medicalRemaining", 0.0),
|
||||
isAtmEnabled = item.optBoolean("isAtmEnabled", false),
|
||||
isPosEnabled = item.optBoolean("isPosEnabled", false),
|
||||
atmRemaining = atm.optDouble("remaining", 0.0),
|
||||
atmLimit = atm.optDouble("limit", 0.0),
|
||||
ecomRemaining = ecom.optDouble("remaining", 0.0),
|
||||
ecomLimit = ecom.optDouble("limit", 0.0),
|
||||
posRemaining = pos.optDouble("remaining", 0.0),
|
||||
posLimit = pos.optDouble("limit", 0.0)
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
|
||||
private fun parseContacts(json: String): List<MibBeneficiary> {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return emptyList()
|
||||
|
||||
@@ -4,3 +4,20 @@ data class BmlSession(
|
||||
val accessToken: String,
|
||||
val deviceId: String
|
||||
)
|
||||
|
||||
data class BmlForeignLimit(
|
||||
val type: String,
|
||||
val used: Double,
|
||||
val totalLimit: Double,
|
||||
val generalCap: Double,
|
||||
val generalRemaining: Double,
|
||||
val medicalRemaining: Double,
|
||||
val isAtmEnabled: Boolean,
|
||||
val isPosEnabled: Boolean,
|
||||
val atmRemaining: Double,
|
||||
val atmLimit: Double,
|
||||
val ecomRemaining: Double,
|
||||
val ecomLimit: Double,
|
||||
val posRemaining: Double,
|
||||
val posLimit: Double
|
||||
)
|
||||
|
||||
@@ -8,9 +8,11 @@ import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||
|
||||
class DashboardFragment : Fragment() {
|
||||
|
||||
@@ -26,6 +28,7 @@ class DashboardFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
|
||||
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
|
||||
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
||||
|
||||
binding.btnTransfer.setOnClickListener {
|
||||
(requireActivity() as HomeActivity).showWithBackStack(TransferFragment())
|
||||
@@ -52,6 +55,29 @@ class DashboardFragment : Fragment() {
|
||||
binding.tvUsdBalance.text = "USD %,.2f".format(usdTotal)
|
||||
}
|
||||
|
||||
private fun updateForeignLimits(entries: List<HomeViewModel.BmlLimitsData>) {
|
||||
binding.containerForeignLimits.removeAllViews()
|
||||
for (entry in entries) {
|
||||
for (limit in entry.limits) {
|
||||
val card = ItemForeignLimitBinding.inflate(layoutInflater, binding.containerForeignLimits, false)
|
||||
card.tvLimitUserName.text = entry.userName.ifBlank { "BML" }
|
||||
card.tvLimitType.text = limit.type
|
||||
card.tvLimitGeneral.text = "USD %,.0f / %,.0f".format(limit.generalRemaining, limit.generalCap)
|
||||
card.tvLimitMedical.text = "USD %,.0f".format(limit.medicalRemaining)
|
||||
card.tvLimitAtm.text = if (!limit.isAtmEnabled)
|
||||
"USD %,.0f / %,.0f · Disabled".format(limit.atmRemaining, limit.atmLimit)
|
||||
else
|
||||
"USD %,.0f / %,.0f".format(limit.atmRemaining, limit.atmLimit)
|
||||
card.tvLimitEcom.text = "USD %,.0f / %,.0f".format(limit.ecomRemaining, limit.ecomLimit)
|
||||
card.tvLimitPos.text = if (!limit.isPosEnabled)
|
||||
"USD %,.0f / %,.0f · Disabled".format(limit.posRemaining, limit.posLimit)
|
||||
else
|
||||
"USD %,.0f / %,.0f".format(limit.posRemaining, limit.posLimit)
|
||||
binding.containerForeignLimits.addView(card.root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePendingFinances(deals: List<MibFinanceDeal>) {
|
||||
val total = deals.sumOf { it.outstandingAmount }
|
||||
binding.tvPendingFinances.text = "MVR %,.2f".format(total)
|
||||
|
||||
@@ -10,11 +10,14 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.AuthExpiredException
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.bml.BmlSession
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||
import sh.sar.basedbank.databinding.ActivityHomeBinding
|
||||
@@ -30,6 +33,7 @@ import sh.sar.basedbank.util.AccountCache
|
||||
import sh.sar.basedbank.util.ContactsCache
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.util.FinancingCache
|
||||
import sh.sar.basedbank.util.ForeignLimitsCache
|
||||
|
||||
class HomeActivity : AppCompatActivity() {
|
||||
|
||||
@@ -82,7 +86,10 @@ class HomeActivity : AppCompatActivity() {
|
||||
|
||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||
refreshContacts(app.mibSession, app.mibProfiles)
|
||||
if (app.bmlSession != null) refreshBmlContacts(app)
|
||||
if (app.bmlSession != null) {
|
||||
refreshBmlContacts(app)
|
||||
refreshBmlLimits(app.bmlSession!!)
|
||||
}
|
||||
} else {
|
||||
// Came from lock screen — show caches immediately, refresh everything in background
|
||||
val cachedMib = AccountCache.load(this)
|
||||
@@ -97,10 +104,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
||||
|
||||
val store = CredentialStore(this)
|
||||
val mibCreds = store.loadMibCredentials()
|
||||
val bmlCreds = store.loadBmlCredentials()
|
||||
if (mibCreds != null) autoRefreshMib(mibCreds, bmlCreds)
|
||||
else if (bmlCreds != null) autoRefreshBml(bmlCreds)
|
||||
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store)
|
||||
}
|
||||
|
||||
// Show dashboard on first create
|
||||
@@ -123,67 +127,99 @@ class HomeActivity : AppCompatActivity() {
|
||||
.commit()
|
||||
}
|
||||
|
||||
private fun autoRefreshMib(
|
||||
mibCreds: CredentialStore.MibCredentials,
|
||||
bmlCreds: CredentialStore.BmlCredentials?
|
||||
private fun autoRefresh(
|
||||
mibCreds: CredentialStore.MibCredentials?,
|
||||
bmlCreds: CredentialStore.BmlCredentials?,
|
||||
store: CredentialStore
|
||||
) {
|
||||
if (mibCreds == null && bmlCreds == null) return
|
||||
binding.refreshIndicator.visibility = View.VISIBLE
|
||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||
val flow = MibLoginFlow(prefs)
|
||||
lifecycleScope.launch {
|
||||
var mibAccounts: List<MibAccount> = AccountCache.load(this@HomeActivity)
|
||||
try {
|
||||
mibAccounts = withContext(Dispatchers.IO) {
|
||||
flow.login(mibCreds.username, mibCreds.passwordHash, mibCreds.otpSeed)
|
||||
}
|
||||
val app = application as BasedBankApp
|
||||
app.accounts = mibAccounts
|
||||
app.mibSession = flow.lastSession
|
||||
app.mibProfiles = flow.lastProfiles
|
||||
AccountCache.save(this@HomeActivity, mibAccounts)
|
||||
} catch (_: Exception) { /* keep cached */ }
|
||||
finally { binding.refreshIndicator.visibility = View.GONE }
|
||||
|
||||
val bmlAccounts = AccountCache.loadBml(this@HomeActivity).toMutableList()
|
||||
if (bmlCreds != null) {
|
||||
try {
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
|
||||
}
|
||||
val app = application as BasedBankApp
|
||||
app.bmlSession = session
|
||||
app.bmlAccounts = accounts
|
||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||
bmlAccounts.clear()
|
||||
bmlAccounts.addAll(accounts)
|
||||
refreshBmlContacts(app)
|
||||
} catch (_: Exception) { /* keep cached */ }
|
||||
lifecycleScope.launch {
|
||||
// MIB and BML login run in parallel
|
||||
val mibJob = mibCreds?.let {
|
||||
async(Dispatchers.IO) {
|
||||
try {
|
||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||
val flow = MibLoginFlow(prefs)
|
||||
val accounts = flow.login(it.username, it.passwordHash, it.otpSeed)
|
||||
val app = application as BasedBankApp
|
||||
app.accounts = accounts
|
||||
app.mibSession = flow.lastSession
|
||||
app.mibProfiles = flow.lastProfiles
|
||||
AccountCache.save(this@HomeActivity, accounts)
|
||||
accounts
|
||||
} catch (_: Exception) { AccountCache.load(this@HomeActivity) }
|
||||
}
|
||||
}
|
||||
|
||||
val bmlJob = bmlCreds?.let {
|
||||
async(Dispatchers.IO) {
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
val savedToken = store.loadBmlSession()
|
||||
|
||||
// Try cached token first
|
||||
if (savedToken != null) {
|
||||
try {
|
||||
val session = BmlSession(savedToken.first, savedToken.second)
|
||||
val accounts = bmlFlow.fetchAccounts(session)
|
||||
val app = application as BasedBankApp
|
||||
app.bmlSession = session
|
||||
app.bmlAccounts = accounts
|
||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||
return@async Pair(session, accounts)
|
||||
} catch (_: AuthExpiredException) {
|
||||
// Token expired — fall through to full login
|
||||
} catch (_: Exception) {
|
||||
// Network or other error — fall through to full login
|
||||
}
|
||||
}
|
||||
|
||||
// Full login (token missing or expired)
|
||||
try {
|
||||
val (session, accounts) = bmlFlow.login(it.username, it.password, it.otpSeed)
|
||||
store.saveBmlSession(session.accessToken, session.deviceId)
|
||||
val app = application as BasedBankApp
|
||||
app.bmlSession = session
|
||||
app.bmlAccounts = accounts
|
||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||
Pair(session, accounts)
|
||||
} catch (_: Exception) {
|
||||
Pair(null, AccountCache.loadBml(this@HomeActivity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
|
||||
val (bmlSession, bmlAccounts) = bmlJob?.await() ?: Pair(null, AccountCache.loadBml(this@HomeActivity))
|
||||
|
||||
viewModel.accounts.postValue(mibAccounts + bmlAccounts)
|
||||
binding.refreshIndicator.visibility = View.GONE
|
||||
|
||||
val app = application as BasedBankApp
|
||||
if (bmlSession != null) {
|
||||
refreshBmlContacts(app)
|
||||
refreshBmlLimits(bmlSession)
|
||||
}
|
||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||
refreshContacts(app.mibSession, app.mibProfiles)
|
||||
}
|
||||
}
|
||||
|
||||
private fun autoRefreshBml(bmlCreds: CredentialStore.BmlCredentials) {
|
||||
binding.refreshIndicator.visibility = View.VISIBLE
|
||||
private fun refreshBmlLimits(session: BmlSession) {
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
lifecycleScope.launch {
|
||||
val cachedMib = AccountCache.load(this@HomeActivity)
|
||||
try {
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
|
||||
val (userName, limits) = withContext(Dispatchers.IO) {
|
||||
Pair(bmlFlow.fetchUserInfo(session), bmlFlow.fetchForeignLimits(session))
|
||||
}
|
||||
val app = application as BasedBankApp
|
||||
app.bmlSession = session
|
||||
app.bmlAccounts = accounts
|
||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||
viewModel.accounts.postValue(cachedMib + accounts)
|
||||
refreshBmlContacts(app)
|
||||
} catch (_: Exception) { /* keep cached */ }
|
||||
finally { binding.refreshIndicator.visibility = View.GONE }
|
||||
val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
|
||||
val idx = existing.indexOfFirst { it.userName == userName }
|
||||
val entry = HomeViewModel.BmlLimitsData(userName, limits)
|
||||
if (idx >= 0) existing[idx] = entry else existing.add(entry)
|
||||
viewModel.bmlLimits.postValue(existing)
|
||||
ForeignLimitsCache.save(this@HomeActivity, existing)
|
||||
} catch (_: Exception) { /* keep previous */ }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package sh.sar.basedbank.ui.home
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||
@@ -12,4 +13,7 @@ class HomeViewModel : ViewModel() {
|
||||
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
|
||||
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(emptyList())
|
||||
|
||||
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
||||
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
||||
}
|
||||
|
||||
@@ -159,7 +159,9 @@ class CredentialsFragment : Fragment() {
|
||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||
flow.login(username, password, otpSeed)
|
||||
}
|
||||
CredentialStore(requireContext()).saveBmlCredentials(username, password, otpSeed)
|
||||
val store = CredentialStore(requireContext())
|
||||
store.saveBmlCredentials(username, password, otpSeed)
|
||||
store.saveBmlSession(session.accessToken, session.deviceId)
|
||||
AccountCache.saveBml(requireContext(), accounts)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
app.bmlSession = session
|
||||
|
||||
@@ -82,6 +82,30 @@ class CredentialStore(context: Context) {
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun saveBmlSession(accessToken: String, deviceId: String) {
|
||||
val key = getOrCreateKey()
|
||||
prefs.edit()
|
||||
.putString("bml_enc_token", encrypt(accessToken, key))
|
||||
.putString("bml_enc_device_id", encrypt(deviceId, key))
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun loadBmlSession(): Pair<String, String>? {
|
||||
val key = getOrCreateKey()
|
||||
val encToken = prefs.getString("bml_enc_token", null) ?: return null
|
||||
val encDeviceId = prefs.getString("bml_enc_device_id", null) ?: return null
|
||||
return try {
|
||||
Pair(decrypt(encToken, key), decrypt(encDeviceId, key))
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun clearBmlSession() {
|
||||
prefs.edit()
|
||||
.remove("bml_enc_token")
|
||||
.remove("bml_enc_device_id")
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun getOrCreateKey(): SecretKey {
|
||||
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
||||
ks.getKey(keyAlias, null)?.let { return it as SecretKey }
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package sh.sar.basedbank.util
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||
import sh.sar.basedbank.ui.home.HomeViewModel
|
||||
|
||||
object ForeignLimitsCache {
|
||||
|
||||
private const val PREFS = "foreign_limits_cache"
|
||||
private const val KEY = "bml_foreign_limits"
|
||||
|
||||
fun save(context: Context, entries: List<HomeViewModel.BmlLimitsData>) {
|
||||
val arr = JSONArray()
|
||||
for (entry in entries) {
|
||||
val limitsArr = JSONArray()
|
||||
for (l in entry.limits) {
|
||||
limitsArr.put(JSONObject().apply {
|
||||
put("type", l.type)
|
||||
put("used", l.used)
|
||||
put("totalLimit", l.totalLimit)
|
||||
put("generalCap", l.generalCap)
|
||||
put("generalRemaining", l.generalRemaining)
|
||||
put("medicalRemaining", l.medicalRemaining)
|
||||
put("isAtmEnabled", l.isAtmEnabled)
|
||||
put("isPosEnabled", l.isPosEnabled)
|
||||
put("atmRemaining", l.atmRemaining)
|
||||
put("atmLimit", l.atmLimit)
|
||||
put("ecomRemaining", l.ecomRemaining)
|
||||
put("ecomLimit", l.ecomLimit)
|
||||
put("posRemaining", l.posRemaining)
|
||||
put("posLimit", l.posLimit)
|
||||
})
|
||||
}
|
||||
arr.put(JSONObject().apply {
|
||||
put("userName", entry.userName)
|
||||
put("limits", limitsArr)
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.edit().putString(KEY, arr.toString()).apply()
|
||||
}
|
||||
|
||||
fun load(context: Context): List<HomeViewModel.BmlLimitsData> {
|
||||
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getString(KEY, null) ?: return emptyList()
|
||||
return try {
|
||||
val arr = JSONArray(json)
|
||||
(0 until arr.length()).map { i ->
|
||||
val entry = arr.getJSONObject(i)
|
||||
val limitsArr = entry.optJSONArray("limits") ?: JSONArray()
|
||||
val limits = (0 until limitsArr.length()).map { j ->
|
||||
val l = limitsArr.getJSONObject(j)
|
||||
BmlForeignLimit(
|
||||
type = l.optString("type", "Debit"),
|
||||
used = l.optDouble("used", 0.0),
|
||||
totalLimit = l.optDouble("totalLimit", 0.0),
|
||||
generalCap = l.optDouble("generalCap", 0.0),
|
||||
generalRemaining = l.optDouble("generalRemaining", 0.0),
|
||||
medicalRemaining = l.optDouble("medicalRemaining", 0.0),
|
||||
isAtmEnabled = l.optBoolean("isAtmEnabled", false),
|
||||
isPosEnabled = l.optBoolean("isPosEnabled", false),
|
||||
atmRemaining = l.optDouble("atmRemaining", 0.0),
|
||||
atmLimit = l.optDouble("atmLimit", 0.0),
|
||||
ecomRemaining = l.optDouble("ecomRemaining", 0.0),
|
||||
ecomLimit = l.optDouble("ecomLimit", 0.0),
|
||||
posRemaining = l.optDouble("posRemaining", 0.0),
|
||||
posLimit = l.optDouble("posLimit", 0.0)
|
||||
)
|
||||
}
|
||||
HomeViewModel.BmlLimitsData(entry.optString("userName"), limits)
|
||||
}
|
||||
} catch (_: Exception) { emptyList() }
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,13 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- BML foreign limits (populated dynamically) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/containerForeignLimits"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<!-- Card support WIP -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
|
||||
192
app/src/main/res/layout/item_foreign_limit.xml
Normal file
192
app/src/main/res/layout/item_foreign_limit.xml
Normal file
@@ -0,0 +1,192 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardElevation="1dp"
|
||||
app:cardCornerRadius="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Header: name + type chip -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLimitUserName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="USD Foreign Transaction Limits"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLimitType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="5dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:background="@drawable/pill_segment_bg" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<!-- General -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="6dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="General"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLimitGeneral"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Medical -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Medical"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLimitMedical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<!-- ATM -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="6dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="ATM"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLimitAtm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ECOM -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="6dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Online (ECOM)"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLimitEcom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- POS -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="POS"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLimitPos"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
Reference in New Issue
Block a user