Compare commits

..

4 Commits

Author SHA1 Message Date
ahusan 045eb1b1a2 ui: pass 3 — skeleton loading, a11y, snackbars, dark mode
Skeleton loading state:
  * Wire item_skeleton_transaction (previously dormant in res) into
    AccountHistoryFragment and TransferHistoryFragment. When a load
    starts with no cached transactions, 8 skeleton rows appear under
    the search bar instead of a blank screen. Skeleton hides as soon
    as transactions arrive or the load completes.
  * Decision: ActivitiesFragment loads from local storage synchronously,
    so no skeleton there — would only flash for a frame.

Accessibility:
  * Decorative icons in item_more_nav, item_nav_slot, item_picker_row,
    and item_account_dropdown now declare importantForAccessibility="no"
    so TalkBack skips them (the adjacent text label conveys meaning).
  * Also added tnum to item_picker_row.tvBalance and
    item_account_dropdown.tvDropdownBalance — missed amounts from Pass 2.

Snackbar:
  * ContactsFragment.deleteContact's success/failure feedback is now
    a Snackbar instead of Toast — the fragment stays visible, so a
    bottom-anchored Snackbar fits better than a floating Toast.
  * Other Toasts are either followed by navigation away (Snackbar
    couldn't display), or purely informational ("copied", "press back
    to exit") where Toast is the right primitive. Left as-is.

Dark mode:
  * Connectivity banner in activity_home no longer hardcodes
    #C62828 / #FFFFFF — uses colorErrorContainer / colorOnErrorContainer
    so it adapts to the user's theme.
  * Other hex literals (card-artwork text, receipt screens that
    intentionally emulate paper) are intentional and stay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:02:50 +05:00
ahusan 22806da1b3 ui: pass 2 — pill style, tabular figures, ripple feedback, empty strings
Status pills:
  * New Widget.App.StatusChip style bundling the 10dp/4dp padding +
    chip_background + label-small text used in 3 places (item_bml_loan,
    item_finance_deal, item_profile_header). The 3 layouts now apply
    the style instead of repeating 4 attributes each.
  * Extract the bindCardStatus card-overlay pill background from inline
    GradientDrawable code into a real drawable resource
    (card_overlay_pill_bg.xml). PayWithCardFragment.bindCardStatus just
    sets a background resource now.

Tabular figures (android:fontFeatureSettings="tnum"):
  * Applied to amount TextViews on the highest-traffic surfaces —
    transactions list, accounts list, card list, dashboard balance /
    credit / blocked / overdue / pending finances totals, and the
    account history header (available / balance / blocked).
  * Digits become same-width, so right-aligned amounts in a column
    line up.

Ripple feedback:
  * item_finance_deal, item_bml_loan, item_foreign_limit get
    android:clickable="true" android:focusable="true" so their
    MaterialCardView roots show the M3 ripple when the adapter
    attaches a click listener. All three are clickable from code
    but were missing the ripple cue.

Empty-state strings:
  * Hardcoded "No recent transfers" / "No transactions found" in
    fragment_activities, fragment_transfer_history, and
    fragment_account_history moved to strings.xml as
    activities_empty / transactions_empty, matching the existing
    accounts_empty / contacts_empty / financing_empty pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:48:23 +05:00
ahusan 29ac4352ff ui: swap list-item and screen-margin paddings onto tokens
Replaces hardcoded paddings in the three layouts that share the
standard 16dp/14dp list-row pattern (item_account, item_card,
item_more_nav) with @dimen/list_item_padding_h / list_item_padding_v.

Dashboard root content padding becomes @dimen/screen_margin. Card
content paddings inside the dashboard are left as raw 16dp — they're
a separate semantic (card-interior padding, not screen edge) and
will be addressed once we either need a distinct token or have a
reason to change them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:32:18 +05:00
ahusan df892ec8d5 ui: tokenize card surfaces in dimens.xml and sweep layouts
Introduces a small set of design tokens for card surfaces so the same
look is enforced everywhere instead of being hand-tuned per layout:

  card_radius_lg / card_radius_md       — 16dp / 12dp two-tier radius
  card_elevation_flat / *_raised        — 0dp outlined / 1dp subtle raise

Sweeps the existing layouts onto these tokens. Two-tier policy:

  lg → hero/standalone cards (account history header, bml loan,
       finance deal, otp card, bank selection tiles, security setup
       options)
  md → repeating row/summary cards (dashboard summary cards,
       credentials inset, foreign limit, add-contact sheet)

Card-visual representations (item_card_dashboard / _stack / _wallet
/ _settings_entry) deliberately keep their bespoke radius/elevation
so they continue to read as credit cards — the tokens are for
content cards only. Tight inset frames inside fragment_bml_qr_pay
and fragment_transfer also remain on 4dp/0dp as form details.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:20:16 +05:00
36 changed files with 257 additions and 363 deletions
@@ -61,51 +61,38 @@ class MibContactsClient {
}
}
/** Fetch a single page of contacts. Returns the list and the server-reported total count.
* [start] is 1-based; [count] is the desired page size. */
fun fetchContactsPage(
session: MibSession,
start: Int,
count: Int
): Pair<List<MibBeneficiary>, Int> {
val end = start + count - 1
// Server uses 100-row pages internally for its own pagination; we mirror that
// by computing a page index that aligns with our start offset.
val page = ((start - 1) / 100) + 1
val body = FormBody.Builder()
.add("page", page.toString())
.add("search", "")
.add("searchCategoryId", "0")
.add("benefType", "A")
.add("sortBenef", "name")
.add("sortDir", "asc")
.add("start", start.toString())
.add("end", end.toString())
.add("includeCount", "1")
.build()
val request = Request.Builder()
.url("$BASE_WV_URL/ajaxBeneficiary/main")
.post(body)
.withSessionHeaders(session)
.build()
return client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return Pair(emptyList(), 0)
parseContacts(response.body?.string() ?: return Pair(emptyList(), 0))
}
}
/** Convenience: fetch all pages and return the union. Used by callers that don't paginate. */
fun fetchContacts(session: MibSession): List<MibBeneficiary> {
val all = mutableListOf<MibBeneficiary>()
var start = 1
var page = 1
val pageSize = 100
while (true) {
val (contacts, totalCount) = fetchContactsPage(session, start, pageSize)
val start = (page - 1) * pageSize + 1
val end = page * pageSize
val body = FormBody.Builder()
.add("page", page.toString())
.add("search", "")
.add("searchCategoryId", "0")
.add("benefType", "A")
.add("sortBenef", "name")
.add("sortDir", "asc")
.add("start", start.toString())
.add("end", end.toString())
.add("includeCount", "1")
.build()
val request = Request.Builder()
.url("$BASE_WV_URL/ajaxBeneficiary/main")
.post(body)
.withSessionHeaders(session)
.build()
val (contacts, totalCount) = client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return all
parseContacts(response.body?.string() ?: return all)
}
all.addAll(contacts)
if (all.size >= totalCount || contacts.isEmpty()) break
start += pageSize
page++
}
return all
}
@@ -1,80 +0,0 @@
package sh.sar.basedbank.api.mib
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
/**
* Streams MIB contacts one page at a time, walking the user's MIB profiles in order.
*
* For multi-profile users we stay on the current profile until its server-reported
* `total_count` is reached, then advance to the next profile. Each profile switch is
* protected by the global `mibMutex` to keep the session state coherent.
*
* Categories are fetched lazily on the first page of each profile and accumulated
* across calls — the latest snapshot is exposed via [allCategories].
*/
class MibContactsPaginator(
private val app: BasedBankApp,
val loginId: String,
private val session: MibSession,
private val profiles: List<MibProfile>,
private val client: MibContactsClient = MibContactsClient()
) {
private var profileIndex = 0
private var nextStart = 1
private var totalForCurrentProfile = -1
private var categoriesFetchedForProfile = false
private val accumulatedCategories = LinkedHashMap<String, BankContactCategory>()
val allCategories: List<BankContactCategory>
get() = accumulatedCategories.values.toList()
fun hasMore(): Boolean = profileIndex < profiles.size
/** Fetch one page of up to [count] contacts. Returns the contacts contributed by
* this call (may be empty if a profile errored or returned no rows). Advances
* the internal cursor; subsequent calls continue from where this left off. */
suspend fun nextPage(count: Int): List<BankContact> = withContext(Dispatchers.IO) {
if (!hasMore()) return@withContext emptyList()
val profile = profiles[profileIndex]
val flow = app.mibFlowFor(loginId)
val contacts = app.mibMutex.withLock {
try {
flow.switchProfile(session, profile)
if (!categoriesFetchedForProfile) {
for (cat in client.fetchCategories(session)) {
accumulatedCategories.putIfAbsent(
cat.id,
BankContactCategory(cat.id, cat.categoryName, cat.numBenef)
)
}
categoriesFetchedForProfile = true
}
val (page, total) = client.fetchContactsPage(session, nextStart, count)
if (total > 0) totalForCurrentProfile = total
nextStart += page.size.coerceAtLeast(count)
page.map { it.copy(profileId = profile.profileId) }
} catch (_: Exception) {
// Bail on this profile and advance — keeps the paginator robust.
emptyList()
}
}
val exhausted =
(totalForCurrentProfile > 0 && nextStart - 1 >= totalForCurrentProfile) ||
contacts.isEmpty()
if (exhausted) {
profileIndex++
nextStart = 1
totalForCurrentProfile = -1
categoriesFetchedForProfile = false
}
contacts
}
}
@@ -142,6 +142,8 @@ class AccountHistoryFragment : Fragment() {
}
adapter.setTransactions(filtered)
binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE
binding.skeletonContainer.visibility =
if (allTransactions.isEmpty() && isLoading) View.VISIBLE else View.GONE
}
private fun resetAndReload() {
@@ -168,6 +170,9 @@ class AccountHistoryFragment : Fragment() {
if (firstPageDone && allTransactions.isNotEmpty()) {
adapter.showLoadingFooter = true
} else if (allTransactions.isEmpty()) {
binding.skeletonContainer.visibility = View.VISIBLE
binding.emptyView.visibility = View.GONE
}
val app = requireActivity().application as BasedBankApp
@@ -187,6 +192,7 @@ class AccountHistoryFragment : Fragment() {
isLoading = false
if (_binding == null) return@launch
binding.skeletonContainer.visibility = View.GONE
if (!firstPageDone) {
firstPageDone = true
@@ -8,7 +8,6 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
@@ -21,7 +20,7 @@ class ContactsAdapter(
private val onImageNeeded: (hash: String) -> Unit,
private val onDeleteClick: (ContactDisplay) -> Unit,
private val onTransferClick: (ContactDisplay) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
private var allContacts: List<ContactDisplay> = emptyList()
private var displayed: List<ContactDisplay> = emptyList()
@@ -29,15 +28,6 @@ class ContactsAdapter(
private var activeCategoryId: String? = null
private var searchQuery: String = ""
/** When true, a loading footer row is shown after the last contact. */
var showLoadingFooter: Boolean = false
set(value) {
if (field == value) return
field = value
if (value) notifyItemInserted(displayed.size)
else notifyItemRemoved(displayed.size)
}
fun updateContacts(contacts: List<ContactDisplay>) {
allContacts = contacts
applyFilter()
@@ -68,32 +58,24 @@ class ContactsAdapter(
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int =
if (position == displayed.size && showLoadingFooter) VIEW_TYPE_FOOTER else VIEW_TYPE_CONTACT
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == VIEW_TYPE_FOOTER) {
val footer = LayoutInflater.from(parent.context)
.inflate(R.layout.item_loading_footer, parent, false)
return FooterHolder(footer)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemContactBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = ViewHolder(binding)
binding.btnTransferContact.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION && pos < displayed.size) onTransferClick(displayed[pos])
if (pos != RecyclerView.NO_POSITION) onTransferClick(displayed[pos])
}
binding.btnEditContact.setOnClickListener {
Toast.makeText(it.context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
binding.btnDeleteContact.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION && pos < displayed.size) onDeleteClick(displayed[pos])
if (pos != RecyclerView.NO_POSITION) onDeleteClick(displayed[pos])
}
binding.root.setOnLongClickListener {
val pos = holder.bindingAdapterPosition
if (pos == RecyclerView.NO_POSITION || pos >= displayed.size) return@setOnLongClickListener false
if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener false
val account = displayed[pos].accountNumber
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("account", account))
@@ -104,23 +86,15 @@ class ContactsAdapter(
return holder
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is FooterHolder) return
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val contact = displayed[position]
val cachedImage = contact.imageHash?.let { hash ->
imageCache[hash] ?: run { onImageNeeded(hash); null }
}
(holder as ViewHolder).bind(contact, cachedImage)
holder.bind(contact, cachedImage)
}
override fun getItemCount() = displayed.size + if (showLoadingFooter) 1 else 0
private class FooterHolder(view: View) : RecyclerView.ViewHolder(view)
private companion object {
const val VIEW_TYPE_CONTACT = 0
const val VIEW_TYPE_FOOTER = 1
}
override fun getItemCount() = displayed.size
inner class ViewHolder(val binding: ItemContactBinding) :
RecyclerView.ViewHolder(binding.root) {
@@ -9,6 +9,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.widget.addTextChangedListener
@@ -76,9 +77,6 @@ class ContactsFragment : Fragment() {
fun updateImage(hash: String, bitmap: Bitmap) =
contactAdapters.forEach { it.updateImage(hash, bitmap) }
fun setLoadingFooter(visible: Boolean) =
contactAdapters.forEach { it.showLoadingFooter = visible }
inner class PageHolder(val rv: RecyclerView) : RecyclerView.ViewHolder(rv)
override fun getItemCount() = pages.size
@@ -96,7 +94,6 @@ class ContactsFragment : Fragment() {
val p80 = (65 * density).toInt()
setPadding(0, p4, 0, p80)
adapter = contactAdapters[viewType]
addOnScrollListener(scrollListener)
}
return PageHolder(rv)
}
@@ -104,24 +101,6 @@ class ContactsFragment : Fragment() {
override fun onBindViewHolder(holder: PageHolder, position: Int) {}
}
/** Triggers paginated MIB contact loading when the user scrolls within
* LOAD_MORE_THRESHOLD rows of the end of any tab. */
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
if (dy <= 0) return
val lm = rv.layoutManager as? LinearLayoutManager ?: return
val total = lm.itemCount
if (total == 0) return
if (lm.findLastVisibleItemPosition() >= total - LOAD_MORE_THRESHOLD) {
(activity as? HomeActivity)?.loadMoreMibContacts()
}
}
}
private companion object {
const val LOAD_MORE_THRESHOLD = 5
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentContactsBinding.inflate(inflater, container, false)
return binding.root
@@ -171,15 +150,6 @@ class ContactsFragment : Fragment() {
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
binding.loadingView.visibility = View.GONE
}
// Show a loading footer on each contact list while more MIB pages are arriving.
val updateFooter: () -> Unit = {
val loading = viewModel.mibContactsLoading.value == true
val hasMore = viewModel.mibContactsHasMore.value == true
pagerAdapter.setLoadingFooter(loading && hasMore)
}
viewModel.mibContactsLoading.observe(viewLifecycleOwner) { updateFooter() }
viewModel.mibContactsHasMore.observe(viewLifecycleOwner) { updateFooter() }
}
private fun attachMediator(pages: List<TabPage>) {
@@ -196,10 +166,6 @@ class ContactsFragment : Fragment() {
}
val savedPosition = binding.viewPager.currentItem
pagerAdapter = ContactsPagerAdapter(pages)
pagerAdapter.setLoadingFooter(
(viewModel.mibContactsLoading.value == true) &&
(viewModel.mibContactsHasMore.value == true)
)
binding.viewPager.adapter = pagerAdapter
attachMediator(pages)
binding.viewPager.setCurrentItem(savedPosition.coerceIn(0, pages.size - 1), false)
@@ -229,10 +195,10 @@ class ContactsFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch {
val success = withContext(Dispatchers.IO) { ContactManager.delete(contact, app) }
if (success) {
Toast.makeText(requireContext(), R.string.contact_deleted, Toast.LENGTH_SHORT).show()
removeFromViewModel(contact)
Snackbar.make(binding.root, R.string.contact_deleted, Snackbar.LENGTH_SHORT).show()
} else {
Toast.makeText(requireContext(), R.string.contact_delete_failed, Toast.LENGTH_SHORT).show()
Snackbar.make(binding.root, R.string.contact_delete_failed, Snackbar.LENGTH_LONG).show()
}
}
}
@@ -57,7 +57,6 @@ import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibContactsPaginator
import sh.sar.basedbank.api.mib.MibCardsClient
import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.api.mib.MibProfile
@@ -74,10 +73,6 @@ class HomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityHomeBinding
private val viewModel: HomeViewModel by viewModels()
/** One paginator per MIB login. Reset on every refresh. */
private var mibContactPaginators: List<MibContactsPaginator> = emptyList()
private var mibLoadMoreInFlight = false
private lateinit var toggle: ActionBarDrawerToggle
private var suppressBottomNavCallback = false
@@ -928,78 +923,44 @@ fun applyNavLabelVisibility() {
return result
}
/** Spin up (or replace) the paginator for this MIB login and fetch the first page.
* Subsequent pages arrive via [loadMoreMibContacts] in response to user scrolling. */
private fun refreshContacts(loginId: String, session: MibSession, profiles: List<MibProfile>) {
if (profiles.isEmpty()) return
val paginator = MibContactsPaginator(
app = application as BasedBankApp,
loginId = loginId,
session = session,
profiles = profiles
)
// Replace any existing paginator for this login.
mibContactPaginators = mibContactPaginators.filter { it.loginId != loginId } + paginator
viewModel.mibContactsHasMore.value = mibContactPaginators.any { it.hasMore() }
val flow = (application as BasedBankApp).mibFlowFor(loginId)
val contactsClient = MibContactsClient()
lifecycleScope.launch {
viewModel.mibContactsLoading.postValue(true)
val firstPage = try {
paginator.nextPage(MIB_FIRST_PAGE_SIZE)
} catch (_: Exception) {
null
}
// Treat a successful refresh as authoritative: replace the cached MIB list
// with the first page, even if it shrinks (so deletions take effect).
if (firstPage != null) {
ContactsCache.save(this@HomeActivity, firstPage, paginator.allCategories)
publishContacts(paginator.allCategories)
}
viewModel.mibContactsLoading.postValue(false)
viewModel.mibContactsHasMore.postValue(mibContactPaginators.any { it.hasMore() })
try {
val (allContacts, allCategories) = withContext(Dispatchers.IO) {
val seenContacts = mutableSetOf<String>()
val seenCategories = mutableSetOf<String>()
val contacts = mutableListOf<BankContact>()
val categories = mutableListOf<BankContactCategory>()
for (profile in profiles) {
try {
flow.switchProfile(session, profile)
for (cat in contactsClient.fetchCategories(session)) {
if (seenCategories.add(cat.id)) categories.add(cat)
}
for (contact in contactsClient.fetchContacts(session)) {
if (seenContacts.add(contact.benefNo))
contacts.add(contact.copy(profileId = profile.profileId))
}
} catch (_: Exception) { /* profile has no contacts access */ }
}
Pair(contacts, categories)
}
if (allContacts.isNotEmpty()) {
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
val store = sh.sar.basedbank.util.CredentialStore(this@HomeActivity)
val bmlContacts = ContactsCache.loadBml(this@HomeActivity, store.getBmlLoginIds())
val fahipayContacts = ContactsCache.loadFahipay(this@HomeActivity)
val fahipayCategories = ContactsCache.loadFahipayCategories(this@HomeActivity)
viewModel.contacts.postValue(mergeContacts(mergeContacts(allContacts, bmlContacts), fahipayContacts))
viewModel.contactCategories.postValue(allCategories + fahipayCategories)
}
} catch (_: Exception) { /* keep cached data */ }
}
}
/** Called by ContactsFragment when the user scrolls near the end of the loaded list. */
fun loadMoreMibContacts() {
if (mibLoadMoreInFlight) return
val paginator = mibContactPaginators.firstOrNull { it.hasMore() } ?: return
mibLoadMoreInFlight = true
lifecycleScope.launch {
viewModel.mibContactsLoading.postValue(true)
val page = try {
paginator.nextPage(MIB_NEXT_PAGE_SIZE)
} catch (_: Exception) {
emptyList()
}
if (page.isNotEmpty()) {
ContactsCache.appendContacts(this@HomeActivity, page, paginator.allCategories)
publishContacts(paginator.allCategories)
}
mibLoadMoreInFlight = false
viewModel.mibContactsLoading.postValue(false)
viewModel.mibContactsHasMore.postValue(mibContactPaginators.any { it.hasMore() })
}
}
/** Merge the current MIB cache with BML and Fahipay caches and post to the view model. */
private fun publishContacts(mibCategories: List<BankContactCategory>) {
val store = sh.sar.basedbank.util.CredentialStore(this)
val mibContacts = ContactsCache.loadContacts(this)
val bmlContacts = ContactsCache.loadBml(this, store.getBmlLoginIds())
val fahipayContacts = ContactsCache.loadFahipay(this)
val fahipayCategories = ContactsCache.loadFahipayCategories(this)
viewModel.contacts.postValue(
mergeContacts(mergeContacts(mibContacts, bmlContacts), fahipayContacts)
)
viewModel.contactCategories.postValue(mibCategories + fahipayCategories)
}
companion object {
private const val MIB_FIRST_PAGE_SIZE = 30
private const val MIB_NEXT_PAGE_SIZE = 100
}
fun refreshBalances(src: BankAccount) {
val app = application as BasedBankApp
lifecycleScope.launch {
@@ -23,11 +23,6 @@ class HomeViewModel : ViewModel() {
val contacts = MutableLiveData<List<BankContact>>(emptyList())
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())
/** True while MIB contacts are paging in (initial load or scroll-triggered next page). */
val mibContactsLoading = MutableLiveData<Boolean>(false)
/** True while any MIB paginator still has pages to deliver. */
val mibContactsHasMore = MutableLiveData<Boolean>(false)
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
@@ -305,12 +305,7 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
if (statusLabel == null) { tv.visibility = View.GONE; return }
tv.visibility = View.VISIBLE
tv.text = statusLabel
val dp = tv.context.resources.displayMetrics.density
tv.background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 12 * dp
setColor(0xCC212121.toInt())
}
tv.setBackgroundResource(R.drawable.card_overlay_pill_bg)
}
fun formatMasked(masked: String): String {
@@ -181,6 +181,9 @@ class TransferHistoryFragment : Fragment() {
if (firstBatchDone && allTransactions.isNotEmpty()) {
adapter.showLoadingFooter = true
} else if (allTransactions.isEmpty()) {
binding.skeletonContainer.visibility = View.VISIBLE
binding.emptyView.visibility = View.GONE
}
val app = requireActivity().application as BasedBankApp
@@ -286,6 +289,7 @@ class TransferHistoryFragment : Fragment() {
isLoading = false
if (_binding == null) return@launch
binding.skeletonContainer.visibility = View.GONE
val raw = bannerMsg.get()
when {
@@ -329,6 +333,8 @@ class TransferHistoryFragment : Fragment() {
}
adapter.setTransactions(filtered)
binding.emptyView.visibility = if (filtered.isEmpty() && !isLoading) View.VISIBLE else View.GONE
binding.skeletonContainer.visibility =
if (allTransactions.isEmpty() && isLoading) View.VISIBLE else View.GONE
}
private fun loadContactImage(name: String) {
@@ -51,21 +51,6 @@ object ContactsCache {
prefs.apply()
}
/** Merge new MIB contacts into the cached list, deduping by benefNo. Categories
* are replaced wholesale (the latest snapshot is canonical). Safe to call
* repeatedly as pages arrive. */
fun appendContacts(
context: Context,
newContacts: List<BankContact>,
categories: List<BankContactCategory>
) {
if (newContacts.isEmpty() && categories.isEmpty()) return
val existing = loadContacts(context)
val seen = existing.map { it.benefNo }.toHashSet()
val merged = existing + newContacts.filter { seen.add(it.benefNo) }
save(context, merged, categories)
}
fun clear(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Translucent dark pill used as a status overlay on card-visual surfaces
(item_card_dashboard / _stack / _wallet / _settings_entry). Theme-independent
by design: it sits on top of card artwork, not the surface color. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="12dp" />
<solid android:color="#CC212121" />
</shape>
+2 -2
View File
@@ -44,8 +44,8 @@
android:id="@+id/connectivityBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#C62828"
android:textColor="#FFFFFF"
android:background="?attr/colorErrorContainer"
android:textColor="?attr/colorOnErrorContainer"
android:gravity="center"
android:paddingTop="6dp"
android:paddingBottom="6dp"
@@ -48,12 +48,28 @@
android:paddingBottom="16dp"
android:clipToPadding="false" />
<LinearLayout
android:id="@+id/skeletonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
</LinearLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No transactions found"
android:text="@string/transactions_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
@@ -49,7 +49,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No recent transfers"
android:text="@string/activities_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
@@ -38,7 +38,7 @@
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:cardCornerRadius="@dimen/card_radius_lg"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
@@ -81,7 +81,7 @@
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:cardCornerRadius="@dimen/card_radius_lg"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
@@ -125,7 +125,7 @@
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:cardCornerRadius="@dimen/card_radius_lg"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
@@ -117,8 +117,8 @@
android:layout_marginBottom="8dp"
android:visibility="invisible"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
app:cardCornerRadius="@dimen/card_radius_md"
app:cardElevation="@dimen/card_elevation_flat">
<LinearLayout
android:layout_width="match_parent"
+30 -22
View File
@@ -21,7 +21,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:padding="@dimen/screen_margin">
<!-- Balance cards row -->
<LinearLayout
@@ -34,8 +34,8 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp">
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md">
<LinearLayout
android:layout_width="match_parent"
@@ -56,7 +56,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium" />
android:textAppearance="?attr/textAppearanceTitleMedium"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -67,8 +68,8 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp">
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md">
<LinearLayout
android:layout_width="match_parent"
@@ -89,7 +90,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="USD —"
android:textAppearance="?attr/textAppearanceTitleMedium" />
android:textAppearance="?attr/textAppearanceTitleMedium"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -112,8 +114,8 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp">
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md">
<LinearLayout
android:layout_width="match_parent"
@@ -134,7 +136,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium" />
android:textAppearance="?attr/textAppearanceTitleMedium"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -146,8 +149,8 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp">
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md">
<LinearLayout
android:layout_width="match_parent"
@@ -168,7 +171,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="USD —"
android:textAppearance="?attr/textAppearanceTitleMedium" />
android:textAppearance="?attr/textAppearanceTitleMedium"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -191,8 +195,8 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:visibility="gone">
@@ -216,7 +220,8 @@
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
android:textColor="?attr/colorOnErrorContainer"
android:fontFeatureSettings="tnum" />
<TextView
android:id="@+id/tvBlockedSecondary"
@@ -225,6 +230,7 @@
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer"
android:fontFeatureSettings="tnum"
android:alpha="0.75"
android:visibility="gone" />
@@ -238,8 +244,8 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:clickable="true"
android:focusable="true"
@@ -265,7 +271,8 @@
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
android:textColor="?attr/colorOnErrorContainer"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -279,8 +286,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md"
android:clickable="true"
android:focusable="true">
@@ -303,7 +310,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium" />
android:textAppearance="?attr/textAppearanceTitleMedium"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -52,7 +52,7 @@
android:layout_marginBottom="12dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:cardCornerRadius="@dimen/card_radius_lg"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
<LinearLayout
@@ -94,7 +94,7 @@
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:cardCornerRadius="@dimen/card_radius_lg"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline">
<LinearLayout
@@ -49,12 +49,28 @@
android:paddingBottom="16dp"
android:clipToPadding="false" />
<LinearLayout
android:id="@+id/skeletonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
<include layout="@layout/item_skeleton_transaction" />
</LinearLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="No transactions found"
android:text="@string/transactions_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
+5 -3
View File
@@ -9,8 +9,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="14dp"
android:paddingHorizontal="@dimen/list_item_padding_h"
android:paddingVertical="@dimen/list_item_padding_v"
android:gravity="center_vertical"
android:foreground="?attr/selectableItemBackground">
@@ -70,7 +70,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface" />
android:textColor="?attr/colorOnSurface"
android:fontFeatureSettings="tnum" />
<TextView
android:id="@+id/tvBlocked"
@@ -79,6 +80,7 @@
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorError"
android:fontFeatureSettings="tnum"
android:visibility="gone" />
<ImageButton
@@ -15,6 +15,7 @@
android:scaleType="fitCenter"
android:layout_marginEnd="10dp"
android:visibility="gone"
android:importantForAccessibility="no"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Circle"
xmlns:app="http://schemas.android.com/apk/res-auto" />
@@ -49,7 +50,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurface" />
android:textColor="?attr/colorOnSurface"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -7,8 +7,8 @@
android:layout_marginHorizontal="16dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:cardCornerRadius="@dimen/card_radius_lg"
app:cardElevation="@dimen/card_elevation_flat"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutlineVariant">
@@ -114,7 +114,8 @@
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginTop="2dp" />
android:layout_marginTop="2dp"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -137,7 +138,8 @@
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
android:layout_marginTop="2dp" />
android:layout_marginTop="2dp"
android:fontFeatureSettings="tnum" />
</LinearLayout>
@@ -162,7 +164,8 @@
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorError"
android:layout_marginTop="2dp" />
android:layout_marginTop="2dp"
android:fontFeatureSettings="tnum" />
</LinearLayout>
+5 -7
View File
@@ -5,8 +5,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="@dimen/card_radius_lg"
app:cardElevation="@dimen/card_elevation_flat"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutlineVariant">
@@ -51,11 +53,7 @@
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" />
style="@style/Widget.App.StatusChip" />
</LinearLayout>
+4 -3
View File
@@ -9,8 +9,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="14dp"
android:paddingHorizontal="@dimen/list_item_padding_h"
android:paddingVertical="@dimen/list_item_padding_v"
android:gravity="center_vertical"
android:foreground="?attr/selectableItemBackground">
@@ -83,7 +83,8 @@
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface"
android:layout_marginTop="6dp" />
android:layout_marginTop="6dp"
android:fontFeatureSettings="tnum" />
<ImageButton
android:id="@+id/btnTransfer"
@@ -5,8 +5,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="@dimen/card_radius_lg"
app:cardElevation="@dimen/card_elevation_flat"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutlineVariant">
@@ -51,11 +53,7 @@
android:id="@+id/tvStatus"
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" />
style="@style/Widget.App.StatusChip" />
</LinearLayout>
@@ -5,8 +5,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp">
android:clickable="true"
android:focusable="true"
app:cardElevation="@dimen/card_elevation_raised"
app:cardCornerRadius="@dimen/card_radius_md">
<LinearLayout
android:layout_width="match_parent"
+4 -3
View File
@@ -5,15 +5,16 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="14dp"
android:paddingHorizontal="@dimen/list_item_padding_h"
android:paddingVertical="@dimen/list_item_padding_v"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/ivIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp" />
android:layout_marginEnd="16dp"
android:importantForAccessibility="no" />
<LinearLayout
android:layout_width="0dp"
+2 -1
View File
@@ -13,7 +13,8 @@
android:id="@+id/ivNavIcon"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginBottom="6dp" />
android:layout_marginBottom="6dp"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/tvNavLabel"
+2 -2
View File
@@ -6,8 +6,8 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
app:cardCornerRadius="@dimen/card_radius_lg"
app:cardElevation="@dimen/card_elevation_raised">
<LinearLayout
android:layout_width="match_parent"
@@ -16,6 +16,7 @@
android:layout_width="44dp"
android:layout_height="44dp"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Circle" />
<LinearLayout
@@ -53,6 +54,7 @@
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
android:fontFeatureSettings="tnum"
android:paddingHorizontal="8dp"
android:visibility="gone" />
@@ -20,10 +20,6 @@
android:id="@+id/tvProfileType"
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" />
style="@style/Widget.App.StatusChip" />
</LinearLayout>
@@ -86,6 +86,7 @@
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textStyle="bold"
android:fontFeatureSettings="tnum"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/fvAvatar"
app:layout_constraintBottom_toBottomOf="@id/fvAvatar" />
@@ -65,8 +65,8 @@
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
app:cardCornerRadius="@dimen/card_radius_md"
app:cardElevation="@dimen/card_elevation_raised">
<LinearLayout
android:layout_width="match_parent"
+33 -1
View File
@@ -5,4 +5,36 @@
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>
<!-- ====== Design tokens ======================================== -->
<!-- Card surfaces.
Two-tier radius:
lg → hero/standalone cards (bank tiles, history header, finance deals, OTP cards)
md → repeating row/summary cards (dashboard cards, foreign limit, bottom sheets)
Card-visual representations (item_card_*) keep their own larger radius/elevation
to read as real credit cards — they are not tokenized. -->
<dimen name="card_radius_lg">16dp</dimen>
<dimen name="card_radius_md">12dp</dimen>
<!-- Card elevation. Two tiers:
flat → outlined cards (paired with a colorOutlineVariant stroke)
raised → subtle Material 3 default raise
Larger elevations belong to specific card-visual surfaces only. -->
<dimen name="card_elevation_flat">0dp</dimen>
<dimen name="card_elevation_raised">1dp</dimen>
<!-- Screen-level spacing -->
<dimen name="screen_margin">16dp</dimen>
<dimen name="section_gap">16dp</dimen>
<!-- Standard list/row padding -->
<dimen name="list_item_padding_h">16dp</dimen>
<dimen name="list_item_padding_v">14dp</dimen>
<!-- Gutters: small inline spacing units -->
<dimen name="gutter_xs">2dp</dimen>
<dimen name="gutter_sm">4dp</dimen>
<dimen name="gutter_md">8dp</dimen>
<dimen name="gutter_lg">12dp</dimen>
</resources>
+2
View File
@@ -255,6 +255,8 @@
<!-- Accounts -->
<string name="accounts_empty">No accounts found</string>
<string name="activities_empty">No recent transfers</string>
<string name="transactions_empty">No transactions found</string>
<!-- Contacts -->
<string name="contacts_empty">No contacts found</string>
+9
View File
@@ -1,6 +1,15 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="ShapeAppearance.Circle" parent="ShapeAppearance.Material3.Corner.Full" />
<!-- Reusable status chip (filled, secondary container). Apply to a TextView via style. -->
<style name="Widget.App.StatusChip" parent="">
<item name="android:paddingHorizontal">10dp</item>
<item name="android:paddingVertical">4dp</item>
<item name="android:background">@drawable/chip_background</item>
<item name="android:textAppearance">?attr/textAppearanceLabelSmall</item>
<item name="android:textColor">?attr/colorOnSecondaryContainer</item>
</style>
<style name="Theme.BasedBank" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/seed_primary</item>
<item name="colorSecondary">@color/seed_secondary</item>