6 Commits

Author SHA1 Message Date
105518e147 release version 1.0.6
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
Build and Release APK / build (push) Successful in 3m52s
2026-05-21 23:24:14 +05:00
38570615dd optmize dashboard (seperate credit section, bars for spending limits
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-21 23:23:45 +05:00
e82218e897 added support for BML loans
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-21 22:58:58 +05:00
50150b826f remove auto lock off and optimize session keepalive for mib
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
2026-05-21 22:31:58 +05:00
2d705457f8 animate lock and eye icons in action bar (top)
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-21 01:37:37 +05:00
f03e23062b you can now hold to copy text from recipts even in full screen mode
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 6s
2026-05-21 01:04:23 +05:00
33 changed files with 1420 additions and 297 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "sh.sar.basedbank"
minSdk = 26
targetSdk = 36
versionCode = 4
versionName = "1.0.5"
versionCode = 5
versionName = "1.0.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -49,6 +49,30 @@ class BmlAccountClient {
} catch (_: Exception) { null }
}
fun fetchLoanDetail(session: BmlSession, internalId: String): BmlLoanDetail? {
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/account/$internalId")).execute()
val code = resp.code
val json = resp.body?.string() ?: return null
resp.close()
if (code == 401 || code == 419) throw AuthExpiredException()
return try {
val root = JSONObject(json)
if (!root.optBoolean("success")) return null
val p = root.optJSONObject("payload") ?: return null
BmlLoanDetail(
loanAmount = p.optDouble("loanAmount", 0.0),
outstandingAmt = p.optDouble("outstandingAmt", 0.0),
repayAmount = p.optDouble("repayAmount", 0.0),
intRate = p.optDouble("intRate", 0.0),
loanStatus = p.optString("loanStatus"),
startDate = p.optString("startDate"),
endDate = p.optString("endDate"),
noOfRepayOverdue = p.optInt("noOfRepayOverdue", 0),
overdueAmount = p.optDouble("overdueAmount", 0.0)
)
} catch (_: Exception) { null }
}
private fun parseDashboard(
json: String,
loginTag: String,
@@ -61,6 +85,7 @@ class BmlAccountClient {
val casaAccounts = mutableListOf<BankAccount>()
val prepaidCards = mutableListOf<BankAccount>()
val loanAccounts = mutableListOf<BankAccount>()
for (i in 0 until dashboard.length()) {
val item = dashboard.getJSONObject(i)
@@ -91,6 +116,26 @@ class BmlAccountClient {
profileId = profileId,
internalId = internalId
))
} else if (accountType == "Loan") {
val outstanding = Math.abs(item.optDouble("availableBalance", 0.0))
loanAccounts.add(BankAccount(
bank = "BML",
profileName = profileName,
profileType = "BML_LOAN",
accountNumber = accountNumber,
accountBriefName = item.optString("alias"),
currencyName = currency,
accountTypeName = product,
availableBalance = "%.2f".format(outstanding),
currentBalance = "%.2f".format(outstanding),
blockedAmount = "0.00",
mvrBalance = "0.00",
statusDesc = status,
profileImageHash = null,
loginTag = loginTag,
profileId = profileId,
internalId = internalId
))
} else if (accountType == "Card") {
val isVisible = item.optBoolean("account_visible", false)
if (!isVisible) continue
@@ -119,6 +164,6 @@ class BmlAccountClient {
}
}
return casaAccounts + prepaidCards
return casaAccounts + prepaidCards + loanAccounts
}
}

View File

@@ -50,6 +50,18 @@ data class BmlTransferResult(
val errorMessage: String = ""
)
data class BmlLoanDetail(
val loanAmount: Double,
val outstandingAmt: Double, // negative as returned by API
val repayAmount: Double,
val intRate: Double,
val loanStatus: String,
val startDate: String, // ISO8601 e.g. "2023-10-26T00:00:00+05:00"
val endDate: String,
val noOfRepayOverdue: Int,
val overdueAmount: Double
)
data class BmlForeignLimit(
val type: String,
val used: Double,

View File

@@ -68,6 +68,7 @@ class MibTransferClient {
.withWvHeaders(session)
.build()
return client.newCall(request).execute().use { response ->
if (response.code == 419) throw SessionExpiredException()
val bodyStr = response.body?.string() ?: ""
val json = try { JSONObject(bodyStr) } catch (_: Exception) { null }
if (json == null || !json.optBoolean("success")) {

View File

@@ -118,6 +118,14 @@ class AccountHistoryFragment : Fragment() {
}
(activity as? HomeActivity)?.setRefreshing(true)
loadNextPage()
binding.swipeRefresh.setOnRefreshListener {
if (isLoading) {
binding.swipeRefresh.isRefreshing = false
} else {
resetAndReload()
}
}
}
override fun onResume() {
@@ -135,6 +143,17 @@ class AccountHistoryFragment : Fragment() {
binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE
}
private fun resetAndReload() {
allTransactions.clear()
pendingImageNames.clear()
pendingIconUrls.clear()
firstPageDone = false
fetcher = HistoryFetcher(account)
adapter.setTransactions(emptyList())
binding.emptyView.visibility = View.GONE
loadNextPage()
}
private fun loadNextPage() {
if (isLoading || !fetcher.hasMore()) return
isLoading = true
@@ -153,6 +172,7 @@ class AccountHistoryFragment : Fragment() {
if (!firstPageDone) {
firstPageDone = true
(activity as? HomeActivity)?.setRefreshing(false)
binding.swipeRefresh.isRefreshing = false
}
if (transactions.isNotEmpty()) {

View File

@@ -45,6 +45,11 @@ class AccountsFragment : Fragment() {
viewModel.accounts.observe(viewLifecycleOwner) { adapter.updateAccounts(it) }
viewModel.hideAmounts.observe(viewLifecycleOwner) { adapter.setHideAmounts(it) }
binding.swipeRefresh.setOnRefreshListener {
(activity as? HomeActivity)?.triggerRefresh()
binding.swipeRefresh.isRefreshing = false
}
}
override fun onResume() {

View File

@@ -48,6 +48,7 @@ class ContactsFragment : Fragment() {
private var currentSearch: String = ""
private var mediator: TabLayoutMediator? = null
private lateinit var pagerAdapter: ContactsPagerAdapter
private var contactsRefreshing = false
private data class TabPage(val categoryId: String?, val label: String)
@@ -134,6 +135,11 @@ class ContactsFragment : Fragment() {
(activity as? HomeActivity)?.loadAllContacts()
binding.swipeRefresh.setOnRefreshListener {
contactsRefreshing = true
(activity as? HomeActivity)?.loadAllContacts()
}
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
rebuildPager(cats)
}
@@ -143,6 +149,10 @@ class ContactsFragment : Fragment() {
pagerAdapter.updateContacts(allContacts)
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
binding.loadingView.visibility = View.GONE
if (contactsRefreshing) {
contactsRefreshing = false
binding.swipeRefresh.isRefreshing = false
}
}
}

View File

@@ -14,6 +14,7 @@ import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlForeignLimit
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibFinanceDeal
import kotlin.math.abs
import sh.sar.basedbank.databinding.FragmentDashboardBinding
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
@@ -30,14 +31,20 @@ class DashboardFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() }
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() }
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
viewModel.hideAmounts.observe(viewLifecycleOwner) {
updateBalances(viewModel.accounts.value ?: emptyList())
updatePendingFinances(viewModel.financing.value ?: emptyList())
updatePendingFinances()
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
}
binding.swipeRefresh.setOnRefreshListener {
(activity as? HomeActivity)?.triggerRefresh()
binding.swipeRefresh.isRefreshing = false
}
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(binding.buttonBar) { v, insets ->
val isBottomNav = requireContext().getSharedPreferences("prefs", android.content.Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
@@ -70,57 +77,132 @@ class DashboardFragment : Fragment() {
private fun updateBalances(accounts: List<BankAccount>) {
val hide = viewModel.hideAmounts.value ?: false
val nonCreditAccounts = accounts.filter { it.profileType != "BML_CREDIT" }
val creditAccounts = accounts.filter { it.profileType == "BML_CREDIT" }
if (hide) {
binding.tvMvrBalance.text = "MVR ••••••"
binding.tvUsdBalance.text = "USD ••••••"
if (creditAccounts.isNotEmpty()) {
binding.rowCreditCards.visibility = View.VISIBLE
val hasMvrCredit = creditAccounts.any { it.currencyName.equals("MVR", ignoreCase = true) }
val hasUsdCredit = creditAccounts.any { it.currencyName.equals("USD", ignoreCase = true) }
binding.cardMvrCredit.visibility = if (hasMvrCredit) View.VISIBLE else View.GONE
binding.cardUsdCredit.visibility = if (hasUsdCredit) View.VISIBLE else View.GONE
binding.tvMvrCredit.text = "MVR ••••••"
binding.tvUsdCredit.text = "USD ••••••"
} else {
binding.rowCreditCards.visibility = View.GONE
}
return
}
val mvrTotal = accounts
val mvrTotal = nonCreditAccounts
.filter { it.currencyName.equals("MVR", ignoreCase = true) }
.sumOf { it.availableBalance.replace(",", "").toDoubleOrNull() ?: 0.0 }
val usdTotal = accounts
val usdTotal = nonCreditAccounts
.filter { it.currencyName.equals("USD", ignoreCase = true) }
.sumOf { it.availableBalance.replace(",", "").toDoubleOrNull() ?: 0.0 }
binding.tvMvrBalance.text = "MVR %,.2f".format(mvrTotal)
binding.tvUsdBalance.text = "USD %,.2f".format(usdTotal)
val mvrCredit = creditAccounts
.filter { it.currencyName.equals("MVR", ignoreCase = true) }
.sumOf { it.availableBalance.replace(",", "").toDoubleOrNull() ?: 0.0 }
val usdCredit = creditAccounts
.filter { it.currencyName.equals("USD", ignoreCase = true) }
.sumOf { it.availableBalance.replace(",", "").toDoubleOrNull() ?: 0.0 }
if (creditAccounts.isNotEmpty()) {
binding.rowCreditCards.visibility = View.VISIBLE
binding.cardMvrCredit.visibility = if (mvrCredit > 0) View.VISIBLE else View.GONE
binding.cardUsdCredit.visibility = if (usdCredit > 0) View.VISIBLE else View.GONE
binding.tvMvrCredit.text = "MVR %,.2f".format(mvrCredit)
binding.tvUsdCredit.text = "USD %,.2f".format(usdCredit)
} else {
binding.rowCreditCards.visibility = View.GONE
}
}
private val expandedLimits = mutableSetOf<Int>()
private fun updateForeignLimits(entries: List<HomeViewModel.BmlLimitsData>) {
val hide = viewModel.hideAmounts.value ?: false
binding.containerForeignLimits.removeAllViews()
var cardIndex = 0
for (entry in entries) {
for (limit in entry.limits) {
val idx = cardIndex++
val card = ItemForeignLimitBinding.inflate(layoutInflater, binding.containerForeignLimits, false)
card.tvLimitUserName.text = entry.userName.ifBlank { "BML" }
card.tvLimitType.text = limit.type
if (hide) {
card.tvLimitGeneral.text = "USD ••••••"
card.tvLimitMedical.text = "USD ••••••"
card.tvLimitAtm.text = if (!limit.isAtmEnabled) "USD •••••• · Disabled" else "USD ••••••"
card.tvLimitEcom.text = "USD ••••••"
card.tvLimitPos.text = if (!limit.isPosEnabled) "USD •••••• · Disabled" else "USD ••••••"
} else {
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)
bindLimitCard(card, entry.userName, limit, hide, idx in expandedLimits)
card.root.setOnClickListener {
if (idx in expandedLimits) expandedLimits.remove(idx) else expandedLimits.add(idx)
updateForeignLimits(entries)
}
binding.containerForeignLimits.addView(card.root)
}
}
}
private fun updatePendingFinances(deals: List<MibFinanceDeal>) {
private fun bindLimitCard(
card: ItemForeignLimitBinding,
userName: String,
limit: BmlForeignLimit,
hide: Boolean,
expanded: Boolean
) {
card.tvLimitUserName.text = userName.ifBlank { "BML" }
card.tvLimitType.text = limit.type
// ECOM (always visible)
card.tvLimitEcom.text = if (hide) "USD ••••••"
else "USD %,.2f / %,.0f".format(limit.ecomRemaining, limit.ecomLimit)
card.progressEcom.progress = if (hide || limit.ecomLimit <= 0) 0
else ((limit.ecomRemaining / limit.ecomLimit) * 100).toInt().coerceIn(0, 100)
// General (always visible)
card.tvLimitGeneral.text = if (hide) "USD ••••••"
else "USD %,.2f / %,.0f".format(limit.generalRemaining, limit.generalCap)
card.progressGeneral.progress = if (hide || limit.generalCap <= 0) 0
else ((limit.generalRemaining / limit.generalCap) * 100).toInt().coerceIn(0, 100)
// Expanded section
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
card.dividerLimitDetails.visibility = detailsVisible
card.detailsGroup.visibility = detailsVisible
if (expanded) {
// ATM
if (!limit.isAtmEnabled) card.tvAtmLabel.append(" (Disabled)")
card.tvLimitAtm.text = if (hide) "USD ••••••"
else "USD %,.2f / %,.0f".format(limit.atmRemaining, limit.atmLimit)
card.progressAtm.progress = if (hide || limit.atmLimit <= 0) 0
else ((limit.atmRemaining / limit.atmLimit) * 100).toInt().coerceIn(0, 100)
// POS
if (!limit.isPosEnabled) card.tvPosLabel.append(" (Disabled)")
card.tvLimitPos.text = if (hide) "USD ••••••"
else "USD %,.2f / %,.0f".format(limit.posRemaining, limit.posLimit)
card.progressPos.progress = if (hide || limit.posLimit <= 0) 0
else ((limit.posRemaining / limit.posLimit) * 100).toInt().coerceIn(0, 100)
// Medical
card.tvLimitMedical.text = if (hide) "USD ••••••"
else "USD %,.2f / %,.0f".format(limit.medicalRemaining, limit.totalLimit)
card.progressMedical.progress = if (hide || limit.totalLimit <= 0) 0
else ((limit.medicalRemaining / limit.totalLimit) * 100).toInt().coerceIn(0, 100)
}
}
private fun updatePendingFinances() {
val hide = viewModel.hideAmounts.value ?: false
binding.tvPendingFinances.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(deals.sumOf { it.outstandingAmount })
val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount }
val bmlLoanDetails = viewModel.bmlLoanDetails.value ?: emptyMap()
val bmlTotal = bmlLoanDetails.values.sumOf { abs(it.outstandingAmt) }
val total = mibTotal + bmlTotal
binding.tvPendingFinances.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(total)
}
override fun onDestroyView() {

View File

@@ -5,56 +5,97 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlLoanDetail
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.databinding.ItemBmlLoanBinding
import sh.sar.basedbank.databinding.ItemFinanceDealBinding
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import kotlin.math.abs
import kotlin.math.ceil
class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
RecyclerView.Adapter<FinancingAdapter.ViewHolder>() {
class FinancingAdapter(mibDeals: List<MibFinanceDeal>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private sealed class Item {
data class Mib(val deal: MibFinanceDeal) : Item()
data class Bml(val account: BankAccount, val detail: BmlLoanDetail?) : Item()
}
private var items: List<Item> = mibDeals.map { Item.Mib(it) }
private var hideAmounts: Boolean = false
private val expandedPositions = mutableSetOf<Int>()
private val amountFmt = NumberFormat.getNumberInstance(Locale.US).apply {
minimumFractionDigits = 2
maximumFractionDigits = 2
}
private val mibDateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
private val isoDateFmt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US)
private val outputDateFmt = SimpleDateFormat("d MMM yyyy", Locale.US)
fun setHideAmounts(hide: Boolean) {
if (hideAmounts == hide) return
hideAmounts = hide
notifyDataSetChanged()
}
private val expandedPositions = mutableSetOf<Int>()
private val amountFmt = NumberFormat.getNumberInstance(Locale.US).apply {
minimumFractionDigits = 2
maximumFractionDigits = 2
}
private val inputDateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
private val outputDateFmt = SimpleDateFormat("d MMM yyyy", Locale.US)
fun updateDeals(newDeals: List<MibFinanceDeal>) {
deals = newDeals
fun update(mibDeals: List<MibFinanceDeal>, bmlLoans: List<Pair<BankAccount, BmlLoanDetail?>>) {
expandedPositions.clear()
items = mibDeals.map { Item.Mib(it) } + bmlLoans.map { (acc, detail) -> Item.Bml(acc, detail) }
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemFinanceDealBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
// Legacy compatibility — used on initial empty construction
fun updateDeals(newDeals: List<MibFinanceDeal>) {
expandedPositions.clear()
val bmlItems = items.filterIsInstance<Item.Bml>()
items = newDeals.map { Item.Mib(it) } + bmlItems
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(deals[position], position in expandedPositions)
holder.binding.root.setOnClickListener {
fun updateBmlLoans(loans: List<Pair<BankAccount, BmlLoanDetail?>>) {
expandedPositions.clear()
val mibItems = items.filterIsInstance<Item.Mib>()
items = mibItems + loans.map { (acc, detail) -> Item.Bml(acc, detail) }
notifyDataSetChanged()
}
override fun getItemViewType(position: Int) = when (items[position]) {
is Item.Mib -> TYPE_MIB
is Item.Bml -> TYPE_BML
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_BML -> BmlViewHolder(ItemBmlLoanBinding.inflate(inflater, parent, false))
else -> MibViewHolder(ItemFinanceDealBinding.inflate(inflater, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val expanded = position in expandedPositions
when (val item = items[position]) {
is Item.Mib -> (holder as MibViewHolder).bind(item.deal, expanded)
is Item.Bml -> (holder as BmlViewHolder).bind(item.account, item.detail, expanded)
}
holder.itemView.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos in expandedPositions) expandedPositions.remove(pos) else expandedPositions.add(pos)
notifyItemChanged(pos)
}
}
override fun getItemCount() = deals.size
override fun getItemCount() = items.size
inner class ViewHolder(val binding: ItemFinanceDealBinding) :
// ── MIB ViewHolder ────────────────────────────────────────────────────────
inner class MibViewHolder(val binding: ItemFinanceDealBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(deal: MibFinanceDeal, expanded: Boolean) {
@@ -69,25 +110,22 @@ class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
binding.tvPaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.paidAmount)}"
binding.tvUnpaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.outstandingAmount)}"
// Progress bar
val progress = if (deal.dealAmount > 0)
((deal.paidAmount / deal.dealAmount) * 100).toInt().coerceIn(0, 100)
else 0
binding.progressBar.progress = if (hide) 0 else progress
// Completion estimate
binding.tvCompletion.text = completionText(deal, ctx)
binding.tvCompletion.text = mibCompletionText(deal, ctx)
// Expanded details
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
binding.dividerDetails.visibility = detailsVisible
binding.detailsGroup.visibility = detailsVisible
if (expanded) {
binding.tvDealDate.text = formatDate(deal.dealDate)
binding.tvDealDate.text = formatMibDate(deal.dealDate)
binding.tvInstallment.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.installmentAmount)}"
binding.tvNumInstallments.text = deal.noOfInstallments.toString()
binding.tvLastPaidDate.text = formatDate(deal.lastPaidDate)
binding.tvLastPaidDate.text = formatMibDate(deal.lastPaidDate)
binding.tvLastPayAmount.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.lastPayAmount)}"
if (deal.overdueAmount > 0) {
@@ -99,7 +137,7 @@ class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
}
}
private fun completionText(deal: MibFinanceDeal, ctx: android.content.Context): String {
private fun mibCompletionText(deal: MibFinanceDeal, ctx: android.content.Context): String {
if (deal.outstandingAmount <= 0.0) return ctx.getString(R.string.financing_completion_done)
val remaining = MibFinancingClient.remainingMonths(deal)
if (remaining <= 0) return ctx.getString(R.string.financing_completion_done)
@@ -109,12 +147,84 @@ class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
return ctx.getString(R.string.financing_completion_fmt, month)
}
private fun formatDate(raw: String): String {
private fun formatMibDate(raw: String): String {
return try {
outputDateFmt.format(inputDateFmt.parse(raw)!!)
} catch (_: Exception) {
raw.take(10)
}
outputDateFmt.format(mibDateFmt.parse(raw)!!)
} catch (_: Exception) { raw.take(10) }
}
}
// ── BML ViewHolder ────────────────────────────────────────────────────────
inner class BmlViewHolder(val binding: ItemBmlLoanBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: BankAccount, detail: BmlLoanDetail?, expanded: Boolean) {
val ctx = binding.root.context
val currency = account.currencyName
val hide = hideAmounts
binding.tvLoanProduct.text = account.accountTypeName
.trim().lowercase().split(" ")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercaseChar() } }
binding.tvLoanAccount.text = account.accountNumber
binding.tvLoanStatus.text = detail?.loanStatus?.ifBlank { account.statusDesc } ?: account.statusDesc
val loanAmt = detail?.loanAmount ?: 0.0
val outstanding = if (detail != null) abs(detail.outstandingAmt) else account.availableBalance.toDoubleOrNull() ?: 0.0
val paid = (loanAmt - outstanding).coerceAtLeast(0.0)
binding.tvLoanTotal.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(loanAmt)}"
binding.tvLoanPaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(paid)}"
binding.tvLoanOutstanding.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(outstanding)}"
val progress = if (loanAmt > 0) ((paid / loanAmt) * 100).toInt().coerceIn(0, 100) else 0
binding.loanProgressBar.progress = if (hide) 0 else progress
binding.tvLoanCompletion.text = bmlCompletionText(detail, ctx)
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
binding.loanDividerDetails.visibility = detailsVisible
binding.loanDetailsGroup.visibility = detailsVisible
if (expanded && detail != null) {
binding.tvLoanRepayment.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(detail.repayAmount)}"
binding.tvLoanIntRate.text = ctx.getString(R.string.loan_rate_fmt, detail.intRate)
binding.tvLoanStartDate.text = formatIsoDate(detail.startDate)
binding.tvLoanEndDate.text = formatIsoDate(detail.endDate)
if (detail.overdueAmount > 0) {
binding.loanRowOverdue.visibility = View.VISIBLE
binding.tvLoanOverdue.text = if (hide) "$currency ••••••"
else "$currency ${amountFmt.format(detail.overdueAmount)} (${detail.noOfRepayOverdue})"
} else {
binding.loanRowOverdue.visibility = View.GONE
}
}
}
private fun bmlCompletionText(detail: BmlLoanDetail?, ctx: android.content.Context): String {
if (detail == null) return ""
val outstanding = abs(detail.outstandingAmt)
if (outstanding <= 0.0 || detail.repayAmount <= 0.0)
return ctx.getString(R.string.financing_completion_done)
val remaining = ceil(outstanding / detail.repayAmount).toInt()
if (remaining <= 0) return ctx.getString(R.string.financing_completion_done)
val cal = Calendar.getInstance()
cal.add(Calendar.MONTH, remaining)
val month = SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(cal.time)
return ctx.getString(R.string.financing_completion_fmt, month)
}
private fun formatIsoDate(raw: String): String {
return try {
outputDateFmt.format(isoDateFmt.parse(raw)!!)
} catch (_: Exception) { raw.take(10) }
}
}
companion object {
private const val TYPE_MIB = 0
private const val TYPE_BML = 1
}
}

View File

@@ -10,6 +10,9 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlLoanDetail
import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.databinding.FragmentFinancingBinding
class FinancingFragment : Fragment() {
@@ -18,6 +21,10 @@ class FinancingFragment : Fragment() {
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
private lateinit var adapter: FinancingAdapter
private var financingRefreshing = false
private var latestMibDeals: List<MibFinanceDeal> = emptyList()
private var latestBmlLoanDetails: Map<String, BmlLoanDetail> = emptyMap()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFinancingBinding.inflate(inflater, container, false)
@@ -38,15 +45,41 @@ class FinancingFragment : Fragment() {
insets
}
binding.swipeRefresh.setOnRefreshListener {
financingRefreshing = true
(activity as? HomeActivity)?.triggerRefreshFinancing()
}
viewModel.accounts.observe(viewLifecycleOwner) { rebuildAdapter() }
viewModel.financing.observe(viewLifecycleOwner) { deals ->
adapter.updateDeals(deals)
binding.recyclerView.visibility = if (deals.isEmpty()) View.GONE else View.VISIBLE
binding.emptyView.visibility = if (deals.isEmpty()) View.VISIBLE else View.GONE
binding.loadingView.visibility = View.GONE
latestMibDeals = deals
rebuildAdapter()
}
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { details ->
latestBmlLoanDetails = details
rebuildAdapter()
}
viewModel.hideAmounts.observe(viewLifecycleOwner) { adapter.setHideAmounts(it) }
}
private fun rebuildAdapter() {
val accounts = viewModel.accounts.value ?: emptyList()
val loanAccounts = accounts.filter { it.profileType == "BML_LOAN" }
val bmlLoans: List<Pair<BankAccount, BmlLoanDetail?>> =
loanAccounts.map { acc -> acc to latestBmlLoanDetails[acc.internalId] }
adapter.update(latestMibDeals, bmlLoans)
val isEmpty = latestMibDeals.isEmpty() && bmlLoans.isEmpty()
binding.recyclerView.visibility = if (isEmpty) View.GONE else View.VISIBLE
binding.emptyView.visibility = if (isEmpty) View.VISIBLE else View.GONE
binding.loadingView.visibility = View.GONE
if (financingRefreshing) {
financingRefreshing = false
binding.swipeRefresh.isRefreshing = false
}
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_finances)

View File

@@ -40,6 +40,7 @@ import sh.sar.basedbank.api.bml.BmlAccountClient
import sh.sar.basedbank.api.bml.BmlActivationResult
import sh.sar.basedbank.api.bml.BmlContactsClient
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
import sh.sar.basedbank.api.bml.BmlLoanDetail
import sh.sar.basedbank.api.bml.BmlProfile
import sh.sar.basedbank.api.bml.BmlSession
import sh.sar.basedbank.api.fahipay.FahipayAccountClient
@@ -170,6 +171,8 @@ class HomeActivity : AppCompatActivity() {
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
if (cachedBmlLoans.isNotEmpty()) viewModel.bmlLoanDetails.value = cachedBmlLoans
val cachedLimits = ForeignLimitsCache.load(this)
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
@@ -178,6 +181,7 @@ class HomeActivity : AppCompatActivity() {
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
}
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
refreshBmlLoanDetails()
} else {
// Came from lock screen — show caches immediately, refresh everything in background
val store = CredentialStore(this)
@@ -188,6 +192,8 @@ class HomeActivity : AppCompatActivity() {
if (merged.isNotEmpty()) viewModel.accounts.value = merged
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
if (cachedBmlLoans.isNotEmpty()) viewModel.bmlLoanDetails.value = cachedBmlLoans
val cachedLimits = ForeignLimitsCache.load(this)
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
@@ -324,6 +330,19 @@ fun applyNavLabelVisibility() {
}
}
fun triggerRefresh() {
autoRefresh(CredentialStore(this))
}
fun triggerRefreshFinancing() {
val app = application as BasedBankApp
for ((loginId, session) in app.mibSessions) {
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
}
refreshBmlLoanDetails()
}
fun setRefreshing(visible: Boolean) {
binding.refreshIndicator.visibility = if (visible) View.VISIBLE else View.GONE
}
@@ -337,11 +356,12 @@ fun applyNavLabelVisibility() {
override fun onResume() {
super.onResume()
// Returning from LockActivity — skip the elapsed check and reset state.
// Returning from LockActivity — refresh sessions since they may have expired.
if (isLocked) {
isLocked = false
pauseTime = 0L
resetAutolockTimer()
autoRefresh(CredentialStore(this))
return
}
// If we were away long enough to have hit the autolock timeout (e.g. while
@@ -354,6 +374,9 @@ fun applyNavLabelVisibility() {
lock()
return
}
if (elapsed > 45_000L) {
autoRefresh(CredentialStore(this))
}
}
resetAutolockTimer()
}
@@ -427,14 +450,24 @@ fun applyNavLabelVisibility() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_lock) {
lock()
val avd = getDrawable(R.drawable.avd_lock) as? android.graphics.drawable.AnimatedVectorDrawable
if (avd != null) {
item.icon = avd
avd.start()
Handler(Looper.getMainLooper()).postDelayed({ lock() }, 200)
} else {
lock()
}
return true
}
if (item.itemId == R.id.action_hide_amounts) {
val newHidden = !(viewModel.hideAmounts.value ?: false)
viewModel.hideAmounts.value = newHidden
getSharedPreferences("prefs", MODE_PRIVATE).edit().putBoolean("hide_amounts", newHidden).apply()
invalidateOptionsMenu()
val avd = getDrawable(if (newHidden) R.drawable.avd_hide_amounts else R.drawable.avd_show_amounts)
as? android.graphics.drawable.AnimatedVectorDrawable
item.icon = avd
avd?.start()
return true
}
return super.onOptionsItemSelected(item)
@@ -635,6 +668,7 @@ fun applyNavLabelVisibility() {
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
}
refreshBmlLoanDetails()
}
}
@@ -872,6 +906,32 @@ fun applyNavLabelVisibility() {
}
}
private fun refreshBmlLoanDetails() {
val app = application as BasedBankApp
val loanAccounts = app.bmlAccounts.filter { it.profileType == "BML_LOAN" }
if (loanAccounts.isEmpty()) return
lifecycleScope.launch {
try {
val details = withContext(Dispatchers.IO) {
val map = mutableMapOf<String, BmlLoanDetail>()
for (acc in loanAccounts) {
val session = app.bmlSessionFor(acc) ?: continue
try {
val detail = BmlAccountClient().fetchLoanDetail(session, acc.internalId)
if (detail != null) map[acc.internalId] = detail
} catch (_: Exception) { /* keep existing */ }
}
map
}
if (details.isNotEmpty()) {
val merged = (viewModel.bmlLoanDetails.value ?: emptyMap()) + details
FinancingCache.saveBmlLoans(this@HomeActivity, merged)
viewModel.bmlLoanDetails.postValue(merged)
}
} catch (_: Exception) { /* keep cached data */ }
}
}
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
if (profiles.isEmpty()) return
val flow = (application as BasedBankApp).mibFlowFor(loginId)

View File

@@ -3,6 +3,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.bml.BmlLoanDetail
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
@@ -11,6 +12,8 @@ import sh.sar.basedbank.api.mib.MibFinanceDeal
class HomeViewModel : ViewModel() {
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
/** BML loan details keyed by account internalId. */
val bmlLoanDetails = MutableLiveData<Map<String, BmlLoanDetail>>(emptyMap())
val contacts = MutableLiveData<List<BankContact>>(emptyList())
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())

View File

@@ -60,7 +60,6 @@ class SettingsSecurityFragment : Fragment() {
// Auto-lock
binding.autolockToggle.check(when (prefs.getLong("autolock_timeout", 60_000L)) {
0L -> R.id.btnAutolockOff
30_000L -> R.id.btnAutolock30s
180_000L -> R.id.btnAutolock3m
300_000L -> R.id.btnAutolock5m
@@ -69,7 +68,6 @@ class SettingsSecurityFragment : Fragment() {
binding.autolockToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked) return@addOnButtonCheckedListener
val timeout = when (checkedId) {
R.id.btnAutolockOff -> 0L
R.id.btnAutolock30s -> 30_000L
R.id.btnAutolock3m -> 180_000L
R.id.btnAutolock5m -> 300_000L

View File

@@ -652,7 +652,7 @@ class TransferFragment : Fragment() {
ReceiptStore.save(requireContext(), receipt)
clearForm()
val activity = requireActivity() as HomeActivity
activity.refreshBalances(src)
activity.triggerRefresh()
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
} else if (!ok) {
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()

View File

@@ -138,6 +138,14 @@ class TransferHistoryFragment : Fragment() {
}
(activity as? HomeActivity)?.setRefreshing(true)
loadNextPages()
binding.swipeRefresh.setOnRefreshListener {
if (isLoading) {
binding.swipeRefresh.isRefreshing = false
} else {
resetAndReload()
}
}
}
override fun onResume() {
@@ -145,6 +153,19 @@ class TransferHistoryFragment : Fragment() {
requireActivity().title = getString(R.string.nav_transfer_history)
}
private fun resetAndReload() {
allTransactions.clear()
pendingImageNames.clear()
pendingIconUrls.clear()
firstBatchDone = false
val accounts = accountStates.map { it.account }
accountStates.clear()
accounts.forEach { accountStates.add(AccountState(it)) }
adapter.setTransactions(emptyList())
binding.emptyView.visibility = View.GONE
loadNextPages()
}
private fun loadNextPages() {
val activeStates = accountStates.filter { it.hasMore() }
if (isLoading || activeStates.isEmpty()) return
@@ -250,6 +271,7 @@ class TransferHistoryFragment : Fragment() {
if (!firstBatchDone) {
firstBatchDone = true
(activity as? HomeActivity)?.setRefreshing(false)
binding.swipeRefresh.isRefreshing = false
}
if (newTransactions.isNotEmpty()) {

View File

@@ -330,36 +330,48 @@ class TransferReceiptFragment : Fragment() {
}
private fun showFullScreenReceipt() {
captureReceiptBitmap { bitmap ->
if (bitmap == null) return@captureReceiptBitmap
val ctx = requireContext()
val dialog = Dialog(ctx, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
val iv = android.widget.ImageView(ctx).apply {
setImageBitmap(bitmap)
scaleType = android.widget.ImageView.ScaleType.FIT_CENTER
setBackgroundColor(Color.BLACK)
}
iv.setOnClickListener { dialog.dismiss() }
dialog.setContentView(iv)
val actWin = requireActivity().window
val prevColor = actWin.statusBarColor
val insetsCtrl = androidx.core.view.WindowInsetsControllerCompat(actWin, actWin.decorView)
actWin.statusBarColor = Color.BLACK
insetsCtrl.isAppearanceLightStatusBars = false
dialog.setOnDismissListener {
actWin.statusBarColor = prevColor
val isLight = (resources.configuration.uiMode and
android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
android.content.res.Configuration.UI_MODE_NIGHT_NO
insetsCtrl.isAppearanceLightStatusBars = isLight
}
dialog.show()
dialog.window?.let { win ->
androidx.core.view.WindowCompat.setDecorFitsSystemWindows(win, false)
androidx.core.view.WindowInsetsControllerCompat(win, iv).apply {
hide(androidx.core.view.WindowInsetsCompat.Type.systemBars())
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
val ctx = requireContext()
val bank = arguments?.getString(ARG_BANK, "MIB") ?: "MIB"
val dialog = Dialog(ctx, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
val scrollView = android.widget.ScrollView(ctx).apply {
setBackgroundColor(Color.BLACK)
}
val cardView = if (bank == "MIB") {
val binding = FragmentReceiptMibBinding.inflate(layoutInflater)
bindMib(binding)
binding.receiptCard
} else {
val binding = FragmentReceiptBmlBinding.inflate(layoutInflater)
bindBml(binding)
binding.receiptCard
}
(cardView.parent as? ViewGroup)?.removeView(cardView)
cardView.setOnClickListener { dialog.dismiss() }
scrollView.addView(cardView)
scrollView.setOnTouchListener { _, _ -> dialog.dismiss(); true }
dialog.setContentView(scrollView)
val actWin = requireActivity().window
val prevColor = actWin.statusBarColor
val insetsCtrl = androidx.core.view.WindowInsetsControllerCompat(actWin, actWin.decorView)
actWin.statusBarColor = Color.BLACK
insetsCtrl.isAppearanceLightStatusBars = false
dialog.setOnDismissListener {
actWin.statusBarColor = prevColor
val isLight = (resources.configuration.uiMode and
android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
android.content.res.Configuration.UI_MODE_NIGHT_NO
insetsCtrl.isAppearanceLightStatusBars = isLight
}
dialog.show()
dialog.window?.let { win ->
androidx.core.view.WindowCompat.setDecorFitsSystemWindows(win, false)
androidx.core.view.WindowInsetsControllerCompat(win, scrollView).apply {
hide(androidx.core.view.WindowInsetsCompat.Type.systemBars())
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}

View File

@@ -3,12 +3,14 @@ package sh.sar.basedbank.util
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import sh.sar.basedbank.api.bml.BmlLoanDetail
import sh.sar.basedbank.api.mib.MibFinanceDeal
object FinancingCache {
private const val PREFS = "financing_cache"
private const val KEY_MIB = "mib_financing"
private const val KEY_BML_LOANS = "bml_loans"
fun save(context: Context, deals: List<MibFinanceDeal>) {
val arr = JSONArray()
@@ -34,6 +36,52 @@ object FinancingCache {
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
}
fun saveBmlLoans(context: Context, loans: Map<String, BmlLoanDetail>) {
val arr = JSONArray()
for ((internalId, d) in loans) {
arr.put(JSONObject().apply {
put("internalId", internalId)
put("loanAmount", d.loanAmount)
put("outstandingAmt", d.outstandingAmt)
put("repayAmount", d.repayAmount)
put("intRate", d.intRate)
put("loanStatus", d.loanStatus)
put("startDate", d.startDate)
put("endDate", d.endDate)
put("noOfRepayOverdue", d.noOfRepayOverdue)
put("overdueAmount", d.overdueAmount)
})
}
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit().putString(KEY_BML_LOANS, CacheEncryption.encrypt(arr.toString())).apply()
}
fun loadBmlLoans(context: Context): Map<String, BmlLoanDetail> {
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_BML_LOANS, null) ?: return emptyMap()
return try {
val json = CacheEncryption.decrypt(raw)
val arr = JSONArray(json)
buildMap {
for (i in 0 until arr.length()) {
val o = arr.getJSONObject(i)
val id = o.optString("internalId")
if (id.isNotBlank()) put(id, BmlLoanDetail(
loanAmount = o.optDouble("loanAmount", 0.0),
outstandingAmt = o.optDouble("outstandingAmt", 0.0),
repayAmount = o.optDouble("repayAmount", 0.0),
intRate = o.optDouble("intRate", 0.0),
loanStatus = o.optString("loanStatus"),
startDate = o.optString("startDate"),
endDate = o.optString("endDate"),
noOfRepayOverdue = o.optInt("noOfRepayOverdue", 0),
overdueAmount = o.optDouble("overdueAmount", 0.0)
))
}
}
} catch (_: Exception) { emptyMap() }
}
fun clear(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
}

View File

@@ -22,6 +22,7 @@ class HistoryFetcher(private val account: BankAccount) {
private val isMib get() = account.bank == "MIB"
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
private val isBmlLoan get() = account.profileType == "BML_LOAN"
private val isFahipay get() = account.bank == "FAHIPAY"
// MIB pagination
@@ -40,6 +41,7 @@ class HistoryFetcher(private val account: BankAccount) {
private var fahipayTotal = -1
fun hasMore(): Boolean = when {
isBmlLoan -> false
isFahipay -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
isMib -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
isBmlCard -> cardMonthOffset < 3
@@ -47,6 +49,7 @@ class HistoryFetcher(private val account: BankAccount) {
}
suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List<BankTransaction> = when {
isBmlLoan -> emptyList()
isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) }
isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } }
isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) }

View File

@@ -10,7 +10,8 @@ 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: BankAccount): AccountListDisplay {
fun displayData(account: BankAccount): AccountListDisplay? {
if (account.profileType == "BML_LOAN") return null // Loans shown on financing page only
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
return if (isCard) {
val isActive = account.statusDesc.equals("Active", ignoreCase = true)

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
android:tint="?attr/colorControlNormal">
<path
android:name="strike_through"
android:pathData="M3.27,4.27 L19.74,20.74"
android:strokeColor="@android:color/white"
android:strokeLineCap="square"
android:strokeWidth="1.8"
android:trimPathEnd="0"/>
<group>
<clip-path
android:name="eye_mask"
android:pathData="M2,4.27 L2,4.27 L4.54,1.73 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z"/>
<path
android:name="eye"
android:fillColor="@android:color/white"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</group>
</vector>
</aapt:attr>
<target android:name="eye_mask">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="320"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M2,4.27 L2,4.27 L4.54,1.73 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z"
android:valueTo="M2,4.27 L19.73,22 L22.27,19.46 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z"
android:valueType="pathType"/>
</aapt:attr>
</target>
<target android:name="strike_through">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="320"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
android:tint="?attr/colorControlNormal">
<!--
Shackle drawn first (behind body) so it appears to slot into the body.
Starts translateY=-4 (open/raised), animates to 0 (locked).
-->
<group
android:name="shackle"
android:translateY="-4">
<path
android:fillColor="@android:color/transparent"
android:pathData="M8.9,10V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1V10"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"
android:strokeWidth="2.2"/>
</group>
<!--
Body on top — covers the shackle legs once they slide inside.
Even-odd fill cuts out the keyhole.
-->
<path
android:fillColor="@android:color/white"
android:fillType="evenOdd"
android:pathData="M6,8H18c1.1,0 2,0.9 2,2V20c0,1.1-0.9,2-2,2H6c-1.1,0-2,-0.9-2,-2V10c0,-1.1 0.9,-2 2,-2zM12,15c-1.1,0-2,0.9-2,2s0.9,2 2,2 2,-0.9 2,-2-0.9,-2-2,-2z"/>
</vector>
</aapt:attr>
<target android:name="shackle">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="320"
android:interpolator="@android:interpolator/overshoot"
android:propertyName="translateY"
android:valueFrom="-4"
android:valueTo="0"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
android:tint="?attr/colorControlNormal">
<path
android:name="strike_through"
android:pathData="M3.27,4.27 L19.74,20.74"
android:strokeColor="@android:color/white"
android:strokeLineCap="square"
android:strokeWidth="1.8"/>
<group>
<clip-path
android:name="eye_mask"
android:pathData="M2,4.27 L19.73,22 L22.27,19.46 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z"/>
<path
android:name="eye"
android:fillColor="@android:color/white"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</group>
</vector>
</aapt:attr>
<target android:name="eye_mask">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="200"
android:interpolator="@android:interpolator/fast_out_linear_in"
android:propertyName="pathData"
android:valueFrom="M2,4.27 L19.73,22 L22.27,19.46 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z"
android:valueTo="M2,4.27 L2,4.27 L4.54,1.73 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z"
android:valueType="pathType"/>
</aapt:attr>
</target>
<target android:name="strike_through">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="200"
android:interpolator="@android:interpolator/fast_out_linear_in"
android:propertyName="trimPathEnd"
android:valueFrom="1"
android:valueTo="0"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -5,7 +5,19 @@
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<!-- Shackle (behind body) -->
<path
android:fillColor="@android:color/transparent"
android:pathData="M8.9,10V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1V10"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"
android:strokeWidth="2.2"/>
<!-- Body + keyhole cutout -->
<path
android:fillColor="@android:color/white"
android:pathData="M18,8h-1V6c0-2.76-2.24-5-5-5S7,3.24 7,6v2H6c-1.1,0-2,0.9-2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1-0.9,-2-2,-2zm-6,9c-1.1,0-2,-0.9-2,-2s0.9,-2 2,-2 2,0.9 2,2-0.9,2-2,2zm3.1,-9H8.9V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
android:fillType="evenOdd"
android:pathData="M6,8H18c1.1,0 2,0.9 2,2V20c0,1.1-0.9,2-2,2H6c-1.1,0-2,-0.9-2,-2V10c0,-1.1 0.9,-2 2,-2zM12,15c-1.1,0-2,0.9-2,2s0.9,2 2,2 2,-0.9 2,-2-0.9,-2-2,-2z"/>
</vector>

View File

@@ -31,28 +31,35 @@
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
android:clipToPadding="false" />
android:layout_height="match_parent">
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No transactions found"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
android:clipToPadding="false" />
</FrameLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No transactions found"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@@ -1,11 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerView"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false"
android:background="?attr/colorSurface" />
android:background="?attr/colorSurface">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -41,34 +41,41 @@
app:tabMode="scrollable"
app:tabGravity="start" />
<FrameLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loadingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/contacts_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
<ProgressBar
android:id="@+id/loadingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/contacts_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@@ -7,11 +7,16 @@
android:orientation="vertical"
android:background="?attr/colorSurface">
<androidx.core.widget.NestedScrollView
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -92,6 +97,85 @@
</LinearLayout>
<!-- Available Credit row (hidden when no credit cards) -->
<LinearLayout
android:id="@+id/rowCreditCards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp"
android:visibility="gone">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardMvrCredit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/balance_mvr_credit"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvMvrCredit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardUsdCredit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/balance_usd_credit"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvUsdCredit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="USD —"
android:textAppearance="?attr/textAppearanceTitleMedium" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Pending Finances card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
@@ -172,6 +256,8 @@
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- Quick actions fixed at bottom -->
<LinearLayout
android:id="@+id/buttonBar"

View File

@@ -1,41 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false" />
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/loadingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/loadingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
</LinearLayout>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/financing_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
</LinearLayout>
</FrameLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/financing_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -112,14 +112,6 @@
app:singleSelection="true"
app:selectionRequired="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAutolockOff"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/autolock_off" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAutolock30s"
style="@style/Widget.Material3.Button.OutlinedButton"

View File

@@ -31,29 +31,36 @@
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="4dp"
android:paddingBottom="16dp"
android:clipToPadding="false" />
android:layout_height="match_parent">
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No transactions found"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="4dp"
android:paddingBottom="16dp"
android:clipToPadding="false" />
</FrameLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No transactions found"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@@ -0,0 +1,304 @@
<?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_marginBottom="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutlineVariant">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<!-- Header row: product name + status chip -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvLoanProduct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvLoanAccount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginTop="2dp" />
</LinearLayout>
<TextView
android:id="@+id/tvLoanStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="4dp"
android:background="@drawable/chip_background"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
<!-- Loan amount -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/financing_total"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvLoanTotal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginStart="8dp" />
</LinearLayout>
<!-- Progress bar -->
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loanProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:trackCornerRadius="4dp"
app:trackThickness="8dp" />
<!-- Paid / Outstanding row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/financing_paid"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvLoanPaid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface"
android:layout_marginTop="2dp" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="end">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loan_outstanding"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvLoanOutstanding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="@color/color_unpaid"
android:layout_marginTop="2dp" />
</LinearLayout>
</LinearLayout>
<!-- Completion estimate -->
<TextView
android:id="@+id/tvLoanCompletion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<!-- Divider -->
<View
android:id="@+id/loanDividerDetails"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorOutlineVariant"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:visibility="gone" />
<!-- Expanded details -->
<LinearLayout
android:id="@+id/loanDetailsGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<!-- Monthly repayment -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/loan_monthly_repayment"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvLoanRepayment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurface" />
</LinearLayout>
<!-- Interest rate -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/loan_interest_rate"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvLoanIntRate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurface" />
</LinearLayout>
<!-- Start date -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/loan_start_date"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvLoanStartDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurface" />
</LinearLayout>
<!-- End date -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/loan_end_date"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tvLoanEndDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurface" />
</LinearLayout>
<!-- Overdue (only shown when > 0) -->
<LinearLayout
android:id="@+id/loanRowOverdue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/loan_overdue_payments"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="@color/color_unpaid" />
<TextView
android:id="@+id/tvLoanOverdue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="@color/color_unpaid" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -12,14 +12,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:padding="20dp">
<!-- Header: name + type chip -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
@@ -31,7 +32,7 @@
android:id="@+id/tvLimitUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
@@ -49,103 +50,19 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="5dp"
android:paddingVertical="4dp"
android:background="@drawable/chip_background"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurface"
android:background="@drawable/pill_segment_bg" />
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="12dp"
android:background="?attr/colorOutlineVariant" />
<!-- General -->
<!-- ECOM bar (always visible) -->
<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">
android:layout_marginBottom="4dp">
<TextView
android:layout_width="0dp"
@@ -164,22 +81,31 @@
</LinearLayout>
<!-- POS -->
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressEcom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:trackCornerRadius="4dp"
app:trackThickness="8dp" />
<!-- General bar (always visible) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="POS"
android:text="General"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvLimitPos"
android:id="@+id/tvLimitGeneral"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodyMedium"
@@ -187,6 +113,131 @@
</LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressGeneral"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:trackCornerRadius="4dp"
app:trackThickness="8dp" />
<!-- Divider (visible when expanded) -->
<View
android:id="@+id/dividerLimitDetails"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorOutlineVariant"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:visibility="gone" />
<!-- Expanded details -->
<LinearLayout
android:id="@+id/detailsGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<!-- ATM bar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:id="@+id/tvAtmLabel"
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>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressAtm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:trackCornerRadius="4dp"
app:trackThickness="8dp" />
<!-- POS bar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:id="@+id/tvPosLabel"
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>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressPos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:trackCornerRadius="4dp"
app:trackThickness="8dp" />
<!-- Medical bar -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<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>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressMedical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:trackCornerRadius="4dp"
app:trackThickness="8dp" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -95,6 +95,8 @@
<string name="dashboard_quick_actions">Quick Actions</string>
<string name="balance_mvr">MVR Total</string>
<string name="balance_usd">USD Total</string>
<string name="balance_mvr_credit">MVR Available Credit</string>
<string name="balance_usd_credit">USD Available Credit</string>
<string name="card_support_wip">Card Support</string>
<string name="transfer">Transfer</string>
<string name="pay_mv_qr">PayMV QR</string>
@@ -261,4 +263,13 @@
<string name="financing_completion_done">Fully paid</string>
<string name="financing_deal_no_fmt">Deal #%s</string>
<string name="financing_completion_fmt">Completes %s</string>
<!-- BML Loans -->
<string name="loan_outstanding">Outstanding</string>
<string name="loan_monthly_repayment">Monthly Repayment</string>
<string name="loan_interest_rate">Interest Rate</string>
<string name="loan_start_date">Start Date</string>
<string name="loan_end_date">End Date</string>
<string name="loan_overdue_payments">Overdue Payments</string>
<string name="loan_rate_fmt">%.2f%%</string>
</resources>