3 Commits

Author SHA1 Message Date
shihaam ddd64e8624 descriptive menus
Auto Tag on Version Change / check-version (push) Successful in 3s
2026-05-22 02:03:20 +05:00
shihaam 77f367844d rework back butotn 2026-05-22 01:50:12 +05:00
shihaam e2729b1d1a add support for fetching mib cards
Auto Tag on Version Change / check-version (push) Successful in 7s
2026-05-22 01:40:14 +05:00
25 changed files with 894 additions and 19 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

@@ -0,0 +1,62 @@
package sh.sar.basedbank.api.mib
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class MibCardsClient {
private val BASE_WV_URL = "https://faisamobilex-wv.mib.com.mv"
private val client = OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build()
private fun cookieHeader(session: MibSession) =
"mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " +
"mbnonce=${session.nonceGenerator}; time-tracker=597"
fun fetchCards(session: MibSession, loginTag: String): List<MibCard> {
val body = FormBody.Builder()
.add("name", "")
.add("start", "1")
.add("end", "50")
.add("includeCount", "1")
.build()
val request = Request.Builder()
.url("$BASE_WV_URL/ajaxDebitCard/fetchCardInfos")
.post(body)
.header("Cookie", cookieHeader(session))
.header("User-Agent", "Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36")
.header("X-Requested-With", "XMLHttpRequest")
.header("Accept", "*/*")
.header("Origin", BASE_WV_URL)
.header("Referer", "$BASE_WV_URL//debitCards?dashurl=1")
.build()
return client.newCall(request).execute().use { response ->
val bodyStr = response.body?.string() ?: return emptyList()
val json = try { JSONObject(bodyStr) } catch (_: Exception) { return emptyList() }
if (!json.optBoolean("success")) return emptyList()
val data = json.optJSONArray("data") ?: return emptyList()
(0 until data.length()).map { i ->
val item = data.getJSONObject(i)
MibCard(
cardId = item.optString("cardId"),
maskedCardNumber = item.optString("maskedCardNumber"),
cardStatus = item.optString("cardStatus"),
cardType = item.optString("cardType"),
cardTypeDesc = item.optString("cardTypeDesc"),
customerId = item.optString("customerId"),
phoneNumber = item.optString("phoneNumber"),
cardHolderName = item.optString("cardHolderName"),
loginTag = loginTag
)
}
}
}
}
@@ -46,6 +46,18 @@ data class MibIpsAccountInfo(
)
data class MibCard(
val cardId: String,
val maskedCardNumber: String,
val cardStatus: String,
val cardType: String,
val cardTypeDesc: String,
val customerId: String,
val phoneNumber: String,
val cardHolderName: String,
val loginTag: String
)
data class MibFinanceDeal(
val dealNo: String,
val productDesc: String,
@@ -0,0 +1,114 @@
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.api.mib.MibCard
import sh.sar.basedbank.databinding.FragmentCardSettingsBinding
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)?.triggerRefreshCards()
}
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
if (cards == null) return@observe
adapter.update(cards)
binding.loadingView.visibility = View.GONE
binding.swipeRefresh.isRefreshing = false
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
}
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<MibCard>,
private val context: Context
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
fun update(newCards: List<MibCard>) {
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 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(card: MibCard) {
tvCardOwner.text = card.cardHolderName
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
tvCardType.text = card.cardTypeDesc
PayWithCardFragment.loadCardImage(ivCardImage, PayWithCardFragment.cardImageAsset(card))
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)
}
}
}
}
@@ -14,8 +14,10 @@ import android.widget.Toast
import sh.sar.basedbank.ui.home.NavCustomization
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.GravityCompat
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
@@ -54,6 +56,7 @@ import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibCardsClient
import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.api.mib.MibProfile
import sh.sar.basedbank.api.mib.MibSession
@@ -71,6 +74,10 @@ class HomeActivity : AppCompatActivity() {
private lateinit var toggle: ActionBarDrawerToggle
private var suppressBottomNavCallback = false
private var backPressedOnce = false
private val backPressHandler = Handler(Looper.getMainLooper())
private val resetBackPress = Runnable { backPressedOnce = false }
private val autolockHandler = Handler(Looper.getMainLooper())
private var warningDialog: AlertDialog? = null
private var countdownTimer: CountDownTimer? = null
@@ -138,6 +145,8 @@ 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()
else -> null
}
if (frag != null) show(frag)
@@ -208,6 +217,42 @@ class HomeActivity : AppCompatActivity() {
binding.navigationView.setCheckedItem(R.id.nav_dashboard)
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Close drawer if open (drawer-nav mode)
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawers()
return
}
// Pop fragment back stack if there's anything on it (e.g. showWithBackStack)
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
return
}
// 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())
return
}
binding.bottomNavigation.selectedItemId = R.id.nav_dashboard
return
}
// At top level — require double-tap to exit
if (backPressedOnce) {
backPressHandler.removeCallbacks(resetBackPress)
finish()
} else {
backPressedOnce = true
Toast.makeText(this@HomeActivity, R.string.press_back_to_exit, Toast.LENGTH_SHORT).show()
backPressHandler.postDelayed(resetBackPress, 2000)
}
}
})
// Keep all MIB sessions alive every 25 seconds while the app is in the foreground
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -307,6 +352,8 @@ 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()
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
}
show(dest)
@@ -932,6 +979,43 @@ fun applyNavLabelVisibility() {
}
}
fun triggerRefreshCards() {
val app = application as BasedBankApp
for ((loginId, session) in app.mibSessions) {
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
refreshMibCards(loginId, session, profiles)
}
}
private fun refreshMibCards(loginId: String, session: MibSession, profiles: List<MibProfile>) {
if (profiles.isEmpty()) return
val flow = (application as BasedBankApp).mibFlowFor(loginId)
val client = MibCardsClient()
lifecycleScope.launch {
try {
val cards = withContext(Dispatchers.IO) {
val result = mutableListOf<sh.sar.basedbank.api.mib.MibCard>()
val seen = mutableSetOf<String>()
for (profile in profiles) {
try {
flow.switchProfile(session, profile)
for (card in client.fetchCards(session, "mib_$loginId")) {
if (seen.add(card.cardId)) result += card
}
} catch (_: Exception) { }
}
result
}
if (cards.isNotEmpty()) {
val existing = viewModel.mibCards.value?.toMutableList() ?: mutableListOf()
existing.removeAll { it.loginTag == "mib_$loginId" }
existing += cards
viewModel.mibCards.postValue(existing)
}
} catch (_: Exception) { }
}
}
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
if (profiles.isEmpty()) return
val flow = (application as BasedBankApp).mibFlowFor(loginId)
@@ -7,6 +7,7 @@ import sh.sar.basedbank.api.bml.BmlLoanDetail
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.models.BankContact
import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.api.mib.MibFinanceDeal
class HomeViewModel : ViewModel() {
@@ -20,5 +21,7 @@ class HomeViewModel : ViewModel() {
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
val mibCards = MutableLiveData<List<MibCard>?>(null)
val hideAmounts = MutableLiveData<Boolean>(false)
}
@@ -25,6 +25,7 @@ class MoreFragment : Fragment() {
val row = inflater.inflate(R.layout.item_more_nav, list, false)
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.iconRes)
row.findViewById<TextView>(R.id.tvLabel).setText(item.titleRes)
row.findViewById<TextView>(R.id.tvDescription).setText(item.descriptionRes)
row.setOnClickListener { (requireActivity() as HomeActivity).navigateTo(item.id) }
list.addView(row)
}
@@ -10,21 +10,23 @@ object NavCustomization {
data class NavItemDef(
val id: Int,
@DrawableRes val iconRes: Int,
@StringRes val titleRes: Int
@StringRes val titleRes: Int,
@StringRes val descriptionRes: Int
)
/** All items that can occupy either a bottom nav slot or the "More" screen. */
val ALL_SWAPPABLE = listOf(
NavItemDef(R.id.nav_accounts, R.drawable.ic_nav_accounts, R.string.nav_accounts),
NavItemDef(R.id.nav_contacts, R.drawable.ic_contacts, R.string.nav_contacts),
NavItemDef(R.id.nav_transfer, R.drawable.ic_send, R.string.transfer),
NavItemDef(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr),
NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities),
NavItemDef(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history),
NavItemDef(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances),
NavItemDef(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings),
NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp),
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings),
NavItemDef(R.id.nav_accounts, R.drawable.ic_nav_accounts, R.string.nav_accounts, R.string.nav_desc_accounts),
NavItemDef(R.id.nav_contacts, R.drawable.ic_contacts, R.string.nav_contacts, R.string.nav_desc_contacts),
NavItemDef(R.id.nav_transfer, R.drawable.ic_send, R.string.transfer, R.string.nav_desc_transfer),
NavItemDef(R.id.nav_pay_mv_qr, R.drawable.ic_qr_scan, R.string.pay_mv_qr, R.string.nav_desc_pay_mv_qr),
NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities, R.string.nav_desc_activities),
NavItemDef(R.id.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, R.drawable.ic_nav_finances, R.string.nav_finances, R.string.nav_desc_finances),
NavItemDef(R.id.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, R.drawable.ic_nav_card, R.string.nav_card_settings, R.string.nav_desc_card_settings),
NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp, R.string.nav_desc_otp),
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings, R.string.nav_desc_settings),
)
fun getSlots(prefs: SharedPreferences): List<Int> = listOf(
@@ -0,0 +1,141 @@
package sh.sar.basedbank.ui.home
import android.content.Context
import android.graphics.BitmapFactory
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.api.mib.MibCard
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding
class PayWithCardFragment : Fragment() {
private var _binding: FragmentPayWithCardBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentPayWithCardBinding.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 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)?.triggerRefreshCards()
}
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
if (cards == null) return@observe
adapter.update(cards)
binding.loadingView.visibility = View.GONE
binding.swipeRefresh.isRefreshing = false
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
}
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_pay_with_card)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private inner class CardWalletAdapter(
private var cards: List<MibCard>,
private val context: Context
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
fun update(newCards: List<MibCard>) {
cards = newCards
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
VH(LayoutInflater.from(context).inflate(R.layout.item_card_wallet, parent, false))
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
override fun 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 btnPayQr: View = view.findViewById(R.id.btnPayQr)
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
fun bind(card: MibCard) {
tvCardOwner.text = card.cardHolderName
tvCardNumber.text = formatMasked(card.maskedCardNumber)
tvCardType.text = card.cardTypeDesc
loadCardImage(ivCardImage, cardImageAsset(card))
btnPayQr.setOnClickListener {
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
btnPayNfc.setOnClickListener {
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
}
}
}
companion object {
fun cardImageAsset(card: MibCard): String {
val desc = card.cardTypeDesc.lowercase()
return when {
"corporate" in desc || "business" in desc -> "cards/mib/visa_business.jpg"
"platinum" in desc -> "cards/mib/cards-platinum-bg-w-logo-1.jpg"
else -> "cards/mib/cards-blue-bg-w-logo-1.jpg"
}
}
fun loadCardImage(imageView: ImageView, assetPath: String) {
try {
val bitmap = imageView.context.assets.open(assetPath).use {
BitmapFactory.decodeStream(it)
}
imageView.setImageBitmap(bitmap)
} catch (_: Exception) {
imageView.setImageResource(android.R.color.darker_gray)
}
}
fun formatMasked(masked: String): String {
if (masked.length < 4) return masked
return "\u2022\u2022\u2022\u2022 ${masked.takeLast(4)}"
}
}
}
@@ -17,14 +17,15 @@ class SettingsFragment : Fragment() {
private data class SettingsItem(
@DrawableRes val icon: Int,
@StringRes val title: Int,
@StringRes val description: Int,
val dest: () -> Fragment
)
private val items = listOf(
SettingsItem(R.drawable.ic_contacts, R.string.settings_logins) { SettingsLoginsFragment() },
SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance) { SettingsAppearanceFragment() },
SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security) { SettingsSecurityFragment() },
SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage) { SettingsStorageFragment() },
SettingsItem(R.drawable.ic_contacts, R.string.settings_logins, R.string.settings_desc_logins) { SettingsLoginsFragment() },
SettingsItem(R.drawable.ic_settings_appearance, R.string.settings_appearance, R.string.settings_desc_appearance) { SettingsAppearanceFragment() },
SettingsItem(R.drawable.ic_lock, R.string.settings_privacy_security, R.string.settings_desc_privacy_security) { SettingsSecurityFragment() },
SettingsItem(R.drawable.ic_settings_storage, R.string.settings_storage, R.string.settings_desc_storage) { SettingsStorageFragment() },
)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
@@ -37,6 +38,7 @@ class SettingsFragment : Fragment() {
val row = inflater.inflate(R.layout.item_more_nav, list, false)
row.findViewById<ImageView>(R.id.ivIcon).setImageResource(item.icon)
row.findViewById<TextView>(R.id.tvLabel).setText(item.title)
row.findViewById<TextView>(R.id.tvDescription).setText(item.description)
row.setOnClickListener {
(requireActivity() as HomeActivity).showWithBackStack(item.dest())
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#00000000"
android:endColor="#CC000000"
android:angle="270"/>
</shape>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.68L5.68,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.68L18.32,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M22,11h-4.17l3.24,-3.24 -1.41,-1.42L15,11h-2V9l4.66,-4.66 -1.42,-1.41L13,6.17V2h-2v4.17L7.76,2.93 6.34,4.34 11,9v2H9L4.34,6.34 2.93,7.76 6.17,11H2v2h4.17l-3.24,3.24 1.41,1.42L9,13h2v2l-4.66,4.66 1.42,1.41L11,17.83V22h2v-4.17l3.24,3.24 1.42,-1.41L13,15v-2h2l4.66,4.66 1.41,-1.42L17.83,13H22z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M20,2L4,2C2.9,2 2,2.9 2,4l0,16c0,1.1 0.9,2 2,2l16,0c1.1,0 2,-0.9 2,-2L22,4C22,2.9 21.1,2 20,2zM13,18l-2,0 0,-1c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5l-2,0c0,-1.65 -1.35,-3 -3,-3s-3,1.35 -3,3 1.35,3 3,3l0,-2 2,3zM19,12l-2,0c0,-2.76 -2.24,-5 -5,-5l0,-2C15.87,5 19,8.13 19,12z"/>
</vector>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false"/>
<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>
<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>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false"/>
<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>
<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>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
app:cardCornerRadius="20dp"
app:cardElevation="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<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_card_settings"/>
<!-- Bottom gradient for text legibility -->
<View
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_gravity="bottom"
android:background="@drawable/bg_card_overlay_gradient"/>
<!-- Bottom-left: card owner name + masked number -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvCardOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
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>
<!-- Card type label -->
<TextView
android:id="@+id/tvCardType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="10dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"/>
<!-- Action buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="12dp"
android:gravity="center">
<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_marginEnd="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_change_pin"
android:textSize="11sp"
app:icon="@drawable/ic_edit"
app:iconSize="16dp"
app:iconPadding="4dp"/>
<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_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_freeze"
android:textSize="11sp"
app:icon="@drawable/ic_freeze"
app:iconSize="16dp"
app:iconPadding="4dp"/>
<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_marginStart="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_action_block"
android:textSize="11sp"
app:icon="@drawable/ic_block"
app:iconSize="16dp"
app:iconPadding="4dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
app:cardCornerRadius="20dp"
app:cardElevation="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<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_card_settings"/>
<!-- Bottom gradient for text legibility -->
<View
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_gravity="bottom"
android:background="@drawable/bg_card_overlay_gradient"/>
<!-- Bottom-left: card owner name + masked number -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvCardOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleSmall"
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>
<!-- Card type label -->
<TextView
android:id="@+id/tvCardType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="10dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"/>
<!-- Action buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp"
android:paddingBottom="12dp"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPayQr"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_pay_qr"
android:textSize="11sp"
app:icon="@drawable/ic_qr_scan"
app:iconSize="16dp"
app:iconPadding="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPayNfc"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:text="@string/card_pay_nfc"
android:textSize="11sp"
app:icon="@drawable/ic_nfc"
app:iconSize="16dp"
app:iconPadding="4dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
+17 -4
View File
@@ -15,13 +15,26 @@
android:layout_height="24dp"
android:layout_marginEnd="16dp" />
<TextView
android:id="@+id/tvLabel"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
android:orientation="vertical">
<TextView
android:id="@+id/tvLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</LinearLayout>
+3
View File
@@ -29,6 +29,9 @@
<item android:id="@+id/nav_finances"
android:icon="@drawable/ic_nav_finances"
android:title="@string/nav_finances" />
<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" />
+3
View File
@@ -12,6 +12,9 @@
<item android:id="@+id/nav_finances"
android:icon="@drawable/ic_nav_finances"
android:title="@string/nav_finances" />
<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" />
+15
View File
@@ -74,6 +74,17 @@
<string name="nav_finances">ފައިނޭންސް</string>
<string name="nav_card_settings">ކާޑް ސެޓިންގ</string>
<string name="nav_settings">ސެޓިންގ</string>
<string name="nav_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string>
<string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string>
<string name="nav_desc_transfer">ކޮންޓެކްޓަކަށް ފައިސާ ފޮނުވާ</string>
<string name="nav_desc_pay_mv_qr">PayMV QR ކޯޑް ސްކޭން ނުވަތަ ތައްޔާރު ކުރޭ</string>
<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>
<string name="nav_close_drawer">ނެވިގޭޝަން ލައްޕާ</string>
<string name="work_in_progress">ތައްޔާރުވަމުން ދަނީ</string>
@@ -106,6 +117,10 @@
<string name="settings_clear_cache">ކޭޝް ސާފުކުރޭ</string>
<string name="settings_cache_cleared">ކޭޝް ސާފުކުރެވިއްޖެ</string>
<string name="settings_logins">ލޮގިންތައް</string>
<string name="settings_desc_logins">ބޭންކް ލޮގިންތައް މެނޭޖް ކުރޭ</string>
<string name="settings_desc_appearance">ތީމް، ބަސް، އަދި ދައްކުވާ ގޮތް</string>
<string name="settings_desc_privacy_security">އެޕް ލޮކް، ޕިން، އަދި ސަލާމަތީ ސެޓިންގ</string>
<string name="settings_desc_storage">ކޭޝް ޑޭޓާ އަދި ސްޓޯރޭޖް</string>
<string name="settings_logout">ލޮގްއައުޓް</string>
<string name="settings_logout_confirm_title">%s އިން ލޮގްއައުޓް ވަންތަ؟</string>
<string name="settings_logout_confirm_message">ހުރިހާ ކޭޝް ޑޭޓާ ސާފުވެ، ބާކީ ހުރި އެކައުންޓްތައް އަލުން ލޯޑްވާނެ.</string>
+25
View File
@@ -86,9 +86,21 @@
<string name="nav_otp">OTP Codes</string>
<string name="nav_settings">Settings</string>
<string name="nav_more">More</string>
<string name="nav_desc_accounts">View all your bank accounts</string>
<string name="nav_desc_contacts">Manage your transfer contacts</string>
<string name="nav_desc_transfer">Send money to a contact</string>
<string name="nav_desc_pay_mv_qr">Scan or generate a PayMV QR code</string>
<string name="nav_desc_activities">View your recent transfers</string>
<string name="nav_desc_transfer_history">Full transaction history by account</string>
<string name="nav_desc_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_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>
<string name="nav_close_drawer">Close navigation</string>
<string name="work_in_progress">Work in progress</string>
<string name="press_back_to_exit">Press back again to exit</string>
<!-- Dashboard -->
<string name="dashboard_pending_finances">Pending Finances</string>
@@ -161,6 +173,10 @@
<string name="settings_privacy_security">Privacy &amp; Security</string>
<string name="settings_storage">Storage</string>
<string name="settings_logins">Logins</string>
<string name="settings_desc_logins">Manage your bank account logins</string>
<string name="settings_desc_appearance">Theme, language, and display options</string>
<string name="settings_desc_privacy_security">App lock, PIN, and security preferences</string>
<string name="settings_desc_storage">Manage cached data and storage usage</string>
<string name="settings_logout">Log out</string>
<string name="settings_logout_confirm_title">Log out of %s?</string>
<string name="settings_logout_confirm_message">All cached data will be cleared and remaining accounts will be refreshed.</string>
@@ -272,4 +288,13 @@
<string name="loan_end_date">End Date</string>
<string name="loan_overdue_payments">Overdue Payments</string>
<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="card_action_change_pin">Change PIN</string>
<string name="card_action_freeze">Freeze</string>
<string name="card_action_block">Block</string>
<string name="cards_empty">No cards found</string>
</resources>