forked from shihaam/thijooree
add support for mib loans view
This commit is contained in:
@@ -4,12 +4,16 @@ import android.app.Application
|
|||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import sh.sar.basedbank.api.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
|
import sh.sar.basedbank.api.mib.MibSession
|
||||||
|
|
||||||
class BasedBankApp : Application() {
|
class BasedBankApp : Application() {
|
||||||
|
|
||||||
// Held in memory after successful login; cleared on logout
|
// Held in memory after successful login; cleared on logout
|
||||||
var accounts: List<MibAccount> = emptyList()
|
var accounts: List<MibAccount> = emptyList()
|
||||||
var fullName: String = ""
|
var fullName: String = ""
|
||||||
|
var mibSession: MibSession? = null
|
||||||
|
var mibProfiles: List<MibProfile> = emptyList()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package sh.sar.basedbank.api.mib
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
class MibFinancingClient {
|
||||||
|
|
||||||
|
private val TAG = "MibFinancingClient"
|
||||||
|
private val BASE_WV_URL = "https://faisamobilex-wv.mib.com.mv"
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun fetchFinancing(session: MibSession): List<MibFinanceDeal> {
|
||||||
|
val cookieHeader = "mbmodel=IOS-1.0; " +
|
||||||
|
"xxid=${session.xxid}; " +
|
||||||
|
"IBSID=${session.xxid}; " +
|
||||||
|
"mbnonce=${session.nonceGenerator}; " +
|
||||||
|
"time-tracker=597"
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_WV_URL/financing?dashurl=1")
|
||||||
|
.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"
|
||||||
|
)
|
||||||
|
.header("X-Requested-With", "mv.com.mib.faisamobilex")
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val html = client.newCall(request).execute().use { response ->
|
||||||
|
Log.d(TAG, "fetchFinancing: HTTP ${response.code}")
|
||||||
|
if (!response.isSuccessful) return emptyList()
|
||||||
|
response.body?.string() ?: return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseFinancingHtml(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseFinancingHtml(html: String): List<MibFinanceDeal> {
|
||||||
|
val cardPattern = Regex("""finance-card-holder[^>]+>""")
|
||||||
|
val attrPattern = Regex("""data-(\w+)\s*=\s*"([^"]*)"""")
|
||||||
|
|
||||||
|
return cardPattern.findAll(html).mapNotNull { cardMatch ->
|
||||||
|
val attrs = attrPattern.findAll(cardMatch.value)
|
||||||
|
.associate { it.groupValues[1] to it.groupValues[2] }
|
||||||
|
|
||||||
|
val dealNo = attrs["dealNo"] ?: return@mapNotNull null
|
||||||
|
|
||||||
|
MibFinanceDeal(
|
||||||
|
dealNo = dealNo,
|
||||||
|
productDesc = attrs["productDesc"] ?: "",
|
||||||
|
dealStatus = attrs["dealStatus"] ?: "",
|
||||||
|
statusDesc = attrs["statusDesc"] ?: "",
|
||||||
|
dealAmount = attrs["dealAmount"]?.toDoubleOrNull() ?: 0.0,
|
||||||
|
paidAmount = attrs["paidAmount"]?.toDoubleOrNull() ?: 0.0,
|
||||||
|
outstandingAmount = attrs["outstandingAmount"]?.toDoubleOrNull() ?: 0.0,
|
||||||
|
dealDate = attrs["dealDate"] ?: "",
|
||||||
|
overdueAmount = attrs["overdueAmount"]?.toDoubleOrNull() ?: 0.0,
|
||||||
|
installmentAmount = attrs["installmentAmount"]?.toDoubleOrNull() ?: 0.0,
|
||||||
|
noOfInstallments = attrs["noOfInstallments"]?.toIntOrNull() ?: 0,
|
||||||
|
lastPaidDate = attrs["lastPaidDate"] ?: "",
|
||||||
|
lastPayAmount = attrs["lastPayAmount"]?.toDoubleOrNull() ?: 0.0,
|
||||||
|
currency = attrs["curCodeDesc"] ?: "MVR"
|
||||||
|
)
|
||||||
|
}.toList().also { Log.d(TAG, "parsed ${it.size} financing deals") }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Estimate remaining months until financing is fully paid. */
|
||||||
|
fun remainingMonths(deal: MibFinanceDeal): Int {
|
||||||
|
if (deal.installmentAmount <= 0.0) return 0
|
||||||
|
return ceil(deal.outstandingAmount / deal.installmentAmount).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,14 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
|||||||
private val TAG = "MibLoginFlow"
|
private val TAG = "MibLoginFlow"
|
||||||
private val BASE_URL = "https://faisanet.mib.com.mv/faisamobilex_smvc/"
|
private val BASE_URL = "https://faisanet.mib.com.mv/faisamobilex_smvc/"
|
||||||
|
|
||||||
|
/** The active session after a successful login, usable for subsequent WebView requests. */
|
||||||
|
var lastSession: MibSession? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
/** Profiles returned by the last successful login. */
|
||||||
|
var lastProfiles: List<MibProfile> = emptyList()
|
||||||
|
private set
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
private val client = OkHttpClient.Builder()
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
@@ -140,6 +148,8 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
|||||||
val profiles = parseProfiles(loginResp)
|
val profiles = parseProfiles(loginResp)
|
||||||
Log.d(TAG, "[login] parsed ${profiles.size} profiles")
|
Log.d(TAG, "[login] parsed ${profiles.size} profiles")
|
||||||
|
|
||||||
|
lastSession = session2
|
||||||
|
lastProfiles = profiles
|
||||||
Log.d(TAG, "[login] step 7: fetch all profiles")
|
Log.d(TAG, "[login] step 7: fetch all profiles")
|
||||||
return fetchAllProfiles(session2, profiles)
|
return fetchAllProfiles(session2, profiles)
|
||||||
}
|
}
|
||||||
@@ -213,6 +223,19 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
|||||||
put("xxid", session.xxid)
|
put("xxid", session.xxid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates [profile] server-side via P47, setting the session role context so that
|
||||||
|
* subsequent WebView requests run under that profile.
|
||||||
|
*/
|
||||||
|
fun switchProfile(session: MibSession, profile: MibProfile) {
|
||||||
|
Log.d(TAG, "switchProfile: profileId=${profile.profileId} cifType=${profile.cifType}")
|
||||||
|
val payload = baseData(session, "P47").apply {
|
||||||
|
put("profileType", profile.profileType)
|
||||||
|
put("profileId", profile.profileId)
|
||||||
|
}
|
||||||
|
doRequest(session, payload, "n")
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchAllProfiles(session: MibSession, profiles: List<MibProfile>): List<MibAccount> {
|
private fun fetchAllProfiles(session: MibSession, profiles: List<MibProfile>): List<MibAccount> {
|
||||||
val allAccounts = mutableListOf<MibAccount>()
|
val allAccounts = mutableListOf<MibAccount>()
|
||||||
for (profile in profiles) {
|
for (profile in profiles) {
|
||||||
|
|||||||
@@ -31,3 +31,20 @@ data class MibAccount(
|
|||||||
val mvrBalance: String,
|
val mvrBalance: String,
|
||||||
val statusDesc: String
|
val statusDesc: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class MibFinanceDeal(
|
||||||
|
val dealNo: String,
|
||||||
|
val productDesc: String,
|
||||||
|
val dealStatus: String,
|
||||||
|
val statusDesc: String,
|
||||||
|
val dealAmount: Double,
|
||||||
|
val paidAmount: Double,
|
||||||
|
val outstandingAmount: Double,
|
||||||
|
val dealDate: String,
|
||||||
|
val overdueAmount: Double,
|
||||||
|
val installmentAmount: Double,
|
||||||
|
val noOfInstallments: Int,
|
||||||
|
val lastPaidDate: String,
|
||||||
|
val lastPayAmount: Double,
|
||||||
|
val currency: String
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
|
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||||
|
import sh.sar.basedbank.databinding.ItemFinanceDealBinding
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
|
||||||
|
RecyclerView.Adapter<FinancingAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private val expandedPositions = mutableSetOf<Int>()
|
||||||
|
private val amountFmt = NumberFormat.getNumberInstance(Locale.US).apply {
|
||||||
|
minimumFractionDigits = 2
|
||||||
|
maximumFractionDigits = 2
|
||||||
|
}
|
||||||
|
private val inputDateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||||
|
private val outputDateFmt = SimpleDateFormat("d MMM yyyy", Locale.US)
|
||||||
|
|
||||||
|
fun updateDeals(newDeals: List<MibFinanceDeal>) {
|
||||||
|
deals = newDeals
|
||||||
|
expandedPositions.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemFinanceDealBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(deals[position], position in expandedPositions)
|
||||||
|
holder.binding.root.setOnClickListener {
|
||||||
|
val pos = holder.bindingAdapterPosition
|
||||||
|
if (pos in expandedPositions) expandedPositions.remove(pos) else expandedPositions.add(pos)
|
||||||
|
notifyItemChanged(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = deals.size
|
||||||
|
|
||||||
|
inner class ViewHolder(val binding: ItemFinanceDealBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(deal: MibFinanceDeal, expanded: Boolean) {
|
||||||
|
val ctx = binding.root.context
|
||||||
|
val currency = deal.currency
|
||||||
|
|
||||||
|
binding.tvProductName.text = deal.productDesc
|
||||||
|
binding.tvDealNo.text = ctx.getString(R.string.financing_deal_no_fmt, deal.dealNo)
|
||||||
|
binding.tvStatus.text = deal.statusDesc
|
||||||
|
binding.tvTotal.text = "$currency ${amountFmt.format(deal.dealAmount)}"
|
||||||
|
binding.tvPaid.text = "$currency ${amountFmt.format(deal.paidAmount)}"
|
||||||
|
binding.tvUnpaid.text = "$currency ${amountFmt.format(deal.outstandingAmount)}"
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
val progress = if (deal.dealAmount > 0)
|
||||||
|
((deal.paidAmount / deal.dealAmount) * 100).toInt().coerceIn(0, 100)
|
||||||
|
else 0
|
||||||
|
binding.progressBar.progress = progress
|
||||||
|
|
||||||
|
// Completion estimate
|
||||||
|
binding.tvCompletion.text = completionText(deal, ctx)
|
||||||
|
|
||||||
|
// Expanded details
|
||||||
|
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
|
||||||
|
binding.dividerDetails.visibility = detailsVisible
|
||||||
|
binding.detailsGroup.visibility = detailsVisible
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
binding.tvDealDate.text = formatDate(deal.dealDate)
|
||||||
|
binding.tvInstallment.text = "$currency ${amountFmt.format(deal.installmentAmount)}"
|
||||||
|
binding.tvNumInstallments.text = deal.noOfInstallments.toString()
|
||||||
|
binding.tvLastPaidDate.text = formatDate(deal.lastPaidDate)
|
||||||
|
binding.tvLastPayAmount.text = "$currency ${amountFmt.format(deal.lastPayAmount)}"
|
||||||
|
|
||||||
|
if (deal.overdueAmount > 0) {
|
||||||
|
binding.rowOverdue.visibility = View.VISIBLE
|
||||||
|
binding.tvOverdue.text = "$currency ${amountFmt.format(deal.overdueAmount)}"
|
||||||
|
} else {
|
||||||
|
binding.rowOverdue.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun completionText(deal: MibFinanceDeal, ctx: android.content.Context): String {
|
||||||
|
if (deal.outstandingAmount <= 0.0) return ctx.getString(R.string.financing_completion_done)
|
||||||
|
val remaining = MibFinancingClient.remainingMonths(deal)
|
||||||
|
if (remaining <= 0) return ctx.getString(R.string.financing_completion_done)
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
cal.add(Calendar.MONTH, remaining)
|
||||||
|
val month = SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(cal.time)
|
||||||
|
return ctx.getString(R.string.financing_completion_fmt, month)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDate(raw: String): String {
|
||||||
|
return try {
|
||||||
|
outputDateFmt.format(inputDateFmt.parse(raw)!!)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
raw.take(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.databinding.FragmentFinancingBinding
|
||||||
|
|
||||||
|
class FinancingFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentFinancingBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
private lateinit var adapter: FinancingAdapter
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
_binding = FragmentFinancingBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
adapter = FinancingAdapter(emptyList())
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
viewModel.financing.observe(viewLifecycleOwner) { deals ->
|
||||||
|
adapter.updateDeals(deals)
|
||||||
|
binding.recyclerView.visibility = if (deals.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
binding.emptyView.visibility = if (deals.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
binding.loadingView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
requireActivity().title = getString(R.string.nav_finances)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,13 @@ import sh.sar.basedbank.R
|
|||||||
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
|
||||||
import sh.sar.basedbank.ui.login.LoginActivity
|
import sh.sar.basedbank.ui.login.LoginActivity
|
||||||
|
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||||
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
|
import sh.sar.basedbank.api.mib.MibSession
|
||||||
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
import sh.sar.basedbank.util.AccountCache
|
import sh.sar.basedbank.util.AccountCache
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
|
import sh.sar.basedbank.util.FinancingCache
|
||||||
|
|
||||||
class HomeActivity : AppCompatActivity() {
|
class HomeActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -44,6 +49,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
R.id.nav_dashboard -> show(DashboardFragment())
|
R.id.nav_dashboard -> show(DashboardFragment())
|
||||||
R.id.nav_add_account -> startActivity(Intent(this, LoginActivity::class.java))
|
R.id.nav_add_account -> startActivity(Intent(this, LoginActivity::class.java))
|
||||||
R.id.nav_accounts -> show(AccountsFragment())
|
R.id.nav_accounts -> show(AccountsFragment())
|
||||||
|
R.id.nav_finances -> show(FinancingFragment())
|
||||||
R.id.nav_settings -> show(SettingsFragment())
|
R.id.nav_settings -> show(SettingsFragment())
|
||||||
else -> Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
else -> Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -53,13 +59,18 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
// Load data
|
// Load data
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
if (app.accounts.isNotEmpty()) {
|
if (app.accounts.isNotEmpty()) {
|
||||||
// Came from fresh manual login
|
// Came from fresh manual login — accounts ready, financing fetched in background
|
||||||
viewModel.accounts.value = app.accounts
|
viewModel.accounts.value = app.accounts
|
||||||
AccountCache.save(this, app.accounts)
|
AccountCache.save(this, app.accounts)
|
||||||
|
val cached = FinancingCache.load(this)
|
||||||
|
if (cached.isNotEmpty()) viewModel.financing.value = cached
|
||||||
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
} else {
|
} else {
|
||||||
// Came from lock screen — show cache immediately, refresh in background
|
// Came from lock screen — show caches immediately, refresh everything in background
|
||||||
val cached = AccountCache.load(this)
|
val cached = AccountCache.load(this)
|
||||||
if (cached.isNotEmpty()) viewModel.accounts.value = cached
|
if (cached.isNotEmpty()) viewModel.accounts.value = cached
|
||||||
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
|
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||||
val creds = CredentialStore(this).loadMibCredentials()
|
val creds = CredentialStore(this).loadMibCredentials()
|
||||||
if (creds != null) autoRefresh(creds)
|
if (creds != null) autoRefresh(creds)
|
||||||
}
|
}
|
||||||
@@ -86,7 +97,10 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
val accounts = withContext(Dispatchers.IO) {
|
val accounts = withContext(Dispatchers.IO) {
|
||||||
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||||
}
|
}
|
||||||
(application as BasedBankApp).accounts = accounts
|
val app = application as BasedBankApp
|
||||||
|
app.accounts = accounts
|
||||||
|
app.mibSession = flow.lastSession
|
||||||
|
app.mibProfiles = flow.lastProfiles
|
||||||
AccountCache.save(this@HomeActivity, accounts)
|
AccountCache.save(this@HomeActivity, accounts)
|
||||||
viewModel.accounts.postValue(accounts)
|
viewModel.accounts.postValue(accounts)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@@ -94,6 +108,36 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
} finally {
|
} finally {
|
||||||
binding.refreshIndicator.visibility = View.GONE
|
binding.refreshIndicator.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
val app = application as BasedBankApp
|
||||||
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshFinancing(session: MibSession?, profiles: List<MibProfile>) {
|
||||||
|
if (session == null || profiles.isEmpty()) return
|
||||||
|
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||||
|
val flow = MibLoginFlow(prefs)
|
||||||
|
val client = MibFinancingClient()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val deals = withContext(Dispatchers.IO) {
|
||||||
|
val allDeals = mutableListOf<MibFinanceDeal>()
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
for (profile in profiles) {
|
||||||
|
try {
|
||||||
|
flow.switchProfile(session, profile)
|
||||||
|
for (deal in client.fetchFinancing(session)) {
|
||||||
|
if (seen.add(deal.dealNo)) allDeals.add(deal)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* profile has no financing access */ }
|
||||||
|
}
|
||||||
|
allDeals
|
||||||
|
}
|
||||||
|
if (deals.isNotEmpty()) {
|
||||||
|
FinancingCache.save(this@HomeActivity, deals)
|
||||||
|
viewModel.financing.postValue(deals)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* keep cached data */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ 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.mib.MibAccount
|
import sh.sar.basedbank.api.mib.MibAccount
|
||||||
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val accounts = MutableLiveData<List<MibAccount>>(emptyList())
|
val accounts = MutableLiveData<List<MibAccount>>(emptyList())
|
||||||
|
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,10 @@ class CredentialsFragment : Fragment() {
|
|||||||
Log.d(TAG, "Login succeeded, got ${accounts.size} accounts")
|
Log.d(TAG, "Login succeeded, got ${accounts.size} accounts")
|
||||||
CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed)
|
CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed)
|
||||||
AccountCache.save(requireContext(), accounts)
|
AccountCache.save(requireContext(), accounts)
|
||||||
(requireActivity().application as BasedBankApp).accounts = accounts
|
val app = requireActivity().application as BasedBankApp
|
||||||
|
app.accounts = accounts
|
||||||
|
app.mibSession = flow.lastSession
|
||||||
|
app.mibProfiles = flow.lastProfiles
|
||||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
|
|
||||||
|
object FinancingCache {
|
||||||
|
|
||||||
|
private const val PREFS = "financing_cache"
|
||||||
|
private const val KEY_MIB = "mib_financing"
|
||||||
|
|
||||||
|
fun save(context: Context, deals: List<MibFinanceDeal>) {
|
||||||
|
val arr = JSONArray()
|
||||||
|
for (d in deals) {
|
||||||
|
arr.put(JSONObject().apply {
|
||||||
|
put("dealNo", d.dealNo)
|
||||||
|
put("productDesc", d.productDesc)
|
||||||
|
put("dealStatus", d.dealStatus)
|
||||||
|
put("statusDesc", d.statusDesc)
|
||||||
|
put("dealAmount", d.dealAmount)
|
||||||
|
put("paidAmount", d.paidAmount)
|
||||||
|
put("outstandingAmount", d.outstandingAmount)
|
||||||
|
put("dealDate", d.dealDate)
|
||||||
|
put("overdueAmount", d.overdueAmount)
|
||||||
|
put("installmentAmount", d.installmentAmount)
|
||||||
|
put("noOfInstallments", d.noOfInstallments)
|
||||||
|
put("lastPaidDate", d.lastPaidDate)
|
||||||
|
put("lastPayAmount", d.lastPayAmount)
|
||||||
|
put("currency", d.currency)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit().putString(KEY_MIB, arr.toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(context: Context): List<MibFinanceDeal> {
|
||||||
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_MIB, null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
MibFinanceDeal(
|
||||||
|
dealNo = o.optString("dealNo"),
|
||||||
|
productDesc = o.optString("productDesc"),
|
||||||
|
dealStatus = o.optString("dealStatus"),
|
||||||
|
statusDesc = o.optString("statusDesc"),
|
||||||
|
dealAmount = o.optDouble("dealAmount", 0.0),
|
||||||
|
paidAmount = o.optDouble("paidAmount", 0.0),
|
||||||
|
outstandingAmount = o.optDouble("outstandingAmount", 0.0),
|
||||||
|
dealDate = o.optString("dealDate"),
|
||||||
|
overdueAmount = o.optDouble("overdueAmount", 0.0),
|
||||||
|
installmentAmount = o.optDouble("installmentAmount", 0.0),
|
||||||
|
noOfInstallments = o.optInt("noOfInstallments", 0),
|
||||||
|
lastPaidDate = o.optString("lastPaidDate"),
|
||||||
|
lastPayAmount = o.optDouble("lastPayAmount", 0.0),
|
||||||
|
currency = o.optString("currency", "MVR")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:clipToPadding="false" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/loadingView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/emptyView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/financing_empty"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
app:cardElevation="0dp"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
app:strokeColor="?attr/colorOutlineVariant">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<!-- Header row: product name + status chip -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProductName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDealNo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:background="@drawable/chip_background"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSecondaryContainer" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Total amount -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/financing_total"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTotal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:trackCornerRadius="4dp"
|
||||||
|
app:trackThickness="8dp" />
|
||||||
|
|
||||||
|
<!-- Paid / Unpaid row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/financing_paid"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPaid"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/financing_unpaid"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvUnpaid"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
android:textColor="@color/color_unpaid"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Completion estimate -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCompletion"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/dividerDetails"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?attr/colorOutlineVariant"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<!-- Expanded details -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/detailsGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<!-- Deal date -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/financing_deal_date"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDealDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Installment amount -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/financing_installment"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvInstallment"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Number of installments -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/financing_num_installments"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvNumInstallments"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Last paid date -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/financing_last_paid_date"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLastPaidDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Last payment amount -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/financing_last_pay_amount"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLastPayAmount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Overdue amount (only shown when > 0) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/rowOverdue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/financing_overdue"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="@color/color_unpaid" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvOverdue"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="@color/color_unpaid" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
<!-- Seed colors from MIB brand (blue + green) — used as M3 fallback on Android <12 -->
|
<!-- Seed colors from MIB brand (blue + green) — used as M3 fallback on Android <12 -->
|
||||||
<color name="seed_primary">#3F65AD</color>
|
<color name="seed_primary">#3F65AD</color>
|
||||||
<color name="seed_secondary">#9AD141</color>
|
<color name="seed_secondary">#9AD141</color>
|
||||||
|
<color name="color_unpaid">#E85D04</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -92,4 +92,19 @@
|
|||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<string name="accounts">Accounts</string>
|
<string name="accounts">Accounts</string>
|
||||||
<string name="available_balance">Available Balance</string>
|
<string name="available_balance">Available Balance</string>
|
||||||
|
|
||||||
|
<!-- Financing -->
|
||||||
|
<string name="financing_empty">No financing deals found</string>
|
||||||
|
<string name="financing_total">Total</string>
|
||||||
|
<string name="financing_paid">Paid</string>
|
||||||
|
<string name="financing_unpaid">Unpaid</string>
|
||||||
|
<string name="financing_deal_date">Deal Date</string>
|
||||||
|
<string name="financing_installment">Monthly Installment</string>
|
||||||
|
<string name="financing_num_installments">Total Installments</string>
|
||||||
|
<string name="financing_last_paid_date">Last Payment Date</string>
|
||||||
|
<string name="financing_last_pay_amount">Last Payment</string>
|
||||||
|
<string name="financing_overdue">Overdue</string>
|
||||||
|
<string name="financing_completion_done">Fully paid</string>
|
||||||
|
<string name="financing_deal_no_fmt">Deal #%s</string>
|
||||||
|
<string name="financing_completion_fmt">Completes %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# MIB Financing API
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Financing data is fetched from the MIB **WebView** host (`faisamobilex-wv.mib.com.mv`), which is separate from the API host (`faisanet.mib.com.mv`). The response is an HTML page; financing deal data is embedded in `data-*` attributes on card elements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://faisamobilex-wv.mib.com.mv/financing?dashurl=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
Session cookies from the login flow must be sent with the request:
|
||||||
|
|
||||||
|
| Cookie | Value |
|
||||||
|
|----------------|---------------------------------------------|
|
||||||
|
| `mbmodel` | `IOS-1.0` (literal string) |
|
||||||
|
| `xxid` | Session ID from login (`MibSession.xxid`) |
|
||||||
|
| `IBSID` | Same as `xxid` |
|
||||||
|
| `mbnonce` | `nonceGenerator` string from login response |
|
||||||
|
| `time-tracker` | `597` (literal string) |
|
||||||
|
|
||||||
|
### Request Headers
|
||||||
|
|
||||||
|
| Header | Value |
|
||||||
|
|--------------------|------------------------------------|
|
||||||
|
| `User-Agent` | Standard Android WebView UA string |
|
||||||
|
| `X-Requested-With` | `mv.com.mib.faisamobilex` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
**Content-Type:** `text/html; charset=UTF-8`
|
||||||
|
|
||||||
|
The response is a full HTML page. Each financing deal is represented as a `<div>` with the class `finance-card-holder` and all deal fields embedded as `data-*` attributes:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="card border finance-card-holder"
|
||||||
|
data-productDesc = "Product Name"
|
||||||
|
data-dealStatus = "P"
|
||||||
|
data-statusDesc = "Approved"
|
||||||
|
data-dealAmount = "10000.00"
|
||||||
|
data-dealNo = "12345"
|
||||||
|
data-paidAmount = "2500.00"
|
||||||
|
data-outstandingAmount = "7500.00"
|
||||||
|
data-dealDate = "2024-01-15 00:00:00"
|
||||||
|
data-overdueAmount = "0"
|
||||||
|
data-installmentAmount = "500.00"
|
||||||
|
data-noOfInstallments = "24"
|
||||||
|
data-lastPaidDate = "2026-05-01 00:00:00"
|
||||||
|
data-lastPayAmount = "500.00"
|
||||||
|
data-financeCurrency = "462"
|
||||||
|
data-curCodeDesc = "MVR">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-----------------------|---------|------------------------------------------------------|
|
||||||
|
| `productDesc` | String | Product name (e.g. "Ujalaa CG Finance") |
|
||||||
|
| `dealStatus` | String | Status code: `P` = Active/Pending |
|
||||||
|
| `statusDesc` | String | Human-readable status (e.g. "Approved") |
|
||||||
|
| `dealAmount` | Decimal | Total financing amount |
|
||||||
|
| `dealNo` | Integer | Unique deal/contract number |
|
||||||
|
| `paidAmount` | Decimal | Amount paid to date |
|
||||||
|
| `outstandingAmount` | Decimal | Remaining unpaid balance |
|
||||||
|
| `dealDate` | String | Contract start date (`yyyy-MM-dd HH:mm:ss`) |
|
||||||
|
| `overdueAmount` | Decimal | Amount currently overdue (0 if none) |
|
||||||
|
| `installmentAmount` | Decimal | Monthly installment amount |
|
||||||
|
| `noOfInstallments` | Integer | Total number of installments |
|
||||||
|
| `lastPaidDate` | String | Date of most recent payment (`yyyy-MM-dd HH:mm:ss`) |
|
||||||
|
| `lastPayAmount` | Decimal | Amount of most recent payment |
|
||||||
|
| `financeCurrency` | Integer | Currency code (462 = MVR) |
|
||||||
|
| `curCodeDesc` | String | Currency abbreviation (e.g. "MVR") |
|
||||||
|
|
||||||
|
### Parsing Strategy
|
||||||
|
|
||||||
|
Use a regex to find all elements with class `finance-card-holder`, then extract all `data-*` attribute key/value pairs from each match:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val cardPattern = Regex("""finance-card-holder[^>]+>""")
|
||||||
|
val attrPattern = Regex("""data-(\w+)\s*=\s*"([^"]*)"""")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Completion Date Estimation
|
||||||
|
|
||||||
|
Remaining installments can be estimated from outstanding and installment amounts:
|
||||||
|
|
||||||
|
```
|
||||||
|
remainingInstallments = ceil(outstandingAmount / installmentAmount)
|
||||||
|
completionDate = today + remainingInstallments months
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The WebView endpoint uses a different subdomain (`faisamobilex-wv`) from the encrypted API (`faisanet`).
|
||||||
|
- No encryption is used; the session is maintained purely via cookies.
|
||||||
|
- The HTML is served gzip/brotli compressed; OkHttp handles decompression automatically.
|
||||||
|
- The `time-tracker` cookie value appears to be static at `597` — its purpose is unclear, but omitting it may affect behavior.
|
||||||
|
- Known product names include consumer goods finance and cash financing variants.
|
||||||
Reference in New Issue
Block a user