Compare commits

...

18 Commits

Author SHA1 Message Date
shihaam dc0f1b96c1 remove cards and quick action lables from dashboard 2026-05-28 18:26:09 +05:00
shihaam 640dd5de22 theme customizations support 2026-05-28 18:21:38 +05:00
shihaam f0a0e7857c optimize button customizations 2026-05-28 17:25:37 +05:00
shihaam 836f4c493a eye always enabled and removed setting to hide eye 2026-05-28 17:19:04 +05:00
shihaam 6325f4fd7a debug builds get name suffix and different launcher icon color 2026-05-28 17:15:14 +05:00
shihaam 69aa172eff quick actions are drawer-only, bottom bar shortcuts disabled when using drawer 2026-05-28 17:15:04 +05:00
shihaam ed2054fb81 fixbug that took user to empty dashboard without any accounts 2026-05-28 16:32:06 +05:00
shihaam e9583f0580 Merge pull request 'fix/accounts-list-balance-consistency' (#5) from ahusan/fksar:fix/accounts-list-balance-consistency into main
Reviewed-on: shihaam/thijooree#5
2026-05-28 16:23:33 +05:00
shihaam a32841a319 compress images 2026-05-28 15:58:26 +05:00
shihaam 7a66dd836c add faisawear images (intergration later) 2026-05-28 15:57:23 +05:00
shihaam 68dd49b90c rename fisa to fisa card 2026-05-28 15:55:49 +05:00
shihaam 76090525e1 add binga mvr usd and fiasa cards 2026-05-28 15:33:49 +05:00
shihaam f7fd06cdf3 Unified card settings and pay with card into 1 page and redsigned it 2026-05-28 15:28:45 +05:00
ahusan 8d09e760a8 Enhance dashboard: add attention row for blocked and overdue funds
Introduces a new attention row in the dashboard to display blocked funds and overdue financing. The row is conditionally visible based on the presence of blocked amounts or overdue totals. Updates the account display logic to show blocked amounts where applicable, ensuring users have a clear view of their financial status. Additionally, new string resources for "Blocked Funds" and "Overdue Financing" are added for localization.
2026-05-28 15:24:49 +05:00
ahusan 62ccae602d accounts list: use available balance, show blocked amount as secondary line
BML CASA rows on the accounts list were showing currentBalance (the
working/ledger balance, which includes blocked funds). Every other
balance display in the app — transfer screen, contact picker, QR pay,
dashboard totals — uses availableBalance, so the same account was
showing a different figure depending on where you looked at it.

This switches the accounts list to availableBalance for consistency,
and adds a small muted "MVR X.XX blocked" line beneath the balance
when blocked > 0 so the blocked funds are still visible at a glance.
Only BML reports a non-zero blocked amount; MIB and Fahipay rows are
unaffected.

The per-account history page header is untouched — its three-column
Available / Balance / Blocked breakdown still works as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:54:33 +05:00
ahusan 9011ef2f5a debug builds: separate applicationId so they coexist with release
Adds applicationIdSuffix=.debug and versionNameSuffix=-debug so a
side-loaded debug build can be installed alongside the Play/release
build without conflicting on package id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:53:45 +05:00
shihaam dd620763ec new feature: add launcher shortcuts 2026-05-28 14:06:49 +05:00
shihaam 86063d600f Fix Bug that allowed lockscreen bypass on rooted androids 2026-05-28 13:41:39 +05:00
83 changed files with 1325 additions and 349 deletions
+4
View File
@@ -27,6 +27,10 @@ android {
} }
buildTypes { buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release { release {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false isMinifyEnabled = false
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#CC0000"
android:pathData="M0,0h108v108h-108z" />
</vector>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Thijooree Debug</string>
</resources>
+4 -1
View File
@@ -10,7 +10,7 @@
<application <application
android:name=".BasedBankApp" android:name=".BasedBankApp"
android:allowBackup="true" android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@@ -30,6 +30,9 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

+1
View File
@@ -0,0 +1 @@
visa_bingaa.png
+1
View File
@@ -0,0 +1 @@
visa_bingaa.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 36 KiB

@@ -16,6 +16,13 @@ import sh.sar.basedbank.util.CredentialStore
class BasedBankApp : Application() { class BasedBankApp : Application() {
/**
* Set to true only after the user passes LockActivity or completes fresh login.
* Resets to false on every process restart so direct ADB/root activity launches
* cannot reach HomeActivity without re-authenticating.
*/
var isUnlocked = false
// Held in memory after successful login; cleared on logout // Held in memory after successful login; cleared on logout
var accounts: List<BankAccount> = emptyList() var accounts: List<BankAccount> = emptyList()
var fullName: String = "" var fullName: String = ""
@@ -108,7 +115,11 @@ class BasedBankApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this) // Only apply wallpaper-based dynamic colors in system theme mode.
// Light/dark modes use content-based accent colors applied per-activity via ThemeHelper.
DynamicColors.applyToActivitiesIfAvailable(this) { _, _ ->
getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system") == "system"
}
val theme = getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system") val theme = getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system")
AppCompatDelegate.setDefaultNightMode(when (theme) { AppCompatDelegate.setDefaultNightMode(when (theme) {
@@ -21,6 +21,8 @@ import kotlinx.coroutines.withContext
import sh.sar.basedbank.databinding.ActivityLockBinding import sh.sar.basedbank.databinding.ActivityLockBinding
import sh.sar.basedbank.ui.home.HomeActivity import sh.sar.basedbank.ui.home.HomeActivity
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.ThemeHelper
import sh.sar.basedbank.BasedBankApp
import javax.crypto.SecretKeyFactory import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.PBEKeySpec
@@ -45,6 +47,7 @@ class LockActivity : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.applyAccent(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityLockBinding.inflate(layoutInflater) binding = ActivityLockBinding.inflate(layoutInflater)
@@ -54,6 +57,9 @@ class LockActivity : AppCompatActivity() {
isAppearanceLightStatusBars = isLight isAppearanceLightStatusBars = isLight
isAppearanceLightNavigationBars = isLight isAppearanceLightNavigationBars = isLight
} }
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
ta.recycle()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(bars.left, bars.top, bars.right, bars.bottom) view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
@@ -259,10 +265,23 @@ class LockActivity : AppCompatActivity() {
} }
private fun proceed() { private fun proceed() {
(application as BasedBankApp).isUnlocked = true
if (intent.getBooleanExtra(EXTRA_RESUME, false)) { if (intent.getBooleanExtra(EXTRA_RESUME, false)) {
finish() finish()
} else { } else {
startActivity(Intent(this, HomeActivity::class.java)) val store = CredentialStore(this)
val hasCredentials = store.hasMibCredentials() || store.hasBmlCredentials() || store.hasFahipayCredentials()
if (!hasCredentials) {
startActivity(Intent(this, sh.sar.basedbank.ui.login.LoginActivity::class.java))
finish()
return
}
val navDest = intent.getIntExtra("nav_destination", -1)
val autoScan = intent.getBooleanExtra("auto_scan", false)
startActivity(Intent(this, HomeActivity::class.java).apply {
if (navDest != -1) putExtra("nav_destination", navDest)
if (autoScan) putExtra("auto_scan", true)
})
finish() finish()
} }
} }
@@ -7,24 +7,43 @@ import sh.sar.basedbank.ui.home.HomeActivity
import sh.sar.basedbank.ui.login.LoginActivity import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.ui.onboarding.OnboardingActivity import sh.sar.basedbank.ui.onboarding.OnboardingActivity
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val prefs = getSharedPreferences("prefs", MODE_PRIVATE) val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
val onboardingDone = prefs.getBoolean("onboarding_done", false) val onboardingDone = prefs.getBoolean("onboarding_done", false)
val securitySet = prefs.getString("security_method", null) != null val securitySet = prefs.getString("security_method", null) != null
val store = CredentialStore(this) val store = CredentialStore(this)
val hasCredentials = store.hasMibCredentials() || store.hasBmlCredentials() || store.hasFahipayCredentials() val hasCredentials = store.hasMibCredentials() || store.hasBmlCredentials() || store.hasFahipayCredentials()
val navDestination = when (intent?.action) {
"sh.sar.basedbank.OPEN_TRANSFER" -> R.id.nav_transfer
"sh.sar.basedbank.OPEN_SCAN_QR" -> R.id.nav_transfer
"sh.sar.basedbank.OPEN_PAY_WITH_CARD" -> R.id.nav_pay_with_card
else -> -1
}
val autoScan = intent?.action == "sh.sar.basedbank.OPEN_SCAN_QR"
val target = when { val target = when {
!onboardingDone -> OnboardingActivity::class.java !onboardingDone -> OnboardingActivity::class.java
!hasCredentials -> LoginActivity::class.java !hasCredentials -> LoginActivity::class.java
securitySet -> LockActivity::class.java // proceed() → HomeActivity securitySet -> LockActivity::class.java // proceed() → HomeActivity
else -> HomeActivity::class.java else -> HomeActivity::class.java
} }
startActivity(Intent(this, target)) // No lock screen configured — mark as unlocked so HomeActivity's guard passes
if (target == HomeActivity::class.java) {
(application as BasedBankApp).isUnlocked = true
}
startActivity(Intent(this, target).apply {
if (navDestination != -1) putExtra("nav_destination", navDestination)
if (autoScan) putExtra("auto_scan", true)
})
finish() finish()
} }
} }
@@ -9,6 +9,7 @@ import org.json.JSONObject
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import kotlin.random.Random import kotlin.random.Random
class SessionExpiredException : Exception("MIB session expired") class SessionExpiredException : Exception("MIB session expired")
@@ -168,7 +169,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
accountTypeName = a.optString("accountTypeName"), accountTypeName = a.optString("accountTypeName"),
availableBalance = a.optString("availableBalance"), availableBalance = a.optString("availableBalance"),
currentBalance = a.optString("currentBalance"), currentBalance = a.optString("currentBalance"),
blockedAmount = a.optString("blockedAmount"), blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
mvrBalance = a.optString("mvrBalance"), mvrBalance = a.optString("mvrBalance"),
statusDesc = a.optString("statusDesc"), statusDesc = a.optString("statusDesc"),
profileImageHash = profile.customerImage, profileImageHash = profile.customerImage,
@@ -188,6 +189,13 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
// ─── Helpers ───────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────
/** MIB returns blockedAmount as a signed decimal where negative = funds held.
* Normalize to a positive magnitude so downstream code can treat it uniformly. */
private fun absBlockedAmount(raw: String): String {
val v = raw.toDoubleOrNull() ?: return raw
return "%.2f".format(abs(v))
}
private fun initialKeyExchange( private fun initialKeyExchange(
appId: String, encKey: String, sfunc: String, key2: String? = null appId: String, encKey: String, sfunc: String, key2: String? = null
): Pair<MibSession, String> { ): Pair<MibSession, String> {
@@ -325,7 +333,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
accountTypeName = a.optString("accountTypeName"), accountTypeName = a.optString("accountTypeName"),
availableBalance = a.optString("availableBalance"), availableBalance = a.optString("availableBalance"),
currentBalance = a.optString("currentBalance"), currentBalance = a.optString("currentBalance"),
blockedAmount = a.optString("blockedAmount"), blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
mvrBalance = a.optString("mvrBalance"), mvrBalance = a.optString("mvrBalance"),
statusDesc = a.optString("statusDesc"), statusDesc = a.optString("statusDesc"),
profileImageHash = profile.customerImage, profileImageHash = profile.customerImage,
@@ -125,6 +125,14 @@ class AccountsAdapter(
binding.tvAccountNumber.text = display.number binding.tvAccountNumber.text = display.number
binding.tvAccountType.text = display.typeLabel binding.tvAccountType.text = display.typeLabel
binding.tvBalance.text = if (hideAmounts) maskAmount(display.balance) else display.balance binding.tvBalance.text = if (hideAmounts) maskAmount(display.balance) else display.balance
val blocked = display.blockedBalance
if (blocked != null) {
val shown = if (hideAmounts) maskAmount(blocked) else blocked
binding.tvBlocked.text = binding.root.context.getString(R.string.account_blocked_label, shown)
binding.tvBlocked.visibility = View.VISIBLE
} else {
binding.tvBlocked.visibility = View.GONE
}
binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) } binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) }
binding.root.setOnClickListener { onAccountClick(account) } binding.root.setOnClickListener { onAccountClick(account) }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
@@ -1,139 +1,3 @@
package sh.sar.basedbank.ui.home package sh.sar.basedbank.ui.home
import android.content.Context // Merged into CardsFragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentCardSettingsBinding
import sh.sar.basedbank.util.bmlapi.BmlCardParser
class CardSettingsFragment : Fragment() {
private var _binding: FragmentCardSettingsBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCardSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = CardSettingsAdapter(emptyList(), requireContext())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val extraBottom = if (isBottomNav) 0 else navBar.bottom
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
insets
}
binding.swipeRefresh.setOnRefreshListener {
(activity as? HomeActivity)?.triggerRefresh()
}
val updateCardList = {
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
val bmlItems = (viewModel.accounts.value ?: emptyList())
.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
.map { CardItem.Bml(it) }
val all = mibItems + bmlItems
adapter.update(all)
binding.loadingView.visibility = View.GONE
binding.swipeRefresh.isRefreshing = false
binding.emptyView.visibility = if (all.isEmpty()) View.VISIBLE else View.GONE
binding.recyclerView.visibility = if (all.isEmpty()) View.GONE else View.VISIBLE
}
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
if (viewModel.mibCards.value == null) {
binding.loadingView.visibility = View.VISIBLE
(activity as? HomeActivity)?.triggerRefreshCards()
}
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_card_settings)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private inner class CardSettingsAdapter(
private var cards: List<CardItem>,
private val context: Context
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
fun update(newCards: List<CardItem>) {
cards = newCards
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
VH(LayoutInflater.from(context).inflate(R.layout.item_card_settings_entry, parent, false))
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
override fun getItemCount() = cards.size
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
private val tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus)
private val btnChangePin: View = view.findViewById(R.id.btnChangePin)
private val btnFreeze: View = view.findViewById(R.id.btnFreeze)
private val btnBlock: View = view.findViewById(R.id.btnBlock)
fun bind(item: CardItem) {
when (item) {
is CardItem.Mib -> {
tvCardOwner.text = item.card.cardHolderName
tvCardNumber.text = PayWithCardFragment.formatMasked(item.card.maskedCardNumber)
tvCardType.text = item.card.cardTypeDesc
val assetPath = PayWithCardFragment.cardImageAsset(item.card)
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
else ivCardImage.setImageDrawable(null)
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.mibCardStatusLabel(item.card.cardStatus))
itemView.alpha = 1f
}
is CardItem.Bml -> {
tvCardOwner.text = item.account.accountBriefName
tvCardNumber.text = PayWithCardFragment.formatMasked(item.account.accountNumber)
tvCardType.text = item.account.accountTypeName
PayWithCardFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
val bmlStatus = item.account.statusDesc.takeUnless { isActive }
PayWithCardFragment.bindCardStatus(tvCardStatus, bmlStatus)
itemView.alpha = if (isActive) 1f else 0.45f
}
}
val wip = View.OnClickListener {
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
btnChangePin.setOnClickListener(wip)
btnFreeze.setOnClickListener(wip)
btnBlock.setOnClickListener(wip)
}
}
}
}
@@ -57,14 +57,24 @@ class DashboardFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) } viewModel.accounts.observe(viewLifecycleOwner) {
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() } updateBalances(it)
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() } updateAttentionRow()
}
viewModel.financing.observe(viewLifecycleOwner) {
updatePendingFinances()
updateAttentionRow()
}
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) {
updatePendingFinances()
updateAttentionRow()
}
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) } viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
viewModel.hideAmounts.observe(viewLifecycleOwner) { viewModel.hideAmounts.observe(viewLifecycleOwner) {
updateBalances(viewModel.accounts.value ?: emptyList()) updateBalances(viewModel.accounts.value ?: emptyList())
updatePendingFinances() updatePendingFinances()
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList()) updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
updateAttentionRow()
} }
binding.swipeRefresh.setOnRefreshListener { binding.swipeRefresh.setOnRefreshListener {
@@ -76,6 +86,10 @@ class DashboardFragment : Fragment() {
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances) (activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
} }
binding.cardOverdue.setOnClickListener {
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
}
val cardAdapter = DashboardCardAdapter() val cardAdapter = DashboardCardAdapter()
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.rvCards.adapter = cardAdapter binding.rvCards.adapter = cardAdapter
@@ -112,6 +126,12 @@ class DashboardFragment : Fragment() {
private fun refreshQuickActions() { private fun refreshQuickActions() {
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE) val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
val isBottom = prefs.getBoolean("bottom_nav", false)
if (isBottom) {
binding.buttonBar.visibility = View.GONE
return
}
binding.buttonBar.visibility = View.VISIBLE
val ids = NavCustomization.getQuickActions(prefs) val ids = NavCustomization.getQuickActions(prefs)
listOf(binding.btnQuickAction1, binding.btnQuickAction2).forEachIndexed { i, btn -> listOf(binding.btnQuickAction1, binding.btnQuickAction2).forEachIndexed { i, btn ->
val def = NavCustomization.ALL_SWAPPABLE.find { it.id == ids[i] } val def = NavCustomization.ALL_SWAPPABLE.find { it.id == ids[i] }
@@ -244,6 +264,56 @@ class DashboardFragment : Fragment() {
} }
} }
private fun updateAttentionRow() {
val hide = viewModel.hideAmounts.value ?: false
val accounts = viewModel.accounts.value ?: emptyList()
// Blocked: sum across CASA-style accounts (exclude cards and loans) per currency.
val blockedByCurrency = accounts
.filter { it.profileType != "BML_CREDIT" && it.profileType != "BML_PREPAID" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN" }
.mapNotNull { acc ->
val v = acc.blockedAmount.replace(",", "").toDoubleOrNull() ?: 0.0
if (v > 0.0) acc.currencyName.uppercase() to v else null
}
.groupBy({ it.first }, { it.second })
.mapValues { (_, vs) -> vs.sum() }
val blockedTotal = blockedByCurrency.values.sum()
if (blockedTotal > 0.0) {
// Primary line: prefer MVR if present, otherwise the first currency.
val primaryCcy = if ("MVR" in blockedByCurrency) "MVR" else blockedByCurrency.keys.first()
val primaryAmt = blockedByCurrency.getValue(primaryCcy)
binding.tvBlockedTotal.text = if (hide) "$primaryCcy ••••••" else "$primaryCcy %,.2f".format(primaryAmt)
val secondary = blockedByCurrency.filterKeys { it != primaryCcy }
if (secondary.isNotEmpty()) {
binding.tvBlockedSecondary.text = secondary.entries.joinToString(" · ") { (ccy, amt) ->
if (hide) "$ccy ••••••" else "$ccy %,.2f".format(amt)
}
binding.tvBlockedSecondary.visibility = View.VISIBLE
} else {
binding.tvBlockedSecondary.visibility = View.GONE
}
binding.cardBlocked.visibility = View.VISIBLE
} else {
binding.cardBlocked.visibility = View.GONE
}
// Overdue: MIB finance deals + BML loan details (assumed MVR — matches existing Pending Finances).
val mibOverdue = (viewModel.financing.value ?: emptyList()).sumOf { it.overdueAmount }
val bmlOverdue = (viewModel.bmlLoanDetails.value ?: emptyMap()).values.sumOf { it.overdueAmount }
val overdueTotal = mibOverdue + bmlOverdue
if (overdueTotal > 0.0) {
binding.tvOverdueTotal.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(overdueTotal)
binding.cardOverdue.visibility = View.VISIBLE
} else {
binding.cardOverdue.visibility = View.GONE
}
binding.rowAttention.visibility =
if (blockedTotal > 0.0 || overdueTotal > 0.0) View.VISIBLE else View.GONE
}
private fun updatePendingFinances() { private fun updatePendingFinances() {
val hide = viewModel.hideAmounts.value ?: false val hide = viewModel.hideAmounts.value ?: false
val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount } val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount }
@@ -287,17 +357,17 @@ class DashboardFragment : Fragment() {
when (item) { when (item) {
is CardItem.Mib -> { is CardItem.Mib -> {
tvCardOwner.text = item.card.cardHolderName tvCardOwner.text = item.card.cardHolderName
tvCardNumber.text = PayWithCardFragment.formatMasked(item.card.maskedCardNumber) tvCardNumber.text = CardsFragment.formatMasked(item.card.maskedCardNumber)
val assetPath = PayWithCardFragment.cardImageAsset(item.card) val assetPath = CardsFragment.cardImageAsset(item.card)
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath) if (assetPath != null) CardsFragment.loadCardImage(ivCardImage, assetPath)
else ivCardImage.setImageDrawable(null) else ivCardImage.setImageDrawable(null)
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.mibCardStatusLabel(item.card.cardStatus)) CardsFragment.bindCardStatus(tvCardStatus, CardsFragment.mibCardStatusLabel(item.card.cardStatus))
} }
is CardItem.Bml -> { is CardItem.Bml -> {
tvCardOwner.text = item.account.accountBriefName tvCardOwner.text = item.account.accountBriefName
tvCardNumber.text = PayWithCardFragment.formatMasked(item.account.accountNumber) tvCardNumber.text = CardsFragment.formatMasked(item.account.accountNumber)
PayWithCardFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account)) CardsFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
PayWithCardFragment.bindCardStatus(tvCardStatus, null) // only Active BML cards reach dashboard CardsFragment.bindCardStatus(tvCardStatus, null) // only Active BML cards reach dashboard
} }
} }
val isMib = item is CardItem.Mib val isMib = item is CardItem.Mib
@@ -68,6 +68,7 @@ import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.CardsCache import sh.sar.basedbank.util.CardsCache
import sh.sar.basedbank.util.FinancingCache import sh.sar.basedbank.util.FinancingCache
import sh.sar.basedbank.util.ForeignLimitsCache import sh.sar.basedbank.util.ForeignLimitsCache
import sh.sar.basedbank.util.ThemeHelper
class HomeActivity : AppCompatActivity() { class HomeActivity : AppCompatActivity() {
@@ -106,6 +107,7 @@ class HomeActivity : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.applyAccent(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
@@ -118,6 +120,21 @@ class HomeActivity : AppCompatActivity() {
isAppearanceLightStatusBars = isLight isAppearanceLightStatusBars = isLight
isAppearanceLightNavigationBars = isLight isAppearanceLightNavigationBars = isLight
} }
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
ta.recycle()
// Auth guard: HomeActivity must only be reachable after LockActivity or fresh login.
// Using loadSecurityHash() (EncryptedSharedPreferences) instead of plain prefs so
// a rooted device cannot bypass this by editing security_method in plain prefs.
val app = application as BasedBankApp
if (CredentialStore(this).loadSecurityHash() != null && !app.isUnlocked) {
startActivity(
android.content.Intent(this, sh.sar.basedbank.LockActivity::class.java)
)
finish()
return
}
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
toggle = ActionBarDrawerToggle( toggle = ActionBarDrawerToggle(
@@ -147,8 +164,7 @@ class HomeActivity : AppCompatActivity() {
R.id.nav_finances -> FinancingFragment() R.id.nav_finances -> FinancingFragment()
R.id.nav_otp -> OtpFragment() R.id.nav_otp -> OtpFragment()
R.id.nav_settings -> SettingsFragment() R.id.nav_settings -> SettingsFragment()
R.id.nav_pay_with_card -> PayWithCardFragment() R.id.nav_pay_with_card -> CardsFragment()
R.id.nav_card_settings -> CardSettingsFragment()
else -> null else -> null
} }
if (frag != null) show(frag) if (frag != null) show(frag)
@@ -165,7 +181,6 @@ class HomeActivity : AppCompatActivity() {
} }
// Load data // Load data
val app = application as BasedBankApp
if (app.mibAccounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) { if (app.mibAccounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
// Came from fresh manual login — accounts ready, rest fetched in background // Came from fresh manual login — accounts ready, rest fetched in background
val merged = app.mibAccounts + app.bmlAccounts + app.fahipayAccounts val merged = app.mibAccounts + app.bmlAccounts + app.fahipayAccounts
@@ -218,11 +233,20 @@ class HomeActivity : AppCompatActivity() {
viewModel.hideAmounts.value = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("hide_amounts", false) viewModel.hideAmounts.value = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("hide_amounts", false)
// Show dashboard on first create // Show dashboard on first create, or navigate to shortcut destination
if (savedInstanceState == null) { if (savedInstanceState == null) {
val navDest = intent.getIntExtra("nav_destination", -1)
val autoScan = intent.getBooleanExtra("auto_scan", false)
if (navDest != -1) {
val fragment = if (autoScan && navDest == R.id.nav_transfer)
TransferFragment.newInstanceWithAutoScan()
else null
navigateTo(navDest, fragment)
} else {
show(DashboardFragment()) show(DashboardFragment())
binding.navigationView.setCheckedItem(R.id.nav_dashboard) binding.navigationView.setCheckedItem(R.id.nav_dashboard)
} }
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -359,8 +383,7 @@ fun applyNavLabelVisibility() {
R.id.nav_finances -> FinancingFragment() R.id.nav_finances -> FinancingFragment()
R.id.nav_otp -> OtpFragment() R.id.nav_otp -> OtpFragment()
R.id.nav_settings -> SettingsFragment() R.id.nav_settings -> SettingsFragment()
R.id.nav_pay_with_card -> PayWithCardFragment() R.id.nav_pay_with_card -> CardsFragment()
R.id.nav_card_settings -> CardSettingsFragment()
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return } else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
} }
show(dest) show(dest)
@@ -494,9 +517,8 @@ fun applyNavLabelVisibility() {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.toolbar_menu, menu) menuInflater.inflate(R.menu.toolbar_menu, menu)
val eyeEnabled = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("hide_sensitive_info", false)
val eyeItem = menu.findItem(R.id.action_hide_amounts) val eyeItem = menu.findItem(R.id.action_hide_amounts)
eyeItem?.isVisible = eyeEnabled eyeItem?.isVisible = true
val hidden = viewModel.hideAmounts.value ?: false val hidden = viewModel.hideAmounts.value ?: false
eyeItem?.setIcon(if (hidden) R.drawable.ic_visibility_off else R.drawable.ic_visibility) eyeItem?.setIcon(if (hidden) R.drawable.ic_visibility_off else R.drawable.ic_visibility)
return true return true
@@ -25,7 +25,6 @@ object NavCustomization {
NavItemDef(R.id.nav_transfer_history, "nav_transfer_history", R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history, R.string.nav_desc_transfer_history), NavItemDef(R.id.nav_transfer_history, "nav_transfer_history", R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history, R.string.nav_desc_transfer_history),
NavItemDef(R.id.nav_finances, "nav_finances", R.drawable.ic_nav_finances, R.string.nav_finances, R.string.nav_desc_finances), NavItemDef(R.id.nav_finances, "nav_finances", R.drawable.ic_nav_finances, R.string.nav_finances, R.string.nav_desc_finances),
NavItemDef(R.id.nav_pay_with_card, "nav_pay_with_card", R.drawable.ic_nav_card, R.string.nav_pay_with_card, R.string.nav_desc_pay_with_card), NavItemDef(R.id.nav_pay_with_card, "nav_pay_with_card", R.drawable.ic_nav_card, R.string.nav_pay_with_card, R.string.nav_desc_pay_with_card),
NavItemDef(R.id.nav_card_settings, "nav_card_settings", R.drawable.ic_nav_card, R.string.nav_card_settings, R.string.nav_desc_card_settings),
NavItemDef(R.id.nav_otp, "nav_otp", R.drawable.ic_nav_otp, R.string.nav_otp, R.string.nav_desc_otp), NavItemDef(R.id.nav_otp, "nav_otp", R.drawable.ic_nav_otp, R.string.nav_otp, R.string.nav_desc_otp),
NavItemDef(R.id.nav_settings, "nav_settings", R.drawable.ic_nav_settings, R.string.nav_settings, R.string.nav_desc_settings), NavItemDef(R.id.nav_settings, "nav_settings", R.drawable.ic_nav_settings, R.string.nav_settings, R.string.nav_desc_settings),
) )
@@ -3,13 +3,14 @@ package sh.sar.basedbank.ui.home
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.Color
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Bundle import android.os.Bundle
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.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -18,20 +19,25 @@ import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import sh.sar.basedbank.R import sh.sar.basedbank.R
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.mib.MibCard import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding import sh.sar.basedbank.databinding.FragmentCardsBinding
import sh.sar.basedbank.util.CardsCache import sh.sar.basedbank.util.CardsCache
import sh.sar.basedbank.util.bmlapi.BmlCardParser import sh.sar.basedbank.util.bmlapi.BmlCardParser
import kotlin.math.abs
class PayWithCardFragment : Fragment() { class CardsFragment : Fragment() {
private var _binding: FragmentPayWithCardBinding? = null private var _binding: FragmentCardsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels() private val viewModel: HomeViewModel by activityViewModels()
private var cards: List<CardItem> = emptyList()
private var currentCardPosition: Int = 0
private var cardWidth: Int = 0
private var pendingQrAccountNumber: String? = null private var pendingQrAccountNumber: String? = null
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@@ -49,39 +55,67 @@ class PayWithCardFragment : Fragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentPayWithCardBinding.inflate(inflater, container, false) _binding = FragmentCardsBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = CardWalletAdapter(emptyList(), requireContext()) val screenW = resources.displayMetrics.widthPixels
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) val peekPx = screenW / 8
binding.recyclerView.adapter = adapter cardWidth = screenW - 2 * peekPx
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt() val stackAdapter = CardStackAdapter(cardWidth)
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets -> binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false) binding.rvCards.adapter = stackAdapter
binding.rvCards.setPadding(peekPx, 0, peekPx, 0)
binding.rvCards.clipToPadding = false
val snapHelper = PagerSnapHelper()
snapHelper.attachToRecyclerView(binding.rvCards)
binding.rvCards.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
applyCardScales()
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val lm = recyclerView.layoutManager ?: return
val snapView = snapHelper.findSnapView(lm) ?: return
val position = lm.getPosition(snapView)
if (position >= 0) {
currentCardPosition = position
buildDots(cards.size, position)
updateCardInfo(position)
}
applyCardScales()
}
}
})
ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
.getBoolean("bottom_nav", false)
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val extraBottom = if (isBottomNav) 0 else navBar.bottom val extraBottom = if (isBottomNav) 0 else navBar.bottom
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom) v.setPadding(0, 0, 0, (16 * resources.displayMetrics.density).toInt() + extraBottom)
insets insets
} }
binding.swipeRefresh.setOnRefreshListener {
(activity as? HomeActivity)?.triggerRefresh()
}
val updateCardList = { val updateCardList = {
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) } val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
val bmlItems = (viewModel.accounts.value ?: emptyList()) val bmlItems = (viewModel.accounts.value ?: emptyList())
.filter { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.statusDesc.equals("Active", ignoreCase = true) } .filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
.map { CardItem.Bml(it) } .map { CardItem.Bml(it) }
val all = mibItems + bmlItems cards = mibItems + bmlItems
adapter.update(all) stackAdapter.update(cards)
binding.loadingView.visibility = View.GONE binding.loadingView.visibility = View.GONE
binding.swipeRefresh.isRefreshing = false val empty = cards.isEmpty()
binding.emptyView.visibility = if (all.isEmpty()) View.VISIBLE else View.GONE binding.emptyView.visibility = if (empty) View.VISIBLE else View.GONE
binding.recyclerView.visibility = if (all.isEmpty()) View.GONE else View.VISIBLE binding.contentLayout.visibility = if (empty) View.GONE else View.VISIBLE
if (!empty) {
buildDots(cards.size, currentCardPosition)
updateCardInfo(currentCardPosition)
}
} }
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() } viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() } viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
@@ -93,6 +127,84 @@ class PayWithCardFragment : Fragment() {
binding.loadingView.visibility = View.VISIBLE binding.loadingView.visibility = View.VISIBLE
} }
(activity as? HomeActivity)?.triggerRefreshCards() (activity as? HomeActivity)?.triggerRefreshCards()
binding.btnScanToPay.setOnClickListener {
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
if (item is CardItem.Mib) {
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
} else {
pendingQrAccountNumber = (item as CardItem.Bml).account.accountNumber
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
}
}
val nfcAvailable = android.nfc.NfcAdapter.getDefaultAdapter(requireContext()) != null
binding.btnTapToPay.isEnabled = nfcAvailable
binding.btnTapToPay.setOnClickListener {
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
val msg = if (item is CardItem.Mib) R.string.mib_qr_nfc_not_supported else R.string.work_in_progress
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
}
val wip = View.OnClickListener {
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
binding.btnChangePin.setOnClickListener(wip)
binding.btnFreeze.setOnClickListener(wip)
binding.btnBlock.setOnClickListener(wip)
}
private fun applyCardScales() {
val rv = binding.rvCards
val rvCenter = rv.paddingStart + (rv.width - rv.paddingStart - rv.paddingEnd) / 2f
val lm = rv.layoutManager as? LinearLayoutManager ?: return
val first = lm.findFirstVisibleItemPosition()
val last = lm.findLastVisibleItemPosition()
if (first < 0) return
for (i in first..last) {
val child = lm.findViewByPosition(i) ?: continue
val childCenter = (child.left + child.right) / 2f
val fraction = (abs(childCenter - rvCenter) / cardWidth.toFloat()).coerceIn(0f, 1f)
val scale = 1f - 0.18f * fraction
child.scaleX = scale
child.scaleY = scale
child.alpha = 1f - 0.4f * fraction
}
}
private fun buildDots(count: Int, selected: Int) {
binding.pageIndicator.removeAllViews()
if (count <= 1) {
binding.pageIndicator.visibility = View.GONE
return
}
binding.pageIndicator.visibility = View.VISIBLE
val dp = resources.displayMetrics.density
val activeColor = MaterialColors.getColor(
requireContext(), com.google.android.material.R.attr.colorPrimary, Color.GRAY)
val inactiveColor = MaterialColors.getColor(
requireContext(), com.google.android.material.R.attr.colorOutlineVariant, Color.LTGRAY)
val size = (8 * dp).toInt()
val margin = (4 * dp).toInt()
repeat(count) { i ->
val dot = View(requireContext())
dot.layoutParams = LinearLayout.LayoutParams(size, size).apply {
setMargins(margin, 0, margin, 0)
}
dot.background = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(if (i == selected) activeColor else inactiveColor)
}
binding.pageIndicator.addView(dot)
}
}
private fun updateCardInfo(position: Int) {
val item = cards.getOrNull(position) ?: return
binding.tvSelectedCardType.text = when (item) {
is CardItem.Mib -> item.card.cardTypeDesc
is CardItem.Bml -> item.account.accountTypeName
}
} }
override fun onResume() { override fun onResume() {
@@ -105,83 +217,78 @@ class PayWithCardFragment : Fragment() {
_binding = null _binding = null
} }
private inner class CardWalletAdapter( private inner class CardStackAdapter(private val cardWidth: Int) : RecyclerView.Adapter<CardStackAdapter.VH>() {
private var cards: List<CardItem>, private var items: List<CardItem> = emptyList()
private val context: Context
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
fun update(newCards: List<CardItem>) { fun update(newItems: List<CardItem>) {
cards = newCards items = newItems
notifyDataSetChanged() notifyDataSetChanged()
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH = override fun getItemCount() = items.size
VH(LayoutInflater.from(context).inflate(R.layout.item_card_wallet, parent, false))
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position]) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
override fun getItemCount() = cards.size VH(LayoutInflater.from(parent.context).inflate(R.layout.item_card_stack, parent, false))
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(items[position])
// Pre-scale based on data position so initial render and off-screen cards are correct
val fraction = abs(position - currentCardPosition).toFloat().coerceIn(0f, 1f)
val scale = 1f - 0.18f * fraction
holder.itemView.scaleX = scale
holder.itemView.scaleY = scale
holder.itemView.alpha = 1f - 0.4f * fraction
}
inner class VH(view: View) : RecyclerView.ViewHolder(view) { inner class VH(view: View) : RecyclerView.ViewHolder(view) {
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage) private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner) private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber) private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
private val tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus) private val tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus)
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc) init {
itemView.layoutParams = RecyclerView.LayoutParams(cardWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
}
fun bind(item: CardItem) { fun bind(item: CardItem) {
when (item) { when (item) {
is CardItem.Mib -> { is CardItem.Mib -> {
tvCardOwner.text = item.card.cardHolderName tvCardOwner.text = item.card.cardHolderName
tvCardNumber.text = formatMasked(item.card.maskedCardNumber) tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
tvCardType.text = item.card.cardTypeDesc
val assetPath = cardImageAsset(item.card) val assetPath = cardImageAsset(item.card)
if (assetPath != null) loadCardImage(ivCardImage, assetPath) if (assetPath != null) loadCardImage(ivCardImage, assetPath)
else ivCardImage.setImageDrawable(null) else ivCardImage.setImageDrawable(null)
bindCardStatus(tvCardStatus, mibCardStatusLabel(item.card.cardStatus)) bindCardStatus(tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
itemView.alpha = 1f
} }
is CardItem.Bml -> { is CardItem.Bml -> {
tvCardOwner.text = item.account.accountBriefName tvCardOwner.text = item.account.accountBriefName
tvCardNumber.text = formatMasked(item.account.accountNumber) tvCardNumber.text = formatMasked(item.account.accountNumber)
tvCardType.text = item.account.accountTypeName
loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account)) loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
val bmlStatus = item.account.statusDesc.takeUnless { it.equals("Active", ignoreCase = true) } val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
bindCardStatus(tvCardStatus, bmlStatus) bindCardStatus(tvCardStatus, item.account.statusDesc.takeUnless { isActive })
itemView.alpha = if (isActive) 1f else 0.45f
} }
} }
val isMib = item is CardItem.Mib
btnPayQr.setOnClickListener {
if (isMib) {
Toast.makeText(context, R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
} else {
pendingQrAccountNumber = (item as CardItem.Bml).account.accountNumber
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
}
}
val nfcAdapter = android.nfc.NfcAdapter.getDefaultAdapter(context)
val nfcSupported = nfcAdapter != null
btnPayNfc.isEnabled = nfcSupported
btnPayNfc.setOnClickListener {
val msg = if (isMib) R.string.mib_qr_nfc_not_supported else R.string.work_in_progress
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
} }
} }
} }
companion object { companion object {
fun cardImageAsset(card: MibCard): String? = when (card.cardType) { fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
"51" -> "cards/mib/faisa_card.png"
"53" -> "cards/mib/visa_black_platinum.png" "53" -> "cards/mib/visa_black_platinum.png"
"57" -> "cards/mib/visa_blue_everyday.png" "57" -> "cards/mib/visa_blue_everyday.png"
"70" -> "cards/mib/visa_business.png" "70" -> "cards/mib/visa_business.png"
"701" -> "cards/mib/visa_bingaa_mvr.png"
"702" -> "cards/mib/visa_bingaa_usd.png"
else -> null else -> null
} }
fun loadCardImage(imageView: ImageView, assetPath: String) { fun loadCardImage(imageView: ImageView, assetPath: String) {
try { try {
val bitmap = imageView.context.assets.open(assetPath).use { val bitmap = imageView.context.assets.open(assetPath).use {
BitmapFactory.decodeStream(it) android.graphics.BitmapFactory.decodeStream(it)
} }
imageView.setImageBitmap(bitmap) imageView.setImageBitmap(bitmap)
} catch (_: Exception) { } catch (_: Exception) {
@@ -190,7 +297,7 @@ class PayWithCardFragment : Fragment() {
} }
fun mibCardStatusLabel(cardStatus: String): String? = when (cardStatus) { fun mibCardStatusLabel(cardStatus: String): String? = when (cardStatus) {
"CHST0" -> null // Active — no badge "CHST0" -> null
else -> cardStatus else -> cardStatus
} }
@@ -37,8 +37,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.ActivityQrScannerBinding import sh.sar.basedbank.databinding.ActivityQrScannerBinding
import sh.sar.basedbank.util.CredentialStore
import java.util.concurrent.Executors import java.util.concurrent.Executors
class QrScannerActivity : AppCompatActivity() { class QrScannerActivity : AppCompatActivity() {
@@ -95,6 +97,14 @@ class QrScannerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (CredentialStore(this).loadSecurityHash() != null &&
!(application as BasedBankApp).isUnlocked) {
startActivity(Intent(this, sh.sar.basedbank.LockActivity::class.java))
finish()
return
}
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityQrScannerBinding.inflate(layoutInflater) binding = ActivityQrScannerBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -2,7 +2,9 @@ package sh.sar.basedbank.ui.home
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -10,6 +12,7 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
@@ -18,8 +21,11 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import sh.sar.basedbank.R import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding
import sh.sar.basedbank.util.ThemeHelper
import java.util.Collections import java.util.Collections
class SettingsAppearanceFragment : Fragment() { class SettingsAppearanceFragment : Fragment() {
@@ -54,19 +60,29 @@ class SettingsAppearanceFragment : Fragment() {
// Quick actions // Quick actions
quickActions.clear() quickActions.clear()
quickActions.addAll(NavCustomization.getQuickActions(prefs)) quickActions.addAll(NavCustomization.getQuickActions(prefs))
quickActionAdapter = NavItemAdapter(quickActions) { quickActionAdapter = NavItemAdapter(
NavCustomization.saveQuickActions(prefs, quickActions) items = quickActions,
onSave = { NavCustomization.saveQuickActions(prefs, quickActions) },
isEnabled = { !prefs.getBoolean("bottom_nav", false) }
)
setupNavItemRecyclerView(binding.rvQuickActions, quickActionAdapter, quickActions) {
!prefs.getBoolean("bottom_nav", false)
} }
setupNavItemRecyclerView(binding.rvQuickActions, quickActionAdapter, quickActions)
// Bottom bar shortcuts // Bottom bar shortcuts
slots.clear() slots.clear()
slots.addAll(NavCustomization.getSlots(prefs)) slots.addAll(NavCustomization.getSlots(prefs))
slotAdapter = NavItemAdapter(slots) { slotAdapter = NavItemAdapter(
items = slots,
onSave = {
NavCustomization.saveSlots(prefs, slots) NavCustomization.saveSlots(prefs, slots)
(activity as? HomeActivity)?.rebuildBottomNav(prefs) (activity as? HomeActivity)?.rebuildBottomNav(prefs)
},
isEnabled = { prefs.getBoolean("bottom_nav", false) }
)
setupNavItemRecyclerView(binding.rvNavSlots, slotAdapter, slots) {
prefs.getBoolean("bottom_nav", false)
} }
setupNavItemRecyclerView(binding.rvNavSlots, slotAdapter, slots)
// Show labels toggle // Show labels toggle
val showLabels = prefs.getBoolean("bottom_nav_show_labels", true) val showLabels = prefs.getBoolean("bottom_nav_show_labels", true)
binding.switchShowLabels.isChecked = showLabels binding.switchShowLabels.isChecked = showLabels
@@ -93,8 +109,45 @@ class SettingsAppearanceFragment : Fragment() {
} }
prefs.edit().putString("theme", key).apply() prefs.edit().putString("theme", key).apply()
AppCompatDelegate.setDefaultNightMode(mode) AppCompatDelegate.setDefaultNightMode(mode)
updateAccentState(key == "system")
updatePitchBlackState(key == "dark")
} }
// Pitch black
binding.switchPitchBlack.isChecked = prefs.getBoolean("pitch_black", false)
binding.switchPitchBlack.setOnCheckedChangeListener { _, checked ->
prefs.edit().putBoolean("pitch_black", checked).apply()
requireActivity().recreate()
}
val isDark = prefs.getString("theme", "system") == "dark"
updatePitchBlackState(isDark)
// Accent color
val savedPreset = prefs.getString("accent_preset", ThemeHelper.PRESET_BLUE)
binding.accentToggle.check(when (savedPreset) {
ThemeHelper.PRESET_RED -> R.id.btnAccentOrange
ThemeHelper.PRESET_GREEN -> R.id.btnAccentGreen
ThemeHelper.PRESET_CUSTOM -> R.id.btnAccentCustom
else -> R.id.btnAccentBlue
})
binding.accentToggle.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked) return@addOnButtonCheckedListener
val preset = when (checkedId) {
R.id.btnAccentOrange -> ThemeHelper.PRESET_RED
R.id.btnAccentGreen -> ThemeHelper.PRESET_GREEN
R.id.btnAccentCustom -> ThemeHelper.PRESET_CUSTOM
else -> ThemeHelper.PRESET_BLUE
}
if (preset == ThemeHelper.PRESET_CUSTOM) {
showCustomColorPicker()
} else {
prefs.edit().putString("accent_preset", preset).apply()
requireActivity().recreate()
}
}
val isSystem = prefs.getString("theme", "system") == "system"
updateAccentState(isSystem)
// Language // Language
val currentLocales = AppCompatDelegate.getApplicationLocales() val currentLocales = AppCompatDelegate.getApplicationLocales()
val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en" val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en"
@@ -109,13 +162,18 @@ class SettingsAppearanceFragment : Fragment() {
private fun setupNavItemRecyclerView( private fun setupNavItemRecyclerView(
rv: RecyclerView, rv: RecyclerView,
adapter: NavItemAdapter, adapter: NavItemAdapter,
items: MutableList<Int> items: MutableList<Int>,
isEnabled: () -> Boolean
) { ) {
rv.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) rv.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
rv.adapter = adapter rv.adapter = adapter
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.START or ItemTouchHelper.END, 0 ItemTouchHelper.START or ItemTouchHelper.END, 0
) { ) {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
if (!isEnabled()) return 0
return super.getMovementFlags(recyclerView, viewHolder)
}
override fun onMove( override fun onMove(
rv: RecyclerView, rv: RecyclerView,
from: RecyclerView.ViewHolder, from: RecyclerView.ViewHolder,
@@ -134,11 +192,79 @@ class SettingsAppearanceFragment : Fragment() {
private fun updateShortcutsVisibility() { private fun updateShortcutsVisibility() {
val isBottom = prefs.getBoolean("bottom_nav", false) val isBottom = prefs.getBoolean("bottom_nav", false)
binding.sectionQuickActions.alpha = if (isBottom) 0.38f else 1f
binding.sectionBottomBarShortcuts.alpha = if (isBottom) 1f else 0.38f binding.sectionBottomBarShortcuts.alpha = if (isBottom) 1f else 0.38f
binding.switchShowLabels.isClickable = isBottom
quickActionAdapter.notifyDataSetChanged()
slotAdapter.notifyDataSetChanged()
}
private fun updatePitchBlackState(isDark: Boolean) {
binding.rowPitchBlack.alpha = if (isDark) 1f else 0.38f
binding.switchPitchBlack.isEnabled = isDark
}
private fun updateAccentState(isSystem: Boolean) {
binding.sectionAccentColor.alpha = if (isSystem) 0.38f else 1f
for (i in 0 until binding.accentToggle.childCount) {
binding.accentToggle.getChildAt(i)?.isEnabled = !isSystem
}
}
private fun showCustomColorPicker() {
val ctx = requireContext()
val currentHex = prefs.getString("accent_custom_color", "") ?: ""
val inputLayout = TextInputLayout(ctx).apply {
hint = getString(R.string.accent_custom_hint)
val pad = (16 * resources.displayMetrics.density).toInt()
setPadding(pad, pad / 2, pad, 0)
}
val input = TextInputEditText(ctx).apply {
setText(currentHex)
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
setSingleLine(true)
}
inputLayout.addView(input)
val dialog = MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.accent_custom_pick)
.setView(inputLayout)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.cancel) { _, _ -> revertAccentToggle() }
.setOnCancelListener { revertAccentToggle() }
.show()
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val raw = input.text.toString().trim()
val hex = if (raw.startsWith("#")) raw else "#$raw"
try {
Color.parseColor(hex)
prefs.edit()
.putString("accent_preset", ThemeHelper.PRESET_CUSTOM)
.putString("accent_custom_color", hex)
.apply()
dialog.dismiss()
requireActivity().recreate()
} catch (_: Exception) {
Toast.makeText(ctx, R.string.accent_invalid_color, Toast.LENGTH_SHORT).show()
}
}
}
private fun revertAccentToggle() {
val saved = prefs.getString("accent_preset", ThemeHelper.PRESET_BLUE)
binding.accentToggle.check(when (saved) {
ThemeHelper.PRESET_RED -> R.id.btnAccentOrange
ThemeHelper.PRESET_GREEN -> R.id.btnAccentGreen
ThemeHelper.PRESET_CUSTOM -> R.id.btnAccentCustom
else -> R.id.btnAccentBlue
})
} }
private fun showItemPicker(items: MutableList<Int>, slotIndex: Int, adapter: NavItemAdapter) { private fun showItemPicker(items: MutableList<Int>, slotIndex: Int, adapter: NavItemAdapter) {
if (items === slots && !prefs.getBoolean("bottom_nav", false)) return val isBottom = prefs.getBoolean("bottom_nav", false)
if (items === slots && !isBottom) return
if (items === quickActions && isBottom) return
val ctx = requireContext() val ctx = requireContext()
val otherIds = items.filterIndexed { i, _ -> i != slotIndex }.toSet() val otherIds = items.filterIndexed { i, _ -> i != slotIndex }.toSet()
val available = NavCustomization.ALL_SWAPPABLE.filter { it.id !in otherIds } val available = NavCustomization.ALL_SWAPPABLE.filter { it.id !in otherIds }
@@ -147,6 +273,7 @@ class SettingsAppearanceFragment : Fragment() {
LayoutInflater.from(ctx).inflate(R.layout.item_more_nav, listLayout, false).also { row -> LayoutInflater.from(ctx).inflate(R.layout.item_more_nav, listLayout, false).also { row ->
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes) row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes) row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
row.findViewById<TextView>(R.id.tvDescription).visibility = View.GONE
listLayout.addView(row) listLayout.addView(row)
} }
} }
@@ -169,7 +296,8 @@ class SettingsAppearanceFragment : Fragment() {
private inner class NavItemAdapter( private inner class NavItemAdapter(
val items: MutableList<Int>, val items: MutableList<Int>,
val onSave: () -> Unit val onSave: () -> Unit,
val isEnabled: () -> Boolean = { true }
) : RecyclerView.Adapter<NavItemAdapter.VH>() { ) : RecyclerView.Adapter<NavItemAdapter.VH>() {
inner class VH(view: View) : RecyclerView.ViewHolder(view) { inner class VH(view: View) : RecyclerView.ViewHolder(view) {
@@ -191,7 +319,12 @@ class SettingsAppearanceFragment : Fragment() {
val def = NavCustomization.ALL_SWAPPABLE.find { it.id == items[position] } ?: return val def = NavCustomization.ALL_SWAPPABLE.find { it.id == items[position] } ?: return
holder.ivNavIcon.setImageResource(def.iconRes) holder.ivNavIcon.setImageResource(def.iconRes)
holder.tvNavLabel.setText(def.titleRes) holder.tvNavLabel.setText(def.titleRes)
holder.itemView.setOnClickListener { showItemPicker(items, holder.adapterPosition, this) } val enabled = isEnabled()
holder.itemView.setOnClickListener(
if (enabled) View.OnClickListener { showItemPicker(items, holder.adapterPosition, this) }
else null
)
holder.itemView.isClickable = enabled
} }
} }
@@ -86,17 +86,6 @@ class SettingsSecurityFragment : Fragment() {
(activity as? HomeActivity)?.resetAutolockTimer() (activity as? HomeActivity)?.resetAutolockTimer()
} }
// Hide sensitive information (enables/disables the eye icon in toolbar)
val viewModel = (requireActivity() as HomeActivity).let {
androidx.lifecycle.ViewModelProvider(it)[HomeViewModel::class.java]
}
binding.switchHideAmounts.isChecked = prefs.getBoolean("hide_sensitive_info", false)
binding.switchHideAmounts.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("hide_sensitive_info", isChecked).apply()
if (!isChecked) viewModel.hideAmounts.value = false
requireActivity().invalidateOptionsMenu()
}
// Block screenshots // Block screenshots
val blockScreenshots = prefs.getBoolean("block_screenshots", true) val blockScreenshots = prefs.getBoolean("block_screenshots", true)
binding.switchBlockScreenshots.isChecked = blockScreenshots binding.switchBlockScreenshots.isChecked = blockScreenshots
@@ -151,6 +151,11 @@ class TransferFragment : Fragment() {
private const val ARG_AMOUNT_PREFILL = "amount_prefill" private const val ARG_AMOUNT_PREFILL = "amount_prefill"
private const val ARG_REMARKS_PREFILL = "remarks_prefill" private const val ARG_REMARKS_PREFILL = "remarks_prefill"
private const val ARG_BML_QR_URL = "bml_qr_url" private const val ARG_BML_QR_URL = "bml_qr_url"
private const val ARG_AUTO_SCAN = "auto_scan"
fun newInstanceWithAutoScan() = TransferFragment().apply {
arguments = Bundle().apply { putBoolean(ARG_AUTO_SCAN, true) }
}
fun newInstanceFromBmlQr(qrUrl: String, fromAccountNumber: String? = null) = TransferFragment().apply { fun newInstanceFromBmlQr(qrUrl: String, fromAccountNumber: String? = null) = TransferFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
@@ -253,6 +258,10 @@ class TransferFragment : Fragment() {
arguments?.getString(ARG_REMARKS_PREFILL)?.let { binding.etRemarks.setText(it) } arguments?.getString(ARG_REMARKS_PREFILL)?.let { binding.etRemarks.setText(it) }
arguments?.getString(ARG_BML_QR_URL)?.let { lookupBmlQrMerchant(it) } arguments?.getString(ARG_BML_QR_URL)?.let { lookupBmlQrMerchant(it) }
if (arguments?.getBoolean(ARG_AUTO_SCAN, false) == true) {
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
}
} }
private fun lookupBmlQrMerchant(qrUrl: String) { private fun lookupBmlQrMerchant(qrUrl: String) {
@@ -215,6 +215,7 @@ class CredentialsFragment : Fragment() {
app.mibSessions[loginId] = flow.lastSession!! app.mibSessions[loginId] = flow.lastSession!!
app.mibProfilesMap[loginId] = flow.lastProfiles app.mibProfilesMap[loginId] = flow.lastProfiles
app.mibLoginFlows[loginId] = flow app.mibLoginFlows[loginId] = flow
app.isUnlocked = true
val intent = Intent(requireContext(), HomeActivity::class.java) val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent) startActivity(intent)
@@ -364,6 +365,7 @@ class CredentialsFragment : Fragment() {
if (hasBusinessProfiles) { if (hasBusinessProfiles) {
Toast.makeText(requireContext(), "Business profiles can be enabled in Settings → Logins", Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), "Business profiles can be enabled in Settings → Logins", Toast.LENGTH_LONG).show()
} }
(requireActivity().application as BasedBankApp).isUnlocked = true
val intent = Intent(requireContext(), HomeActivity::class.java) val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent) startActivity(intent)
@@ -496,6 +498,7 @@ class CredentialsFragment : Fragment() {
app.fahipaySessions[loginId] = session app.fahipaySessions[loginId] = session
app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != loginTag } + listOf(account) app.fahipayAccounts = app.fahipayAccounts.filter { it.loginTag != loginTag } + listOf(account)
app.accounts = app.accounts.filter { it.loginTag != loginTag } + listOf(account) app.accounts = app.accounts.filter { it.loginTag != loginTag } + listOf(account)
app.isUnlocked = true
val intent = Intent(requireContext(), HomeActivity::class.java) val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent) startActivity(intent)
@@ -1,17 +1,33 @@
package sh.sar.basedbank.ui.login package sh.sar.basedbank.ui.login
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.LockActivity
import sh.sar.basedbank.databinding.ActivityLoginBinding import sh.sar.basedbank.databinding.ActivityLoginBinding
import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.ThemeHelper
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.applyAccent(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// If security is configured and the user hasn't unlocked this session,
// they must authenticate first before being able to add more accounts.
val app = application as BasedBankApp
if (CredentialStore(this).loadSecurityHash() != null && !app.isUnlocked) {
startActivity(Intent(this, LockActivity::class.java))
finish()
return
}
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityLoginBinding.inflate(layoutInflater) binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -20,5 +36,8 @@ class LoginActivity : AppCompatActivity() {
isAppearanceLightStatusBars = isLight isAppearanceLightStatusBars = isLight
isAppearanceLightNavigationBars = isLight isAppearanceLightNavigationBars = isLight
} }
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
ta.recycle()
} }
} }
@@ -13,9 +13,13 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.LockActivity
import sh.sar.basedbank.R import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.ActivityOnboardingBinding import sh.sar.basedbank.databinding.ActivityOnboardingBinding
import sh.sar.basedbank.ui.login.LoginActivity import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.ThemeHelper
class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback { class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
@@ -24,7 +28,17 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
private var countDownTimer: CountDownTimer? = null private var countDownTimer: CountDownTimer? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.applyAccent(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// If security is already configured, onboarding is complete. Redirect to lock screen
// to prevent overwriting an existing PIN/pattern via direct activity launch.
if (CredentialStore(this).loadSecurityHash() != null) {
startActivity(Intent(this, LockActivity::class.java))
finish()
return
}
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
binding = ActivityOnboardingBinding.inflate(layoutInflater) binding = ActivityOnboardingBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -33,6 +47,9 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
isAppearanceLightStatusBars = isLight isAppearanceLightStatusBars = isLight
isAppearanceLightNavigationBars = isLight isAppearanceLightNavigationBars = isLight
} }
val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground))
window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK)
ta.recycle()
prefs = getSharedPreferences("prefs", MODE_PRIVATE) prefs = getSharedPreferences("prefs", MODE_PRIVATE)
val originalBottomPadding = binding.bottomBar.paddingBottom val originalBottomPadding = binding.bottomBar.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets ->
@@ -97,6 +114,9 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
binding.btnGetStarted.setOnClickListener { binding.btnGetStarted.setOnClickListener {
prefs.edit().putBoolean("onboarding_done", true).apply() prefs.edit().putBoolean("onboarding_done", true).apply()
// Mark as unlocked so LoginActivity doesn't redirect to LockActivity.
// The user just completed setup — they shouldn't have to re-authenticate immediately.
(application as BasedBankApp).isUnlocked = true
startActivity(Intent(this, LoginActivity::class.java)) startActivity(Intent(this, LoginActivity::class.java))
finish() finish()
} }
@@ -5,6 +5,7 @@ data class AccountListDisplay(
val number: String, val number: String,
val typeLabel: String, val typeLabel: String,
val balance: String, val balance: String,
val blockedBalance: String? = null, // null when zero or not applicable
val isCard: Boolean = false, val isCard: Boolean = false,
val cardBrandIcon: Int = 0, // drawable res, only meaningful if isCard val cardBrandIcon: Int = 0, // drawable res, only meaningful if isCard
val statusLabel: String? = null // null = active; shown as status pill if set val statusLabel: String? = null // null = active; shown as status pill if set
@@ -0,0 +1,78 @@
package sh.sar.basedbank.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
import sh.sar.basedbank.R
object ThemeHelper {
const val PRESET_BLUE = "blue"
const val PRESET_RED = "red"
const val PRESET_GREEN = "green"
const val PRESET_CUSTOM = "custom"
fun isSystemTheme(context: Context): Boolean =
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.getString("theme", "system") == "system"
fun getAccentPreset(context: Context): String =
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.getString("accent_preset", PRESET_BLUE) ?: PRESET_BLUE
fun getCustomColor(context: Context): Int? {
val hex = context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.getString("accent_custom_color", null) ?: return null
return try { Color.parseColor(hex) } catch (_: Exception) { null }
}
fun presetSeedColor(context: Context, preset: String): Int = when (preset) {
PRESET_RED -> Color.parseColor("#D32F2F")
PRESET_GREEN -> Color.parseColor("#4CAF50")
PRESET_CUSTOM -> getCustomColor(context) ?: Color.parseColor("#3F65AD")
else -> Color.parseColor("#3F65AD")
}
fun isPitchBlackEnabled(context: Context): Boolean =
context.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.getBoolean("pitch_black", false)
/**
* Apply the user-chosen accent theme to the given activity.
* Must be called BEFORE super.onCreate() so window-level attributes
* (status bar color, etc.) are resolved from the correct overlay.
* Has no effect when the theme is set to "system" (dynamic colors are
* handled by BasedBankApp via DynamicColors.applyToActivitiesIfAvailable).
*/
fun applyAccent(activity: AppCompatActivity) {
if (isSystemTheme(activity)) return
val preset = getAccentPreset(activity)
val seed = presetSeedColor(activity, preset)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
bitmap.setPixel(0, 0, seed)
DynamicColors.applyToActivityIfAvailable(
activity,
DynamicColorsOptions.Builder().setContentBasedSource(bitmap).build()
)
} else {
// API < 31: apply a simple style overlay (partial palette, but functional)
val styleRes = when (preset) {
PRESET_RED -> R.style.ThemeOverlay_Accent_Orange
PRESET_GREEN -> R.style.ThemeOverlay_Accent_Green
else -> R.style.ThemeOverlay_Accent_Blue
}
activity.theme.applyStyle(styleRes, true)
}
val prefs = activity.getSharedPreferences("prefs", Context.MODE_PRIVATE)
val isDark = prefs.getString("theme", "system") == "dark"
if (isDark && prefs.getBoolean("pitch_black", false)) {
activity.theme.applyStyle(R.style.ThemeOverlay_PitchBlack, true)
}
}
}
@@ -26,11 +26,13 @@ object BmlDashboardParser {
statusLabel = if (isActive) null else account.statusDesc statusLabel = if (isActive) null else account.statusDesc
) )
} else { } else {
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
AccountListDisplay( AccountListDisplay(
name = account.accountBriefName, name = account.accountBriefName,
number = account.accountNumber, number = account.accountNumber,
typeLabel = productLabel(account.accountTypeName), typeLabel = productLabel(account.accountTypeName),
balance = listBalance(account) balance = listBalance(account),
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
) )
} }
} }
@@ -52,9 +54,9 @@ object BmlDashboardParser {
} }
} }
/** Balance shown in the accounts list — ledger (working) balance for BML CASA. */ /** Balance shown in the accounts list — available balance, consistent with transfer/contact picker. */
fun listBalance(account: BankAccount): String = fun listBalance(account: BankAccount): String =
"${account.currencyName} ${account.currentBalance}" "${account.currencyName} ${account.availableBalance}"
fun cardBrandIcon(productName: String): Int = when { fun cardBrandIcon(productName: String): Int = when {
productName.contains("AMEX", ignoreCase = true) || productName.contains("AMEX", ignoreCase = true) ||
@@ -5,12 +5,16 @@ import sh.sar.basedbank.util.AccountListDisplay
object MibAccountParser { object MibAccountParser {
fun displayData(account: BankAccount) = AccountListDisplay( fun displayData(account: BankAccount): AccountListDisplay {
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
return AccountListDisplay(
name = account.accountBriefName, name = account.accountBriefName,
number = account.accountNumber, number = account.accountNumber,
typeLabel = productLabel(account.accountTypeName), typeLabel = productLabel(account.accountTypeName),
balance = "${account.currencyName} ${account.availableBalance}" balance = "${account.currencyName} ${account.availableBalance}",
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
) )
}
/** /**
* Returns a display-ready product label for a MIB (Faisanet) account type name. * Returns a display-ready product label for a MIB (Faisanet) account type name.
@@ -5,13 +5,16 @@ import sh.sar.basedbank.util.AccountHistoryDisplay
object MibHistoryParser { object MibHistoryParser {
fun displayData(account: BankAccount) = AccountHistoryDisplay( fun displayData(account: BankAccount): AccountHistoryDisplay {
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
return AccountHistoryDisplay(
name = account.accountBriefName, name = account.accountBriefName,
number = account.accountNumber, number = account.accountNumber,
bankPill = null, // MIB has no bank pill bankPill = null, // MIB has no bank pill
typeLabel = MibAccountParser.productLabel(account.accountTypeName), typeLabel = MibAccountParser.productLabel(account.accountTypeName),
availableBalance = "${account.currencyName} ${account.availableBalance}", availableBalance = "${account.currencyName} ${account.availableBalance}",
workingBalance = "${account.currencyName} ${account.currentBalance}", workingBalance = "${account.currencyName} ${account.currentBalance}",
blockedBalance = null blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
) )
} }
}
+1 -1
View File
@@ -4,6 +4,6 @@
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="?attr/colorOnSurface" android:fillColor="?attr/colorOnSurfaceVariant"
android:pathData="M9.5,6.5v3h-3v-3H9.5zM11,5L5,5v5.5h6L11,5zM9.5,14.5v3h-3v-3H9.5zM11,13L5,13v5.5h6L11,13zM17.5,6.5v3h-3v-3H17.5zM19,5h-6v5.5h6L19,5zM13,13h1.5v1.5L13,14.5zM14.5,14.5L16,14.5L16,16h-1.5zM16,13h1.5v1.5L16,14.5zM13,16h1.5v1.5L13,17.5zM14.5,17.5L16,17.5L16,19h-1.5zM16,16h1.5v1.5L16,17.5zM17.5,14.5L19,14.5L19,16h-1.5zM17.5,17.5L19,17.5L19,19h-1.5z"/> android:pathData="M9.5,6.5v3h-3v-3H9.5zM11,5L5,5v5.5h6L11,5zM9.5,14.5v3h-3v-3H9.5zM11,13L5,13v5.5h6L11,13zM17.5,6.5v3h-3v-3H17.5zM19,5h-6v5.5h6L19,5zM13,13h1.5v1.5L13,14.5zM14.5,14.5L16,14.5L16,16h-1.5zM16,13h1.5v1.5L16,14.5zM13,16h1.5v1.5L13,17.5zM14.5,17.5L16,17.5L16,19h-1.5zM16,16h1.5v1.5L16,17.5zM17.5,14.5L19,14.5L19,16h-1.5zM17.5,17.5L19,17.5L19,19h-1.5z"/>
</vector> </vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/seed_primary" />
<foreground android:drawable="@drawable/ic_shortcut_pay_card_fg" />
<monochrome android:drawable="@drawable/ic_shortcut_pay_card_fg" />
</adaptive-icon>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:translateX="30"
android:translateY="30"
android:scaleX="2"
android:scaleY="2">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,4H4C2.89,4 2.01,4.89 2.01,6L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V6C22,4.89 21.11,4 20,4zM20,18H4v-6h16V18zM20,8H4V6h16V8z" />
</group>
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/seed_primary" />
<foreground android:drawable="@drawable/ic_shortcut_scan_qr_fg" />
<monochrome android:drawable="@drawable/ic_shortcut_scan_qr_fg" />
</adaptive-icon>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:translateX="30"
android:translateY="30"
android:scaleX="2"
android:scaleY="2">
<path
android:fillColor="#FFFFFF"
android:pathData="M9.5,6.5v3h-3v-3H9.5zM11,5L5,5v5.5h6L11,5zM9.5,14.5v3h-3v-3H9.5zM11,13L5,13v5.5h6L11,13zM17.5,6.5v3h-3v-3H17.5zM19,5h-6v5.5h6L19,5zM13,13h1.5v1.5L13,14.5zM14.5,14.5L16,14.5L16,16h-1.5zM16,13h1.5v1.5L16,14.5zM13,16h1.5v1.5L13,17.5zM14.5,17.5L16,17.5L16,19h-1.5zM16,16h1.5v1.5L16,17.5zM17.5,14.5L19,14.5L19,16h-1.5zM17.5,17.5L19,17.5L19,19h-1.5z" />
</group>
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/seed_primary" />
<foreground android:drawable="@drawable/ic_shortcut_transfer_fg" />
<monochrome android:drawable="@drawable/ic_shortcut_transfer_fg" />
</adaptive-icon>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:translateX="30"
android:translateY="30"
android:scaleX="2"
android:scaleY="2">
<path
android:fillColor="#FFFFFF"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z" />
</group>
</vector>
+194
View File
@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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="match_parent"
android:background="?attr/colorSurface">
<!-- Main content when cards exist -->
<LinearLayout
android:id="@+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<!-- Top spacer: pushes card to vertical center -->
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- Horizontal card stack. Width/padding set programmatically for centering + peek. -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:overScrollMode="never" />
<!-- Page indicator dots -->
<LinearLayout
android:id="@+id/pageIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="horizontal"
android:paddingTop="12dp"
android:paddingBottom="2dp" />
<!-- Selected card type / product name -->
<TextView
android:id="@+id/tvSelectedCardType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingHorizontal="16dp"
android:paddingTop="6dp"
android:paddingBottom="2dp"
android:textAppearance="?attr/textAppearanceLabelLarge"
android:textColor="?attr/colorOnSurfaceVariant" />
<!-- Flexible spacer: absorbs remaining space, pushes buttons to bottom -->
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- Divider -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="4dp"
android:background="?attr/colorOutlineVariant" />
<!-- Primary pay actions -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="4dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnScanToPay"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:text="@string/card_pay_qr"
android:textSize="13sp"
app:icon="@drawable/ic_qr_scan"
app:iconSize="22dp"
app:iconGravity="top"
app:iconPadding="6dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnTapToPay"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:text="@string/card_pay_nfc"
android:textSize="13sp"
app:icon="@drawable/ic_nfc"
app:iconSize="22dp"
app:iconGravity="top"
app:iconPadding="6dp" />
</LinearLayout>
<!-- Secondary card management actions -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="8dp"
android:paddingTop="4dp"
android:paddingBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnChangePin"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_change_pin"
android:textSize="11sp"
app:icon="@drawable/ic_edit"
app:iconSize="16dp"
app:iconPadding="3dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnFreeze"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_freeze"
android:textSize="11sp"
app:icon="@drawable/ic_freeze"
app:iconSize="16dp"
app:iconPadding="3dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnBlock"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_block"
android:textSize="11sp"
app:icon="@drawable/ic_block"
app:iconSize="16dp"
app:iconPadding="3dp" />
</LinearLayout>
</LinearLayout>
<!-- Loading state -->
<LinearLayout
android:id="@+id/loadingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- Empty state -->
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/cards_empty"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:visibility="gone" />
</FrameLayout>
+97 -16
View File
@@ -176,6 +176,103 @@
</LinearLayout> </LinearLayout>
<!-- Attention row: Blocked + Overdue (hidden when nothing applies) -->
<LinearLayout
android:id="@+id/rowAttention"
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/cardBlocked"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:visibility="gone">
<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/dashboard_blocked"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvBlockedTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvBlockedSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer"
android:alpha="0.75"
android:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardOverdue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:clickable="true"
android:focusable="true"
android:visibility="gone">
<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/dashboard_overdue"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvOverdueTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Pending Finances card --> <!-- Pending Finances card -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/cardPendingFinances" android:id="@+id/cardPendingFinances"
@@ -228,14 +325,6 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:visibility="gone"> android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_pay_with_card"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="8dp"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCards" android:id="@+id/rvCards"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -261,14 +350,6 @@
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="16dp"> android:paddingBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dashboard_quick_actions"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="8dp" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -45,7 +45,14 @@
</com.google.android.material.button.MaterialButtonToggleGroup> </com.google.android.material.button.MaterialButtonToggleGroup>
<!-- Quick actions (always active) --> <!-- Quick actions — shown only when drawer nav is active -->
<LinearLayout
android:id="@+id/sectionQuickActions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -59,8 +66,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" android:nestedScrollingEnabled="false"
android:overScrollMode="never" android:overScrollMode="never" />
android:layout_marginBottom="16dp" />
</LinearLayout>
<!-- Bottom bar shortcuts — shown only when bottom nav is active --> <!-- Bottom bar shortcuts — shown only when bottom nav is active -->
<LinearLayout <LinearLayout
@@ -148,6 +156,87 @@
</com.google.android.material.button.MaterialButtonToggleGroup> </com.google.android.material.button.MaterialButtonToggleGroup>
<!-- Pitch black — enabled only in explicit dark mode -->
<LinearLayout
android:id="@+id/rowPitchBlack"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_pitch_black"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchPitchBlack"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- Accent color — always shown, disabled/grayed in system theme mode -->
<LinearLayout
android:id="@+id/sectionAccentColor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_accent_color"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:layout_marginBottom="12dp" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/accentToggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:singleSelection="true"
app:selectionRequired="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAccentBlue"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/accent_blue" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAccentOrange"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/accent_orange" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAccentGreen"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/accent_green" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAccentCustom"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/accent_custom" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -194,44 +194,6 @@
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
<LinearLayout
android:id="@+id/rowHideAmounts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<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/settings_hide_amounts"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_hide_amounts_desc"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchHideAmounts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/rowBlockScreenshots" android:id="@+id/rowBlockScreenshots"
android:layout_width="match_parent" android:layout_width="match_parent"
+9
View File
@@ -72,6 +72,15 @@
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface" /> android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvBlocked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorError"
android:visibility="gone" />
<ImageButton <ImageButton
android:id="@+id/btnTransfer" android:id="@+id/btnTransfer"
android:layout_width="40dp" android:layout_width="40dp"
@@ -0,0 +1,79 @@
<?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_marginHorizontal="6dp"
app:cardCornerRadius="16dp"
app:cardElevation="6dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivCardImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:contentDescription="@string/nav_pay_with_card" />
<!-- Bottom gradient for text legibility -->
<View
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_gravity="bottom"
android:background="@drawable/bg_card_overlay_gradient" />
<!-- Status badge (top-right) -->
<TextView
android:id="@+id/tvCardStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="8dp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:textSize="9sp"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:visibility="gone" />
<!-- Card owner name + masked number (bottom-left) -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tvCardOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="3" />
<TextView
android:id="@+id/tvCardNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="#CCFFFFFF"
android:fontFamily="monospace" />
</LinearLayout>
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
-3
View File
@@ -32,9 +32,6 @@
<item android:id="@+id/nav_pay_with_card" <item android:id="@+id/nav_pay_with_card"
android:icon="@drawable/ic_nav_card" android:icon="@drawable/ic_nav_card"
android:title="@string/nav_pay_with_card" /> android:title="@string/nav_pay_with_card" />
<item android:id="@+id/nav_card_settings"
android:icon="@drawable/ic_nav_card"
android:title="@string/nav_card_settings" />
</group> </group>
<group android:id="@+id/group_system" android:checkableBehavior="single"> <group android:id="@+id/group_system" android:checkableBehavior="single">
-3
View File
@@ -15,9 +15,6 @@
<item android:id="@+id/nav_pay_with_card" <item android:id="@+id/nav_pay_with_card"
android:icon="@drawable/ic_nav_card" android:icon="@drawable/ic_nav_card"
android:title="@string/nav_pay_with_card" /> android:title="@string/nav_pay_with_card" />
<item android:id="@+id/nav_card_settings"
android:icon="@drawable/ic_nav_card"
android:title="@string/nav_card_settings" />
<item android:id="@+id/nav_otp" <item android:id="@+id/nav_otp"
android:icon="@drawable/ic_nav_otp" android:icon="@drawable/ic_nav_otp"
android:title="@string/nav_otp" /> android:title="@string/nav_otp" />
+2 -3
View File
@@ -72,7 +72,8 @@
<string name="nav_activities">ހަރަކާތްތައް</string> <string name="nav_activities">ހަރަކާތްތައް</string>
<string name="nav_transfer_history">ޓްރާންސެކްޝަން ތާރީހް</string> <string name="nav_transfer_history">ޓްރާންސެކްޝަން ތާރީހް</string>
<string name="nav_finances">ފައިނޭންސް</string> <string name="nav_finances">ފައިނޭންސް</string>
<string name="nav_card_settings">ކާޑް ސެޓިންގ</string> <string name="nav_pay_with_card">ކާޑްތައް</string>
<string name="nav_desc_pay_with_card">ކާޑް މެނޭޖްކޮށް ފައިސާ ދައްކާ</string>
<string name="nav_settings">ސެޓިންގ</string> <string name="nav_settings">ސެޓިންގ</string>
<string name="nav_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string> <string name="nav_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string>
<string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string> <string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string>
@@ -81,8 +82,6 @@
<string name="nav_desc_activities">ފަހުގެ ޓްރާންސްފަތައް ބަލާ</string> <string name="nav_desc_activities">ފަހުގެ ޓްރާންސްފަތައް ބަލާ</string>
<string name="nav_desc_transfer_history">އެކައުންޓް ތަކުގެ ޓްރާންސެކްޝަން ތާރީހް</string> <string name="nav_desc_transfer_history">އެކައުންޓް ތަކުގެ ޓްރާންސެކްޝަން ތާރީހް</string>
<string name="nav_desc_finances">ލޯން އަދި ފައިނޭންސިންގ</string> <string name="nav_desc_finances">ލޯން އަދި ފައިނޭންސިންގ</string>
<string name="nav_desc_pay_with_card">ކާޑް ބޭނުންކޮށް ފައިސާ ދައްކާ</string>
<string name="nav_desc_card_settings">ކާޑް ސެޓިންގ މެނޭޖް ކުރޭ</string>
<string name="nav_desc_otp">OTP ކޯޑް ތައްޔާރު ކުރޭ</string> <string name="nav_desc_otp">OTP ކޯޑް ތައްޔާރު ކުރޭ</string>
<string name="nav_desc_settings">އެޕްލިކޭޝަންގެ ތަރުތީބު</string> <string name="nav_desc_settings">އެޕްލިކޭޝަންގެ ތަރުތީބު</string>
<string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string> <string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string>
+16 -5
View File
@@ -93,8 +93,7 @@
<string name="nav_desc_activities">View your recent transfers</string> <string name="nav_desc_activities">View your recent transfers</string>
<string name="nav_desc_transfer_history">Full transaction history by account</string> <string name="nav_desc_transfer_history">Full transaction history by account</string>
<string name="nav_desc_finances">Loans and financing overview</string> <string name="nav_desc_finances">Loans and financing overview</string>
<string name="nav_desc_pay_with_card">Make a payment using your card</string> <string name="nav_desc_pay_with_card">Manage and pay with your cards</string>
<string name="nav_desc_card_settings">Manage your card preferences</string>
<string name="nav_desc_otp">Generate OTP codes for authentication</string> <string name="nav_desc_otp">Generate OTP codes for authentication</string>
<string name="nav_desc_settings">App preferences and configuration</string> <string name="nav_desc_settings">App preferences and configuration</string>
<string name="nav_open_drawer">Open navigation</string> <string name="nav_open_drawer">Open navigation</string>
@@ -149,6 +148,15 @@
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="settings_pitch_black">Pitch Black</string>
<string name="settings_accent_color">Accent Color</string>
<string name="accent_blue">Blue</string>
<string name="accent_orange">Red</string>
<string name="accent_green">Green</string>
<string name="accent_custom">Custom</string>
<string name="accent_custom_pick">Custom Accent Color</string>
<string name="accent_custom_hint">#RRGGBB hex color</string>
<string name="accent_invalid_color">Invalid color — enter a valid hex code</string>
<string name="language">Language</string> <string name="language">Language</string>
<string name="lang_english">English</string> <string name="lang_english">English</string>
<string name="lang_dhivehi">ދިވެހި</string> <string name="lang_dhivehi">ދިވެހި</string>
@@ -209,6 +217,9 @@
<string name="accounts">Accounts</string> <string name="accounts">Accounts</string>
<string name="cards">Cards</string> <string name="cards">Cards</string>
<string name="available_balance">Available Balance</string> <string name="available_balance">Available Balance</string>
<string name="account_blocked_label">%1$s blocked</string>
<string name="dashboard_blocked">Blocked Funds</string>
<string name="dashboard_overdue">Overdue Financing</string>
<!-- Transfer --> <!-- Transfer -->
<string name="transfer_tab_quick">Quick Transfer</string> <string name="transfer_tab_quick">Quick Transfer</string>
@@ -311,9 +322,9 @@
<string name="loan_rate_fmt">%.2f%%</string> <string name="loan_rate_fmt">%.2f%%</string>
<!-- Cards --> <!-- Cards -->
<string name="nav_pay_with_card">Pay with Card</string> <string name="nav_pay_with_card">Cards</string>
<string name="card_pay_qr">QR Pay</string> <string name="card_pay_qr">Scan to Pay</string>
<string name="card_pay_nfc">NFC Pay</string> <string name="card_pay_nfc">Tap to Pay</string>
<string name="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string> <string name="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string>
<string name="card_action_change_pin">Change PIN</string> <string name="card_action_change_pin">Change PIN</string>
<string name="card_action_freeze">Freeze</string> <string name="card_action_freeze">Freeze</string>
+28
View File
@@ -6,4 +6,32 @@
<item name="colorSecondary">@color/seed_secondary</item> <item name="colorSecondary">@color/seed_secondary</item>
<item name="android:windowSoftInputMode">adjustResize</item> <item name="android:windowSoftInputMode">adjustResize</item>
</style> </style>
<!-- Accent overlays — applied on API < 31 as fallback when content-based dynamic colors unavailable -->
<style name="ThemeOverlay_Accent_Blue" parent="">
<item name="colorPrimary">#3F65AD</item>
<item name="colorSecondary">#9AD141</item>
</style>
<style name="ThemeOverlay_Accent_Orange" parent="">
<item name="colorPrimary">#D32F2F</item>
<item name="colorSecondary">#EF9A9A</item>
</style>
<style name="ThemeOverlay_Accent_Green" parent="">
<item name="colorPrimary">#4CAF50</item>
<item name="colorSecondary">#80CBC4</item>
</style>
<!-- Pitch black overlay — forces pure black surfaces for OLED displays -->
<style name="ThemeOverlay_PitchBlack" parent="">
<item name="android:colorBackground">#000000</item>
<item name="colorSurface">#000000</item>
<item name="colorSurfaceVariant">#0d0d0d</item>
<item name="colorSurfaceContainer">#0d0d0d</item>
<item name="colorSurfaceContainerLow">#000000</item>
<item name="colorSurfaceContainerLowest">#000000</item>
<item name="colorSurfaceContainerHigh">#1a1a1a</item>
<item name="colorSurfaceContainerHighest">#1a1a1a</item>
</style>
</resources> </resources>
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="transfer"
android:enabled="true"
android:icon="@drawable/ic_shortcut_transfer"
android:shortcutShortLabel="@string/transfer"
android:shortcutLongLabel="@string/transfer">
<intent
android:action="sh.sar.basedbank.OPEN_TRANSFER"
android:targetPackage="sh.sar.basedbank"
android:targetClass="sh.sar.basedbank.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="scan_qr"
android:enabled="true"
android:icon="@drawable/ic_shortcut_scan_qr"
android:shortcutShortLabel="@string/transfer_scan_qr"
android:shortcutLongLabel="@string/transfer_scan_qr">
<intent
android:action="sh.sar.basedbank.OPEN_SCAN_QR"
android:targetPackage="sh.sar.basedbank"
android:targetClass="sh.sar.basedbank.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="pay_with_card"
android:enabled="true"
android:icon="@drawable/ic_shortcut_pay_card"
android:shortcutShortLabel="@string/nav_pay_with_card"
android:shortcutLongLabel="@string/nav_pay_with_card">
<intent
android:action="sh.sar.basedbank.OPEN_PAY_WITH_CARD"
android:targetPackage="sh.sar.basedbank"
android:targetClass="sh.sar.basedbank.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>