Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
473e051282
|
|||
|
f9c182fe9a
|
|||
|
339dae8a37
|
|||
|
a6a1f28144
|
|||
|
523d1248bd
|
|||
|
ee9f98b720
|
|||
|
219ca9bf00
|
|||
|
e9f0cec698
|
|||
|
268f3dada0
|
|||
|
e0a554c769
|
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "sh.sar.basedbank"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 6
|
||||
versionName = "1.0.7"
|
||||
versionCode = 7
|
||||
versionName = "1.0.8"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
BIN
app/src/main/assets/cards/bml/amex_credit_gold.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
app/src/main/assets/cards/bml/amex_credit_green.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
app/src/main/assets/cards/bml/amex_debit_gold.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
app/src/main/assets/cards/bml/amex_debit_green.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
app/src/main/assets/cards/bml/amex_platinum.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
app/src/main/assets/cards/bml/defaultcard.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
app/src/main/assets/cards/bml/master.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
app/src/main/assets/cards/bml/master_business_debit.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
app/src/main/assets/cards/bml/master_gold.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
app/src/main/assets/cards/bml/master_islamic.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
app/src/main/assets/cards/bml/master_masveriyaa.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
app/src/main/assets/cards/bml/master_odiveriyaa.png
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
app/src/main/assets/cards/bml/master_passport.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
app/src/main/assets/cards/bml/master_platinum.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
app/src/main/assets/cards/bml/master_prepaid.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
app/src/main/assets/cards/bml/master_prepaid_business.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
app/src/main/assets/cards/bml/master_prepaid_travel.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
app/src/main/assets/cards/bml/master_world.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
app/src/main/assets/cards/bml/visa_corporate.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/assets/cards/bml/visa_credit.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit_generic.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit_islamic.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
app/src/main/assets/cards/bml/visa_debit_platinum.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
app/src/main/assets/cards/bml/visa_gold.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
app/src/main/assets/cards/bml/visa_infinite.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/assets/cards/bml/visa_platinum.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
app/src/main/assets/cards/bml/visa_student_black.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
app/src/main/assets/cards/bml/visa_student_blue.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 242 KiB |
BIN
app/src/main/assets/cards/mib/visa_black_platinum.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 378 KiB |
BIN
app/src/main/assets/cards/mib/visa_blue_everyday.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 633 KiB |
BIN
app/src/main/assets/cards/mib/visa_business.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -203,15 +203,15 @@ class LockActivity : AppCompatActivity() {
|
||||
if (remaining <= 0) return false
|
||||
val secs = ((remaining + 999L) / 1000L).toInt()
|
||||
val msg = getString(R.string.unlock_locked_out, secs)
|
||||
binding.tvLockPinDots.text = msg
|
||||
binding.root.postDelayed({ updateDots() }, remaining)
|
||||
binding.tvPinHint.text = msg
|
||||
binding.root.postDelayed({ binding.tvPinHint.text = ""; updateDots() }, remaining)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showFailure() {
|
||||
val msg = failureMessage()
|
||||
binding.tvLockPinDots.text = msg
|
||||
binding.root.postDelayed({ updateDots() }, 1200)
|
||||
binding.tvPinHint.text = msg
|
||||
binding.root.postDelayed({ binding.tvPinHint.text = ""; updateDots() }, 1200)
|
||||
}
|
||||
|
||||
private fun failureMessage(): String {
|
||||
|
||||
@@ -2,6 +2,7 @@ package sh.sar.basedbank.api.bml
|
||||
|
||||
import org.json.JSONObject
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.api.models.BankServerException
|
||||
|
||||
data class BmlUserInfo(
|
||||
val fullName: String,
|
||||
@@ -27,6 +28,7 @@ class BmlAccountClient {
|
||||
val json = resp.body?.string()
|
||||
resp.close()
|
||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||
if (code in 500..599) throw BankServerException("BML")
|
||||
return parseDashboard(json ?: return emptyList(), loginTag, profileName, profileId)
|
||||
}
|
||||
|
||||
@@ -36,6 +38,7 @@ class BmlAccountClient {
|
||||
val code = resp.code
|
||||
resp.close()
|
||||
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||
if (code in 500..599) throw BankServerException("BML")
|
||||
}
|
||||
|
||||
fun fetchUserInfo(session: BmlSession): BmlUserInfo? {
|
||||
@@ -166,16 +169,22 @@ class BmlAccountClient {
|
||||
internalId = internalId
|
||||
))
|
||||
} else if (accountType == "Card") {
|
||||
val isVisible = item.optBoolean("account_visible", false)
|
||||
if (!isVisible) continue
|
||||
val isPrepaid = item.optBoolean("prepaid_card", false)
|
||||
val productCode = item.optString("product_code", "")
|
||||
val cardBalance = item.optJSONObject("cardBalance")
|
||||
val available = cardBalance?.optDouble("AvailableLimit", 0.0) ?: 0.0
|
||||
val current = cardBalance?.optDouble("CurrentBalance", 0.0) ?: 0.0
|
||||
val isVisible = item.optBoolean("account_visible", false)
|
||||
val cardProfileType = when {
|
||||
isPrepaid -> "BML_PREPAID"
|
||||
isVisible -> "BML_CREDIT" // non-prepaid, visible = credit card
|
||||
else -> "BML_DEBIT" // non-prepaid, not visible = debit card
|
||||
}
|
||||
prepaidCards.add(BankAccount(
|
||||
bank = "BML",
|
||||
profileName = profileName,
|
||||
profileType = if (isPrepaid) "BML_PREPAID" else "BML_CREDIT",
|
||||
profileType = cardProfileType,
|
||||
productCode = productCode,
|
||||
accountNumber = accountNumber,
|
||||
accountBriefName = item.optString("alias").ifBlank { product },
|
||||
currencyName = currency,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package sh.sar.basedbank.api.bml
|
||||
|
||||
import android.os.Build
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal const val BML_BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
|
||||
internal const val BML_USER_AGENT = "bml-mobile-banking/348 (POCO; Android 14; 22101320I)"
|
||||
internal val BML_USER_AGENT = "bml-mobile-banking/348 (${Build.MANUFACTURER}; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
|
||||
internal const val BML_APP_VERSION = "2.1.44.348"
|
||||
|
||||
internal fun newBmlApiClient(): OkHttpClient = OkHttpClient.Builder()
|
||||
|
||||
@@ -25,9 +25,9 @@ class BmlLoginFlow {
|
||||
private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
|
||||
private val CLIENT_ID = "98C83590-513F-4716-B02B-EC68B7D9E7E7"
|
||||
private val REDIRECT_URI = "https://app.bankofmaldives.com.mv/oauth/mobile-callback"
|
||||
private val APP_USER_AGENT = "bml-mobile-banking/348 (POCO; Android 14; 22101320I)"
|
||||
private val APP_USER_AGENT = "bml-mobile-banking/348 (${android.os.Build.MANUFACTURER}; Android ${android.os.Build.VERSION.RELEASE}; ${android.os.Build.MODEL})"
|
||||
private val APP_VERSION = "2.1.44.348"
|
||||
private val WEB_USER_AGENT = "Mozilla/5.0 (Android 14; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0"
|
||||
private val WEB_USER_AGENT = "Mozilla/5.0 (Android ${android.os.Build.VERSION.RELEASE}; Mobile; rv:150.0) Gecko/150.0 Firefox/150.0"
|
||||
|
||||
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
||||
private val cookieJar = object : CookieJar {
|
||||
|
||||
@@ -4,6 +4,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.api.models.BankServerException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FahipayAccountClient {
|
||||
@@ -27,8 +28,10 @@ class FahipayAccountClient {
|
||||
Request.Builder().url("$BASE_URL/actions/getprofile/?lang=en")
|
||||
.auth(session).build()
|
||||
).execute()
|
||||
val code = resp.code
|
||||
val json = resp.body?.string() ?: throw Exception("Empty profile response")
|
||||
resp.close()
|
||||
if (code in 500..599) throw BankServerException("Fahipay")
|
||||
val obj = JSONObject(json)
|
||||
val props = obj.optJSONObject("props") ?: JSONObject()
|
||||
return FahipayUserProfile(
|
||||
@@ -47,8 +50,10 @@ class FahipayAccountClient {
|
||||
Request.Builder().url("$BASE_URL/actions/getbalance/?lang=en")
|
||||
.auth(session).build()
|
||||
).execute()
|
||||
val code = resp.code
|
||||
val json = resp.body?.string() ?: return 0.0
|
||||
resp.close()
|
||||
if (code in 500..599) throw BankServerException("Fahipay")
|
||||
return try {
|
||||
val obj = JSONObject(json)
|
||||
if (obj.optBoolean("error")) 0.0 else obj.optDouble("balance", 0.0)
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit
|
||||
class FahipayLoginFlow {
|
||||
|
||||
private val BASE_URL = "https://fahipay.mv"
|
||||
private val UA_WEBVIEW = "Mozilla/5.0 (Linux; Android 14; ${Build.MODEL} Build/AP2A.240905.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
|
||||
private val UA_WEBVIEW = "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL} Build/AP2A.240905.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
|
||||
|
||||
private val cookieStore = mutableMapOf<String, MutableList<Cookie>>()
|
||||
private val cookieJar = object : CookieJar {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sh.sar.basedbank.api.mib
|
||||
|
||||
import android.os.Build
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -31,7 +32,7 @@ class MibCardsClient {
|
||||
.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("User-Agent", "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; 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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sh.sar.basedbank.api.mib
|
||||
|
||||
import android.os.Build
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -24,7 +25,7 @@ class MibContactsClient {
|
||||
.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"
|
||||
"Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; 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", "*/*")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sh.sar.basedbank.api.mib
|
||||
|
||||
import android.os.Build
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -27,7 +28,7 @@ class MibFinancingClient {
|
||||
.header("Cookie", cookieHeader)
|
||||
.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"
|
||||
"Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
|
||||
)
|
||||
.header("X-Requested-With", "mv.com.mib.faisamobilex")
|
||||
.get()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sh.sar.basedbank.api.mib
|
||||
|
||||
import android.os.Build
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -50,7 +51,7 @@ class MibHistoryClient {
|
||||
.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"
|
||||
"Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; 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", "*/*")
|
||||
|
||||
@@ -161,7 +161,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
bank = "MIB",
|
||||
profileName = profile.name,
|
||||
profileType = profile.profileType,
|
||||
cifType = profile.cifType,
|
||||
productCode = profile.cifType,
|
||||
accountNumber = a.optString("accountNumber"),
|
||||
accountBriefName = a.optString("accountBriefName"),
|
||||
currencyName = a.optString("currencyName"),
|
||||
@@ -318,7 +318,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
bank = "MIB",
|
||||
profileName = profile.name,
|
||||
profileType = profile.profileType,
|
||||
cifType = profile.cifType,
|
||||
productCode = profile.cifType,
|
||||
accountNumber = a.optString("accountNumber"),
|
||||
accountBriefName = a.optString("accountBriefName"),
|
||||
currencyName = a.optString("currencyName"),
|
||||
@@ -373,6 +373,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
.build()
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.code == 419) throw SessionExpiredException()
|
||||
if (response.code in 500..599) throw sh.sar.basedbank.api.models.BankServerException("MIB")
|
||||
return response.body?.string() ?: throw IllegalStateException("Empty response body")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sh.sar.basedbank.api.mib
|
||||
|
||||
import android.os.Build
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -26,7 +27,7 @@ class MibTransferClient {
|
||||
.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"
|
||||
"Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; 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", "*/*")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package sh.sar.basedbank.api.models
|
||||
|
||||
/** Thrown by a bank API client when the server returns an HTTP 5xx response. */
|
||||
class BankServerException(val bankName: String) : Exception("Server error from $bankName")
|
||||
|
||||
/**
|
||||
* Unified account model used across all banks (MIB, BML, Fahipay, ...).
|
||||
* The [bank] field identifies which bank owns this account.
|
||||
@@ -8,7 +11,7 @@ data class BankAccount(
|
||||
val bank: String, // "MIB", "BML", "FAHIPAY" — set by the login flow
|
||||
val profileName: String,
|
||||
val profileType: String,
|
||||
val cifType: String = "", // MIB: human-readable profile category (e.g. "Individual", "Sole Propr"); empty for other banks
|
||||
val productCode: String = "", // bank-specific product/subtype code: MIB: CIF type label ("Individual", "Sole Propr"); BML: card product code ("C8201", "C1007")
|
||||
val accountNumber: String,
|
||||
val accountBriefName: String,
|
||||
val currencyName: String,
|
||||
|
||||
@@ -72,7 +72,7 @@ class AccountsAdapter(
|
||||
else -> account.bank
|
||||
}
|
||||
val profileLabel = when (account.bank) {
|
||||
"MIB" -> account.cifType.ifBlank { account.profileName }
|
||||
"MIB" -> account.productCode.ifBlank { account.profileName }
|
||||
else -> account.profileName
|
||||
}
|
||||
return if (profileLabel.isNotBlank()) "$profileLabel · $bankName" else bankName
|
||||
|
||||
@@ -15,8 +15,8 @@ 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
|
||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||
|
||||
class CardSettingsFragment : Fragment() {
|
||||
|
||||
@@ -44,17 +44,23 @@ class CardSettingsFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
(activity as? HomeActivity)?.triggerRefresh()
|
||||
}
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards == null) return@observe
|
||||
adapter.update(cards)
|
||||
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 (cards.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
|
||||
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
|
||||
@@ -73,11 +79,11 @@ class CardSettingsFragment : Fragment() {
|
||||
}
|
||||
|
||||
private inner class CardSettingsAdapter(
|
||||
private var cards: List<MibCard>,
|
||||
private var cards: List<CardItem>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
fun update(newCards: List<CardItem>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@@ -93,17 +99,34 @@ class CardSettingsFragment : Fragment() {
|
||||
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(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
|
||||
tvCardType.text = card.cardTypeDesc
|
||||
val assetPath = PayWithCardFragment.cardImageAsset(card)
|
||||
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
|
||||
viewModel.contacts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||
viewModel.hideAmounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||
|
||||
(activity as? HomeActivity)?.loadAllContacts()
|
||||
}
|
||||
@@ -183,6 +184,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
|
||||
private fun buildPageItems(tabTag: String?): List<ContactPickerAdapter.PickerItem> {
|
||||
val search = binding.etSheetSearch.text?.toString()?.trim() ?: ""
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
val items = mutableListOf<ContactPickerAdapter.PickerItem>()
|
||||
|
||||
if (tabTag == RECENTS_TAG) {
|
||||
@@ -209,11 +211,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
val fromAccount = accounts.find { it.accountNumber == fromAccountNumber }
|
||||
val fromCurrency = fromAccount?.currencyName ?: ""
|
||||
val fromLoginTag = fromAccount?.loginTag ?: ""
|
||||
val fromIsCard = fromAccount?.profileType == "BML_PREPAID" || fromAccount?.profileType == "BML_CREDIT"
|
||||
val fromIsCard = fromAccount?.profileType == "BML_PREPAID" || fromAccount?.profileType == "BML_CREDIT" || fromAccount?.profileType == "BML_DEBIT"
|
||||
|
||||
if (tabTag == MY_ACCOUNTS_TAG) {
|
||||
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
|
||||
|
||||
val filteredRegular = if (search.isBlank()) regularAccounts else regularAccounts.filter {
|
||||
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
||||
@@ -223,10 +225,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
for (acc in filteredRegular) {
|
||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||
val isSame = acc.accountNumber == fromAccountNumber
|
||||
val accBal = if (hide) "••••••" else acc.availableBalance
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = acc.accountNumber,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} $accBal",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
@@ -246,10 +249,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||
val isSame = acc.accountNumber == fromAccountNumber
|
||||
val isActive = acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
val cardBal = if (hide) "••••••" else acc.availableBalance
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = acc.accountNumber,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} $cardBal",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
@@ -306,6 +310,11 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun maskAmount(formatted: String): String {
|
||||
val currency = formatted.substringBefore(' ', formatted)
|
||||
return "$currency ••••••"
|
||||
}
|
||||
|
||||
private fun currencyMismatchReason(fromCurrency: String, toCurrency: String): String? =
|
||||
if (fromCurrency == "MVR" && toCurrency == "USD") "Cannot transfer from MVR to USD account" else null
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||
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 kotlin.math.abs
|
||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||
@@ -57,11 +58,17 @@ class DashboardFragment : Fragment() {
|
||||
binding.rvCards.adapter = cardAdapter
|
||||
LinearSnapHelper().attachToRecyclerView(binding.rvCards)
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards.isNullOrEmpty()) return@observe
|
||||
cardAdapter.update(cards)
|
||||
binding.sectionCards.visibility = View.VISIBLE
|
||||
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
|
||||
cardAdapter.update(all)
|
||||
binding.sectionCards.visibility = if (all.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.buttonBar) { v, insets ->
|
||||
@@ -229,9 +236,9 @@ class DashboardFragment : Fragment() {
|
||||
}
|
||||
|
||||
private inner class DashboardCardAdapter : RecyclerView.Adapter<DashboardCardAdapter.VH>() {
|
||||
private var cards: List<MibCard> = emptyList()
|
||||
private var cards: List<CardItem> = emptyList()
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
fun update(newCards: List<CardItem>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@@ -249,15 +256,27 @@ class DashboardFragment : Fragment() {
|
||||
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 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)
|
||||
|
||||
fun bind(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
|
||||
val assetPath = PayWithCardFragment.cardImageAsset(card)
|
||||
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
fun bind(item: CardItem) {
|
||||
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)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.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
|
||||
}
|
||||
}
|
||||
btnPayQr.setOnClickListener {
|
||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.AuthExpiredException
|
||||
import sh.sar.basedbank.api.models.BankServerException
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import sh.sar.basedbank.api.bml.BmlAccountClient
|
||||
import sh.sar.basedbank.api.bml.BmlContactsClient
|
||||
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
|
||||
@@ -547,14 +549,25 @@ fun applyNavLabelVisibility() {
|
||||
autoRefresh(store)
|
||||
}
|
||||
|
||||
private fun showConnectivityBanner(message: String) {
|
||||
binding.connectivityBanner.text = message
|
||||
binding.connectivityBanner.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun hideConnectivityBanner() {
|
||||
binding.connectivityBanner.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun autoRefresh(store: CredentialStore) {
|
||||
val mibLoginIds = store.getMibLoginIds()
|
||||
val bmlLoginIds = store.getBmlLoginIds()
|
||||
val fahipayLoginIds = store.getFahipayLoginIds()
|
||||
if (mibLoginIds.isEmpty() && bmlLoginIds.isEmpty() && fahipayLoginIds.isEmpty()) return
|
||||
binding.refreshIndicator.visibility = View.VISIBLE
|
||||
hideConnectivityBanner()
|
||||
|
||||
lifecycleScope.launch {
|
||||
val refreshErrors = ConcurrentLinkedQueue<String>()
|
||||
// One async job per MIB login, all run in parallel
|
||||
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
||||
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
||||
@@ -568,7 +581,15 @@ fun applyNavLabelVisibility() {
|
||||
app.mibLoginFlows[loginId] = flow
|
||||
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||
accounts
|
||||
} catch (_: Exception) { AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" } }
|
||||
} catch (e: java.io.IOException) {
|
||||
refreshErrors.add("NO_INTERNET")
|
||||
AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||
} catch (e: BankServerException) {
|
||||
refreshErrors.add("SERVER:${e.bankName}")
|
||||
AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||
} catch (_: Exception) {
|
||||
AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,6 +644,14 @@ fun applyNavLabelVisibility() {
|
||||
tryRefresh()
|
||||
}
|
||||
}
|
||||
} catch (e: java.io.IOException) {
|
||||
refreshErrors.add("NO_INTERNET")
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
} catch (e: BankServerException) {
|
||||
refreshErrors.add("SERVER:${e.bankName}")
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
} catch (_: Exception) {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
@@ -638,6 +667,12 @@ fun applyNavLabelVisibility() {
|
||||
val accounts = BmlAccountClient().fetchAccounts(session, loginTag)
|
||||
app.bmlSessions[loginId] = session
|
||||
allAccounts += accounts
|
||||
} catch (e: java.io.IOException) {
|
||||
refreshErrors.add("NO_INTERNET")
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
} catch (e: BankServerException) {
|
||||
refreshErrors.add("SERVER:${e.bankName}")
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
} catch (_: Exception) {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
}
|
||||
@@ -689,6 +724,12 @@ fun applyNavLabelVisibility() {
|
||||
app.fahipaySessions[loginId] = session
|
||||
AccountCache.saveFahipay(this@HomeActivity, loginId, accounts)
|
||||
accounts
|
||||
} catch (e: java.io.IOException) {
|
||||
refreshErrors.add("NO_INTERNET")
|
||||
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||
} catch (e: BankServerException) {
|
||||
refreshErrors.add("SERVER:${e.bankName}")
|
||||
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||
} catch (_: Exception) {
|
||||
AccountCache.loadFahipay(this@HomeActivity, loginId)
|
||||
}
|
||||
@@ -708,6 +749,22 @@ fun applyNavLabelVisibility() {
|
||||
viewModel.accounts.postValue((mibAccounts + bmlAccounts + fahipayAccounts).filterVisibleAccounts())
|
||||
binding.refreshIndicator.visibility = View.GONE
|
||||
|
||||
val noInternet = refreshErrors.any { it == "NO_INTERNET" }
|
||||
val serverErrors = refreshErrors.filter { it.startsWith("SERVER:") }
|
||||
.map { it.removePrefix("SERVER:") }.distinct()
|
||||
when {
|
||||
noInternet -> showConnectivityBanner(getString(R.string.connectivity_no_internet))
|
||||
serverErrors.isNotEmpty() -> showConnectivityBanner(
|
||||
getString(R.string.connectivity_server_error, serverErrors.joinToString(", "))
|
||||
)
|
||||
else -> hideConnectivityBanner()
|
||||
}
|
||||
|
||||
val errors = mutableSetOf<String>()
|
||||
if (noInternet) errors.add("NO_INTERNET")
|
||||
serverErrors.forEach { errors.add(it.uppercase()) }
|
||||
viewModel.connectivityErrors.postValue(errors)
|
||||
|
||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||
for ((loginId, session) in app.mibSessions) {
|
||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||
|
||||
@@ -10,6 +10,11 @@ import sh.sar.basedbank.api.models.BankContactCategory
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||
|
||||
sealed class CardItem {
|
||||
data class Mib(val card: MibCard) : CardItem()
|
||||
data class Bml(val account: BankAccount) : CardItem()
|
||||
}
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
|
||||
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||
@@ -24,4 +29,11 @@ class HomeViewModel : ViewModel() {
|
||||
val mibCards = MutableLiveData<List<MibCard>?>(null)
|
||||
|
||||
val hideAmounts = MutableLiveData<Boolean>(false)
|
||||
|
||||
/**
|
||||
* Set of connectivity error keys from the last refresh.
|
||||
* Contains "NO_INTERNET" for no network, or uppercase bank names ("MIB", "BML", "FAHIPAY")
|
||||
* for HTTP 5xx server errors from specific banks.
|
||||
*/
|
||||
val connectivityErrors = MutableLiveData<Set<String>>(emptySet())
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ 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_LOAN"
|
||||
it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN"
|
||||
}
|
||||
val adapter = QrAccountAdapter(requireContext(), eligible)
|
||||
binding.actvAccount.setAdapter(adapter)
|
||||
|
||||
@@ -2,6 +2,7 @@ package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -16,9 +17,11 @@ 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.models.BankAccount
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding
|
||||
import sh.sar.basedbank.util.CardsCache
|
||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||
|
||||
class PayWithCardFragment : Fragment() {
|
||||
|
||||
@@ -46,17 +49,23 @@ class PayWithCardFragment : Fragment() {
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
(activity as? HomeActivity)?.triggerRefresh()
|
||||
}
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards == null) return@observe
|
||||
adapter.update(cards)
|
||||
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 (cards.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
|
||||
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() }
|
||||
|
||||
val cached = CardsCache.load(requireContext())
|
||||
if (cached.isNotEmpty()) {
|
||||
@@ -78,11 +87,11 @@ class PayWithCardFragment : Fragment() {
|
||||
}
|
||||
|
||||
private inner class CardWalletAdapter(
|
||||
private var cards: List<MibCard>,
|
||||
private var cards: List<CardItem>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
fun update(newCards: List<CardItem>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@@ -98,16 +107,30 @@ class PayWithCardFragment : Fragment() {
|
||||
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)
|
||||
|
||||
fun bind(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = formatMasked(card.maskedCardNumber)
|
||||
tvCardType.text = card.cardTypeDesc
|
||||
val assetPath = cardImageAsset(card)
|
||||
if (assetPath != null) loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
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))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
btnPayQr.setOnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -127,9 +150,9 @@ class PayWithCardFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
|
||||
"53" -> "cards/mib/visa_black_platinum.jpg"
|
||||
"57" -> "cards/mib/visa_blue_everyday.jpg"
|
||||
"70" -> "cards/mib/visa_business.jpg"
|
||||
"53" -> "cards/mib/visa_black_platinum.png"
|
||||
"57" -> "cards/mib/visa_blue_everyday.png"
|
||||
"70" -> "cards/mib/visa_business.png"
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -144,6 +167,23 @@ class PayWithCardFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun mibCardStatusLabel(cardStatus: String): String? = when (cardStatus) {
|
||||
"CHST0" -> null // Active — no badge
|
||||
else -> cardStatus
|
||||
}
|
||||
|
||||
fun bindCardStatus(tv: TextView, statusLabel: String?) {
|
||||
if (statusLabel == null) { tv.visibility = View.GONE; return }
|
||||
tv.visibility = View.VISIBLE
|
||||
tv.text = statusLabel
|
||||
val dp = tv.context.resources.displayMetrics.density
|
||||
tv.background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = 12 * dp
|
||||
setColor(0xCC212121.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
fun formatMasked(masked: String): String {
|
||||
if (masked.length < 4) return masked
|
||||
return "\u2022\u2022\u2022\u2022 ${masked.takeLast(4)}"
|
||||
|
||||
@@ -108,6 +108,7 @@ class TransferFragment : Fragment() {
|
||||
val toAvatar: Bitmap?
|
||||
)
|
||||
private var pendingBmlTransfer: PendingBmlTransfer? = null
|
||||
private var accountDropdownAdapter: AccountDropdownAdapter? = null
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
@@ -178,6 +179,11 @@ class TransferFragment : Fragment() {
|
||||
setupFromDropdown()
|
||||
setupAccountLookup()
|
||||
|
||||
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
||||
accountDropdownAdapter?.notifyDataSetChanged()
|
||||
selectedAccount?.let { showFromCard(it) }
|
||||
}
|
||||
|
||||
childFragmentManager.setFragmentResultListener(ContactPickerSheetFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
|
||||
val accountNumber = bundle.getString(ContactPickerSheetFragment.KEY_ACCOUNT_NUMBER) ?: return@setFragmentResultListener
|
||||
val label = bundle.getString(ContactPickerSheetFragment.KEY_LABEL) ?: ""
|
||||
@@ -196,6 +202,8 @@ class TransferFragment : Fragment() {
|
||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||
}
|
||||
|
||||
viewModel.connectivityErrors.observe(viewLifecycleOwner) { updateTransferButton() }
|
||||
|
||||
binding.btnTransfer.isEnabled = false
|
||||
binding.btnTransfer.setOnClickListener {
|
||||
if (bmlOtpState == BmlOtpState.AWAITING_OTP) verifyBmlOtp()
|
||||
@@ -234,6 +242,13 @@ class TransferFragment : Fragment() {
|
||||
binding.tilTo.endIconDrawable = ContextCompat.getDrawable(requireContext(), android.R.drawable.ic_menu_search)
|
||||
}
|
||||
|
||||
// ── Sensitive masking helpers ─────────────────────────────────────────────
|
||||
|
||||
private fun maskAmount(formatted: String): String {
|
||||
val currency = formatted.substringBefore(' ', formatted)
|
||||
return "$currency ••••••"
|
||||
}
|
||||
|
||||
private fun setupFromDropdown() {
|
||||
binding.btnClearFromInfo.setOnClickListener {
|
||||
selectedAccount = null
|
||||
@@ -245,11 +260,11 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { accounts ->
|
||||
val adapter = AccountDropdownAdapter(requireContext(), accounts)
|
||||
binding.actvFrom.setAdapter(adapter)
|
||||
accountDropdownAdapter = AccountDropdownAdapter(requireContext(), accounts)
|
||||
binding.actvFrom.setAdapter(accountDropdownAdapter)
|
||||
|
||||
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
||||
val picked = adapter.getAccount(position) ?: return@setOnItemClickListener
|
||||
val picked = accountDropdownAdapter?.getAccount(position) ?: return@setOnItemClickListener
|
||||
selectedAccount = picked
|
||||
updateAmountPrefix(picked)
|
||||
showFromCard(picked)
|
||||
@@ -283,13 +298,16 @@ class TransferFragment : Fragment() {
|
||||
val typeLabel = when {
|
||||
account.profileType == "BML_PREPAID" -> "Prepaid Card"
|
||||
account.profileType == "BML_CREDIT" -> "Credit Card"
|
||||
account.profileType == "BML_DEBIT" -> "Debit Card"
|
||||
account.accountTypeName.isNotBlank() -> account.accountTypeName
|
||||
else -> account.profileType
|
||||
}
|
||||
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
val balancePart = "${account.currencyName} ${account.availableBalance}"
|
||||
binding.tvFromAccountName.text = account.accountBriefName
|
||||
binding.tvFromAccountNumber.text = account.accountNumber
|
||||
binding.tvFromAccountDetails.text = listOfNotNull(bankLabel, typeLabel, "${account.currencyName} ${account.availableBalance}").joinToString(" · ")
|
||||
binding.tvFromAccountDetails.text = listOfNotNull(bankLabel, typeLabel, if (hide) maskAmount(balancePart) else balancePart).joinToString(" · ")
|
||||
binding.ivFromPhoto.setImageBitmap(makeInitialsBitmap(account.accountBriefName, colorHex))
|
||||
binding.tilFrom.visibility = View.GONE
|
||||
binding.cardFromInfo.visibility = View.VISIBLE
|
||||
@@ -632,7 +650,7 @@ class TransferFragment : Fragment() {
|
||||
|
||||
val isSrcBml = src.bank == "BML"
|
||||
val isBmlBusiness = isSrcBml && isBusinessProfile(src) // to test on personal accounts: use `isSrcBml`
|
||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT" || src.profileType == "BML_DEBIT"
|
||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||
val currency = src.currencyName.ifBlank { "MVR" }
|
||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
||||
@@ -780,12 +798,6 @@ class TransferFragment : Fragment() {
|
||||
val sess = session ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
val loginId = src.loginTag.removePrefix("mib_")
|
||||
// Switch to the profile that owns the source account
|
||||
if (src.profileId.isNotBlank()) {
|
||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||
val profile = profiles.firstOrNull { it.profileId == src.profileId }
|
||||
if (profile != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
||||
}
|
||||
val otp = CredentialStore(requireContext()).loadMibCredentials(loginId)?.otpSeed
|
||||
?.let { Totp.generate(it) }
|
||||
?: return Triple(false, "OTP unavailable", null)
|
||||
@@ -802,6 +814,12 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
return try {
|
||||
// Switch to the profile that owns the source account
|
||||
if (src.profileId.isNotBlank()) {
|
||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||
val profile = profiles.firstOrNull { it.profileId == src.profileId }
|
||||
if (profile != null) app.mibFlowFor(loginId).switchProfile(sess, profile)
|
||||
}
|
||||
val result = MibTransferClient().transfer(
|
||||
session = sess,
|
||||
fromAccount = src.accountNumber,
|
||||
@@ -860,7 +878,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
// Determine type + credit account
|
||||
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
||||
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount }
|
||||
val (transferType, creditAccount, bank) = when {
|
||||
isSrcCard -> {
|
||||
// CAD: card → own BML account
|
||||
@@ -869,7 +887,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
isDestMyCard -> {
|
||||
// CPA: BML CASA → own card top-up
|
||||
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount }
|
||||
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount }
|
||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
||||
}
|
||||
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
||||
@@ -950,7 +968,7 @@ class TransferFragment : Fragment() {
|
||||
return
|
||||
}
|
||||
val isDestMyCard = allAccounts.any {
|
||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount
|
||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount
|
||||
}
|
||||
val (transferType, creditAccount, bank) = when {
|
||||
isSrcCard -> {
|
||||
@@ -959,7 +977,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
isDestMyCard -> {
|
||||
val card = allAccounts.first {
|
||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && it.accountNumber == destAccount
|
||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount
|
||||
}
|
||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
||||
}
|
||||
@@ -1214,7 +1232,12 @@ class TransferFragment : Fragment() {
|
||||
private fun updateTransferButton() {
|
||||
if (bmlOtpState != BmlOtpState.NONE) return
|
||||
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
||||
binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
|
||||
val hasAll = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
|
||||
if (!hasAll) { binding.btnTransfer.isEnabled = false; return }
|
||||
val errors = viewModel.connectivityErrors.value ?: emptySet()
|
||||
val bankOffline = "NO_INTERNET" in errors ||
|
||||
selectedAccount?.bank?.uppercase()?.let { it in errors } == true
|
||||
binding.btnTransfer.isEnabled = !bankOffline
|
||||
}
|
||||
|
||||
private fun clearForm() {
|
||||
@@ -1337,8 +1360,8 @@ class TransferFragment : Fragment() {
|
||||
) : BaseAdapter(), Filterable {
|
||||
|
||||
private val items: List<Any> = buildList {
|
||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_LOAN" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" }
|
||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" && it.profileType != "BML_CREDIT" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
|
||||
addAll(regular)
|
||||
if (cards.isNotEmpty()) {
|
||||
add(getString(R.string.cards))
|
||||
@@ -1347,7 +1370,7 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun getAccount(position: Int): BankAccount? = (items.getOrNull(position) as? BankAccount)
|
||||
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
?.takeUnless { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
|
||||
override fun getCount() = items.size
|
||||
override fun getItem(position: Int) = items[position]
|
||||
@@ -1378,12 +1401,14 @@ class TransferFragment : Fragment() {
|
||||
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
.also { it.root.tag = it }
|
||||
}
|
||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
val inactive = (acc.profileType == "BML_PREPAID" || acc.profileType == "BML_CREDIT" || acc.profileType == "BML_DEBIT") && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
val isBmlAccount = acc.bank == "BML"
|
||||
val ownerPrefix = if (isBmlAccount && acc.profileName.isNotBlank()) "${acc.profileName} · " else ""
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
b.tvDropdownAccountName.text = "$ownerPrefix${acc.accountBriefName}"
|
||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||
b.tvDropdownBalance.text = AccountListParser.from(acc)?.balance ?: ""
|
||||
val balance = AccountListParser.from(acc)?.balance ?: ""
|
||||
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
|
||||
b.root.alpha = if (inactive) 0.4f else 1f
|
||||
b.root
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class TransferHistoryFragment : Fragment() {
|
||||
) {
|
||||
fun hasMore(): Boolean = when {
|
||||
account.bank == "FAHIPAY" -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" -> cardMonthOffset < 2
|
||||
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" || account.profileType == "BML_DEBIT" -> cardMonthOffset < 2
|
||||
account.bank == "BML" -> bmlTotalPages < 0 || bmlNextPage <= bmlTotalPages
|
||||
else -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||
}
|
||||
@@ -187,7 +187,7 @@ class TransferHistoryFragment : Fragment() {
|
||||
async {
|
||||
try {
|
||||
when {
|
||||
state.account.profileType == "BML_PREPAID" || state.account.profileType == "BML_CREDIT" -> {
|
||||
state.account.profileType == "BML_PREPAID" || state.account.profileType == "BML_CREDIT" || state.account.profileType == "BML_DEBIT" -> {
|
||||
val session = app.bmlSessionFor(state.account) ?: return@async emptyList()
|
||||
val cal = Calendar.getInstance()
|
||||
cal.add(Calendar.MONTH, -state.cardMonthOffset)
|
||||
|
||||
@@ -19,7 +19,7 @@ object AccountCache {
|
||||
put("bank", acc.bank)
|
||||
put("profileName", acc.profileName)
|
||||
put("profileType", acc.profileType)
|
||||
put("cifType", acc.cifType)
|
||||
put("productCode", acc.productCode)
|
||||
put("accountNumber", acc.accountNumber)
|
||||
put("accountBriefName", acc.accountBriefName)
|
||||
put("currencyName", acc.currencyName)
|
||||
@@ -44,6 +44,7 @@ object AccountCache {
|
||||
arr.put(JSONObject().apply {
|
||||
put("profileName", acc.profileName)
|
||||
put("profileType", acc.profileType)
|
||||
put("productCode", acc.productCode)
|
||||
put("accountNumber", acc.accountNumber)
|
||||
put("accountBriefName", acc.accountBriefName)
|
||||
put("currencyName", acc.currencyName)
|
||||
@@ -55,6 +56,7 @@ object AccountCache {
|
||||
put("statusDesc", acc.statusDesc)
|
||||
put("loginTag", acc.loginTag)
|
||||
put("internalId", acc.internalId)
|
||||
put("profileId", acc.profileId)
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
@@ -72,6 +74,7 @@ object AccountCache {
|
||||
bank = "BML",
|
||||
profileName = o.optString("profileName"),
|
||||
profileType = o.optString("profileType"),
|
||||
productCode = o.optString("productCode", ""),
|
||||
accountNumber = o.optString("accountNumber"),
|
||||
accountBriefName = o.optString("accountBriefName"),
|
||||
currencyName = o.optString("currencyName"),
|
||||
@@ -83,7 +86,8 @@ object AccountCache {
|
||||
statusDesc = o.optString("statusDesc"),
|
||||
profileImageHash = null,
|
||||
loginTag = o.optString("loginTag"),
|
||||
internalId = o.optString("internalId", "")
|
||||
internalId = o.optString("internalId", ""),
|
||||
profileId = o.optString("profileId", "")
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) { emptyList() }
|
||||
@@ -162,7 +166,7 @@ object AccountCache {
|
||||
bank = o.optString("bank", "MIB"),
|
||||
profileName = o.optString("profileName"),
|
||||
profileType = o.optString("profileType"),
|
||||
cifType = o.optString("cifType", ""),
|
||||
productCode = o.optString("productCode", ""),
|
||||
accountNumber = o.optString("accountNumber"),
|
||||
accountBriefName = o.optString("accountBriefName"),
|
||||
currencyName = o.optString("currencyName"),
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.Locale
|
||||
class HistoryFetcher(private val account: BankAccount) {
|
||||
|
||||
private val isMib get() = account.bank == "MIB"
|
||||
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" || account.profileType == "BML_DEBIT"
|
||||
private val isBmlLoan get() = account.profileType == "BML_LOAN"
|
||||
private val isFahipay get() = account.bank == "FAHIPAY"
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package sh.sar.basedbank.util.bmlapi
|
||||
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
|
||||
object BmlCardParser {
|
||||
|
||||
/**
|
||||
* Returns the asset path for the card image.
|
||||
* The product code is stored in [BankAccount.productCode] for BML Card accounts.
|
||||
*/
|
||||
fun cardImageAsset(account: BankAccount): String =
|
||||
productCodeToAsset(account.productCode)
|
||||
|
||||
fun productCodeToAsset(productCode: String): String = when (productCode) {
|
||||
"C8201" -> "cards/bml/master_prepaid.png"
|
||||
"C8205" -> "cards/bml/master_prepaid_travel.png"
|
||||
"C8005" -> "cards/bml/master_prepaid_travel.png"
|
||||
"C3007" -> "cards/bml/amex_debit_green.png"
|
||||
"C1007" -> "cards/bml/visa_debit.png"
|
||||
"C1003" -> "cards/bml/visa_gold.png"
|
||||
"C8022" -> "cards/bml/master_gold.png"
|
||||
"C1020" -> "cards/bml/visa_debit_platinum.png"
|
||||
"C8902" -> "cards/bml/master_islamic.png"
|
||||
"C8101" -> "cards/bml/master_masveriyaa.png"
|
||||
// "?????" -> "cards/bml/amex_credit_gold.png"
|
||||
// "?????" -> "cards/bml/amex_credit_green.png"
|
||||
// "?????" -> "cards/bml/amex_debit_gold.png"
|
||||
// "?????" -> "cards/bml/amex_platinum.png"
|
||||
// "?????" -> "cards/bml/master.png"
|
||||
// "?????" -> "cards/bml/master_business_debit.png"
|
||||
// "?????" -> "cards/bml/master_odiveriyaa.png"
|
||||
// "?????" -> "cards/bml/master_passport.png"
|
||||
// "?????" -> "cards/bml/master_platinum.png"
|
||||
// "?????" -> "cards/bml/master_prepaid_business.png"
|
||||
// "?????" -> "cards/bml/master_world.png"
|
||||
// "?????" -> "cards/bml/visa_corporate.png"
|
||||
// "?????" -> "cards/bml/visa_credit.png"
|
||||
// "?????" -> "cards/bml/visa_debit_generic.png"
|
||||
// "?????" -> "cards/bml/visa_debit_islamic.png"
|
||||
// "?????" -> "cards/bml/visa_infinite.png"
|
||||
// "?????" -> "cards/bml/visa_platinum.png"
|
||||
// "?????" -> "cards/bml/visa_student_black.png"
|
||||
// "?????" -> "cards/bml/visa_student_blue.png"
|
||||
else -> "cards/bml/defaultcard.png"
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ object BmlDashboardParser {
|
||||
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
||||
*/
|
||||
fun displayData(account: BankAccount): AccountListDisplay? {
|
||||
if (account.profileType == "BML_LOAN") return null // Loans shown on financing page only
|
||||
if (account.profileType == "BML_LOAN") return null // Loans shown on financing page only
|
||||
if (account.profileType == "BML_DEBIT") return null // Debit cards shown on card screens only
|
||||
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||
return if (isCard) {
|
||||
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
||||
|
||||
@@ -40,6 +40,20 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connectivityBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#C62828"
|
||||
android:textColor="#FFFFFF"
|
||||
android:gravity="center"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -52,7 +52,16 @@
|
||||
android:textSize="26sp"
|
||||
android:letterSpacing="0.3"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="24dp" />
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPinHint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lockNumpadContainer"
|
||||
|
||||
@@ -31,6 +31,21 @@
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/bg_card_overlay_gradient"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -32,6 +32,21 @@
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/bg_card_overlay_gradient"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="10dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:textSize="10sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- Bottom-left: card owner name + masked number -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -32,6 +32,21 @@
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/bg_card_overlay_gradient"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="10dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:textSize="10sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- Bottom-left: card owner name + masked number -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -141,4 +141,8 @@
|
||||
<!-- Home -->
|
||||
<string name="accounts">އެކައުންޓްތައް</string>
|
||||
<string name="available_balance">ލިބެން ހުރި ބެލެންސް</string>
|
||||
|
||||
<!-- Connectivity banner -->
|
||||
<string name="connectivity_no_internet">އިންޓަނެޓް ބައްލަވާ، ދެން ތިޖޫރީ ލޯޑް ކުރޭ</string>
|
||||
<string name="connectivity_server_error">%s އާ ގުޅުމުގައި މައްސަލައެއް</string>
|
||||
</resources>
|
||||
|
||||
@@ -302,4 +302,8 @@
|
||||
<string name="card_action_freeze">Freeze</string>
|
||||
<string name="card_action_block">Block</string>
|
||||
<string name="cards_empty">No cards found</string>
|
||||
|
||||
<!-- Connectivity banner -->
|
||||
<string name="connectivity_no_internet">Please check your connection and reload Thijooree</string>
|
||||
<string name="connectivity_server_error">Connectivity issue with %s</string>
|
||||
</resources>
|
||||
|
||||