7 Commits
Author SHA1 Message Date
shihaam 1753d648bd loading indicator for account info fetch and tranfer
Auto Tag on Version Change / check-version (push) Successful in 4s
2026-05-18 05:31:20 +05:00
shihaam 423b0bf1e1 dark theme on bml recipt and hide bottom nav bar on recipt page 2026-05-18 05:21:46 +05:00
shihaam d713047970 keep bottom bar behind keyboard
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-18 05:12:49 +05:00
shihaam d59a6fad82 new feature: resume last activity after unlocking instead of goind to dashboard everytime
Auto Tag on Version Change / check-version (push) Successful in 5s
2026-05-18 05:09:42 +05:00
shihaam 8e47101401 refactor: extract BML/MIB product label parsing into dedicated parser utilities
Auto Tag on Version Change / check-version (push) Successful in 4s
2026-05-18 04:53:40 +05:00
shihaam 9431a90cd0 add card icons
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-18 04:35:20 +05:00
shihaam 3a10f36c39 fix prod build
Auto Tag on Version Change / check-version (push) Successful in 5s
2026-05-18 00:43:05 +05:00
16 changed files with 201 additions and 71 deletions
+3
View File
@@ -64,6 +64,9 @@ dependencies {
// RecyclerView for accounts list // RecyclerView for accounts list
implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.recyclerview:recyclerview:1.3.2")
// CircularProgressDrawable for spinning search icons
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
// OkHttp for API calls // OkHttp for API calls
implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:okhttp:4.11.0")
+2 -1
View File
@@ -48,7 +48,8 @@
<activity <activity
android:name=".ui.home.HomeActivity" android:name=".ui.home.HomeActivity"
android:exported="false" /> android:exported="false"
android:windowSoftInputMode="adjustPan" />
<activity <activity
android:name=".ui.home.QrScannerActivity" android:name=".ui.home.QrScannerActivity"
@@ -39,6 +39,7 @@ class LockActivity : AppCompatActivity() {
companion object { companion object {
private const val MAX_ATTEMPTS = 5 private const val MAX_ATTEMPTS = 5
private const val LOCKOUT_MS = 30_000L private const val LOCKOUT_MS = 30_000L
const val EXTRA_RESUME = "resume"
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -270,8 +271,12 @@ class LockActivity : AppCompatActivity() {
} }
private fun proceed() { private fun proceed() {
startActivity(Intent(this, HomeActivity::class.java)) if (intent.getBooleanExtra(EXTRA_RESUME, false)) {
finish() finish()
} else {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
} }
// ── Brute-force tracking ────────────────────────────────────────────────── // ── Brute-force tracking ──────────────────────────────────────────────────
@@ -3,17 +3,18 @@ package sh.sar.basedbank.ui.home
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibAccount import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.databinding.ItemAccountBinding import sh.sar.basedbank.databinding.ItemAccountBinding
import sh.sar.basedbank.databinding.ItemCardBinding import sh.sar.basedbank.databinding.ItemCardBinding
import sh.sar.basedbank.databinding.ItemDateHeaderBinding import sh.sar.basedbank.databinding.ItemDateHeaderBinding
import sh.sar.basedbank.util.BmlDashboardParser
import sh.sar.basedbank.util.MibAccountParser
class AccountsAdapter( class AccountsAdapter(
accounts: List<MibAccount>, accounts: List<MibAccount>,
@@ -106,7 +107,11 @@ class AccountsAdapter(
fun bind(account: MibAccount) { fun bind(account: MibAccount) {
binding.tvAccountName.text = account.accountBriefName binding.tvAccountName.text = account.accountBriefName
binding.tvAccountNumber.text = account.accountNumber binding.tvAccountNumber.text = account.accountNumber
binding.tvPillType.text = friendlyAccountType(account.accountTypeName) val label = if (account.profileType.startsWith("BML"))
BmlDashboardParser.productLabel(account.accountTypeName)
else
MibAccountParser.productLabel(account.accountTypeName)
binding.tvPillType.text = label
binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}" binding.tvBalance.text = "${account.currencyName} ${account.availableBalance}"
binding.root.setOnClickListener { onAccountClick(account) } binding.root.setOnClickListener { onAccountClick(account) }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
@@ -119,15 +124,10 @@ class AccountsAdapter(
private inner class CardViewHolder(private val binding: ItemCardBinding) : private inner class CardViewHolder(private val binding: ItemCardBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(account: MibAccount) { fun bind(account: MibAccount) {
val brand = cardBrand(account.accountTypeName) binding.ivCardBrand.setImageResource(cardBrandIcon(account.accountTypeName))
binding.tvCardBrand.text = brand.label binding.tvCardName.text = account.accountBriefName
binding.tvCardBrand.background = GradientDrawable().apply { binding.tvCardNumber.text = account.accountNumber
shape = GradientDrawable.RECTANGLE binding.tvCardProduct.text = BmlDashboardParser.productLabel(account.accountTypeName)
cornerRadius = 100f
setColor(Color.parseColor(brand.color))
}
binding.tvCardName.text = account.accountBriefName
binding.tvCardNumber.text = account.accountNumber
binding.layoutCardBalance.visibility = View.VISIBLE binding.layoutCardBalance.visibility = View.VISIBLE
binding.tvCardBalance.text = "${account.currencyName} ${account.availableBalance}" binding.tvCardBalance.text = "${account.currencyName} ${account.availableBalance}"
@@ -155,29 +155,12 @@ class AccountsAdapter(
Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Account number copied", Toast.LENGTH_SHORT).show()
} }
private fun friendlyAccountType(raw: String): String { private fun cardBrandIcon(productName: String): Int = when {
val u = raw.trim().uppercase()
return when {
u == "SAVINGS ACCOUNT" ||
u == "SAVING ACCOUNT" -> "Savings"
u == "CURRENT ACCOUNT" ||
u == "CURRENT ACCOUNT(PERSONAL)" ||
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
else -> raw.trim()
}
}
private data class Brand(val label: String, val color: String)
private fun cardBrand(productName: String): Brand = when {
productName.contains("AMEX", ignoreCase = true) || productName.contains("AMEX", ignoreCase = true) ||
productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> Brand("AMEX", "#016FD0") productName.contains("AMERICAN EXPRESS", ignoreCase = true) -> R.drawable.americanexpress
productName.contains("VISA", ignoreCase = true) -> Brand("VISA", "#1A1F71") productName.contains("VISA", ignoreCase = true) -> R.drawable.visa
productName.contains("MASTERCARD", ignoreCase = true) -> Brand("MC", "#FF5F00") productName.contains("MASTERCARD", ignoreCase = true) -> R.drawable.mastercard
else -> Brand("CARD", "#555555") else -> R.drawable.ic_nav_card
} }
} }
} }
@@ -14,7 +14,9 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import sh.sar.basedbank.util.ContactsCache import sh.sar.basedbank.util.ContactsCache
@@ -129,6 +131,22 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
} }
} }
private fun startLookupLoading() {
val spinner = CircularProgressDrawable(requireContext()).apply {
setStyle(CircularProgressDrawable.DEFAULT)
setColorSchemeColors(com.google.android.material.color.MaterialColors.getColor(
requireView(), com.google.android.material.R.attr.colorPrimary, Color.GRAY))
start()
}
binding.tilAccount.endIconDrawable = spinner
binding.tilAccount.isEnabled = false
}
private fun stopLookupLoading() {
binding.tilAccount.isEnabled = true
binding.tilAccount.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
}
private fun setupAccountSearch() { private fun setupAccountSearch() {
binding.tilAccount.setEndIconOnClickListener { performLookup() } binding.tilAccount.setEndIconOnClickListener { performLookup() }
} }
@@ -167,7 +185,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
return return
} }
binding.tilAccount.isEnabled = false startLookupLoading()
binding.tilDestination.isEnabled = false binding.tilDestination.isEnabled = false
binding.btnSave.isEnabled = false binding.btnSave.isEnabled = false
@@ -175,7 +193,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
if (dest.isBml) lookupForBml(input) else lookupForMib(dest, input) if (dest.isBml) lookupForBml(input) else lookupForMib(dest, input)
} }
binding.tilAccount.isEnabled = true stopLookupLoading()
binding.tilDestination.isEnabled = true binding.tilDestination.isEnabled = true
if (result != null) { if (result != null) {
showLookupResult(result, input) showLookupResult(result, input)
@@ -341,6 +359,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
binding.tilAlias.error = null binding.tilAlias.error = null
binding.btnSave.isEnabled = false binding.btnSave.isEnabled = false
binding.btnSave.text = "Saving..."
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
val success = withContext(Dispatchers.IO) { val success = withContext(Dispatchers.IO) {
@@ -352,6 +371,7 @@ class AddContactSheetFragment : BottomSheetDialogFragment() {
dismiss() dismiss()
} else { } else {
binding.btnSave.isEnabled = true binding.btnSave.isEnabled = true
binding.btnSave.text = "Save"
Toast.makeText(requireContext(), R.string.contact_save_failed, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.contact_save_failed, Toast.LENGTH_SHORT).show()
} }
} }
@@ -64,15 +64,22 @@ class HomeActivity : AppCompatActivity() {
private val warningRunnable = Runnable { showAutolockWarning() } private val warningRunnable = Runnable { showAutolockWarning() }
private var isLocked = false
private val autolockRunnable = Runnable { private val autolockRunnable = Runnable {
countdownTimer?.cancel(); countdownTimer = null countdownTimer?.cancel(); countdownTimer = null
warningDialog?.dismiss(); warningDialog = null warningDialog?.dismiss(); warningDialog = null
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE) val securitySet = getSharedPreferences("prefs", MODE_PRIVATE)
.getString("security_method", null) != null .getString("security_method", null) != null
if (securitySet) { if (securitySet) lock()
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java)) }
finish()
} private fun lock() {
isLocked = true
startActivity(
Intent(this, sh.sar.basedbank.LockActivity::class.java)
.putExtra(sh.sar.basedbank.LockActivity.EXTRA_RESUME, true)
)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -224,6 +231,13 @@ class HomeActivity : AppCompatActivity() {
} }
} }
fun setBottomNavVisible(visible: Boolean) {
val isBottom = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
if (isBottom) {
binding.bottomNavigation.visibility = if (visible) View.VISIBLE else View.GONE
}
}
fun setRefreshing(visible: Boolean) { fun setRefreshing(visible: Boolean) {
binding.refreshIndicator.visibility = if (visible) View.VISIBLE else View.GONE binding.refreshIndicator.visibility = if (visible) View.VISIBLE else View.GONE
} }
@@ -237,6 +251,13 @@ class HomeActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Returning from LockActivity — skip the elapsed check and reset state.
if (isLocked) {
isLocked = false
pauseTime = 0L
resetAutolockTimer()
return
}
// If we were away long enough to have hit the autolock timeout (e.g. while // If we were away long enough to have hit the autolock timeout (e.g. while
// QrScannerActivity was in the foreground), lock immediately. // QrScannerActivity was in the foreground), lock immediately.
if (pauseTime > 0L) { if (pauseTime > 0L) {
@@ -244,8 +265,7 @@ class HomeActivity : AppCompatActivity() {
val timeout = getSharedPreferences("prefs", MODE_PRIVATE).getLong("autolock_timeout", 60_000L) val timeout = getSharedPreferences("prefs", MODE_PRIVATE).getLong("autolock_timeout", 60_000L)
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE).getString("security_method", null) != null val securitySet = getSharedPreferences("prefs", MODE_PRIVATE).getString("security_method", null) != null
if (timeout > 0L && elapsed >= timeout && securitySet) { if (timeout > 0L && elapsed >= timeout && securitySet) {
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java)) lock()
finish()
return return
} }
} }
@@ -316,8 +336,7 @@ class HomeActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_lock) { if (item.itemId == R.id.action_lock) {
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java)) lock()
finish()
return true return true
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
@@ -23,6 +23,7 @@ import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -159,6 +160,22 @@ class TransferFragment : Fragment() {
} }
} }
private fun startLookupLoading() {
val spinner = CircularProgressDrawable(requireContext()).apply {
setStyle(CircularProgressDrawable.DEFAULT)
setColorSchemeColors(com.google.android.material.color.MaterialColors.getColor(
requireView(), com.google.android.material.R.attr.colorPrimary, Color.GRAY))
start()
}
binding.tilTo.endIconDrawable = spinner
binding.tilTo.isEnabled = false
}
private fun stopLookupLoading() {
binding.tilTo.isEnabled = true
binding.tilTo.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
}
private fun setupFromDropdown() { private fun setupFromDropdown() {
binding.btnClearFromInfo.setOnClickListener { binding.btnClearFromInfo.setOnClickListener {
selectedAccount = null selectedAccount = null
@@ -313,7 +330,7 @@ class TransferFragment : Fragment() {
val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true val isBmlSource = selectedAccount?.profileType?.startsWith("BML") == true
binding.tilTo.isEnabled = false startLookupLoading()
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
var errorMsg: String? = null var errorMsg: String? = null
@@ -357,7 +374,7 @@ class TransferFragment : Fragment() {
} }
} }
} }
binding.tilTo.isEnabled = true stopLookupLoading()
if (info != null) { if (info != null) {
val accounts = viewModel.accounts.value ?: emptyList() val accounts = viewModel.accounts.value ?: emptyList()
val matchedAcc = accounts.firstOrNull { it.accountNumber == info.accountNumber } val matchedAcc = accounts.firstOrNull { it.accountNumber == info.accountNumber }
@@ -392,7 +409,7 @@ class TransferFragment : Fragment() {
} }
private fun lookupFahipayTarget(number: String) { private fun lookupFahipayTarget(number: String) {
binding.tilTo.isEnabled = false startLookupLoading()
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
data class LookupResult( data class LookupResult(
val dhiraagu: DhiraaguClient.Result, val dhiraagu: DhiraaguClient.Result,
@@ -419,7 +436,7 @@ class TransferFragment : Fragment() {
LookupResult(d, o) LookupResult(d, o)
} }
} }
binding.tilTo.isEnabled = true stopLookupLoading()
val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() } val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() }
@@ -586,6 +603,7 @@ class TransferFragment : Fragment() {
val doTransfer: () -> Unit = { val doTransfer: () -> Unit = {
binding.btnTransfer.isEnabled = false binding.btnTransfer.isEnabled = false
(activity as? HomeActivity)?.setRefreshing(true)
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
val (ok, msg, receipt) = withContext(Dispatchers.IO) { val (ok, msg, receipt) = withContext(Dispatchers.IO) {
if (!isSrcBml) { if (!isSrcBml) {
@@ -595,6 +613,7 @@ class TransferFragment : Fragment() {
} }
} }
binding.btnTransfer.isEnabled = true binding.btnTransfer.isEnabled = true
(activity as? HomeActivity)?.setRefreshing(false)
if (ok && receipt != null) { if (ok && receipt != null) {
clearForm() clearForm()
val activity = requireActivity() as HomeActivity val activity = requireActivity() as HomeActivity
@@ -313,6 +313,12 @@ class TransferReceiptFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
requireActivity().title = "Receipt" requireActivity().title = "Receipt"
(activity as? HomeActivity)?.setBottomNavVisible(false)
}
override fun onPause() {
super.onPause()
(activity as? HomeActivity)?.setBottomNavVisible(true)
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -0,0 +1,28 @@
package sh.sar.basedbank.util
object BmlDashboardParser {
/**
* Returns a display-ready product label for a BML dashboard account or card.
* Known BML product names are mapped to short friendly labels.
* Everything else is title-cased (first letter of each word capitalised).
*/
fun productLabel(raw: String): String {
val u = raw.trim().uppercase()
return when {
u == "SAVINGS ACCOUNT" -> "Savings"
u == "CURRENT ACCOUNT" ||
u == "CURRENT ACCOUNT(PERSONAL)" ||
u == "CURRENT ACCOUNT(BUSINESS)" -> "Current"
u == "WADIAH RETAIL CURRENT ACCOUNT" ||
u == "WADIAH BUSINESS CURRENT ACCOUNT" -> "Islamic Current"
u == "BML ISLAMIC SAVINGS ACCOUNT" -> "Islamic Savings"
else -> toTitleCase(raw)
}
}
fun toTitleCase(input: String): String =
input.trim().lowercase().split(" ").joinToString(" ") { word ->
word.replaceFirstChar { it.uppercaseChar() }
}
}
@@ -0,0 +1,18 @@
package sh.sar.basedbank.util
object MibAccountParser {
/**
* Returns a display-ready product label for a MIB (Faisanet) account type name.
* Known MIB accountTypeName values are mapped to short friendly labels.
* Everything else is returned trimmed as-is.
*/
fun productLabel(raw: String): String {
val u = raw.trim().uppercase()
return when {
u == "SAVING ACCOUNT" -> "Savings"
u == "CURRENT ACCOUNT" -> "Current"
else -> raw.trim()
}
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#016FD0"
android:pathData="M16.015 14.378c0-.32-.135-.496-.344-.622-.21-.12-.464-.135-.81-.135h-1.543v2.82h.675v-1.027h.72c.24 0 .39.024.478.125.12.13.104.38.104.55v.35h.66v-.555c-.002-.25-.017-.376-.108-.516-.06-.08-.18-.18-.33-.234l.02-.008c.18-.072.48-.297.48-.747zm-.87.407l-.028-.002c-.09.053-.195.058-.33.058h-.81v-.63h.824c.12 0 .24 0 .33.05.098.048.156.147.15.255 0 .12-.045.215-.134.27zM20.297 15.837H19v.6h1.304c.676 0 1.05-.278 1.05-.884 0-.28-.066-.448-.187-.582-.153-.133-.392-.193-.73-.207l-.376-.015c-.104 0-.18 0-.255-.03-.09-.03-.15-.105-.15-.21 0-.09.017-.166.09-.21.083-.046.177-.066.272-.06h1.23v-.602h-1.35c-.704 0-.958.437-.958.84 0 .9.776.855 1.407.87.104 0 .18.015.225.06.046.03.082.106.082.18 0 .077-.035.15-.08.18-.06.053-.15.07-.277.07zM0 0v10.096L.81 8.22h1.75l.225.464V8.22h2.043l.45 1.02.437-1.013h6.502c.295 0 .56.057.756.236v-.23h1.787v.23c.307-.17.686-.23 1.12-.23h2.606l.24.466v-.466h1.918l.254.465v-.466h1.858v3.948H20.87l-.36-.6v.585h-2.353l-.256-.63h-.583l-.27.614h-1.213c-.48 0-.84-.104-1.08-.24v.24h-2.89v-.884c0-.12-.03-.12-.105-.135h-.105v1.036H6.067v-.48l-.21.48H4.69l-.202-.48v.465H2.235l-.256-.624H1.4l-.256.624H0V24h23.786v-7.108c-.27.135-.613.18-.973.18H21.09v-.255c-.21.165-.57.255-.914.255H14.71v-.9c0-.12-.018-.12-.12-.12h-.075v1.022h-1.8v-1.066c-.298.136-.643.15-.928.136h-.214v.915h-2.18l-.54-.617-.57.6H4.742v-3.93h3.61l.518.602.554-.6h2.412c.28 0 .74.03.942.225v-.24h2.177c.202 0 .644.045.903.225v-.24h3.265v.24c.163-.164.508-.24.803-.24h1.89v.24c.194-.15.464-.24.84-.24h1.176V0H0zM21.156 14.955c.004.005.006.012.01.016.01.01.024.01.032.02l-.042-.035zM23.828 13.082h.065v.555h-.065zM23.865 15.03v-.005c-.03-.025-.046-.048-.075-.07-.15-.153-.39-.215-.764-.225l-.36-.012c-.12 0-.194-.007-.27-.03-.09-.03-.15-.105-.15-.21 0-.09.03-.16.09-.204.076-.045.15-.05.27-.05h1.223v-.588h-1.283c-.69 0-.96.437-.96.84 0 .9.78.855 1.41.87.104 0 .18.015.224.06.046.03.076.106.076.18 0 .07-.034.138-.09.18-.045.056-.136.07-.27.07h-1.288v.605h1.287c.42 0 .734-.118.9-.36h.03c.09-.134.135-.3.135-.523 0-.24-.045-.39-.135-.526zM18.597 14.208v-.583h-2.235V16.458h2.235v-.585h-1.57v-.57h1.533v-.584h-1.532v-.51M13.51 8.787h.685V11.6h-.684zM13.126 9.543l-.007.006c0-.314-.13-.5-.34-.624-.217-.125-.47-.135-.81-.135H10.43v2.82h.674v-1.034h.72c.24 0 .39.03.487.12.122.136.107.378.107.548v.354h.677v-.553c0-.25-.016-.375-.11-.516-.09-.107-.202-.19-.33-.237.172-.07.472-.3.472-.75zm-.855.396h-.015c-.09.054-.195.056-.33.056H11.1v-.623h.825c.12 0 .24.004.33.05.09.04.15.128.15.25s-.047.22-.134.266zM15.92 9.373h.632v-.6h-.644c-.464 0-.804.105-1.02.33-.286.3-.362.69-.362 1.11 0 .512.123.833.36 1.074.232.238.645.31.97.31h.78l.255-.627h1.39l.262.627h1.36v-2.11l1.272 2.11h.95l.002.002V8.786h-.684v1.963l-1.18-1.96h-1.02V11.4L18.11 8.744h-1.004l-.943 2.22h-.3c-.177 0-.362-.03-.468-.134-.125-.15-.186-.36-.186-.662 0-.285.08-.51.194-.63.133-.135.272-.165.516-.165zm1.668-.108l.464 1.118v.002h-.93l.466-1.12zM2.38 10.97l.254.628H4V9.393l.972 2.205h.584l.973-2.202.015 2.202h.69v-2.81H6.118l-.807 1.904-.876-1.905H3.343v2.663L2.205 8.787h-.997L.01 11.597h.72l.26-.626h1.39zm-.688-1.705l.46 1.118-.003.002h-.915l.457-1.12zM11.856 13.62H9.714l-.85.923-.825-.922H5.346v2.82H8l.855-.932.824.93h1.302v-.94h.838c.6 0 1.17-.164 1.17-.945l-.006-.003c0-.78-.598-.93-1.128-.93zM7.67 15.853l-.014-.002H6.02v-.557h1.47v-.574H6.02v-.51H7.7l.733.82-.764.824zm2.642.33l-1.03-1.147 1.03-1.108v2.253zm1.553-1.258h-.885v-.717h.885c.24 0 .42.098.42.344 0 .243-.15.372-.42.372zM9.967 9.373v-.586H7.73V11.6h2.237v-.58H8.4v-.564h1.527V9.88H8.4v-.507" />
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF5F00"
android:pathData="M11.343 18.031c.058.049.12.098.181.146-1.177.783-2.59 1.238-4.107 1.238C3.32 19.416 0 16.096 0 12c0-4.095 3.32-7.416 7.416-7.416 1.518 0 2.931.456 4.105 1.238-.06.051-.12.098-.165.15C9.6 7.489 8.595 9.688 8.595 12c0 2.311 1.001 4.51 2.748 6.031zm5.241-13.447c-1.52 0-2.931.456-4.105 1.238.06.051.12.098.165.15C14.4 7.489 15.405 9.688 15.405 12c0 2.31-1.001 4.507-2.748 6.031-.058.049-.12.098-.181.146 1.177.783 2.588 1.238 4.107 1.238C20.68 19.416 24 16.096 24 12c0-4.094-3.32-7.416-7.416-7.416zM12 6.174c-.096.075-.189.15-.28.231C10.156 7.764 9.169 9.765 9.169 12c0 2.236.987 4.236 2.551 5.595.09.08.185.158.28.232.096-.074.189-.152.28-.232 1.563-1.359 2.551-3.359 2.551-5.595 0-2.235-.987-4.236-2.551-5.595-.09-.08-.184-.156-.28-.231z" />
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1A1F71"
android:pathData="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-0.094,-0.368,-0.175,-0.503,-0.461,-0.658C1.447 8.864 0.677 8.627 0 8.479l0.046,-0.217h3.3a0.904,0.904,0,0,1,0.894,0.764l0.817 4.338 2.018,-5.102zM17.145 13.311c0.008,-1.979,-2.736,-2.088,-2.717,-2.972 0.006,-0.269 0.262,-0.555 0.822,-0.628a3.66,3.66,0,0,1,1.913,0.336l0.34,-1.59a5.207,5.207,0,0,0,-1.814,-0.333c-1.917 0,-3.266 1.02,-3.278 2.479,-0.012 1.079 0.963 1.68 1.698 2.04 0.756 0.367 1.01 0.603 1.006 0.931,-0.005 0.504,-0.602 0.725,-1.16 0.734,-0.975 0.015,-1.54,-0.263,-1.992,-0.473l-0.351 1.642c0.453 0.208 1.289 0.39 2.156 0.398 2.037 0 3.37,-1.006 3.377,-2.564zM22.206 15.758H24l-1.565,-7.496h-1.656a0.883,0.883,0,0,0,-0.826,0.55l-2.909 6.946h2.036l0.405,-1.12h2.488zM20.043 13.102l1.02,-2.815 0.588 2.815zM11.883 8.262l-1.603 7.496H8.34l1.605,-7.496z" />
</vector>
@@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:background="#FFFFFF"> android:background="?attr/colorSurface">
<!-- ══════════════════════════════════════════════════════════════════════ --> <!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- Renderable receipt card --> <!-- Renderable receipt card -->
@@ -206,12 +206,6 @@
</LinearLayout> </LinearLayout>
<!-- Pushes buttons to bottom of screen -->
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- ══════════════════════════════════════════════════════════════════════ --> <!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- Action buttons — outside renderable area --> <!-- Action buttons — outside renderable area -->
<!-- ══════════════════════════════════════════════════════════════════════ --> <!-- ══════════════════════════════════════════════════════════════════════ -->
@@ -219,7 +213,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:background="#FFFFFF" android:background="?attr/colorSurface"
android:paddingHorizontal="12dp" android:paddingHorizontal="12dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="12dp"> android:paddingBottom="12dp">
+15 -11
View File
@@ -14,18 +14,14 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:foreground="?attr/selectableItemBackground"> android:foreground="?attr/selectableItemBackground">
<!-- Brand chip --> <!-- Brand logo -->
<TextView <ImageView
android:id="@+id/tvCardBrand" android:id="@+id/ivCardBrand"
android:layout_width="wrap_content" android:layout_width="48dp"
android:layout_height="wrap_content" android:layout_height="32dp"
android:paddingHorizontal="10dp"
android:paddingVertical="6dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:textAppearance="?attr/textAppearanceLabelSmall" android:scaleType="fitCenter"
android:textColor="@android:color/white" android:contentDescription="Card brand" />
android:fontFamily="monospace"
android:background="@drawable/chip_background" />
<!-- Card name + number --> <!-- Card name + number -->
<LinearLayout <LinearLayout
@@ -50,6 +46,14 @@
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:fontFamily="monospace" /> android:fontFamily="monospace" />
<TextView
android:id="@+id/tvCardProduct"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginTop="2dp" />
</LinearLayout> </LinearLayout>
<!-- Status pill + balance (prepaid only) --> <!-- Status pill + balance (prepaid only) -->
+1 -1
View File
@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.9.1" agp = "8.7.3"
kotlin = "2.1.21" kotlin = "2.1.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"