diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt index 948c5a6..57d446a 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt @@ -121,7 +121,8 @@ class DashboardFragment : Fragment() { 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) && !hidden.contains(it.accountNumber) } .map { CardItem.Bml(it) } - val all = bmlItems + mibItems + val rank = credStore.loginRank() + val all = (bmlItems + mibItems).sortedBy { rank(it.loginTag) } val defaultNum = credStore.getDefaultCardAccountNumber() val ordered = if (defaultNum != null) { val def = all.filterIsInstance().firstOrNull { it.account.accountNumber == defaultNum } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt index 08d130c..35ada5d 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingAdapter.kt @@ -44,9 +44,11 @@ class FinancingAdapter(mibDeals: List) : notifyDataSetChanged() } - fun update(mibDeals: List, bmlLoans: List>) { + fun update(mibDeals: List, bmlLoans: List>, mibFirst: Boolean = true) { expandedPositions.clear() - items = mibDeals.map { Item.Mib(it) } + bmlLoans.map { (acc, detail) -> Item.Bml(acc, detail) } + val mibItems = mibDeals.map { Item.Mib(it) } + val bmlItems = bmlLoans.map { (acc, detail) -> Item.Bml(acc, detail) } + items = if (mibFirst) mibItems + bmlItems else bmlItems + mibItems notifyDataSetChanged() } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt index fbda63d..5ceb7ce 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/FinancingFragment.kt @@ -14,6 +14,7 @@ import sh.sar.basedbank.api.bml.BmlLoanDetail import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.databinding.FragmentFinancingBinding +import sh.sar.basedbank.util.CredentialStore class FinancingFragment : Fragment() { @@ -67,7 +68,11 @@ class FinancingFragment : Fragment() { val bmlLoans: List> = loanAccounts.map { acc -> acc to latestBmlLoanDetails[acc.internalId] } - adapter.update(latestMibDeals, bmlLoans) + // MIB deals carry no login tag, so order the MIB and BML blocks by each bank's first login + val order = CredentialStore(requireContext()).getLoginOrder() + val firstMib = order.indexOfFirst { it.startsWith("mib_") } + val firstBml = order.indexOfFirst { it.startsWith("bml_") } + adapter.update(latestMibDeals, bmlLoans, mibFirst = firstBml < 0 || (firstMib in 0 until firstBml)) val isEmpty = latestMibDeals.isEmpty() && bmlLoans.isEmpty() binding.recyclerView.visibility = if (isEmpty) View.GONE else View.VISIBLE diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt index 298eebc..ffac7a9 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt @@ -1,7 +1,8 @@ package sh.sar.basedbank.ui.home +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import sh.sar.basedbank.api.bml.BmlForeignLimit import sh.sar.basedbank.api.bml.BmlLoanDetail import sh.sar.basedbank.api.models.BankAccount @@ -9,14 +10,21 @@ 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 +import sh.sar.basedbank.util.CredentialStore sealed class CardItem { - data class Mib(val card: MibCard) : CardItem() - data class Bml(val account: BankAccount) : CardItem() + abstract val loginTag: String + data class Mib(val card: MibCard) : CardItem() { override val loginTag get() = card.loginTag } + data class Bml(val account: BankAccount) : CardItem() { override val loginTag get() = account.loginTag } } -class HomeViewModel : ViewModel() { - val accounts = MutableLiveData>(emptyList()) +class HomeViewModel(application: Application) : AndroidViewModel(application) { + /** Always kept in the user's login order (Settings > Logins), so every screen lists accounts the same way. */ + val accounts: MutableLiveData> = object : MutableLiveData>(emptyList()) { + override fun setValue(value: List?) { + super.setValue(value?.let { list -> loginRank().let { rank -> list.sortedBy { rank(it.loginTag) } } }) + } + } val financing = MutableLiveData>(emptyList()) /** BML loan details keyed by account internalId. */ val bmlLoanDetails = MutableLiveData>(emptyMap()) @@ -26,7 +34,19 @@ class HomeViewModel : ViewModel() { data class BmlLimitsData(val userName: String, val limits: List) val bmlLimits = MutableLiveData>(emptyList()) - val mibCards = MutableLiveData?>(null) + val mibCards: MutableLiveData?> = object : MutableLiveData?>(null) { + override fun setValue(value: List?) { + super.setValue(value?.let { list -> loginRank().let { rank -> list.sortedBy { rank(it.loginTag) } } }) + } + } + + fun loginRank(): (String) -> Int = CredentialStore(getApplication()).loginRank() + + /** Re-applies the login order after the user changes it. */ + fun resortByLoginOrder() { + accounts.value = accounts.value + mibCards.value = mibCards.value + } val hideAmounts = MutableLiveData(false) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt index 532cf50..27205c5 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt @@ -86,17 +86,19 @@ class OtpFragment : Fragment() { val store = CredentialStore(requireContext()) val app = requireActivity().application as BasedBankApp - val entries = mutableListOf() + val rank = store.loginRank() + val tagged = mutableListOf>() for (loginId in store.getMibLoginIds()) { val creds = store.loadMibCredentials(loginId) ?: continue val name = store.loadMibFullName(loginId) - entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed)) + tagged.add(CredentialStore.loginKey("mib", loginId) to OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed)) } for (loginId in store.getBmlLoginIds()) { val creds = store.loadBmlCredentials(loginId) ?: continue val name = store.loadBmlUserProfile(loginId)?.fullName - entries.add(OtpEntry(if (!name.isNullOrBlank()) "BML · $name" else "BML", creds.otpSeed)) + tagged.add(CredentialStore.loginKey("bml", loginId) to OtpEntry(if (!name.isNullOrBlank()) "BML · $name" else "BML", creds.otpSeed)) } + val entries = tagged.sortedBy { rank(it.first) }.map { it.second }.toMutableList() val adapter = OtpAdapter(entries) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt index 2ca307c..83ae66c 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt @@ -904,7 +904,9 @@ class CardsFragment : Fragment() { val bmlInactive = bmlItems.filter { !it.account.statusDesc.equals("Active", ignoreCase = true) } val mibActive = mibItems.filter { isMibCardActive(it.card.cardStatus) } val mibInactive = mibItems.filter { !isMibCardActive(it.card.cardStatus) } - val all: List = bmlActive + mibActive + bmlInactive + mibInactive + val rank = store.loginRank() + val all: List = (bmlActive + mibActive).sortedBy { rank(it.loginTag) } + + (bmlInactive + mibInactive).sortedBy { rank(it.loginTag) } // Move default BML card to front cards = if (defaultNum != null) { val def = all.filterIsInstance().firstOrNull { it.account.accountNumber == defaultNum } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt index 2109297..2d0cf1a 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt @@ -10,8 +10,11 @@ import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle import android.util.Base64 +import android.annotation.SuppressLint import android.view.Gravity +import android.view.HapticFeedbackConstants import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ImageView @@ -359,65 +362,86 @@ class SettingsLoginsFragment : Fragment() { _binding = null } + private data class LoginEntry(val key: String, val logoRes: Int, val displayName: String, val onClick: () -> Unit) + private fun buildLoginsSection() { val ctx = requireContext() val store = CredentialStore(ctx) val container = binding.loginsContainer container.removeAllViews() - val mibLoginIds = store.getMibLoginIds() - val bmlLoginIds = store.getBmlLoginIds() - val fahipayLoginIds = store.getFahipayLoginIds() - val mfaisaLoginIds = store.getMfaisaLoginIds() + val entries = mutableListOf() - binding.tvLoginsTitle.visibility = if (mibLoginIds.isNotEmpty() || bmlLoginIds.isNotEmpty() || fahipayLoginIds.isNotEmpty() || mfaisaLoginIds.isNotEmpty()) View.VISIBLE else View.GONE - - for (loginId in mibLoginIds) { + for (loginId in store.getMibLoginIds()) { val profile = store.loadMibUserProfile(loginId) val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.mib_name) val mibProfiles = store.loadMibProfiles(loginId) - addLoginRow(container, R.drawable.mib_logo, displayName) { + entries += LoginEntry(CredentialStore.loginKey("mib", loginId), R.drawable.mib_logo, displayName) { showMibLoginDetails(store, loginId, profile, mibProfiles) } } - for (loginId in bmlLoginIds) { + for (loginId in store.getBmlLoginIds()) { val profile = store.loadBmlUserProfile(loginId) val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name) val bmlProfiles = store.loadBmlProfiles(loginId) - addLoginRow(container, R.drawable.bml_logo_vector, displayName) { + entries += LoginEntry(CredentialStore.loginKey("bml", loginId), R.drawable.bml_logo_vector, displayName) { showBmlLoginDetails(store, loginId, profile, bmlProfiles) } } - for (loginId in fahipayLoginIds) { + for (loginId in store.getFahipayLoginIds()) { val profile = store.loadFahipayUserProfile(loginId) val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name) - addLoginRow(container, R.drawable.fahipay_logo, displayName) { + entries += LoginEntry(CredentialStore.loginKey("fahipay", loginId), R.drawable.fahipay_logo, displayName) { showFahipayLoginDetails(store, loginId, profile) } } - for (loginId in mfaisaLoginIds) { + for (loginId in store.getMfaisaLoginIds()) { val profile = store.loadMfaisaUserProfile(loginId) val displayName = profile?.name?.takeIf { it.isNotBlank() } ?: getString(R.string.ooredoo_name) - addLoginRow(container, R.drawable.ooredoo_logo, displayName) { + entries += LoginEntry(CredentialStore.loginKey("mfaisa", loginId), R.drawable.ooredoo_logo, displayName) { showMfaisaLoginDetails(store, loginId, profile) } } + + val rank = store.loginRank() + val sorted = entries.sortedBy { rank(it.key) } + val draggable = sorted.size > 1 + for (entry in sorted) { + addLoginRow( + container, entry.logoRes, entry.displayName, entry.onClick, + dragTag = entry.key.takeIf { draggable }, + onReordered = { order -> + store.setLoginOrder(order) + viewModel.resortByLoginOrder() + } + ) + } + + binding.tvLoginsTitle.visibility = if (entries.isNotEmpty()) View.VISIBLE else View.GONE } - private fun addLoginRow(container: LinearLayout, logoRes: Int, displayName: String, onClick: () -> Unit) { + private fun addLoginRow( + container: LinearLayout, + logoRes: Int, + displayName: String, + onClick: () -> Unit, + dragTag: String? = null, + onReordered: (List) -> Unit = {} + ) { val ctx = requireContext() val dp = ctx.resources.displayMetrics.density val row = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL - setPadding(0, (12 * dp).toInt(), 0, (12 * dp).toInt()) + setPadding((12 * dp).toInt(), (12 * dp).toInt(), (12 * dp).toInt(), (12 * dp).toInt()) isClickable = true; isFocusable = true val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground)) background = ta.getDrawable(0); ta.recycle() setOnClickListener { onClick() } + tag = dragTag } val logo = ImageView(ctx).apply { setImageResource(logoRes) @@ -430,9 +454,82 @@ class SettingsLoginsFragment : Fragment() { layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) } row.addView(logo); row.addView(tvName) + if (dragTag != null) { + val handle = ImageView(ctx).apply { + setImageResource(R.drawable.ic_reorder_lines) + scaleType = ImageView.ScaleType.CENTER_INSIDE + contentDescription = getString(R.string.drag_to_reorder) + layoutParams = LinearLayout.LayoutParams((40 * dp).toInt(), (40 * dp).toInt()).apply { marginStart = (4 * dp).toInt() } + } + row.addView(handle) + attachDragHandle(handle, row, container, onReordered) + } container.addView(row) } + /** Drag [row] vertically within [group] by its [handle]; rows are identified by their String tag. */ + @SuppressLint("ClickableViewAccessibility") + private fun attachDragHandle(handle: View, row: View, group: LinearLayout, onReordered: (List) -> Unit) { + val ctx = handle.context + val lift = 8 * ctx.resources.displayMetrics.density + val dragBg = android.graphics.drawable.ColorDrawable( + com.google.android.material.color.MaterialColors.getColor( + ctx, com.google.android.material.R.attr.colorSurfaceContainerHigh, android.graphics.Color.LTGRAY)) + var lastY = 0f + var startIndex = 0 + var savedBg: android.graphics.drawable.Drawable? = null + + handle.setOnTouchListener { v, ev -> + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + v.parent.requestDisallowInterceptTouchEvent(true) + lastY = ev.rawY + startIndex = group.indexOfChild(row) + row.animate().cancel() + savedBg = row.background + row.background = dragBg + row.translationZ = lift + v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + MotionEvent.ACTION_MOVE -> { + row.translationY += ev.rawY - lastY + lastY = ev.rawY + var index = group.indexOfChild(row) + while (index < group.childCount - 1) { + val next = group.getChildAt(index + 1) + if (row.translationY <= next.height / 2f) break + group.removeView(next); group.addView(next, index) + row.translationY -= next.height + next.translationY = row.height.toFloat() + next.animate().translationY(0f).setDuration(150).start() + index++ + } + while (index > 0) { + val prev = group.getChildAt(index - 1) + if (row.translationY >= -prev.height / 2f) break + group.removeView(prev); group.addView(prev, index) + row.translationY += prev.height + prev.translationY = -row.height.toFloat() + prev.animate().translationY(0f).setDuration(150).start() + index-- + } + if (index == 0) row.translationY = row.translationY.coerceAtLeast(0f) + if (index == group.childCount - 1) row.translationY = row.translationY.coerceAtMost(0f) + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + v.parent.requestDisallowInterceptTouchEvent(false) + row.animate().translationY(0f).translationZ(0f).setDuration(150) + .withEndAction { row.background = savedBg } + .start() + if (group.indexOfChild(row) != startIndex) { + onReordered((0 until group.childCount).mapNotNull { group.getChildAt(it).tag as? String }) + } + } + } + true + } + } + private fun showMibLoginDetails( store: CredentialStore, loginId: String, diff --git a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt index b5fcce8..ddad526 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -14,6 +14,11 @@ import javax.crypto.spec.GCMParameterSpec class CredentialStore(context: Context) { + companion object { + /** Same format as BankAccount.loginTag / MibCard.loginTag, e.g. "bml_". */ + fun loginKey(bank: String, loginId: String) = "${bank}_$loginId" + } + private val prefs = context.getSharedPreferences("credential_store", Context.MODE_PRIVATE) private val keyAlias = "basedbank_credential_key" private val transformation = "AES/GCM/NoPadding" @@ -23,6 +28,37 @@ class CredentialStore(context: Context) { data class FahipayCredentials(val idCard: String, val password: String) data class MfaisaCredentials(val msisdn: String, val pin: String) + // ── Cross-bank login order (keys are login tags, see loginKey) ────────── + + /** User-chosen order of all logins; logins not yet ordered (e.g. newly added) follow, bank by bank. */ + fun getLoginOrder(): List { + val all = getMibLoginIds().map { loginKey("mib", it) } + + getBmlLoginIds().map { loginKey("bml", it) } + + getFahipayLoginIds().map { loginKey("fahipay", it) } + + getMfaisaLoginIds().map { loginKey("mfaisa", it) } + val saved = try { + val arr = org.json.JSONArray(prefs.getString("login_order", null) ?: "[]") + (0 until arr.length()).map { arr.getString(it) } + } catch (_: Exception) { emptyList() } + return saved.filter { it in all } + all.filter { it !in saved } + } + + /** Position of a login tag in [getLoginOrder]; unknown tags sort last. */ + fun loginRank(): (String) -> Int { + val order = getLoginOrder() + return { tag -> order.indexOf(tag).let { if (it < 0) Int.MAX_VALUE else it } } + } + + /** Saves the combined order and keeps each bank's own login list in the same relative order. */ + fun setLoginOrder(keys: List) { + prefs.edit().putString("login_order", org.json.JSONArray(keys).toString()).apply() + fun idsFor(bank: String) = keys.filter { it.startsWith("${bank}_") }.map { it.removePrefix("${bank}_") } + setMibLoginIds(idsFor("mib")) + setBmlLoginIds(idsFor("bml")) + setFahipayLoginIds(idsFor("fahipay")) + setMfaisaLoginIds(idsFor("mfaisa")) + } + // ── MIB login credentials (multi-login, keyed by loginId = username) ───── fun getMibLoginIds(): List { @@ -36,6 +72,11 @@ class CredentialStore(context: Context) { fun hasMibCredentials(): Boolean = getMibLoginIds().isNotEmpty() + fun setMibLoginIds(order: List) { + if (order.toSet() != getMibLoginIds().toSet()) return + prefs.edit().putString("mib_login_ids", org.json.JSONArray(order).toString()).apply() + } + private fun addMibLoginId(loginId: String) { val ids = getMibLoginIds().toMutableList() if (loginId !in ids) { @@ -160,6 +201,11 @@ class CredentialStore(context: Context) { fun hasBmlCredentials(): Boolean = getBmlLoginIds().isNotEmpty() + fun setBmlLoginIds(order: List) { + if (order.toSet() != getBmlLoginIds().toSet()) return + prefs.edit().putString("bml_login_ids", org.json.JSONArray(order).toString()).apply() + } + private fun addBmlLoginId(loginId: String) { val ids = getBmlLoginIds().toMutableList() if (loginId !in ids) { @@ -316,6 +362,11 @@ class CredentialStore(context: Context) { fun hasFahipayCredentials(): Boolean = getFahipayLoginIds().isNotEmpty() + fun setFahipayLoginIds(order: List) { + if (order.toSet() != getFahipayLoginIds().toSet()) return + prefs.edit().putString("fahipay_login_ids", org.json.JSONArray(order).toString()).apply() + } + private fun addFahipayLoginId(loginId: String) { val ids = getFahipayLoginIds().toMutableList() if (loginId !in ids) { @@ -473,6 +524,11 @@ class CredentialStore(context: Context) { fun hasMfaisaCredentials(): Boolean = getMfaisaLoginIds().isNotEmpty() + fun setMfaisaLoginIds(order: List) { + if (order.toSet() != getMfaisaLoginIds().toSet()) return + prefs.edit().putString("mfaisa_login_ids", org.json.JSONArray(order).toString()).apply() + } + private fun addMfaisaLoginId(loginId: String) { val ids = getMfaisaLoginIds().toMutableList() if (loginId !in ids) { diff --git a/app/src/main/res/drawable/ic_reorder_lines.xml b/app/src/main/res/drawable/ic_reorder_lines.xml new file mode 100644 index 0000000..a42abb7 --- /dev/null +++ b/app/src/main/res/drawable/ic_reorder_lines.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/fragment_settings_logins.xml b/app/src/main/res/layout/fragment_settings_logins.xml index 0e73fd1..acffc4b 100644 --- a/app/src/main/res/layout/fragment_settings_logins.xml +++ b/app/src/main/res/layout/fragment_settings_logins.xml @@ -9,7 +9,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="16dp"> + android:padding="16dp" + android:clipToPadding="false"> + އިންޓަނެޓް ބައްލަވާ، ދެން ތިޖޫރީ ލޯޑް ކުރޭ %s އާ ގުޅުމުގައި މައްސަލައެއް + ދަމާ ތަރުތީބު ބަދަލުކުރޭ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1d2177..96ab07e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -391,4 +391,5 @@ Please check your connection and reload Thijooree Connectivity issue with %s + Drag to reorder diff --git a/docs/thijooree/00-app-overview.md b/docs/thijooree/00-app-overview.md index 958b000..0283e61 100644 --- a/docs/thijooree/00-app-overview.md +++ b/docs/thijooree/00-app-overview.md @@ -155,6 +155,15 @@ When the timeout expires a 10-second countdown warning dialog appears. If dismis Each stored profile has a visibility flag. Hidden profiles are excluded from the accounts list and from all API refresh cycles until re-enabled in Settings → Logins. +### Login Order + +The user sets the order of their logins in [Settings → Logins](14-settings.md#login-order). It applies everywhere accounts are listed: + +- `HomeViewModel.accounts` and `HomeViewModel.mibCards` sort themselves by `CredentialStore.loginRank()` on every `setValue` / `postValue`, so every observer (accounts list, transfer dropdown, contact picker "My accounts", PayMV QR, BML QR pay, transfer history, BML loans in Financing) gets the same order with no per-screen sorting. The sort is stable, so accounts from the same login keep the order the bank returned them in. +- Combined MIB + BML card lists (dashboard stack, [Cards](22-cards.md)) are sorted by `CardItem.loginTag`. +- [Financing](13-financing.md) places the MIB and BML sections by whichever bank's first login ranks higher. +- The [OTP Screen](10-otp-screen.md) sorts its entries by login. + --- ## MIB Session KeepAlive diff --git a/docs/thijooree/10-otp-screen.md b/docs/thijooree/10-otp-screen.md index b602037..88bb87f 100644 --- a/docs/thijooree/10-otp-screen.md +++ b/docs/thijooree/10-otp-screen.md @@ -30,7 +30,7 @@ Standard RFC 6238 TOTP: ## Supported Banks -One card is rendered for every MIB and every BML login that has a stored OTP seed (`OtpFragment.kt:93-98`). Seeds are per-`loginId` in `CredentialStore`. +One card is rendered for every MIB and every BML login that has a stored OTP seed (`OtpFragment.kt`), sorted by the user's [login order](00-app-overview.md#login-order). Seeds are per-`loginId` in `CredentialStore`. | Bank | Seed source | Card label | |---|---|---| diff --git a/docs/thijooree/13-financing.md b/docs/thijooree/13-financing.md index f3aa32b..34adcd2 100644 --- a/docs/thijooree/13-financing.md +++ b/docs/thijooree/13-financing.md @@ -8,6 +8,8 @@ Aggregates financing products across banks — MIB promotional deals and BML loa Observes `HomeViewModel.financing` (MIB deals) and `HomeViewModel.bmlLoanDetails`. +MIB deals carry no login tag, so the two sections are ordered as blocks: MIB first if the first MIB login ranks above the first BML login in the user's [login order](00-app-overview.md#login-order), otherwise BML first. BML loans within their section follow `HomeViewModel.accounts`, which is already in login order. + --- ## MIB Deals Section diff --git a/docs/thijooree/14-settings.md b/docs/thijooree/14-settings.md index c587c5b..4be895d 100644 --- a/docs/thijooree/14-settings.md +++ b/docs/thijooree/14-settings.md @@ -33,6 +33,20 @@ Each profile entry shows: - Profile image (circular avatar) - Visibility toggle switch +### Login Order + +All logins are shown as one list, across banks. When there is more than one login, each row gets a three-line handle (`ic_reorder_lines`) on the right; press and drag it to move the login up or down (`SettingsLoginsFragment.attachDragHandle()`). Tapping the rest of the row still opens the login details. + +On drop the order is saved with `CredentialStore.setLoginOrder()`, which: +- writes the combined order to the `login_order` pref as a JSON array of login tags (`mib_`, `bml_`, `fahipay_`, `mfaisa_` — the same format as `BankAccount.loginTag`) +- rewrites each bank's own list (`mib_login_ids`, `bml_login_ids`, …) in the same relative order, so code that walks one bank's logins stays consistent + +It then calls `HomeViewModel.resortByLoginOrder()` so already-open screens re-sort immediately. + +`CredentialStore.getLoginOrder()` returns the effective order: saved tags that still exist, followed by any logins not yet ordered (e.g. newly added) in the default MIB → BML → Fahipay → M-Faisa order. `loginRank()` turns that into a sort key; unknown tags sort last. + +See [Login Order](00-app-overview.md#login-order) for where the order is applied. + ### Visibility Toggle Toggling a profile off hides it from the accounts list and excludes it from API refresh cycles. The session is kept alive — the profile can be re-enabled without re-logging in. diff --git a/docs/thijooree/21-dashboard.md b/docs/thijooree/21-dashboard.md index f6255bd..04a416b 100644 --- a/docs/thijooree/21-dashboard.md +++ b/docs/thijooree/21-dashboard.md @@ -29,6 +29,7 @@ A horizontal `RecyclerView` with `LinearSnapHelper` that paginates BML cards + M - BML cards: visible accounts with `profileType` in (`BML_PREPAID`, `BML_CREDIT`, `BML_DEBIT`) AND `statusDesc == "Active"` - MIB cards: filtered by `CardsFragment.isMibCardActive(cardStatus)` (i.e. `CHST0`) +- Cards are sorted by the user's [login order](00-app-overview.md#login-order) - The user's default card (`CredentialStore.getDefaultCardAccountNumber()`) is moved to the front - Cards hidden via `getHiddenDashboardCardNumbers()` are skipped diff --git a/docs/thijooree/22-cards.md b/docs/thijooree/22-cards.md index e0b10e8..dd374ad 100644 --- a/docs/thijooree/22-cards.md +++ b/docs/thijooree/22-cards.md @@ -29,7 +29,7 @@ A horizontal `RecyclerView` driven by `CardStackAdapter` with a `PagerSnapHelper - BML: `viewModel.accounts` filtered to `profileType in (BML_PREPAID, BML_CREDIT, BML_DEBIT)` - MIB: `viewModel.mibCards` -The combined order is `bmlActive + mibActive + bmlInactive + mibInactive`. The user's default card (`CredentialStore.getDefaultCardAccountNumber()`) is moved to position 0. Initial data is seeded from `CardsCache` so the stack appears immediately; a background `HomeActivity.triggerRefreshCards()` refreshes it. +Active cards come first, then inactive ones; within each group cards are sorted by the user's [login order](00-app-overview.md#login-order) via `CardItem.loginTag`. The user's default card (`CredentialStore.getDefaultCardAccountNumber()`) is moved to position 0. Initial data is seeded from `CardsCache` so the stack appears immediately; a background `HomeActivity.triggerRefreshCards()` refreshes it. ### Card Art diff --git a/docs/thijooree/README.md b/docs/thijooree/README.md index cc2c139..3818984 100644 --- a/docs/thijooree/README.md +++ b/docs/thijooree/README.md @@ -22,7 +22,7 @@ Documentation for app-specific logic — UI flows, routing decisions, and busine | [11 — PayMV QR Screen](11-paymv-qr-screen.md) | Generate receive-payment QR (send/scan lives in Transfer) | | [12 — BML QR Pay](12-bml-qr-pay.md) | (Stub — see Transfer Flows for the live BML QR merchant flow) | | [13 — Financing](13-financing.md) | MIB promotional deals, BML loans, BML foreign spend limits | -| [14 — Settings](14-settings.md) | Settings hub: Logins, Appearance, Privacy & Security, Notifications, Storage, About | +| [14 — Settings](14-settings.md) | Settings hub: Logins (drag to reorder), Appearance, Privacy & Security, Notifications, Storage, About | | [15 — Settings: Security](15-settings-security.md) | Change lock method, biometrics, auto-lock timeout, screenshots | | [16 — Settings: Appearance](16-settings-appearance.md) | Navigation mode, slot drag-reorder, theme, accent colour, language | | [17 — Settings: Storage](17-settings-storage.md) | Single "Clear All Caches" button |