add BML usd limits to dashboard and optmize login flow/session expirey

This commit is contained in:
2026-05-14 05:24:33 +05:00
parent 8119d554cf
commit 25bbbe926f
10 changed files with 506 additions and 55 deletions
@@ -19,6 +19,8 @@ import java.security.SecureRandom
import java.util.Base64 import java.util.Base64
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class AuthExpiredException : Exception("Session expired")
class BmlLoginFlow { class BmlLoginFlow {
private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking" private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
@@ -162,9 +164,43 @@ class BmlLoginFlow {
fun fetchAccounts(session: BmlSession): List<MibAccount> { fun fetchAccounts(session: BmlSession): List<MibAccount> {
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute() 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() 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> { fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
@@ -243,6 +279,37 @@ class BmlLoginFlow {
return casaAccounts + prepaidCards 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> { private fun parseContacts(json: String): List<MibBeneficiary> {
val root = JSONObject(json) val root = JSONObject(json)
if (!root.optBoolean("success")) return emptyList() if (!root.optBoolean("success")) return emptyList()
@@ -4,3 +4,20 @@ data class BmlSession(
val accessToken: String, val accessToken: String,
val deviceId: 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.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import sh.sar.basedbank.R 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.MibAccount
import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.databinding.FragmentDashboardBinding import sh.sar.basedbank.databinding.FragmentDashboardBinding
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
class DashboardFragment : Fragment() { class DashboardFragment : Fragment() {
@@ -26,6 +28,7 @@ class DashboardFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) } viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) } viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
binding.btnTransfer.setOnClickListener { binding.btnTransfer.setOnClickListener {
(requireActivity() as HomeActivity).showWithBackStack(TransferFragment()) (requireActivity() as HomeActivity).showWithBackStack(TransferFragment())
@@ -52,6 +55,29 @@ class DashboardFragment : Fragment() {
binding.tvUsdBalance.text = "USD %,.2f".format(usdTotal) 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>) { private fun updatePendingFinances(deals: List<MibFinanceDeal>) {
val total = deals.sumOf { it.outstandingAmount } val total = deals.sumOf { it.outstandingAmount }
binding.tvPendingFinances.text = "MVR %,.2f".format(total) binding.tvPendingFinances.text = "MVR %,.2f".format(total)
@@ -10,11 +10,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch 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.AuthExpiredException
import sh.sar.basedbank.api.bml.BmlLoginFlow 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.MibAccount
import sh.sar.basedbank.api.mib.MibLoginFlow import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding 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.ContactsCache
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.FinancingCache import sh.sar.basedbank.util.FinancingCache
import sh.sar.basedbank.util.ForeignLimitsCache
class HomeActivity : AppCompatActivity() { class HomeActivity : AppCompatActivity() {
@@ -82,7 +86,10 @@ class HomeActivity : AppCompatActivity() {
refreshFinancing(app.mibSession, app.mibProfiles) refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(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 { } else {
// Came from lock screen — show caches immediately, refresh everything in background // Came from lock screen — show caches immediately, refresh everything in background
val cachedMib = AccountCache.load(this) val cachedMib = AccountCache.load(this)
@@ -97,10 +104,7 @@ class HomeActivity : AppCompatActivity() {
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
val store = CredentialStore(this) val store = CredentialStore(this)
val mibCreds = store.loadMibCredentials() autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store)
val bmlCreds = store.loadBmlCredentials()
if (mibCreds != null) autoRefreshMib(mibCreds, bmlCreds)
else if (bmlCreds != null) autoRefreshBml(bmlCreds)
} }
// Show dashboard on first create // Show dashboard on first create
@@ -123,67 +127,99 @@ class HomeActivity : AppCompatActivity() {
.commit() .commit()
} }
private fun autoRefreshMib( private fun autoRefresh(
mibCreds: CredentialStore.MibCredentials, mibCreds: CredentialStore.MibCredentials?,
bmlCreds: CredentialStore.BmlCredentials? bmlCreds: CredentialStore.BmlCredentials?,
store: CredentialStore
) { ) {
if (mibCreds == null && bmlCreds == null) return
binding.refreshIndicator.visibility = View.VISIBLE 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() lifecycleScope.launch {
if (bmlCreds != null) { // MIB and BML login run in parallel
try { val mibJob = mibCreds?.let {
val bmlFlow = BmlLoginFlow() async(Dispatchers.IO) {
val (session, accounts) = withContext(Dispatchers.IO) { try {
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed) val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
} val flow = MibLoginFlow(prefs)
val app = application as BasedBankApp val accounts = flow.login(it.username, it.passwordHash, it.otpSeed)
app.bmlSession = session val app = application as BasedBankApp
app.bmlAccounts = accounts app.accounts = accounts
AccountCache.saveBml(this@HomeActivity, accounts) app.mibSession = flow.lastSession
bmlAccounts.clear() app.mibProfiles = flow.lastProfiles
bmlAccounts.addAll(accounts) AccountCache.save(this@HomeActivity, accounts)
refreshBmlContacts(app) accounts
} catch (_: Exception) { /* keep cached */ } } 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) viewModel.accounts.postValue(mibAccounts + bmlAccounts)
binding.refreshIndicator.visibility = View.GONE
val app = application as BasedBankApp val app = application as BasedBankApp
if (bmlSession != null) {
refreshBmlContacts(app)
refreshBmlLimits(bmlSession)
}
refreshFinancing(app.mibSession, app.mibProfiles) refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles) refreshContacts(app.mibSession, app.mibProfiles)
} }
} }
private fun autoRefreshBml(bmlCreds: CredentialStore.BmlCredentials) { private fun refreshBmlLimits(session: BmlSession) {
binding.refreshIndicator.visibility = View.VISIBLE val bmlFlow = BmlLoginFlow()
lifecycleScope.launch { lifecycleScope.launch {
val cachedMib = AccountCache.load(this@HomeActivity)
try { try {
val bmlFlow = BmlLoginFlow() val (userName, limits) = withContext(Dispatchers.IO) {
val (session, accounts) = withContext(Dispatchers.IO) { Pair(bmlFlow.fetchUserInfo(session), bmlFlow.fetchForeignLimits(session))
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
} }
val app = application as BasedBankApp val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
app.bmlSession = session val idx = existing.indexOfFirst { it.userName == userName }
app.bmlAccounts = accounts val entry = HomeViewModel.BmlLimitsData(userName, limits)
AccountCache.saveBml(this@HomeActivity, accounts) if (idx >= 0) existing[idx] = entry else existing.add(entry)
viewModel.accounts.postValue(cachedMib + accounts) viewModel.bmlLimits.postValue(existing)
refreshBmlContacts(app) ForeignLimitsCache.save(this@HomeActivity, existing)
} catch (_: Exception) { /* keep cached */ } } catch (_: Exception) { /* keep previous */ }
finally { binding.refreshIndicator.visibility = View.GONE }
} }
} }
@@ -2,6 +2,7 @@ package sh.sar.basedbank.ui.home
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import sh.sar.basedbank.api.bml.BmlForeignLimit
import sh.sar.basedbank.api.mib.MibAccount import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibBeneficiary import sh.sar.basedbank.api.mib.MibBeneficiary
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
@@ -12,4 +13,7 @@ class HomeViewModel : ViewModel() {
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList()) val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList()) val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(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) { val (session, accounts) = withContext(Dispatchers.IO) {
flow.login(username, password, otpSeed) 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) AccountCache.saveBml(requireContext(), accounts)
val app = requireActivity().application as BasedBankApp val app = requireActivity().application as BasedBankApp
app.bmlSession = session app.bmlSession = session
@@ -82,6 +82,30 @@ class CredentialStore(context: Context) {
.apply() .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 { private fun getOrCreateKey(): SecretKey {
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) } val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
ks.getKey(keyAlias, null)?.let { return it as SecretKey } 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> </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 --> <!-- Card support WIP -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -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>