Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ed5b456e3b
|
|||
|
9b284cc8d4
|
|||
|
c0b58061c2
|
|||
|
978da26ff1
|
|||
|
7fe2ba5788
|
|||
|
26a0c7b81d
|
|||
|
83fc340e2b
|
|||
|
bfbb649b33
|
|||
|
b780091bb8
|
|||
|
e4468c4a8f
|
|||
|
b4e1f57347
|
|||
|
907757c893
|
|||
|
1ea0355ce6
|
|||
|
c9b8973b65
|
|||
|
7a0e32f4d6
|
|||
|
d68b8aaf0a
|
|||
|
396f778ad4
|
|||
|
dc0f1b96c1
|
|||
|
640dd5de22
|
|||
|
f0a0e7857c
|
|||
|
836f4c493a
|
|||
|
6325f4fd7a
|
|||
|
69aa172eff
|
|||
|
ed2054fb81
|
|||
|
e9583f0580
|
|||
|
a32841a319
|
|||
|
7a66dd836c
|
|||
|
68dd49b90c
|
|||
|
76090525e1
|
|||
|
f7fd06cdf3
|
|||
|
8d09e760a8
|
|||
|
62ccae602d
|
|||
|
9011ef2f5a
|
|||
|
dd620763ec
|
|||
|
86063d600f
|
@@ -87,3 +87,24 @@ jobs:
|
||||
--data-binary "@${ASSET_PATH}"
|
||||
|
||||
echo "Uploaded asset: $ASSET_NAME"
|
||||
|
||||
- name: Send APK to Telegram
|
||||
env:
|
||||
TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
|
||||
TG_CHAT_ID: ${{ vars.TG_CHAT_ID }}
|
||||
run: |
|
||||
if [ -z "$TG_BOT_TOKEN" ] || [ -z "$TG_CHAT_ID" ]; then
|
||||
echo "TG_BOT_TOKEN or TG_CHAT_ID not set, skipping Telegram upload."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
APP_NAME="${{ gitea.repository }}"
|
||||
APP_NAME="${APP_NAME##*/}"
|
||||
TAG="${{ gitea.ref_name }}"
|
||||
ASSET_PATH=".build/release/release/${APP_NAME}-${TAG}.apk"
|
||||
CAPTION="${APP_NAME} ${TAG}"
|
||||
|
||||
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendDocument" \
|
||||
-F "chat_id=${TG_CHAT_ID}" \
|
||||
-F "document=@${ASSET_PATH}" \
|
||||
-F "caption=${CAPTION}"
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "sh.sar.basedbank"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 8
|
||||
versionName = "1.0.9"
|
||||
versionCode = 10
|
||||
versionName = "1.0.11"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -27,6 +27,10 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
}
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Thijooree Debug</string>
|
||||
</resources>
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<application
|
||||
android:name=".BasedBankApp"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -30,6 +30,9 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 196 KiB |
@@ -0,0 +1 @@
|
||||
visa_bingaa.png
|
||||
@@ -0,0 +1 @@
|
||||
visa_bingaa.png
|
||||
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 192 KiB |
|
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() {
|
||||
|
||||
/**
|
||||
* 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
|
||||
var accounts: List<BankAccount> = emptyList()
|
||||
var fullName: String = ""
|
||||
@@ -108,7 +115,11 @@ class BasedBankApp : Application() {
|
||||
|
||||
override fun 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")
|
||||
AppCompatDelegate.setDefaultNightMode(when (theme) {
|
||||
|
||||
@@ -21,6 +21,8 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.databinding.ActivityLockBinding
|
||||
import sh.sar.basedbank.ui.home.HomeActivity
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.util.ThemeHelper
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
|
||||
@@ -45,6 +47,7 @@ class LockActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeHelper.applyAccent(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = ActivityLockBinding.inflate(layoutInflater)
|
||||
@@ -54,6 +57,9 @@ class LockActivity : AppCompatActivity() {
|
||||
isAppearanceLightStatusBars = 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 ->
|
||||
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
|
||||
@@ -259,10 +265,23 @@ class LockActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun proceed() {
|
||||
(application as BasedBankApp).isUnlocked = true
|
||||
if (intent.getBooleanExtra(EXTRA_RESUME, false)) {
|
||||
finish()
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,24 +7,43 @@ import sh.sar.basedbank.ui.home.HomeActivity
|
||||
import sh.sar.basedbank.ui.login.LoginActivity
|
||||
import sh.sar.basedbank.ui.onboarding.OnboardingActivity
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||
val onboardingDone = prefs.getBoolean("onboarding_done", false)
|
||||
val securitySet = prefs.getString("security_method", null) != null
|
||||
val store = CredentialStore(this)
|
||||
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 {
|
||||
!onboardingDone -> OnboardingActivity::class.java
|
||||
!hasCredentials -> LoginActivity::class.java
|
||||
securitySet -> LockActivity::class.java // proceed() → HomeActivity
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.json.JSONObject
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
class SessionExpiredException : Exception("MIB session expired")
|
||||
@@ -168,7 +169,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
accountTypeName = a.optString("accountTypeName"),
|
||||
availableBalance = a.optString("availableBalance"),
|
||||
currentBalance = a.optString("currentBalance"),
|
||||
blockedAmount = a.optString("blockedAmount"),
|
||||
blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
|
||||
mvrBalance = a.optString("mvrBalance"),
|
||||
statusDesc = a.optString("statusDesc"),
|
||||
profileImageHash = profile.customerImage,
|
||||
@@ -188,6 +189,13 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
|
||||
// ─── 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(
|
||||
appId: String, encKey: String, sfunc: String, key2: String? = null
|
||||
): Pair<MibSession, String> {
|
||||
@@ -325,7 +333,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
accountTypeName = a.optString("accountTypeName"),
|
||||
availableBalance = a.optString("availableBalance"),
|
||||
currentBalance = a.optString("currentBalance"),
|
||||
blockedAmount = a.optString("blockedAmount"),
|
||||
blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
|
||||
mvrBalance = a.optString("mvrBalance"),
|
||||
statusDesc = a.optString("statusDesc"),
|
||||
profileImageHash = profile.customerImage,
|
||||
|
||||
@@ -125,6 +125,14 @@ class AccountsAdapter(
|
||||
binding.tvAccountNumber.text = display.number
|
||||
binding.tvAccountType.text = display.typeLabel
|
||||
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.root.setOnClickListener { onAccountClick(account) }
|
||||
binding.root.setOnLongClickListener {
|
||||
|
||||
@@ -1,139 +1,3 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Merged into CardsFragment
|
||||
|
||||
@@ -25,6 +25,8 @@ import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||
import sh.sar.basedbank.util.PaymvQrParser
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import kotlin.math.abs
|
||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||
@@ -40,10 +42,10 @@ class DashboardFragment : Fragment() {
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") ||
|
||||
raw.startsWith("https://pay.bml.com.mv/app/")) {
|
||||
val bmlUrl = PaymvQrParser.extractBmlGatewayUrl(raw)
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != null) {
|
||||
(requireActivity() as HomeActivity).navigateTo(
|
||||
R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(raw, pendingQrAccountNumber)
|
||||
R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(bmlUrl ?: raw, pendingQrAccountNumber)
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
|
||||
@@ -57,14 +59,24 @@ class DashboardFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
|
||||
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() }
|
||||
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) {
|
||||
updateBalances(it)
|
||||
updateAttentionRow()
|
||||
}
|
||||
viewModel.financing.observe(viewLifecycleOwner) {
|
||||
updatePendingFinances()
|
||||
updateAttentionRow()
|
||||
}
|
||||
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) {
|
||||
updatePendingFinances()
|
||||
updateAttentionRow()
|
||||
}
|
||||
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
||||
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
||||
updateBalances(viewModel.accounts.value ?: emptyList())
|
||||
updatePendingFinances()
|
||||
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
|
||||
updateAttentionRow()
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
@@ -76,6 +88,10 @@ class DashboardFragment : Fragment() {
|
||||
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
|
||||
}
|
||||
|
||||
binding.cardOverdue.setOnClickListener {
|
||||
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
|
||||
}
|
||||
|
||||
val cardAdapter = DashboardCardAdapter()
|
||||
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.rvCards.adapter = cardAdapter
|
||||
@@ -87,8 +103,13 @@ class DashboardFragment : Fragment() {
|
||||
.filter { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
.map { CardItem.Bml(it) }
|
||||
val all = mibItems + bmlItems
|
||||
cardAdapter.update(all)
|
||||
binding.sectionCards.visibility = if (all.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber()
|
||||
val ordered = if (defaultNum != null) {
|
||||
val def = all.filterIsInstance<CardItem.Bml>().firstOrNull { it.account.accountNumber == defaultNum }
|
||||
if (def != null) listOf(def) + all.filter { it !== def } else all
|
||||
} else all
|
||||
cardAdapter.update(ordered)
|
||||
binding.sectionCards.visibility = if (ordered.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||
@@ -112,6 +133,12 @@ class DashboardFragment : Fragment() {
|
||||
|
||||
private fun refreshQuickActions() {
|
||||
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)
|
||||
listOf(binding.btnQuickAction1, binding.btnQuickAction2).forEachIndexed { i, btn ->
|
||||
val def = NavCustomization.ALL_SWAPPABLE.find { it.id == ids[i] }
|
||||
@@ -244,6 +271,52 @@ 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 blockedMvr = blockedByCurrency["MVR"] ?: 0.0
|
||||
val blockedUsd = blockedByCurrency["USD"] ?: 0.0
|
||||
val blockedTotal = blockedByCurrency.values.sum()
|
||||
|
||||
if (blockedMvr > 0.0) {
|
||||
binding.tvBlockedMvr.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(blockedMvr)
|
||||
binding.cardBlockedMvr.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.cardBlockedMvr.visibility = View.GONE
|
||||
}
|
||||
if (blockedUsd > 0.0) {
|
||||
binding.tvBlockedUsd.text = if (hide) "USD ••••••" else "USD %,.2f".format(blockedUsd)
|
||||
binding.cardBlockedUsd.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.cardBlockedUsd.visibility = View.GONE
|
||||
}
|
||||
binding.rowBlocked.visibility = if (blockedTotal > 0.0) View.VISIBLE else 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 (overdueTotal > 0.0) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun updatePendingFinances() {
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount }
|
||||
@@ -287,17 +360,17 @@ class DashboardFragment : Fragment() {
|
||||
when (item) {
|
||||
is CardItem.Mib -> {
|
||||
tvCardOwner.text = item.card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(item.card.maskedCardNumber)
|
||||
val assetPath = PayWithCardFragment.cardImageAsset(item.card)
|
||||
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||
tvCardNumber.text = CardsFragment.formatMasked(item.card.maskedCardNumber)
|
||||
val assetPath = CardsFragment.cardImageAsset(item.card)
|
||||
if (assetPath != null) CardsFragment.loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.mibCardStatusLabel(item.card.cardStatus))
|
||||
CardsFragment.bindCardStatus(tvCardStatus, CardsFragment.mibCardStatusLabel(item.card.cardStatus))
|
||||
}
|
||||
is CardItem.Bml -> {
|
||||
tvCardOwner.text = item.account.accountBriefName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(item.account.accountNumber)
|
||||
PayWithCardFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
PayWithCardFragment.bindCardStatus(tvCardStatus, null) // only Active BML cards reach dashboard
|
||||
tvCardNumber.text = CardsFragment.formatMasked(item.account.accountNumber)
|
||||
CardsFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
CardsFragment.bindCardStatus(tvCardStatus, null) // only Active BML cards reach dashboard
|
||||
}
|
||||
}
|
||||
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.FinancingCache
|
||||
import sh.sar.basedbank.util.ForeignLimitsCache
|
||||
import sh.sar.basedbank.util.ThemeHelper
|
||||
|
||||
class HomeActivity : AppCompatActivity() {
|
||||
|
||||
@@ -106,6 +107,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeHelper.applyAccent(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||
@@ -118,6 +120,21 @@ class HomeActivity : AppCompatActivity() {
|
||||
isAppearanceLightStatusBars = 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)
|
||||
|
||||
toggle = ActionBarDrawerToggle(
|
||||
@@ -147,8 +164,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
R.id.nav_pay_with_card -> CardsFragment()
|
||||
else -> null
|
||||
}
|
||||
if (frag != null) show(frag)
|
||||
@@ -165,7 +181,6 @@ class HomeActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
// Load data
|
||||
val app = application as BasedBankApp
|
||||
if (app.mibAccounts.isNotEmpty() || app.bmlAccounts.isNotEmpty() || app.fahipayAccounts.isNotEmpty()) {
|
||||
// Came from fresh manual login — accounts ready, rest fetched in background
|
||||
val merged = app.mibAccounts + app.bmlAccounts + app.fahipayAccounts
|
||||
@@ -218,10 +233,19 @@ class HomeActivity : AppCompatActivity() {
|
||||
|
||||
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) {
|
||||
show(DashboardFragment())
|
||||
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
|
||||
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())
|
||||
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
@@ -231,6 +255,9 @@ class HomeActivity : AppCompatActivity() {
|
||||
binding.drawerLayout.closeDrawers()
|
||||
return
|
||||
}
|
||||
// Let CardsFragment handle back if in manage mode
|
||||
val currentFrag = supportFragmentManager.findFragmentById(R.id.contentFrame)
|
||||
if (currentFrag is CardsFragment && currentFrag.onBackPressed()) return
|
||||
// Pop fragment back stack if there's anything on it (e.g. showWithBackStack)
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
@@ -239,7 +266,6 @@ class HomeActivity : AppCompatActivity() {
|
||||
// In bottom nav mode, pressing back navigates up the hierarchy
|
||||
val isBottomNav = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
if (isBottomNav && binding.bottomNavigation.selectedItemId != R.id.nav_dashboard) {
|
||||
val currentFrag = supportFragmentManager.findFragmentById(R.id.contentFrame)
|
||||
// Sub-page reached via More (e.g. Settings, Activities) — go back to More
|
||||
if (binding.bottomNavigation.selectedItemId == R.id.nav_more && currentFrag !is MoreFragment) {
|
||||
show(MoreFragment())
|
||||
@@ -359,8 +385,7 @@ fun applyNavLabelVisibility() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
R.id.nav_pay_with_card -> CardsFragment()
|
||||
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
|
||||
}
|
||||
show(dest)
|
||||
@@ -494,9 +519,8 @@ fun applyNavLabelVisibility() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
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)
|
||||
eyeItem?.isVisible = eyeEnabled
|
||||
eyeItem?.isVisible = true
|
||||
val hidden = viewModel.hideAmounts.value ?: false
|
||||
eyeItem?.setIcon(if (hidden) R.drawable.ic_visibility_off else R.drawable.ic_visibility)
|
||||
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_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_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_settings, "nav_settings", R.drawable.ic_nav_settings, R.string.nav_settings, R.string.nav_desc_settings),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -42,6 +47,16 @@ class OtpFragment : Fragment() {
|
||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||
holder.b.tvOtpLabel.text = entries[position].label
|
||||
update(holder.b, entries[position].seed)
|
||||
holder.b.root.setOnClickListener {
|
||||
val code = holder.b.tvOtpCode.text.toString().replace(" ", "")
|
||||
if (code.isNotEmpty()) {
|
||||
val clipboard = it.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("OTP", code))
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
Toast.makeText(it.context, "OTP copied", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tick() {
|
||||
|
||||
@@ -36,6 +36,7 @@ import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.databinding.FragmentPayMvQrBinding
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||
import sh.sar.basedbank.util.AccountListParser
|
||||
import sh.sar.basedbank.util.PaymvQrParser
|
||||
@@ -60,9 +61,9 @@ class PayMvQrFragment : Fragment() {
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
|
||||
// BML card/gateway QR — hand off to dedicated payment screen
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") ||
|
||||
raw.startsWith("https://pay.bml.com.mv/app/")) {
|
||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(raw))
|
||||
val bmlUrl = PaymvQrParser.extractBmlGatewayUrl(raw)
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != null) {
|
||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(bmlUrl ?: raw))
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
@@ -98,6 +99,8 @@ class PayMvQrFragment : Fragment() {
|
||||
}
|
||||
setupDropdown()
|
||||
binding.etAmount.addTextChangedListener { scheduleGenerate() }
|
||||
binding.etReference.addTextChangedListener { scheduleGenerate() }
|
||||
binding.switchIncludePhone.setOnCheckedChangeListener { _, _ -> scheduleGenerate() }
|
||||
binding.btnShare.isEnabled = false
|
||||
binding.btnSave.isEnabled = false
|
||||
binding.btnShare.setOnClickListener { shareQr() }
|
||||
@@ -110,7 +113,9 @@ class PayMvQrFragment : Fragment() {
|
||||
private fun setupDropdown() {
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||
val eligible = accounts.filter {
|
||||
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN"
|
||||
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN" &&
|
||||
it.bank != "MIB" && // TODO: MIB does not support PayMV QR
|
||||
!(it.bank == "BML" && it.currencyName.contains("USD", ignoreCase = true)) // TODO: BML USD not supported by MMA
|
||||
}
|
||||
val adapter = QrAccountAdapter(requireContext(), eligible)
|
||||
binding.actvAccount.setAdapter(adapter)
|
||||
@@ -145,8 +150,28 @@ class PayMvQrFragment : Fragment() {
|
||||
?.let { "%.2f".format(it) }
|
||||
|
||||
val ctx = requireContext()
|
||||
val includePhone = binding.switchIncludePhone.isChecked
|
||||
val loginId = sh.sar.basedbank.util.ProfileImageStore.loginIdFromTag(account.loginTag)
|
||||
val store = CredentialStore(ctx)
|
||||
val mobile = if (includePhone) {
|
||||
when (account.bank) {
|
||||
"BML" -> store.loadBmlUserProfile(loginId)?.mobile
|
||||
"FAHIPAY" -> store.loadFahipayUserProfile(loginId)?.mobile
|
||||
else -> null
|
||||
}?.let { m ->
|
||||
when {
|
||||
m.startsWith("+") -> m
|
||||
m.length == 7 -> "+960$m"
|
||||
else -> m
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
val purpose = binding.etReference.text?.toString()?.trim()
|
||||
?.takeIf { it.isNotBlank() } ?: getString(R.string.paymvqr_reference_default)
|
||||
|
||||
val bmp = withContext(Dispatchers.Default) {
|
||||
val payload = buildQrPayload(account.accountNumber, account.accountBriefName, acquirer, amountFormatted)
|
||||
val payload = buildQrPayload(account.accountNumber, account.accountBriefName, acquirer, amountFormatted, mobile, purpose)
|
||||
renderQrCard(ctx, account, payload, amountFormatted)
|
||||
}
|
||||
if (_binding == null) return
|
||||
@@ -164,7 +189,9 @@ class PayMvQrFragment : Fragment() {
|
||||
accountNumber: String,
|
||||
accountName: String,
|
||||
acquirer: String,
|
||||
amountStr: String?
|
||||
amountStr: String?,
|
||||
mobile: String?,
|
||||
purpose: String
|
||||
): String {
|
||||
fun tlv(tag: String, value: String): String {
|
||||
val len = value.length
|
||||
@@ -174,17 +201,30 @@ class PayMvQrFragment : Fragment() {
|
||||
val poi = tlv("01", "11")
|
||||
val sub00 = tlv("00", "mv.favara.mpqr")
|
||||
val sub01 = tlv("01", acquirer)
|
||||
val sub02 = tlv("02", acquirer) // repeated acquirer, as per official PayMV app
|
||||
val sub03 = tlv("03", accountNumber)
|
||||
val sub05 = if (!mobile.isNullOrBlank()) tlv("05", mobile) else ""
|
||||
val sub10 = tlv("10", "IPAY")
|
||||
val merchantAcct = tlv("26", sub00 + sub01 + sub03 + sub10)
|
||||
val merchantAcct = tlv("26", sub00 + sub01 + sub02 + sub03 + sub05 + sub10)
|
||||
val mcc = tlv("52", "0000")
|
||||
val currency = tlv("53", "462")
|
||||
val amountTLV = if (!amountStr.isNullOrBlank()) tlv("54", amountStr) else ""
|
||||
val country = tlv("58", "MV")
|
||||
val name = tlv("59", accountName.take(25))
|
||||
val prefix = format + poi + merchantAcct + currency + amountTLV + country + name + "6304"
|
||||
val ref = generateReference()
|
||||
val addlData = tlv("62", tlv("05", ref) + tlv("08", purpose))
|
||||
val timestamp = java.time.LocalDateTime.now()
|
||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.00000"))
|
||||
val tag80 = tlv("80", tlv("00", "mv.favara.mpqr") + tlv("01", timestamp))
|
||||
val prefix = format + poi + merchantAcct + mcc + currency + amountTLV + country + name + addlData + tag80 + "6304"
|
||||
return prefix + crc16(prefix)
|
||||
}
|
||||
|
||||
private fun generateReference(): String {
|
||||
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return (1..9).map { chars.random() }.joinToString("")
|
||||
}
|
||||
|
||||
private fun crc16(data: String): String {
|
||||
var crc = 0xFFFF
|
||||
for (c in data) {
|
||||
@@ -427,7 +467,7 @@ class PayMvQrFragment : Fragment() {
|
||||
} else {
|
||||
b.tvDropdownAccountType.visibility = View.GONE
|
||||
}
|
||||
b.tvDropdownBalance.text = displayData?.balance ?: ""
|
||||
b.tvDropdownBalance.visibility = View.GONE
|
||||
b.root.alpha = 1f
|
||||
|
||||
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
||||
|
||||
@@ -3,44 +3,68 @@ package sh.sar.basedbank.ui.home
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
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.CredentialStore
|
||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||
import sh.sar.basedbank.util.PaymvQrParser
|
||||
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 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 isManageMode: Boolean = false
|
||||
|
||||
// Carousel snapshot captured on enter, used to reverse the exit animation
|
||||
private var carouselCardLayoutTop = 0f // card layout top relative to contentLayout
|
||||
private var carouselCardCenterX = 0f // card center X relative to contentLayout
|
||||
private var carouselTextLayoutTop = 0f // tvSelectedCardType layout top relative to contentLayout
|
||||
|
||||
// Swipe-to-dismiss tracking
|
||||
private var swipeDragStartRawY = 0f
|
||||
private var swipeIsDragging = false
|
||||
|
||||
private lateinit var stackAdapter: CardStackAdapter
|
||||
private val store by lazy { CredentialStore(requireContext()) }
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") ||
|
||||
raw.startsWith("https://pay.bml.com.mv/app/")) {
|
||||
val bmlUrl = PaymvQrParser.extractBmlGatewayUrl(raw)
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != null) {
|
||||
(requireActivity() as HomeActivity).navigateTo(
|
||||
R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(raw, pendingQrAccountNumber)
|
||||
R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(bmlUrl ?: raw, pendingQrAccountNumber)
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
|
||||
@@ -49,42 +73,54 @@ class PayWithCardFragment : Fragment() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = CardWalletAdapter(emptyList(), requireContext())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
val screenW = resources.displayMetrics.widthPixels
|
||||
val peekPx = screenW / 8
|
||||
cardWidth = screenW - 2 * peekPx
|
||||
|
||||
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)
|
||||
stackAdapter = CardStackAdapter(cardWidth)
|
||||
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, 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 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
|
||||
}
|
||||
|
||||
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") && it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
.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() }
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { rebuildCards() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { rebuildCards() }
|
||||
|
||||
val cached = CardsCache.load(requireContext())
|
||||
if (cached.isNotEmpty()) {
|
||||
@@ -93,6 +129,351 @@ class PayWithCardFragment : Fragment() {
|
||||
binding.loadingView.visibility = View.VISIBLE
|
||||
}
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
|
||||
binding.btnManageCard.setOnClickListener {
|
||||
setManageMode(!isManageMode)
|
||||
}
|
||||
|
||||
// Swipe-down on the manage card to dismiss manage mode
|
||||
binding.manageCardView.root.setOnTouchListener { _, event ->
|
||||
if (!isManageMode) return@setOnTouchListener false
|
||||
val mgr = binding.manageCardView.root
|
||||
when (event.action) {
|
||||
android.view.MotionEvent.ACTION_DOWN -> {
|
||||
mgr.animate().cancel()
|
||||
binding.tvSelectedCardType.animate().cancel()
|
||||
swipeDragStartRawY = event.rawY
|
||||
swipeIsDragging = false
|
||||
true
|
||||
}
|
||||
android.view.MotionEvent.ACTION_MOVE -> {
|
||||
val dy = (event.rawY - swipeDragStartRawY).coerceAtLeast(0f)
|
||||
if (dy > 12f || swipeIsDragging) {
|
||||
swipeIsDragging = true
|
||||
mgr.translationY = dy
|
||||
binding.tvSelectedCardType.translationY = dy * 0.6f
|
||||
val scale = 1f - (dy / (binding.contentLayout.height * 2.5f)).coerceIn(0f, 0.12f)
|
||||
mgr.scaleX = scale
|
||||
mgr.scaleY = scale
|
||||
true
|
||||
} else false
|
||||
}
|
||||
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
||||
if (swipeIsDragging) {
|
||||
val dy = (event.rawY - swipeDragStartRawY).coerceAtLeast(0f)
|
||||
swipeIsDragging = false
|
||||
if (dy > 130f) {
|
||||
setManageMode(false)
|
||||
} else {
|
||||
// Snap back
|
||||
mgr.animate().translationY(0f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(280).setInterpolator(DecelerateInterpolator()).start()
|
||||
binding.tvSelectedCardType.animate().translationY(0f)
|
||||
.setDuration(280).setInterpolator(DecelerateInterpolator()).start()
|
||||
}
|
||||
true
|
||||
} else false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
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 setManageMode(enabled: Boolean) {
|
||||
isManageMode = enabled
|
||||
requireActivity().title = getString(if (enabled) R.string.card_manage else R.string.nav_pay_with_card)
|
||||
if (enabled) enterManageMode() else exitManageMode()
|
||||
}
|
||||
|
||||
private fun enterManageMode() {
|
||||
val item = cards.getOrNull(currentCardPosition) ?: return
|
||||
|
||||
// Bind card data
|
||||
val cv = binding.manageCardView
|
||||
when (item) {
|
||||
is CardItem.Mib -> {
|
||||
cv.tvCardOwner.text = item.card.cardHolderName
|
||||
cv.tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
|
||||
val assetPath = cardImageAsset(item.card)
|
||||
if (assetPath != null) loadCardImage(cv.ivCardImage, assetPath)
|
||||
else cv.ivCardImage.setImageDrawable(null)
|
||||
bindCardStatus(cv.tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
|
||||
cv.root.alpha = 1f
|
||||
}
|
||||
is CardItem.Bml -> {
|
||||
cv.tvCardOwner.text = item.account.accountBriefName
|
||||
cv.tvCardNumber.text = formatMasked(item.account.accountNumber)
|
||||
loadCardImage(cv.ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
||||
bindCardStatus(cv.tvCardStatus, item.account.statusDesc.takeUnless { isActive })
|
||||
cv.root.alpha = if (isActive) 1f else 0.45f
|
||||
}
|
||||
}
|
||||
|
||||
// Capture positions BEFORE layout changes (for enter animation + exit animation later)
|
||||
val contentLoc = IntArray(2).also { binding.contentLayout.getLocationOnScreen(it) }
|
||||
val lm = binding.rvCards.layoutManager as? LinearLayoutManager
|
||||
val srcView = lm?.findViewByPosition(currentCardPosition)
|
||||
val srcLoc = IntArray(2).also { srcView?.getLocationOnScreen(it) ?: run { it[0] = contentLoc[0]; it[1] = contentLoc[1] } }
|
||||
val srcScreenTop = (srcLoc[1] - contentLoc[1]).toFloat()
|
||||
val srcCenterX = (srcLoc[0] - contentLoc[0]).toFloat() + cardWidth / 2f
|
||||
|
||||
val textLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||
val textSrcScreenTop = (textLoc[1] - contentLoc[1]).toFloat()
|
||||
|
||||
// Apply layout changes
|
||||
binding.btnManageCard.visibility = View.GONE
|
||||
binding.topSpacer.visibility = View.GONE
|
||||
binding.rvCards.visibility = View.GONE
|
||||
binding.pageIndicator.visibility = View.GONE
|
||||
binding.llPayButtons.visibility = View.GONE
|
||||
binding.llManageButtons.visibility = View.VISIBLE
|
||||
binding.llDefaultCardRow.visibility = View.VISIBLE
|
||||
binding.manageCardView.root.visibility = View.VISIBLE
|
||||
|
||||
// Set switch state (clear listener first to avoid triggering on programmatic set)
|
||||
val isBml = item is CardItem.Bml
|
||||
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
||||
binding.switchDefaultCard.isChecked = isBml && store.getDefaultCardAccountNumber() == (item as? CardItem.Bml)?.account?.accountNumber
|
||||
binding.switchDefaultCard.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (item is CardItem.Mib) {
|
||||
// MIB doesn't support NFC/QR pay — same toast as scan/tap to pay
|
||||
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
||||
binding.switchDefaultCard.isChecked = false
|
||||
binding.switchDefaultCard.setOnCheckedChangeListener { _, c ->
|
||||
handleDefaultCardToggle(c)
|
||||
}
|
||||
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
handleDefaultCardToggle(isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
// After layout pass, compute offsets, save carousel snapshot, and animate
|
||||
binding.contentLayout.doOnNextLayout {
|
||||
val mgr = binding.manageCardView.root
|
||||
val dstLoc = IntArray(2).also { mgr.getLocationOnScreen(it) }
|
||||
val dstTop = (dstLoc[1] - contentLoc[1]).toFloat()
|
||||
val dstCenterX = (dstLoc[0] - contentLoc[0]).toFloat() + mgr.width / 2f
|
||||
|
||||
val scaleStart = if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f
|
||||
val transXStart = srcCenterX - dstCenterX
|
||||
val transYStart = srcScreenTop - dstTop
|
||||
|
||||
// Save the carousel card's position (relative to contentLayout) for the exit animation
|
||||
carouselCardLayoutTop = srcScreenTop
|
||||
carouselCardCenterX = srcCenterX
|
||||
carouselTextLayoutTop = textSrcScreenTop
|
||||
|
||||
val textDstLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||
val textDstTop = (textDstLoc[1] - contentLoc[1]).toFloat()
|
||||
|
||||
mgr.pivotX = mgr.width / 2f
|
||||
mgr.pivotY = 0f
|
||||
mgr.scaleX = scaleStart
|
||||
mgr.scaleY = scaleStart
|
||||
mgr.translationX = transXStart
|
||||
mgr.translationY = transYStart
|
||||
|
||||
mgr.animate()
|
||||
.scaleX(1f).scaleY(1f)
|
||||
.translationX(0f).translationY(0f)
|
||||
.setDuration(380)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
binding.tvSelectedCardType.translationY = textSrcScreenTop - textDstTop
|
||||
binding.tvSelectedCardType.animate()
|
||||
.translationY(0f)
|
||||
.setDuration(380)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDefaultCardToggle(isChecked: Boolean) {
|
||||
val item = cards.getOrNull(currentCardPosition) as? CardItem.Bml ?: return
|
||||
store.setDefaultCardAccountNumber(if (isChecked) item.account.accountNumber else null)
|
||||
rebuildCards()
|
||||
}
|
||||
|
||||
private fun exitManageMode() {
|
||||
binding.manageCardView.root.animate().cancel()
|
||||
binding.tvSelectedCardType.animate().cancel()
|
||||
|
||||
val mgr = binding.manageCardView.root
|
||||
val contentLoc = IntArray(2).also { binding.contentLayout.getLocationOnScreen(it) }
|
||||
|
||||
// Compute layout top of manage card (strip current translationY which may be from a swipe drag)
|
||||
val mgrLoc = IntArray(2).also { mgr.getLocationOnScreen(it) }
|
||||
val mgrLayoutTop = (mgrLoc[1] - contentLoc[1]).toFloat() - mgr.translationY
|
||||
|
||||
val textLoc = IntArray(2).also { binding.tvSelectedCardType.getLocationOnScreen(it) }
|
||||
val textLayoutTop = (textLoc[1] - contentLoc[1]).toFloat() - binding.tvSelectedCardType.translationY
|
||||
|
||||
// Target: animate card back to carousel position
|
||||
val scaleEnd = if (mgr.width > 0) cardWidth.toFloat() / mgr.width.toFloat() else 1f
|
||||
val mgrLayoutCenterX = (mgrLoc[0] - contentLoc[0]).toFloat() - mgr.translationX + mgr.width / 2f
|
||||
val targetTransX = carouselCardCenterX - mgrLayoutCenterX
|
||||
val targetTransY = carouselCardLayoutTop - mgrLayoutTop
|
||||
|
||||
val targetTextTransY = carouselTextLayoutTop - textLayoutTop
|
||||
|
||||
mgr.pivotX = mgr.width / 2f
|
||||
mgr.pivotY = 0f
|
||||
|
||||
mgr.animate()
|
||||
.scaleX(scaleEnd).scaleY(scaleEnd)
|
||||
.translationX(targetTransX)
|
||||
.translationY(targetTransY)
|
||||
.setDuration(320)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.withEndAction {
|
||||
mgr.scaleX = 1f; mgr.scaleY = 1f
|
||||
mgr.translationX = 0f; mgr.translationY = 0f
|
||||
mgr.visibility = View.GONE
|
||||
binding.tvSelectedCardType.translationY = 0f
|
||||
|
||||
binding.btnManageCard.visibility = View.VISIBLE
|
||||
binding.topSpacer.visibility = View.VISIBLE
|
||||
binding.rvCards.visibility = View.VISIBLE
|
||||
binding.llPayButtons.visibility = View.VISIBLE
|
||||
binding.llManageButtons.visibility = View.GONE
|
||||
binding.llDefaultCardRow.visibility = View.GONE
|
||||
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
||||
buildDots(cards.size, currentCardPosition)
|
||||
}
|
||||
.start()
|
||||
|
||||
binding.tvSelectedCardType.animate()
|
||||
.translationY(targetTextTransY)
|
||||
.setDuration(320)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.withEndAction { binding.tvSelectedCardType.translationY = 0f }
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun rebuildCards() {
|
||||
// Remember which card is currently selected by identity so we can restore position after reorder
|
||||
val currentCard = cards.getOrNull(currentCardPosition)
|
||||
|
||||
val defaultNum = store.getDefaultCardAccountNumber()
|
||||
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: List<CardItem> = mibItems + bmlItems
|
||||
// Move default BML card to front
|
||||
cards = if (defaultNum != null) {
|
||||
val def = all.filterIsInstance<CardItem.Bml>().firstOrNull { it.account.accountNumber == defaultNum }
|
||||
if (def != null) listOf(def) + all.filter { it !== def } else all
|
||||
} else all
|
||||
|
||||
// Restore position to follow the same card after reorder
|
||||
if (currentCard != null) {
|
||||
val newPos = cards.indexOf(currentCard)
|
||||
if (newPos >= 0 && newPos != currentCardPosition) {
|
||||
currentCardPosition = newPos
|
||||
binding.rvCards.scrollToPosition(newPos)
|
||||
}
|
||||
}
|
||||
|
||||
stackAdapter.update(cards)
|
||||
binding.loadingView.visibility = View.GONE
|
||||
val empty = cards.isEmpty()
|
||||
binding.emptyView.visibility = if (empty) View.VISIBLE else View.GONE
|
||||
binding.contentLayout.visibility = if (empty) View.GONE else View.VISIBLE
|
||||
if (!empty) {
|
||||
buildDots(cards.size, currentCardPosition)
|
||||
updateCardInfo(currentCardPosition)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (isManageMode) return
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
if (isManageMode) {
|
||||
setManageMode(false)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -105,83 +486,78 @@ class PayWithCardFragment : Fragment() {
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class CardWalletAdapter(
|
||||
private var cards: List<CardItem>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
|
||||
private inner class CardStackAdapter(private val cardWidth: Int) : RecyclerView.Adapter<CardStackAdapter.VH>() {
|
||||
private var items: List<CardItem> = emptyList()
|
||||
|
||||
fun update(newCards: List<CardItem>) {
|
||||
cards = newCards
|
||||
fun update(newItems: List<CardItem>) {
|
||||
items = newItems
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(context).inflate(R.layout.item_card_wallet, parent, false))
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
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) {
|
||||
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 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) {
|
||||
when (item) {
|
||||
is CardItem.Mib -> {
|
||||
tvCardOwner.text = item.card.cardHolderName
|
||||
tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
|
||||
tvCardType.text = item.card.cardTypeDesc
|
||||
val assetPath = cardImageAsset(item.card)
|
||||
if (assetPath != null) loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
bindCardStatus(tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
|
||||
itemView.alpha = 1f
|
||||
}
|
||||
is CardItem.Bml -> {
|
||||
tvCardOwner.text = item.account.accountBriefName
|
||||
tvCardNumber.text = formatMasked(item.account.accountNumber)
|
||||
tvCardType.text = item.account.accountTypeName
|
||||
loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
val bmlStatus = item.account.statusDesc.takeUnless { it.equals("Active", ignoreCase = true) }
|
||||
bindCardStatus(tvCardStatus, bmlStatus)
|
||||
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
||||
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 {
|
||||
fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
|
||||
"51" -> "cards/mib/faisa_card.png"
|
||||
"53" -> "cards/mib/visa_black_platinum.png"
|
||||
"57" -> "cards/mib/visa_blue_everyday.png"
|
||||
"70" -> "cards/mib/visa_business.png"
|
||||
"701" -> "cards/mib/visa_bingaa_mvr.png"
|
||||
"702" -> "cards/mib/visa_bingaa_usd.png"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun loadCardImage(imageView: ImageView, assetPath: String) {
|
||||
try {
|
||||
val bitmap = imageView.context.assets.open(assetPath).use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
android.graphics.BitmapFactory.decodeStream(it)
|
||||
}
|
||||
imageView.setImageBitmap(bitmap)
|
||||
} catch (_: Exception) {
|
||||
@@ -190,8 +566,8 @@ class PayWithCardFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun mibCardStatusLabel(cardStatus: String): String? = when (cardStatus) {
|
||||
"CHST0" -> null // Active — no badge
|
||||
else -> cardStatus
|
||||
"CHST0" -> null
|
||||
else -> cardStatus
|
||||
}
|
||||
|
||||
fun bindCardStatus(tv: TextView, statusLabel: String?) {
|
||||
|
||||
@@ -37,8 +37,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.databinding.ActivityQrScannerBinding
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class QrScannerActivity : AppCompatActivity() {
|
||||
@@ -95,6 +97,14 @@ class QrScannerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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)
|
||||
binding = ActivityQrScannerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -2,7 +2,9 @@ package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -10,6 +12,7 @@ import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales
|
||||
import androidx.core.os.LocaleListCompat
|
||||
@@ -18,8 +21,11 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.databinding.FragmentSettingsAppearanceBinding
|
||||
import sh.sar.basedbank.util.ThemeHelper
|
||||
import java.util.Collections
|
||||
|
||||
class SettingsAppearanceFragment : Fragment() {
|
||||
@@ -54,19 +60,29 @@ class SettingsAppearanceFragment : Fragment() {
|
||||
// Quick actions
|
||||
quickActions.clear()
|
||||
quickActions.addAll(NavCustomization.getQuickActions(prefs))
|
||||
quickActionAdapter = NavItemAdapter(quickActions) {
|
||||
NavCustomization.saveQuickActions(prefs, quickActions)
|
||||
quickActionAdapter = NavItemAdapter(
|
||||
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
|
||||
slots.clear()
|
||||
slots.addAll(NavCustomization.getSlots(prefs))
|
||||
slotAdapter = NavItemAdapter(slots) {
|
||||
NavCustomization.saveSlots(prefs, slots)
|
||||
(activity as? HomeActivity)?.rebuildBottomNav(prefs)
|
||||
slotAdapter = NavItemAdapter(
|
||||
items = slots,
|
||||
onSave = {
|
||||
NavCustomization.saveSlots(prefs, slots)
|
||||
(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
|
||||
val showLabels = prefs.getBoolean("bottom_nav_show_labels", true)
|
||||
binding.switchShowLabels.isChecked = showLabels
|
||||
@@ -93,8 +109,45 @@ class SettingsAppearanceFragment : Fragment() {
|
||||
}
|
||||
prefs.edit().putString("theme", key).apply()
|
||||
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
|
||||
val currentLocales = AppCompatDelegate.getApplicationLocales()
|
||||
val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en"
|
||||
@@ -109,13 +162,18 @@ class SettingsAppearanceFragment : Fragment() {
|
||||
private fun setupNavItemRecyclerView(
|
||||
rv: RecyclerView,
|
||||
adapter: NavItemAdapter,
|
||||
items: MutableList<Int>
|
||||
items: MutableList<Int>,
|
||||
isEnabled: () -> Boolean
|
||||
) {
|
||||
rv.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||
rv.adapter = adapter
|
||||
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
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(
|
||||
rv: RecyclerView,
|
||||
from: RecyclerView.ViewHolder,
|
||||
@@ -134,11 +192,79 @@ class SettingsAppearanceFragment : Fragment() {
|
||||
|
||||
private fun updateShortcutsVisibility() {
|
||||
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.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) {
|
||||
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 otherIds = items.filterIndexed { i, _ -> i != slotIndex }.toSet()
|
||||
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 ->
|
||||
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
|
||||
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
|
||||
row.findViewById<TextView>(R.id.tvDescription).visibility = View.GONE
|
||||
listLayout.addView(row)
|
||||
}
|
||||
}
|
||||
@@ -169,7 +296,8 @@ class SettingsAppearanceFragment : Fragment() {
|
||||
|
||||
private inner class NavItemAdapter(
|
||||
val items: MutableList<Int>,
|
||||
val onSave: () -> Unit
|
||||
val onSave: () -> Unit,
|
||||
val isEnabled: () -> Boolean = { true }
|
||||
) : RecyclerView.Adapter<NavItemAdapter.VH>() {
|
||||
|
||||
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
|
||||
holder.ivNavIcon.setImageResource(def.iconRes)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.Settings as AndroidSettings
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
@@ -15,6 +18,7 @@ import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -77,6 +81,29 @@ class SettingsLoginsFragment : Fragment() {
|
||||
handlePickedImage(bitmap)
|
||||
}
|
||||
|
||||
private val cameraPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { granted ->
|
||||
if (granted) cameraLauncher.launch(cameraPhotoUri ?: return@registerForActivityResult)
|
||||
else showCameraPermissionRationale()
|
||||
}
|
||||
|
||||
private fun showCameraPermissionRationale() {
|
||||
val ctx = requireContext()
|
||||
MaterialAlertDialogBuilder(ctx)
|
||||
.setTitle(R.string.qr_camera_permission_title)
|
||||
.setMessage(R.string.camera_permission_profile_message)
|
||||
.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
.setPositiveButton(R.string.go_to_settings) { _, _ ->
|
||||
startActivity(
|
||||
Intent(AndroidSettings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", ctx.packageName, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun loadAndScaleBitmap(uri: Uri): Bitmap? {
|
||||
return try {
|
||||
val ctx = requireContext()
|
||||
@@ -159,7 +186,11 @@ class SettingsLoginsFragment : Fragment() {
|
||||
val photoFile = File(ctx.cacheDir, "profile_photo_tmp.jpg")
|
||||
val uri = FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", photoFile)
|
||||
cameraPhotoUri = uri
|
||||
cameraLauncher.launch(uri)
|
||||
if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
cameraLauncher.launch(uri)
|
||||
} else {
|
||||
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
if (target is PendingImageTarget.Mib || currentBitmap != null || hasSavedImage(ctx, target)) {
|
||||
items += Triple(R.drawable.ic_delete, getString(R.string.profile_image_remove)) {
|
||||
|
||||
@@ -86,17 +86,6 @@ class SettingsSecurityFragment : Fragment() {
|
||||
(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
|
||||
val blockScreenshots = prefs.getBoolean("block_screenshots", true)
|
||||
binding.switchBlockScreenshots.isChecked = blockScreenshots
|
||||
|
||||
@@ -125,9 +125,12 @@ class TransferFragment : Fragment() {
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
|
||||
// BML card/gateway QR — hand off to dedicated payment screen
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") ||
|
||||
raw.startsWith("https://pay.bml.com.mv/app/")) {
|
||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(raw))
|
||||
val bmlUrl = PaymvQrParser.extractBmlGatewayUrl(raw)
|
||||
if (raw.startsWith("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != null) {
|
||||
val fromCard = selectedAccount?.takeIf {
|
||||
it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT"
|
||||
}
|
||||
(requireActivity() as HomeActivity).navigateTo(R.id.nav_transfer, TransferFragment.newInstanceFromBmlQr(bmlUrl ?: raw, fromCard?.accountNumber))
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
@@ -151,6 +154,11 @@ class TransferFragment : Fragment() {
|
||||
private const val ARG_AMOUNT_PREFILL = "amount_prefill"
|
||||
private const val ARG_REMARKS_PREFILL = "remarks_prefill"
|
||||
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 {
|
||||
arguments = Bundle().apply {
|
||||
@@ -253,6 +261,10 @@ class TransferFragment : Fragment() {
|
||||
arguments?.getString(ARG_REMARKS_PREFILL)?.let { binding.etRemarks.setText(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) {
|
||||
@@ -281,6 +293,25 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
bmlQrInfo = info
|
||||
|
||||
// Auto-select the user's default BML card if no card was pre-selected
|
||||
if (selectedAccount == null) {
|
||||
val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber()
|
||||
if (defaultNum != null) {
|
||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
||||
val defaultCard = allAccounts.firstOrNull {
|
||||
it.accountNumber == defaultNum &&
|
||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") &&
|
||||
it.statusDesc.equals("Active", ignoreCase = true)
|
||||
}
|
||||
if (defaultCard != null) {
|
||||
selectedAccount = defaultCard
|
||||
updateAmountPrefix(defaultCard)
|
||||
showFromCard(defaultCard)
|
||||
updateTransferButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show merchant in the "To" card — clear button hidden (can't change recipient for QR)
|
||||
binding.tvToAccountName.text = info.merchantName
|
||||
binding.tvToBankBic.text = info.merchantAddress.ifBlank { "BML Merchant" }
|
||||
@@ -288,7 +319,6 @@ class TransferFragment : Fragment() {
|
||||
binding.tvToBalance.visibility = View.GONE
|
||||
binding.ivToPhoto.scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
||||
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(info.merchantName, "#0066A1"))
|
||||
binding.btnClearToInfo.visibility = View.GONE
|
||||
binding.cardToInfo.visibility = View.VISIBLE
|
||||
|
||||
// Pre-fill amount if dynamic QR
|
||||
@@ -344,6 +374,14 @@ class TransferFragment : Fragment() {
|
||||
|
||||
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
||||
val picked = accountDropdownAdapter?.getAccount(position) ?: return@setOnItemClickListener
|
||||
if (bmlQrInfo != null) {
|
||||
val isCard = picked.profileType == "BML_PREPAID" || picked.profileType == "BML_CREDIT" || picked.profileType == "BML_DEBIT"
|
||||
if (!isCard) {
|
||||
Toast.makeText(requireContext(), "Unsupported for BML QR — select a card", Toast.LENGTH_SHORT).show()
|
||||
binding.actvFrom.setText("", false)
|
||||
return@setOnItemClickListener
|
||||
}
|
||||
}
|
||||
selectedAccount = picked
|
||||
updateAmountPrefix(picked)
|
||||
showFromCard(picked)
|
||||
@@ -497,6 +535,14 @@ class TransferFragment : Fragment() {
|
||||
binding.tilTo.setEndIconOnClickListener { lookupAccount() }
|
||||
|
||||
binding.btnClearToInfo.setOnClickListener {
|
||||
if (bmlQrInfo != null) {
|
||||
bmlQrInfo = null
|
||||
bmlGatewayQr = false
|
||||
binding.tilAmount.isEnabled = true
|
||||
binding.tilRemarks.isEnabled = true
|
||||
binding.tilRemarks.alpha = 1f
|
||||
binding.etAmount.setText("")
|
||||
}
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
resolvedToOwnAccount = null
|
||||
@@ -1728,6 +1774,7 @@ class TransferFragment : Fragment() {
|
||||
.also { it.root.tag = it }
|
||||
}
|
||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT" || acc.profileType == "BML_DEBIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
val isCard = acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT" || acc.profileType == "BML_DEBIT"
|
||||
val isBmlAccount = acc.bank == "BML"
|
||||
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
@@ -1745,7 +1792,11 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
val balance = displayData?.balance ?: ""
|
||||
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
|
||||
b.root.alpha = if (inactive) 0.4f else 1f
|
||||
b.root.alpha = when {
|
||||
inactive -> 0.4f
|
||||
bmlQrInfo != null && !isCard -> 0.35f
|
||||
else -> 1f
|
||||
}
|
||||
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
||||
when {
|
||||
networkIcon != null -> {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package sh.sar.basedbank.ui.login
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -81,6 +84,17 @@ class CredentialsFragment : Fragment() {
|
||||
binding.btnLogin.isEnabled = false
|
||||
binding.btnLogin.setOnClickListener { attemptLogin() }
|
||||
|
||||
binding.cardOtp.setOnClickListener {
|
||||
val code = binding.tvOtpCode.text.toString().replace(" ", "")
|
||||
if (code.isNotEmpty()) {
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("OTP", code))
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
Toast.makeText(requireContext(), "OTP copied", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val loginFieldWatcher = object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) { updateLoginButtonState() }
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
@@ -215,6 +229,7 @@ class CredentialsFragment : Fragment() {
|
||||
app.mibSessions[loginId] = flow.lastSession!!
|
||||
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||
app.mibLoginFlows[loginId] = flow
|
||||
app.isUnlocked = true
|
||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
@@ -364,6 +379,7 @@ class CredentialsFragment : Fragment() {
|
||||
if (hasBusinessProfiles) {
|
||||
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)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
@@ -496,6 +512,7 @@ class CredentialsFragment : Fragment() {
|
||||
app.fahipaySessions[loginId] = session
|
||||
app.fahipayAccounts = app.fahipayAccounts.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)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
package sh.sar.basedbank.ui.login
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.util.CredentialStore
|
||||
import sh.sar.basedbank.util.ThemeHelper
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeHelper.applyAccent(this)
|
||||
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)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -20,5 +36,8 @@ class LoginActivity : AppCompatActivity() {
|
||||
isAppearanceLightStatusBars = 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.viewpager2.widget.ViewPager2
|
||||
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.databinding.ActivityOnboardingBinding
|
||||
import sh.sar.basedbank.ui.login.LoginActivity
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.util.ThemeHelper
|
||||
|
||||
class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
|
||||
@@ -24,7 +28,17 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeHelper.applyAccent(this)
|
||||
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)
|
||||
binding = ActivityOnboardingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -33,6 +47,9 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
isAppearanceLightStatusBars = 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)
|
||||
val originalBottomPadding = binding.bottomBar.paddingBottom
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets ->
|
||||
@@ -97,6 +114,9 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback {
|
||||
|
||||
binding.btnGetStarted.setOnClickListener {
|
||||
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))
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ data class AccountListDisplay(
|
||||
val number: String,
|
||||
val typeLabel: String,
|
||||
val balance: String,
|
||||
val blockedBalance: String? = null, // null when zero or not applicable
|
||||
val isCard: Boolean = false,
|
||||
val cardBrandIcon: Int = 0, // drawable res, only meaningful if isCard
|
||||
val statusLabel: String? = null // null = active; shown as status pill if set
|
||||
|
||||
@@ -615,6 +615,18 @@ class CredentialStore(context: Context) {
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
// ── Default payment card ──────────────────────────────────────────────────
|
||||
|
||||
/** BML card account number the user has pinned as their default payment card, or null. */
|
||||
fun getDefaultCardAccountNumber(): String? = prefs.getString("default_card_account_number", null)
|
||||
|
||||
fun setDefaultCardAccountNumber(accountNumber: String?) {
|
||||
val editor = prefs.edit()
|
||||
if (accountNumber == null) editor.remove("default_card_account_number")
|
||||
else editor.putString("default_card_account_number", accountNumber)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
// ── MIB profile visibility (per loginId) ─────────────────────────────────
|
||||
|
||||
/** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */
|
||||
|
||||
@@ -9,6 +9,24 @@ data class PaymvQrData(
|
||||
|
||||
object PaymvQrParser {
|
||||
|
||||
/**
|
||||
* Returns the BML gateway URL if [raw] is or contains one, otherwise null.
|
||||
* Handles both plain URL QRs and combined EMV QRs (e.g. Fahipay+BML card QR).
|
||||
* For combined EMV QRs the URL is parsed from TLV (root tag 35 → sub-tag 20 → sub-sub-tag 01)
|
||||
* rather than via regex, to avoid greedily consuming subsequent EMV tag bytes.
|
||||
*/
|
||||
fun extractBmlGatewayUrl(raw: String): String? {
|
||||
if (raw.startsWith("https://pay.bml.com.mv/app/")) return raw
|
||||
return try {
|
||||
val root = parseTlv(raw)
|
||||
val bmlMerchantInfo = root["35"]?.let { parseTlv(it) } ?: return null
|
||||
val inner = bmlMerchantInfo["20"]?.let { parseTlv(it) } ?: return null
|
||||
inner["01"]?.takeIf { it.startsWith("https://pay.bml.com.mv/app/") }
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun parse(raw: String): PaymvQrData? {
|
||||
return try {
|
||||
val root = parseTlv(raw)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
} else {
|
||||
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||
AccountListDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
typeLabel = productLabel(account.accountTypeName),
|
||||
balance = listBalance(account)
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
typeLabel = productLabel(account.accountTypeName),
|
||||
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 =
|
||||
"${account.currencyName} ${account.currentBalance}"
|
||||
"${account.currencyName} ${account.availableBalance}"
|
||||
|
||||
fun cardBrandIcon(productName: String): Int = when {
|
||||
productName.contains("AMEX", ignoreCase = true) ||
|
||||
|
||||
@@ -5,12 +5,16 @@ import sh.sar.basedbank.util.AccountListDisplay
|
||||
|
||||
object MibAccountParser {
|
||||
|
||||
fun displayData(account: BankAccount) = AccountListDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
typeLabel = productLabel(account.accountTypeName),
|
||||
balance = "${account.currencyName} ${account.availableBalance}"
|
||||
)
|
||||
fun displayData(account: BankAccount): AccountListDisplay {
|
||||
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||
return AccountListDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
typeLabel = productLabel(account.accountTypeName),
|
||||
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.
|
||||
|
||||
@@ -5,13 +5,16 @@ import sh.sar.basedbank.util.AccountHistoryDisplay
|
||||
|
||||
object MibHistoryParser {
|
||||
|
||||
fun displayData(account: BankAccount) = AccountHistoryDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
bankPill = null, // MIB has no bank pill
|
||||
typeLabel = MibAccountParser.productLabel(account.accountTypeName),
|
||||
availableBalance = "${account.currencyName} ${account.availableBalance}",
|
||||
workingBalance = "${account.currencyName} ${account.currentBalance}",
|
||||
blockedBalance = null
|
||||
)
|
||||
fun displayData(account: BankAccount): AccountHistoryDisplay {
|
||||
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
|
||||
return AccountHistoryDisplay(
|
||||
name = account.accountBriefName,
|
||||
number = account.accountNumber,
|
||||
bankPill = null, // MIB has no bank pill
|
||||
typeLabel = MibAccountParser.productLabel(account.accountTypeName),
|
||||
availableBalance = "${account.currencyName} ${account.availableBalance}",
|
||||
workingBalance = "${account.currencyName} ${account.currentBalance}",
|
||||
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,20 @@
|
||||
android:viewportWidth="60"
|
||||
android:viewportHeight="60">
|
||||
|
||||
<!-- White background -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M0 59.776h59.785V0H0z"/>
|
||||
|
||||
<!-- Red background -->
|
||||
<path
|
||||
android:fillColor="#E21B23"
|
||||
android:pathData="M3.297 56.421h53.191V3.356H3.298z"/>
|
||||
android:pathData="M0 60h60V0H0z"/>
|
||||
|
||||
<!-- White logo mark -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M37.421 6.708v34.059h-3.7V20.853L22.763 40.767H18.65l18.77-34.06zM18.517 51.073l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.03c-.613.313-1.228.626-1.88.626-.65 0-1.265-.313-1.879-.626-.553-.283-1.108-.564-1.624-.564-.514 0-1.069.28-1.62.564-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.068.28-1.62.564c-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.069.28-1.622.564c-.614.313-1.228.626-1.879.626-.6 0-1.167-.265-1.73-.55v-1.446zm0-2.816l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.028c-.613.315-1.228.627-1.88.627-.65 0-1.265-.312-1.879-.627-.553-.281-1.108-.562-1.624-.562-.514 0-1.069.28-1.62.562-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.068.28-1.62.562c-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.069.28-1.622.562c-.614.315-1.228.627-1.879.627-.6 0-1.167-.264-1.73-.55v-1.445zm12.428-6.042h12.41v3.969h-12.41v-.001H18.531l-2.1-3.968h14.514z"/>
|
||||
<group
|
||||
android:scaleX="0.85"
|
||||
android:scaleY="0.85"
|
||||
android:pivotX="30"
|
||||
android:pivotY="30">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M37.421 6.708v34.059h-3.7V20.853L22.763 40.767H18.65l18.77-34.06zM18.517 51.073l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.03c-.613.313-1.228.626-1.88.626-.65 0-1.265-.313-1.879-.626-.553-.283-1.108-.564-1.624-.564-.514 0-1.069.28-1.62.564-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.068.28-1.62.564c-.616.313-1.23.626-1.88.626-.653 0-1.268-.313-1.88-.626-.554-.283-1.108-.564-1.624-.564s-1.069.28-1.622.564c-.614.313-1.228.626-1.879.626-.6 0-1.167-.265-1.73-.55v-1.446zm0-2.816l.108.055c.552.283 1.106.564 1.623.564.515 0 1.068-.281 1.621-.564.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.068-.281 1.621-.564c.615-.313 1.23-.627 1.88-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564c.613-.313 1.228-.627 1.878-.627.652 0 1.267.314 1.88.627.554.283 1.107.564 1.623.564s1.07-.281 1.623-.564l.314-.159v1.444l-.057.028c-.613.315-1.228.627-1.88.627-.65 0-1.265-.312-1.879-.627-.553-.281-1.108-.562-1.624-.562-.514 0-1.069.28-1.62.562-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.068.28-1.62.562c-.616.315-1.23.627-1.88.627-.653 0-1.268-.312-1.88-.627-.554-.281-1.108-.562-1.624-.562s-1.069.28-1.622.562c-.614.315-1.228.627-1.879.627-.6 0-1.167-.264-1.73-.55v-1.445zm12.428-6.042h12.41v3.969h-12.41v-.001H18.531l-2.1-3.968h14.514z"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- Card icon -->
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurfaceVariant"
|
||||
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" />
|
||||
<!-- Background circle separating gear from card -->
|
||||
<path
|
||||
android:fillColor="?attr/colorSurface"
|
||||
android:pathData="M12.5,18 A5.5,5.5 0 1 0 23.5,18 A5.5,5.5 0 1 0 12.5,18 Z" />
|
||||
<!--
|
||||
Simple 6-tooth gear, scaled 0.5x and placed at (18,18) in the icon.
|
||||
The gear path is drawn in a 24×24 space (center 12,12); the group
|
||||
transform maps it: point(x,y) → (x·0.5+12, y·0.5+12).
|
||||
Outer radius=10, root radius=7, hub radius=4.
|
||||
Teeth are rectangular (straight-line) so they stay crisp at small scale.
|
||||
Outer ring is clockwise; hub hole is counterclockwise (nonZero cutout).
|
||||
-->
|
||||
<group
|
||||
android:scaleX="0.5"
|
||||
android:scaleY="0.5"
|
||||
android:translateX="12"
|
||||
android:translateY="12">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurfaceVariant"
|
||||
android:pathData="
|
||||
M18.58,9.61 L21.85,10.26 L21.85,13.74 L18.58,14.39
|
||||
L17.36,16.50 L18.43,19.66 L15.42,21.40 L13.22,18.89
|
||||
L10.78,18.89 L8.58,21.40 L5.57,19.66 L6.64,16.50
|
||||
L5.42,14.39 L2.15,13.74 L2.15,10.26 L5.42,9.61
|
||||
L6.64,7.50 L5.57,4.34 L8.58,2.60 L10.78,5.11
|
||||
L13.22,5.11 L15.42,2.60 L18.43,4.34 L17.36,7.50 Z
|
||||
M12,8 A4,4 0 1 0 12,16 A4,4 0 1 0 12,8 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -4,6 +4,6 @@
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<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"/>
|
||||
</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>
|
||||
@@ -0,0 +1,270 @@
|
||||
<?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">
|
||||
|
||||
<!-- Manage Card button row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="4dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnManageCard"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_manage"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_manage_card"
|
||||
app:iconSize="20dp"
|
||||
app:iconPadding="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Top spacer: pushes card to vertical center (hidden in manage mode) -->
|
||||
<View
|
||||
android:id="@+id/topSpacer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- Manage mode: selected card with overlays -->
|
||||
<include
|
||||
android:id="@+id/manageCardView"
|
||||
layout="@layout/item_card_stack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 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 (normal mode) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llPayButtons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Default card toggle (manage mode only) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llDefaultCardRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/card_set_as_default"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchDefaultCard"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Card management actions (manage mode only) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llManageButtons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChangePin"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:text="@string/card_action_change_pin"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_edit"
|
||||
app:iconSize="22dp"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnFreeze"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:text="@string/card_action_freeze"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_freeze"
|
||||
app:iconSize="22dp"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBlock"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:text="@string/card_action_block"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/ic_block"
|
||||
app:iconSize="22dp"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
</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>
|
||||
@@ -116,6 +116,8 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="invisible"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
@@ -176,6 +176,139 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Blocked funds row: MVR + USD separate cards (hidden when no blocked amounts) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/rowBlocked"
|
||||
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/cardBlockedMvr"
|
||||
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_mvr"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnErrorContainer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBlockedMvr"
|
||||
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>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardBlockedUsd"
|
||||
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: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_usd"
|
||||
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||
android:textColor="?attr/colorOnErrorContainer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBlockedUsd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="USD —"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:textColor="?attr/colorOnErrorContainer" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Overdue row (hidden when no overdue financing) -->
|
||||
<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/cardOverdue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardPendingFinances"
|
||||
@@ -228,14 +361,6 @@
|
||||
android:layout_marginTop="16dp"
|
||||
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
|
||||
android:id="@+id/rvCards"
|
||||
android:layout_width="match_parent"
|
||||
@@ -261,14 +386,6 @@
|
||||
android:paddingTop="8dp"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/paymvqr_amount_hint"
|
||||
app:helperText="@string/paymvqr_amount_helper"
|
||||
app:prefixText="MVR ">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
@@ -76,6 +75,47 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Reference / purpose (optional) -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilReference"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:hint="@string/paymvqr_reference_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etReference"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Include phone number toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/paymvqr_include_phone"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchIncludePhone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Action buttons — always visible; share/save disabled until QR is rendered -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutActions"
|
||||
|
||||
@@ -45,22 +45,30 @@
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<!-- Quick actions (always active) -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dashboard_quick_actions"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvQuickActions"
|
||||
<!-- 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:nestedScrollingEnabled="false"
|
||||
android:overScrollMode="never"
|
||||
android:layout_marginBottom="16dp" />
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dashboard_quick_actions"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvQuickActions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:overScrollMode="never" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Bottom bar shortcuts — shown only when bottom nav is active -->
|
||||
<LinearLayout
|
||||
@@ -148,6 +156,87 @@
|
||||
|
||||
</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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -194,44 +194,6 @@
|
||||
android:layout_marginTop="24dp"
|
||||
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
|
||||
android:id="@+id/rowBlockScreenshots"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -72,6 +72,15 @@
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
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
|
||||
android:id="@+id/btnTransfer"
|
||||
android:layout_width="40dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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="280dp"
|
||||
android:layout_width="320dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
|
||||
@@ -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>
|
||||
@@ -6,6 +6,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
<item android:id="@+id/nav_pay_with_card"
|
||||
android:icon="@drawable/ic_nav_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 android:id="@+id/group_system" android:checkableBehavior="single">
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
<item android:id="@+id/nav_pay_with_card"
|
||||
android:icon="@drawable/ic_nav_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"
|
||||
android:icon="@drawable/ic_nav_otp"
|
||||
android:title="@string/nav_otp" />
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
<string name="nav_activities">ހަރަކާތްތައް</string>
|
||||
<string name="nav_transfer_history">ޓްރާންސެކްޝަން ތާރީހް</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_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string>
|
||||
<string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string>
|
||||
@@ -81,8 +82,6 @@
|
||||
<string name="nav_desc_activities">ފަހުގެ ޓްރާންސްފަތައް ބަލާ</string>
|
||||
<string name="nav_desc_transfer_history">އެކައުންޓް ތަކުގެ ޓްރާންސެކްޝަން ތާރީހް</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_settings">އެޕްލިކޭޝަންގެ ތަރުތީބު</string>
|
||||
<string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string>
|
||||
|
||||
@@ -93,8 +93,7 @@
|
||||
<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_finances">Loans and financing overview</string>
|
||||
<string name="nav_desc_pay_with_card">Make a payment using your card</string>
|
||||
<string name="nav_desc_card_settings">Manage your card preferences</string>
|
||||
<string name="nav_desc_pay_with_card">Manage and pay with your cards</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_open_drawer">Open navigation</string>
|
||||
@@ -116,11 +115,13 @@
|
||||
<!-- PayMV QR Generator -->
|
||||
<string name="paymvqr_select_account">Select account</string>
|
||||
<string name="paymvqr_amount_hint">Amount (optional)</string>
|
||||
<string name="paymvqr_amount_helper">Leave empty to allow payer to enter any amount</string>
|
||||
<string name="paymvqr_share">Share</string>
|
||||
<string name="paymvqr_save_image">Save Image</string>
|
||||
<string name="paymvqr_saved">QR saved to gallery</string>
|
||||
<string name="paymvqr_save_failed">Failed to save image</string>
|
||||
<string name="paymvqr_include_phone">Include phone number</string>
|
||||
<string name="paymvqr_reference_hint">Reference (optional)</string>
|
||||
<string name="paymvqr_reference_default">PayMV QR Transfer</string>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<string name="action_lock">Lock app</string>
|
||||
@@ -149,6 +150,15 @@
|
||||
<string name="theme_system">System</string>
|
||||
<string name="theme_light">Light</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="lang_english">English</string>
|
||||
<string name="lang_dhivehi">ދިވެހި</string>
|
||||
@@ -209,6 +219,10 @@
|
||||
<string name="accounts">Accounts</string>
|
||||
<string name="cards">Cards</string>
|
||||
<string name="available_balance">Available Balance</string>
|
||||
<string name="account_blocked_label">%1$s blocked</string>
|
||||
<string name="dashboard_blocked_mvr">Blocked MVR</string>
|
||||
<string name="dashboard_blocked_usd">Blocked USD</string>
|
||||
<string name="dashboard_overdue">Overdue Financing</string>
|
||||
|
||||
<!-- Transfer -->
|
||||
<string name="transfer_tab_quick">Quick Transfer</string>
|
||||
@@ -229,6 +243,7 @@
|
||||
<string name="transfer_qr_invalid">Invalid or unsupported QR code</string>
|
||||
<string name="qr_camera_permission_title">Camera permission required</string>
|
||||
<string name="qr_camera_permission_message">Camera access is needed to scan QR codes. Please grant the permission in Settings.</string>
|
||||
<string name="camera_permission_profile_message">Camera access is needed to take a photo. Please grant the permission in Settings.</string>
|
||||
<string name="go_to_settings">Go to Settings</string>
|
||||
<string name="transfer_select_source_first">Select a source account first</string>
|
||||
<string name="transfer_enter_account_first">Enter an account number first</string>
|
||||
@@ -311,10 +326,12 @@
|
||||
<string name="loan_rate_fmt">%.2f%%</string>
|
||||
|
||||
<!-- Cards -->
|
||||
<string name="nav_pay_with_card">Pay with Card</string>
|
||||
<string name="card_pay_qr">QR Pay</string>
|
||||
<string name="card_pay_nfc">NFC Pay</string>
|
||||
<string name="nav_pay_with_card">Cards</string>
|
||||
<string name="card_pay_qr">Scan to 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="card_manage">Manage Card</string>
|
||||
<string name="card_set_as_default">Set as Default Card</string>
|
||||
<string name="card_action_change_pin">Change PIN</string>
|
||||
<string name="card_action_freeze">Freeze</string>
|
||||
<string name="card_action_block">Block</string>
|
||||
|
||||
@@ -6,4 +6,32 @@
|
||||
<item name="colorSecondary">@color/seed_secondary</item>
|
||||
<item name="android:windowSoftInputMode">adjustResize</item>
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||