forked from shihaam/thijooree
add BML usd limits to dashboard and optmize login flow/session expirey
This commit is contained in:
@@ -19,6 +19,8 @@ import java.security.SecureRandom
|
|||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class AuthExpiredException : Exception("Session expired")
|
||||||
|
|
||||||
class BmlLoginFlow {
|
class BmlLoginFlow {
|
||||||
|
|
||||||
private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
|
private val BASE_URL = "https://www.bankofmaldives.com.mv/internetbanking"
|
||||||
@@ -162,9 +164,43 @@ class BmlLoginFlow {
|
|||||||
|
|
||||||
fun fetchAccounts(session: BmlSession): List<MibAccount> {
|
fun fetchAccounts(session: BmlSession): List<MibAccount> {
|
||||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
||||||
val json = resp.body?.string() ?: return emptyList()
|
val code = resp.code
|
||||||
|
val json = resp.body?.string()
|
||||||
resp.close()
|
resp.close()
|
||||||
return parseDashboard(json, "bml_${session.deviceId}")
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
return parseDashboard(json ?: return emptyList(), "bml_${session.deviceId}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchForeignLimits(session: BmlSession): List<BmlForeignLimit> {
|
||||||
|
val resp = apiClient.newCall(
|
||||||
|
Request.Builder().url("https://app.bankofmaldives.com.mv/api/v2/foreign-limits")
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", APP_USER_AGENT)
|
||||||
|
.header("x-app-version", APP_VERSION)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
).execute()
|
||||||
|
val code = resp.code
|
||||||
|
val json = resp.body?.string()
|
||||||
|
resp.close()
|
||||||
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
return parseForeignLimits(json ?: return emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchUserInfo(session: BmlSession): String {
|
||||||
|
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/userinfo")).execute()
|
||||||
|
val json = resp.body?.string() ?: return ""
|
||||||
|
resp.close()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return ""
|
||||||
|
val payload = root.optJSONObject("payload") ?: return ""
|
||||||
|
payload.optString("name").ifBlank {
|
||||||
|
payload.optString("fullName").ifBlank {
|
||||||
|
payload.optString("customer_name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
||||||
@@ -243,6 +279,37 @@ class BmlLoginFlow {
|
|||||||
return casaAccounts + prepaidCards
|
return casaAccounts + prepaidCards
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseForeignLimits(json: String): List<BmlForeignLimit> {
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val payload = root.optJSONArray("payload") ?: return emptyList()
|
||||||
|
(0 until payload.length()).map { i ->
|
||||||
|
val item = payload.getJSONObject(i)
|
||||||
|
val usage = item.optJSONObject("usageByCategory") ?: JSONObject()
|
||||||
|
val atm = usage.optJSONObject("ATM") ?: JSONObject()
|
||||||
|
val ecom = usage.optJSONObject("ECOM") ?: JSONObject()
|
||||||
|
val pos = usage.optJSONObject("POS") ?: JSONObject()
|
||||||
|
BmlForeignLimit(
|
||||||
|
type = item.optString("type", "Debit"),
|
||||||
|
used = item.optDouble("used", 0.0),
|
||||||
|
totalLimit = item.optDouble("totalLimit", 0.0),
|
||||||
|
generalCap = item.optDouble("generalCap", 0.0),
|
||||||
|
generalRemaining = item.optDouble("generalRemaining", 0.0),
|
||||||
|
medicalRemaining = item.optDouble("medicalRemaining", 0.0),
|
||||||
|
isAtmEnabled = item.optBoolean("isAtmEnabled", false),
|
||||||
|
isPosEnabled = item.optBoolean("isPosEnabled", false),
|
||||||
|
atmRemaining = atm.optDouble("remaining", 0.0),
|
||||||
|
atmLimit = atm.optDouble("limit", 0.0),
|
||||||
|
ecomRemaining = ecom.optDouble("remaining", 0.0),
|
||||||
|
ecomLimit = ecom.optDouble("limit", 0.0),
|
||||||
|
posRemaining = pos.optDouble("remaining", 0.0),
|
||||||
|
posLimit = pos.optDouble("limit", 0.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseContacts(json: String): List<MibBeneficiary> {
|
private fun parseContacts(json: String): List<MibBeneficiary> {
|
||||||
val root = JSONObject(json)
|
val root = JSONObject(json)
|
||||||
if (!root.optBoolean("success")) return emptyList()
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
|||||||
@@ -4,3 +4,20 @@ data class BmlSession(
|
|||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
val deviceId: String
|
val deviceId: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class BmlForeignLimit(
|
||||||
|
val type: String,
|
||||||
|
val used: Double,
|
||||||
|
val totalLimit: Double,
|
||||||
|
val generalCap: Double,
|
||||||
|
val generalRemaining: Double,
|
||||||
|
val medicalRemaining: Double,
|
||||||
|
val isAtmEnabled: Boolean,
|
||||||
|
val isPosEnabled: Boolean,
|
||||||
|
val atmRemaining: Double,
|
||||||
|
val atmLimit: Double,
|
||||||
|
val ecomRemaining: Double,
|
||||||
|
val ecomLimit: Double,
|
||||||
|
val posRemaining: Double,
|
||||||
|
val posLimit: Double
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import android.widget.Toast
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||||
|
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||||
|
|
||||||
class DashboardFragment : Fragment() {
|
class DashboardFragment : Fragment() {
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ class DashboardFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
|
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
|
||||||
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
|
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
|
||||||
|
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
||||||
|
|
||||||
binding.btnTransfer.setOnClickListener {
|
binding.btnTransfer.setOnClickListener {
|
||||||
(requireActivity() as HomeActivity).showWithBackStack(TransferFragment())
|
(requireActivity() as HomeActivity).showWithBackStack(TransferFragment())
|
||||||
@@ -52,6 +55,29 @@ class DashboardFragment : Fragment() {
|
|||||||
binding.tvUsdBalance.text = "USD %,.2f".format(usdTotal)
|
binding.tvUsdBalance.text = "USD %,.2f".format(usdTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateForeignLimits(entries: List<HomeViewModel.BmlLimitsData>) {
|
||||||
|
binding.containerForeignLimits.removeAllViews()
|
||||||
|
for (entry in entries) {
|
||||||
|
for (limit in entry.limits) {
|
||||||
|
val card = ItemForeignLimitBinding.inflate(layoutInflater, binding.containerForeignLimits, false)
|
||||||
|
card.tvLimitUserName.text = entry.userName.ifBlank { "BML" }
|
||||||
|
card.tvLimitType.text = limit.type
|
||||||
|
card.tvLimitGeneral.text = "USD %,.0f / %,.0f".format(limit.generalRemaining, limit.generalCap)
|
||||||
|
card.tvLimitMedical.text = "USD %,.0f".format(limit.medicalRemaining)
|
||||||
|
card.tvLimitAtm.text = if (!limit.isAtmEnabled)
|
||||||
|
"USD %,.0f / %,.0f · Disabled".format(limit.atmRemaining, limit.atmLimit)
|
||||||
|
else
|
||||||
|
"USD %,.0f / %,.0f".format(limit.atmRemaining, limit.atmLimit)
|
||||||
|
card.tvLimitEcom.text = "USD %,.0f / %,.0f".format(limit.ecomRemaining, limit.ecomLimit)
|
||||||
|
card.tvLimitPos.text = if (!limit.isPosEnabled)
|
||||||
|
"USD %,.0f / %,.0f · Disabled".format(limit.posRemaining, limit.posLimit)
|
||||||
|
else
|
||||||
|
"USD %,.0f / %,.0f".format(limit.posRemaining, limit.posLimit)
|
||||||
|
binding.containerForeignLimits.addView(card.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updatePendingFinances(deals: List<MibFinanceDeal>) {
|
private fun updatePendingFinances(deals: List<MibFinanceDeal>) {
|
||||||
val total = deals.sumOf { it.outstandingAmount }
|
val total = deals.sumOf { it.outstandingAmount }
|
||||||
binding.tvPendingFinances.text = "MVR %,.2f".format(total)
|
binding.tvPendingFinances.text = "MVR %,.2f".format(total)
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.bml.AuthExpiredException
|
||||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||||
|
import sh.sar.basedbank.api.bml.BmlSession
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||||
import sh.sar.basedbank.databinding.ActivityHomeBinding
|
import sh.sar.basedbank.databinding.ActivityHomeBinding
|
||||||
@@ -30,6 +33,7 @@ import sh.sar.basedbank.util.AccountCache
|
|||||||
import sh.sar.basedbank.util.ContactsCache
|
import sh.sar.basedbank.util.ContactsCache
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.util.FinancingCache
|
import sh.sar.basedbank.util.FinancingCache
|
||||||
|
import sh.sar.basedbank.util.ForeignLimitsCache
|
||||||
|
|
||||||
class HomeActivity : AppCompatActivity() {
|
class HomeActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -82,7 +86,10 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
refreshContacts(app.mibSession, app.mibProfiles)
|
refreshContacts(app.mibSession, app.mibProfiles)
|
||||||
if (app.bmlSession != null) refreshBmlContacts(app)
|
if (app.bmlSession != null) {
|
||||||
|
refreshBmlContacts(app)
|
||||||
|
refreshBmlLimits(app.bmlSession!!)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Came from lock screen — show caches immediately, refresh everything in background
|
// Came from lock screen — show caches immediately, refresh everything in background
|
||||||
val cachedMib = AccountCache.load(this)
|
val cachedMib = AccountCache.load(this)
|
||||||
@@ -97,10 +104,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
||||||
|
|
||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
val mibCreds = store.loadMibCredentials()
|
autoRefresh(store.loadMibCredentials(), store.loadBmlCredentials(), store)
|
||||||
val bmlCreds = store.loadBmlCredentials()
|
|
||||||
if (mibCreds != null) autoRefreshMib(mibCreds, bmlCreds)
|
|
||||||
else if (bmlCreds != null) autoRefreshBml(bmlCreds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show dashboard on first create
|
// Show dashboard on first create
|
||||||
@@ -123,67 +127,99 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoRefreshMib(
|
private fun autoRefresh(
|
||||||
mibCreds: CredentialStore.MibCredentials,
|
mibCreds: CredentialStore.MibCredentials?,
|
||||||
bmlCreds: CredentialStore.BmlCredentials?
|
bmlCreds: CredentialStore.BmlCredentials?,
|
||||||
|
store: CredentialStore
|
||||||
) {
|
) {
|
||||||
|
if (mibCreds == null && bmlCreds == null) return
|
||||||
binding.refreshIndicator.visibility = View.VISIBLE
|
binding.refreshIndicator.visibility = View.VISIBLE
|
||||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
|
||||||
val flow = MibLoginFlow(prefs)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
var mibAccounts: List<MibAccount> = AccountCache.load(this@HomeActivity)
|
|
||||||
try {
|
|
||||||
mibAccounts = withContext(Dispatchers.IO) {
|
|
||||||
flow.login(mibCreds.username, mibCreds.passwordHash, mibCreds.otpSeed)
|
|
||||||
}
|
|
||||||
val app = application as BasedBankApp
|
|
||||||
app.accounts = mibAccounts
|
|
||||||
app.mibSession = flow.lastSession
|
|
||||||
app.mibProfiles = flow.lastProfiles
|
|
||||||
AccountCache.save(this@HomeActivity, mibAccounts)
|
|
||||||
} catch (_: Exception) { /* keep cached */ }
|
|
||||||
finally { binding.refreshIndicator.visibility = View.GONE }
|
|
||||||
|
|
||||||
val bmlAccounts = AccountCache.loadBml(this@HomeActivity).toMutableList()
|
lifecycleScope.launch {
|
||||||
if (bmlCreds != null) {
|
// MIB and BML login run in parallel
|
||||||
try {
|
val mibJob = mibCreds?.let {
|
||||||
val bmlFlow = BmlLoginFlow()
|
async(Dispatchers.IO) {
|
||||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
try {
|
||||||
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
|
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||||
}
|
val flow = MibLoginFlow(prefs)
|
||||||
val app = application as BasedBankApp
|
val accounts = flow.login(it.username, it.passwordHash, it.otpSeed)
|
||||||
app.bmlSession = session
|
val app = application as BasedBankApp
|
||||||
app.bmlAccounts = accounts
|
app.accounts = accounts
|
||||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
app.mibSession = flow.lastSession
|
||||||
bmlAccounts.clear()
|
app.mibProfiles = flow.lastProfiles
|
||||||
bmlAccounts.addAll(accounts)
|
AccountCache.save(this@HomeActivity, accounts)
|
||||||
refreshBmlContacts(app)
|
accounts
|
||||||
} catch (_: Exception) { /* keep cached */ }
|
} catch (_: Exception) { AccountCache.load(this@HomeActivity) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bmlJob = bmlCreds?.let {
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
val bmlFlow = BmlLoginFlow()
|
||||||
|
val savedToken = store.loadBmlSession()
|
||||||
|
|
||||||
|
// Try cached token first
|
||||||
|
if (savedToken != null) {
|
||||||
|
try {
|
||||||
|
val session = BmlSession(savedToken.first, savedToken.second)
|
||||||
|
val accounts = bmlFlow.fetchAccounts(session)
|
||||||
|
val app = application as BasedBankApp
|
||||||
|
app.bmlSession = session
|
||||||
|
app.bmlAccounts = accounts
|
||||||
|
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||||
|
return@async Pair(session, accounts)
|
||||||
|
} catch (_: AuthExpiredException) {
|
||||||
|
// Token expired — fall through to full login
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Network or other error — fall through to full login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full login (token missing or expired)
|
||||||
|
try {
|
||||||
|
val (session, accounts) = bmlFlow.login(it.username, it.password, it.otpSeed)
|
||||||
|
store.saveBmlSession(session.accessToken, session.deviceId)
|
||||||
|
val app = application as BasedBankApp
|
||||||
|
app.bmlSession = session
|
||||||
|
app.bmlAccounts = accounts
|
||||||
|
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||||
|
Pair(session, accounts)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Pair(null, AccountCache.loadBml(this@HomeActivity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mibAccounts = mibJob?.await() ?: AccountCache.load(this@HomeActivity)
|
||||||
|
val (bmlSession, bmlAccounts) = bmlJob?.await() ?: Pair(null, AccountCache.loadBml(this@HomeActivity))
|
||||||
|
|
||||||
viewModel.accounts.postValue(mibAccounts + bmlAccounts)
|
viewModel.accounts.postValue(mibAccounts + bmlAccounts)
|
||||||
|
binding.refreshIndicator.visibility = View.GONE
|
||||||
|
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
|
if (bmlSession != null) {
|
||||||
|
refreshBmlContacts(app)
|
||||||
|
refreshBmlLimits(bmlSession)
|
||||||
|
}
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
refreshContacts(app.mibSession, app.mibProfiles)
|
refreshContacts(app.mibSession, app.mibProfiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoRefreshBml(bmlCreds: CredentialStore.BmlCredentials) {
|
private fun refreshBmlLimits(session: BmlSession) {
|
||||||
binding.refreshIndicator.visibility = View.VISIBLE
|
val bmlFlow = BmlLoginFlow()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val cachedMib = AccountCache.load(this@HomeActivity)
|
|
||||||
try {
|
try {
|
||||||
val bmlFlow = BmlLoginFlow()
|
val (userName, limits) = withContext(Dispatchers.IO) {
|
||||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
Pair(bmlFlow.fetchUserInfo(session), bmlFlow.fetchForeignLimits(session))
|
||||||
bmlFlow.login(bmlCreds.username, bmlCreds.password, bmlCreds.otpSeed)
|
|
||||||
}
|
}
|
||||||
val app = application as BasedBankApp
|
val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
|
||||||
app.bmlSession = session
|
val idx = existing.indexOfFirst { it.userName == userName }
|
||||||
app.bmlAccounts = accounts
|
val entry = HomeViewModel.BmlLimitsData(userName, limits)
|
||||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
if (idx >= 0) existing[idx] = entry else existing.add(entry)
|
||||||
viewModel.accounts.postValue(cachedMib + accounts)
|
viewModel.bmlLimits.postValue(existing)
|
||||||
refreshBmlContacts(app)
|
ForeignLimitsCache.save(this@HomeActivity, existing)
|
||||||
} catch (_: Exception) { /* keep cached */ }
|
} catch (_: Exception) { /* keep previous */ }
|
||||||
finally { binding.refreshIndicator.visibility = View.GONE }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package sh.sar.basedbank.ui.home
|
|||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||||
@@ -12,4 +13,7 @@ class HomeViewModel : ViewModel() {
|
|||||||
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||||
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
|
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
|
||||||
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(emptyList())
|
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(emptyList())
|
||||||
|
|
||||||
|
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
||||||
|
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,9 @@ class CredentialsFragment : Fragment() {
|
|||||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||||
flow.login(username, password, otpSeed)
|
flow.login(username, password, otpSeed)
|
||||||
}
|
}
|
||||||
CredentialStore(requireContext()).saveBmlCredentials(username, password, otpSeed)
|
val store = CredentialStore(requireContext())
|
||||||
|
store.saveBmlCredentials(username, password, otpSeed)
|
||||||
|
store.saveBmlSession(session.accessToken, session.deviceId)
|
||||||
AccountCache.saveBml(requireContext(), accounts)
|
AccountCache.saveBml(requireContext(), accounts)
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
app.bmlSession = session
|
app.bmlSession = session
|
||||||
|
|||||||
@@ -82,6 +82,30 @@ class CredentialStore(context: Context) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveBmlSession(accessToken: String, deviceId: String) {
|
||||||
|
val key = getOrCreateKey()
|
||||||
|
prefs.edit()
|
||||||
|
.putString("bml_enc_token", encrypt(accessToken, key))
|
||||||
|
.putString("bml_enc_device_id", encrypt(deviceId, key))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBmlSession(): Pair<String, String>? {
|
||||||
|
val key = getOrCreateKey()
|
||||||
|
val encToken = prefs.getString("bml_enc_token", null) ?: return null
|
||||||
|
val encDeviceId = prefs.getString("bml_enc_device_id", null) ?: return null
|
||||||
|
return try {
|
||||||
|
Pair(decrypt(encToken, key), decrypt(encDeviceId, key))
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBmlSession() {
|
||||||
|
prefs.edit()
|
||||||
|
.remove("bml_enc_token")
|
||||||
|
.remove("bml_enc_device_id")
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getOrCreateKey(): SecretKey {
|
private fun getOrCreateKey(): SecretKey {
|
||||||
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
||||||
ks.getKey(keyAlias, null)?.let { return it as SecretKey }
|
ks.getKey(keyAlias, null)?.let { return it as SecretKey }
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||||
|
import sh.sar.basedbank.ui.home.HomeViewModel
|
||||||
|
|
||||||
|
object ForeignLimitsCache {
|
||||||
|
|
||||||
|
private const val PREFS = "foreign_limits_cache"
|
||||||
|
private const val KEY = "bml_foreign_limits"
|
||||||
|
|
||||||
|
fun save(context: Context, entries: List<HomeViewModel.BmlLimitsData>) {
|
||||||
|
val arr = JSONArray()
|
||||||
|
for (entry in entries) {
|
||||||
|
val limitsArr = JSONArray()
|
||||||
|
for (l in entry.limits) {
|
||||||
|
limitsArr.put(JSONObject().apply {
|
||||||
|
put("type", l.type)
|
||||||
|
put("used", l.used)
|
||||||
|
put("totalLimit", l.totalLimit)
|
||||||
|
put("generalCap", l.generalCap)
|
||||||
|
put("generalRemaining", l.generalRemaining)
|
||||||
|
put("medicalRemaining", l.medicalRemaining)
|
||||||
|
put("isAtmEnabled", l.isAtmEnabled)
|
||||||
|
put("isPosEnabled", l.isPosEnabled)
|
||||||
|
put("atmRemaining", l.atmRemaining)
|
||||||
|
put("atmLimit", l.atmLimit)
|
||||||
|
put("ecomRemaining", l.ecomRemaining)
|
||||||
|
put("ecomLimit", l.ecomLimit)
|
||||||
|
put("posRemaining", l.posRemaining)
|
||||||
|
put("posLimit", l.posLimit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
arr.put(JSONObject().apply {
|
||||||
|
put("userName", entry.userName)
|
||||||
|
put("limits", limitsArr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit().putString(KEY, arr.toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(context: Context): List<HomeViewModel.BmlLimitsData> {
|
||||||
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY, null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val entry = arr.getJSONObject(i)
|
||||||
|
val limitsArr = entry.optJSONArray("limits") ?: JSONArray()
|
||||||
|
val limits = (0 until limitsArr.length()).map { j ->
|
||||||
|
val l = limitsArr.getJSONObject(j)
|
||||||
|
BmlForeignLimit(
|
||||||
|
type = l.optString("type", "Debit"),
|
||||||
|
used = l.optDouble("used", 0.0),
|
||||||
|
totalLimit = l.optDouble("totalLimit", 0.0),
|
||||||
|
generalCap = l.optDouble("generalCap", 0.0),
|
||||||
|
generalRemaining = l.optDouble("generalRemaining", 0.0),
|
||||||
|
medicalRemaining = l.optDouble("medicalRemaining", 0.0),
|
||||||
|
isAtmEnabled = l.optBoolean("isAtmEnabled", false),
|
||||||
|
isPosEnabled = l.optBoolean("isPosEnabled", false),
|
||||||
|
atmRemaining = l.optDouble("atmRemaining", 0.0),
|
||||||
|
atmLimit = l.optDouble("atmLimit", 0.0),
|
||||||
|
ecomRemaining = l.optDouble("ecomRemaining", 0.0),
|
||||||
|
ecomLimit = l.optDouble("ecomLimit", 0.0),
|
||||||
|
posRemaining = l.optDouble("posRemaining", 0.0),
|
||||||
|
posLimit = l.optDouble("posLimit", 0.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HomeViewModel.BmlLimitsData(entry.optString("userName"), limits)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,6 +119,13 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- BML foreign limits (populated dynamically) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/containerForeignLimits"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
<!-- Card support WIP -->
|
<!-- Card support WIP -->
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:cardElevation="1dp"
|
||||||
|
app:cardCornerRadius="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<!-- Header: name + type chip -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLimitUserName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="USD Foreign Transaction Limits"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLimitType"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingVertical="5dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:background="@drawable/pill_segment_bg" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:background="?attr/colorOutlineVariant" />
|
||||||
|
|
||||||
|
<!-- General -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="6dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="General"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLimitGeneral"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Medical -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Medical"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLimitMedical"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:background="?attr/colorOutlineVariant" />
|
||||||
|
|
||||||
|
<!-- ATM -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="6dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="ATM"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLimitAtm"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- ECOM -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="6dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Online (ECOM)"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLimitEcom"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- POS -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="POS"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLimitPos"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
Reference in New Issue
Block a user