forked from shihaam/thijooree
implement viewing contacts
This commit is contained in:
@@ -0,0 +1,155 @@
|
|||||||
|
package sh.sar.basedbank.api.mib
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class MibContactsClient {
|
||||||
|
|
||||||
|
private val TAG = "MibContactsClient"
|
||||||
|
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()
|
||||||
|
|
||||||
|
private fun cookieHeader(session: MibSession) =
|
||||||
|
"mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " +
|
||||||
|
"mbnonce=${session.nonceGenerator}; time-tracker=597"
|
||||||
|
|
||||||
|
private fun Request.Builder.withSessionHeaders(session: MibSession): Request.Builder = this
|
||||||
|
.header("Cookie", cookieHeader(session))
|
||||||
|
.header(
|
||||||
|
"User-Agent",
|
||||||
|
"Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36"
|
||||||
|
)
|
||||||
|
.header("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.header("Accept", "*/*")
|
||||||
|
.header("Origin", BASE_WV_URL)
|
||||||
|
.header("Referer", "$BASE_WV_URL/beneficiary?dashurl=1")
|
||||||
|
|
||||||
|
fun fetchCategories(session: MibSession): List<MibBeneficiaryCategory> {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_WV_URL/ajaxBeneficiary/getCategories")
|
||||||
|
.post(FormBody.Builder().build())
|
||||||
|
.withSessionHeaders(session)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return client.newCall(request).execute().use { response ->
|
||||||
|
Log.d(TAG, "getCategories: HTTP ${response.code}")
|
||||||
|
if (!response.isSuccessful) return emptyList()
|
||||||
|
parseCategories(response.body?.string() ?: return emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCategories(json: String): List<MibBeneficiaryCategory> {
|
||||||
|
return try {
|
||||||
|
val arr = JSONObject(json).getJSONArray("data")
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
MibBeneficiaryCategory(
|
||||||
|
id = o.getString("id"),
|
||||||
|
categoryName = o.getString("categoryName"),
|
||||||
|
numBenef = o.optString("numBenef", "0").toIntOrNull() ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "parseCategories error: $e")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchContacts(session: MibSession): List<MibBeneficiary> {
|
||||||
|
val all = mutableListOf<MibBeneficiary>()
|
||||||
|
var page = 1
|
||||||
|
val pageSize = 100
|
||||||
|
while (true) {
|
||||||
|
val start = (page - 1) * pageSize + 1
|
||||||
|
val end = page * pageSize
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("page", page.toString())
|
||||||
|
.add("search", "")
|
||||||
|
.add("searchCategoryId", "0")
|
||||||
|
.add("benefType", "A")
|
||||||
|
.add("sortBenef", "name")
|
||||||
|
.add("sortDir", "asc")
|
||||||
|
.add("start", start.toString())
|
||||||
|
.add("end", end.toString())
|
||||||
|
.add("includeCount", "1")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_WV_URL/ajaxBeneficiary/main")
|
||||||
|
.post(body)
|
||||||
|
.withSessionHeaders(session)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val (contacts, totalCount) = client.newCall(request).execute().use { response ->
|
||||||
|
Log.d(TAG, "fetchContacts page $page: HTTP ${response.code}")
|
||||||
|
if (!response.isSuccessful) return all
|
||||||
|
parseContacts(response.body?.string() ?: return all)
|
||||||
|
}
|
||||||
|
all.addAll(contacts)
|
||||||
|
if (all.size >= totalCount || contacts.isEmpty()) break
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
Log.d(TAG, "fetchContacts: loaded ${all.size} contacts")
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseContacts(json: String): Pair<List<MibBeneficiary>, Int> {
|
||||||
|
return try {
|
||||||
|
val obj = JSONObject(json)
|
||||||
|
val totalCount = obj.optString("total_count", "0").toIntOrNull() ?: 0
|
||||||
|
val arr = obj.getJSONArray("data")
|
||||||
|
val contacts = (0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
val hash = o.optString("customerImgHash")
|
||||||
|
MibBeneficiary(
|
||||||
|
benefNo = o.optString("benefNo"),
|
||||||
|
benefName = o.optString("benefName"),
|
||||||
|
benefNickName = o.optString("benefNickName")
|
||||||
|
.ifBlank { o.optString("benefName") },
|
||||||
|
benefAccount = o.optString("benefAccount"),
|
||||||
|
benefType = o.optString("benefType"),
|
||||||
|
bankColor = o.optString("bankColor", "#888888"),
|
||||||
|
benefBankName = o.optString("benefBankName"),
|
||||||
|
bankCode = o.optString("bankCode"),
|
||||||
|
benefStatus = o.optString("benefStatus"),
|
||||||
|
transferCyDesc = o.optString("transferCyDesc", "MVR"),
|
||||||
|
customerImgHash = hash.takeIf { it.isNotBlank() && it != "null" },
|
||||||
|
benefCategoryId = o.optString("benefCategoryID", "0")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Pair(contacts, totalCount)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "parseContacts error: $e")
|
||||||
|
Pair(emptyList(), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchProfileImageBase64(session: MibSession, imageHash: String): String? {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("imageHash", imageHash)
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_WV_URL/ajaxBeneficiary/getProfileImage")
|
||||||
|
.post(body)
|
||||||
|
.withSessionHeaders(session)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return client.newCall(request).execute().use { response ->
|
||||||
|
if (!response.isSuccessful) return null
|
||||||
|
try {
|
||||||
|
val json = response.body?.string() ?: return null
|
||||||
|
JSONObject(json).optString("profileImage").takeIf { it.isNotBlank() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,27 @@ data class MibAccount(
|
|||||||
val statusDesc: String
|
val statusDesc: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class MibBeneficiaryCategory(
|
||||||
|
val id: String,
|
||||||
|
val categoryName: String,
|
||||||
|
val numBenef: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MibBeneficiary(
|
||||||
|
val benefNo: String,
|
||||||
|
val benefName: String,
|
||||||
|
val benefNickName: String,
|
||||||
|
val benefAccount: String,
|
||||||
|
val benefType: String, // L=Local, I=Internal(MIB), S=Swift
|
||||||
|
val bankColor: String,
|
||||||
|
val benefBankName: String,
|
||||||
|
val bankCode: String,
|
||||||
|
val benefStatus: String,
|
||||||
|
val transferCyDesc: String,
|
||||||
|
val customerImgHash: String?,
|
||||||
|
val benefCategoryId: String // "0" = uncategorized
|
||||||
|
)
|
||||||
|
|
||||||
data class MibFinanceDeal(
|
data class MibFinanceDeal(
|
||||||
val dealNo: String,
|
val dealNo: String,
|
||||||
val productDesc: String,
|
val productDesc: String,
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.databinding.ItemContactBinding
|
||||||
|
|
||||||
|
class ContactsAdapter(
|
||||||
|
private val onImageNeeded: (hash: String) -> Unit
|
||||||
|
) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private var allContacts: List<MibBeneficiary> = emptyList()
|
||||||
|
private var displayed: List<MibBeneficiary> = emptyList()
|
||||||
|
private val imageCache = mutableMapOf<String, Bitmap>()
|
||||||
|
|
||||||
|
private var activeCategoryId: String? = null
|
||||||
|
private var searchQuery: String = ""
|
||||||
|
|
||||||
|
fun updateContacts(contacts: List<MibBeneficiary>) {
|
||||||
|
allContacts = contacts
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateImage(hash: String, bitmap: Bitmap) {
|
||||||
|
imageCache[hash] = bitmap
|
||||||
|
displayed.forEachIndexed { index, contact ->
|
||||||
|
if (contact.customerImgHash == hash) notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFilter(categoryId: String?, query: String) {
|
||||||
|
activeCategoryId = categoryId
|
||||||
|
searchQuery = query
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyFilter() {
|
||||||
|
displayed = allContacts.filter { contact ->
|
||||||
|
val matchesCategory = activeCategoryId == null || contact.benefCategoryId == activeCategoryId
|
||||||
|
val matchesSearch = searchQuery.isBlank() ||
|
||||||
|
contact.benefNickName.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
contact.benefName.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
contact.benefAccount.contains(searchQuery)
|
||||||
|
matchesCategory && matchesSearch
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding = ItemContactBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val contact = displayed[position]
|
||||||
|
val cachedImage = contact.customerImgHash?.let { hash ->
|
||||||
|
imageCache[hash] ?: run { onImageNeeded(hash); null }
|
||||||
|
}
|
||||||
|
holder.bind(contact, cachedImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = displayed.size
|
||||||
|
|
||||||
|
inner class ViewHolder(private val binding: ItemContactBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(contact: MibBeneficiary, photo: Bitmap?) {
|
||||||
|
binding.tvContactName.text = contact.benefNickName
|
||||||
|
binding.tvContactBank.text = contact.benefBankName
|
||||||
|
binding.tvContactAccount.text = "${contact.benefAccount} · ${contact.transferCyDesc}"
|
||||||
|
|
||||||
|
if (photo != null) {
|
||||||
|
binding.ivContactPhoto.setImageBitmap(photo)
|
||||||
|
} else {
|
||||||
|
binding.ivContactPhoto.setImageBitmap(
|
||||||
|
makeInitialsBitmap(contact.benefNickName, contact.bankColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeInitialsBitmap(name: String, colorHex: String): Bitmap {
|
||||||
|
val sizePx = binding.ivContactPhoto.context.resources
|
||||||
|
.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||||
|
.coerceAtLeast(96)
|
||||||
|
val bgColor = try { Color.parseColor(colorHex) } catch (e: Exception) { Color.GRAY }
|
||||||
|
|
||||||
|
val bm = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bm)
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
|
||||||
|
paint.color = bgColor
|
||||||
|
canvas.drawCircle(sizePx / 2f, sizePx / 2f, sizePx / 2f, paint)
|
||||||
|
|
||||||
|
paint.color = Color.WHITE
|
||||||
|
paint.textSize = sizePx * 0.42f
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
val letter = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
|
||||||
|
val metrics = paint.fontMetrics
|
||||||
|
val textY = sizePx / 2f - (metrics.ascent + metrics.descent) / 2f
|
||||||
|
canvas.drawText(letter, sizePx / 2f, textY, paint)
|
||||||
|
|
||||||
|
return bm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package sh.sar.basedbank.ui.home
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import sh.sar.basedbank.BasedBankApp
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||||
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
|
import sh.sar.basedbank.databinding.FragmentContactsBinding
|
||||||
|
|
||||||
|
class ContactsFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentContactsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private val viewModel: HomeViewModel by activityViewModels()
|
||||||
|
private lateinit var adapter: ContactsAdapter
|
||||||
|
|
||||||
|
private val pendingHashes = mutableSetOf<String>()
|
||||||
|
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||||
|
|
||||||
|
private var categories: List<MibBeneficiaryCategory> = emptyList()
|
||||||
|
private var activeCategoryId: String? = null // null = All
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
adapter = ContactsAdapter { hash -> fetchImage(hash) }
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
binding.etSearch.addTextChangedListener { text ->
|
||||||
|
adapter.setFilter(activeCategoryId, text?.toString() ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
|
activeCategoryId = tab.tag as? String
|
||||||
|
adapter.setFilter(activeCategoryId, binding.etSearch.text?.toString() ?: "")
|
||||||
|
}
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||||
|
categories = cats
|
||||||
|
rebuildTabs(cats)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.contacts.observe(viewLifecycleOwner) { contacts ->
|
||||||
|
adapter.updateContacts(contacts)
|
||||||
|
binding.recyclerView.visibility = if (contacts.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
binding.loadingView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rebuildTabs(cats: List<MibBeneficiaryCategory>) {
|
||||||
|
binding.tabLayout.clearOnTabSelectedListeners()
|
||||||
|
binding.tabLayout.removeAllTabs()
|
||||||
|
|
||||||
|
binding.tabLayout.addTab(
|
||||||
|
binding.tabLayout.newTab().setText(R.string.contacts_tab_all).apply { tag = null }
|
||||||
|
)
|
||||||
|
for (cat in cats) {
|
||||||
|
binding.tabLayout.addTab(
|
||||||
|
binding.tabLayout.newTab().setText(cat.categoryName).apply { tag = cat.id }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
|
activeCategoryId = tab.tag as? String
|
||||||
|
adapter.setFilter(activeCategoryId, binding.etSearch.text?.toString() ?: "")
|
||||||
|
}
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchImage(hash: String) {
|
||||||
|
if (!pendingHashes.add(hash)) return
|
||||||
|
val sess = session ?: return
|
||||||
|
val client = MibContactsClient()
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val base64 = client.fetchProfileImageBase64(sess, hash) ?: return@launch
|
||||||
|
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
adapter.updateImage(hash, bitmap)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
pendingHashes.remove(hash) // allow retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
requireActivity().title = getString(R.string.nav_contacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,15 @@ 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.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||||
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibFinancingClient
|
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||||
import sh.sar.basedbank.api.mib.MibProfile
|
import sh.sar.basedbank.api.mib.MibProfile
|
||||||
import sh.sar.basedbank.api.mib.MibSession
|
import sh.sar.basedbank.api.mib.MibSession
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
import sh.sar.basedbank.util.AccountCache
|
import sh.sar.basedbank.util.AccountCache
|
||||||
|
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
|
||||||
|
|
||||||
@@ -49,6 +53,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_contacts -> show(ContactsFragment())
|
||||||
R.id.nav_finances -> show(FinancingFragment())
|
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()
|
||||||
@@ -59,18 +64,27 @@ 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 — accounts ready, financing fetched in background
|
// Came from fresh manual login — accounts ready, rest 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)
|
val cached = FinancingCache.load(this)
|
||||||
if (cached.isNotEmpty()) viewModel.financing.value = cached
|
if (cached.isNotEmpty()) viewModel.financing.value = cached
|
||||||
|
val cachedContacts = ContactsCache.loadContacts(this)
|
||||||
|
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
|
||||||
|
val cachedCats = ContactsCache.loadCategories(this)
|
||||||
|
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
|
refreshContacts(app.mibSession, app.mibProfiles)
|
||||||
} 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 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)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||||
|
val cachedContacts = ContactsCache.loadContacts(this)
|
||||||
|
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
|
||||||
|
val cachedCats = ContactsCache.loadCategories(this)
|
||||||
|
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
|
||||||
val creds = CredentialStore(this).loadMibCredentials()
|
val creds = CredentialStore(this).loadMibCredentials()
|
||||||
if (creds != null) autoRefresh(creds)
|
if (creds != null) autoRefresh(creds)
|
||||||
}
|
}
|
||||||
@@ -117,6 +131,41 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
|
refreshContacts(app.mibSession, app.mibProfiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshContacts(session: MibSession?, profiles: List<MibProfile>) {
|
||||||
|
if (session == null || profiles.isEmpty()) return
|
||||||
|
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||||
|
val flow = MibLoginFlow(prefs)
|
||||||
|
val contactsClient = MibContactsClient()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val (allContacts, allCategories) = withContext(Dispatchers.IO) {
|
||||||
|
val seenContacts = mutableSetOf<String>()
|
||||||
|
val seenCategories = mutableSetOf<String>()
|
||||||
|
val contacts = mutableListOf<MibBeneficiary>()
|
||||||
|
val categories = mutableListOf<MibBeneficiaryCategory>()
|
||||||
|
for (profile in profiles) {
|
||||||
|
try {
|
||||||
|
flow.switchProfile(session, profile)
|
||||||
|
for (cat in contactsClient.fetchCategories(session)) {
|
||||||
|
if (seenCategories.add(cat.id)) categories.add(cat)
|
||||||
|
}
|
||||||
|
for (contact in contactsClient.fetchContacts(session)) {
|
||||||
|
if (seenContacts.add(contact.benefNo)) contacts.add(contact)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* profile has no contacts access */ }
|
||||||
|
}
|
||||||
|
Pair(contacts, categories)
|
||||||
|
}
|
||||||
|
if (allContacts.isNotEmpty()) {
|
||||||
|
ContactsCache.save(this@HomeActivity, allContacts, allCategories)
|
||||||
|
viewModel.contacts.postValue(allContacts)
|
||||||
|
viewModel.contactCategories.postValue(allCategories)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* keep cached data */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ 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.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
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())
|
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||||
|
val contacts = MutableLiveData<List<MibBeneficiary>>(emptyList())
|
||||||
|
val contactCategories = MutableLiveData<List<MibBeneficiaryCategory>>(emptyList())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package sh.sar.basedbank.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiaryCategory
|
||||||
|
|
||||||
|
object ContactsCache {
|
||||||
|
|
||||||
|
private const val PREFS = "contacts_cache"
|
||||||
|
private const val KEY_CONTACTS = "mib_contacts"
|
||||||
|
private const val KEY_CATEGORIES = "mib_categories"
|
||||||
|
|
||||||
|
fun save(
|
||||||
|
context: Context,
|
||||||
|
contacts: List<MibBeneficiary>,
|
||||||
|
categories: List<MibBeneficiaryCategory>
|
||||||
|
) {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||||
|
|
||||||
|
val contactsArr = JSONArray()
|
||||||
|
for (c in contacts) {
|
||||||
|
contactsArr.put(JSONObject().apply {
|
||||||
|
put("benefNo", c.benefNo)
|
||||||
|
put("benefName", c.benefName)
|
||||||
|
put("benefNickName", c.benefNickName)
|
||||||
|
put("benefAccount", c.benefAccount)
|
||||||
|
put("benefType", c.benefType)
|
||||||
|
put("bankColor", c.bankColor)
|
||||||
|
put("benefBankName", c.benefBankName)
|
||||||
|
put("bankCode", c.bankCode)
|
||||||
|
put("benefStatus", c.benefStatus)
|
||||||
|
put("transferCyDesc", c.transferCyDesc)
|
||||||
|
put("customerImgHash", c.customerImgHash ?: "")
|
||||||
|
put("benefCategoryId", c.benefCategoryId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
prefs.putString(KEY_CONTACTS, contactsArr.toString())
|
||||||
|
|
||||||
|
val catArr = JSONArray()
|
||||||
|
for (cat in categories) {
|
||||||
|
catArr.put(JSONObject().apply {
|
||||||
|
put("id", cat.id)
|
||||||
|
put("categoryName", cat.categoryName)
|
||||||
|
put("numBenef", cat.numBenef)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
prefs.putString(KEY_CATEGORIES, catArr.toString())
|
||||||
|
prefs.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadContacts(context: Context): List<MibBeneficiary> {
|
||||||
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_CONTACTS, null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
MibBeneficiary(
|
||||||
|
benefNo = o.optString("benefNo"),
|
||||||
|
benefName = o.optString("benefName"),
|
||||||
|
benefNickName = o.optString("benefNickName"),
|
||||||
|
benefAccount = o.optString("benefAccount"),
|
||||||
|
benefType = o.optString("benefType"),
|
||||||
|
bankColor = o.optString("bankColor", "#888888"),
|
||||||
|
benefBankName = o.optString("benefBankName"),
|
||||||
|
bankCode = o.optString("bankCode"),
|
||||||
|
benefStatus = o.optString("benefStatus"),
|
||||||
|
transferCyDesc = o.optString("transferCyDesc", "MVR"),
|
||||||
|
customerImgHash = o.optString("customerImgHash").takeIf { it.isNotBlank() },
|
||||||
|
benefCategoryId = o.optString("benefCategoryId", "0")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadCategories(context: Context): List<MibBeneficiaryCategory> {
|
||||||
|
val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_CATEGORIES, null) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
MibBeneficiaryCategory(
|
||||||
|
id = o.optString("id"),
|
||||||
|
categoryName = o.optString("categoryName"),
|
||||||
|
numBenef = o.optInt("numBenef", 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
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="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tabLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:tabMode="scrollable"
|
||||||
|
app:tabGravity="start" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||||
|
app:startIconDrawable="@android:drawable/ic_menu_search"
|
||||||
|
app:boxCornerRadiusTopStart="24dp"
|
||||||
|
app:boxCornerRadiusTopEnd="24dp"
|
||||||
|
app:boxCornerRadiusBottomStart="24dp"
|
||||||
|
app:boxCornerRadiusBottomEnd="24dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etSearch"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/contacts_search_hint"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:imeOptions="actionSearch" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingVertical="4dp" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loadingView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/emptyView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/contacts_empty"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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:paddingHorizontal="16dp"
|
||||||
|
android:paddingVertical="10dp">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/ivContactPhoto"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearance.Circle"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvContactName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/ivContactPhoto"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/ivContactPhoto" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvContactBank"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/ivContactPhoto"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tvContactName" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvContactAccount"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="1dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/ivContactPhoto"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tvContactBank"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/ivContactPhoto" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -99,6 +99,11 @@
|
|||||||
<string name="transfer_amount">Amount</string>
|
<string name="transfer_amount">Amount</string>
|
||||||
<string name="transfer_remarks">Remarks</string>
|
<string name="transfer_remarks">Remarks</string>
|
||||||
|
|
||||||
|
<!-- Contacts -->
|
||||||
|
<string name="contacts_empty">No contacts found</string>
|
||||||
|
<string name="contacts_search_hint">Search contacts</string>
|
||||||
|
<string name="contacts_tab_all">All</string>
|
||||||
|
|
||||||
<!-- Financing -->
|
<!-- Financing -->
|
||||||
<string name="financing_empty">No financing deals found</string>
|
<string name="financing_empty">No financing deals found</string>
|
||||||
<string name="financing_total">Total</string>
|
<string name="financing_total">Total</string>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<style name="ShapeAppearance.Circle" parent="ShapeAppearance.Material3.Corner.Full" />
|
||||||
|
|
||||||
<style name="Theme.BasedBank" parent="Theme.Material3.DayNight.NoActionBar">
|
<style name="Theme.BasedBank" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
<item name="colorPrimary">@color/seed_primary</item>
|
<item name="colorPrimary">@color/seed_primary</item>
|
||||||
<item name="colorSecondary">@color/seed_secondary</item>
|
<item name="colorSecondary">@color/seed_secondary</item>
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# MIB Contacts (Beneficiary) API
|
||||||
|
|
||||||
|
The contacts/beneficiary system is served from the MIB WebView subdomain. All endpoints use
|
||||||
|
session-cookie authentication (same cookies as the financing WebView).
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All requests use the same session cookies:
|
||||||
|
|
||||||
|
```
|
||||||
|
Cookie: mbmodel=IOS-1.0; xxid=<session_xxid>; IBSID=<session_xxid>; mbnonce=<nonceGenerator>; time-tracker=597
|
||||||
|
```
|
||||||
|
|
||||||
|
All AJAX POST requests also require:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Requested-With: XMLHttpRequest
|
||||||
|
Origin: https://faisamobilex-wv.mib.com.mv
|
||||||
|
Referer: https://faisamobilex-wv.mib.com.mv/beneficiary?dashurl=1
|
||||||
|
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### 1. Get Categories
|
||||||
|
|
||||||
|
**POST** `https://faisamobilex-wv.mib.com.mv/ajaxBeneficiary/getCategories`
|
||||||
|
|
||||||
|
No request body required (empty POST).
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"responseCode": "1",
|
||||||
|
"reasonText": "Category retrieval success",
|
||||||
|
"reasonCode": "105",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "100001",
|
||||||
|
"categoryName": "Myself",
|
||||||
|
"icon": "f091",
|
||||||
|
"createdDate": "2023-01-01 00:00:00",
|
||||||
|
"modifiedDate": null,
|
||||||
|
"numBenef": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "100002",
|
||||||
|
"categoryName": "Friends",
|
||||||
|
"icon": "f095",
|
||||||
|
"createdDate": "2023-01-01 00:00:00",
|
||||||
|
"modifiedDate": "2023-01-02 00:00:00",
|
||||||
|
"numBenef": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "100003",
|
||||||
|
"categoryName": "Business",
|
||||||
|
"icon": "f097",
|
||||||
|
"createdDate": "2023-01-01 00:00:00",
|
||||||
|
"modifiedDate": "2023-01-02 00:00:00",
|
||||||
|
"numBenef": "8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "100004",
|
||||||
|
"categoryName": "Family",
|
||||||
|
"icon": "f090",
|
||||||
|
"createdDate": "2023-01-01 00:00:00",
|
||||||
|
"modifiedDate": "2023-01-02 00:00:00",
|
||||||
|
"numBenef": "5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `id` — category ID (used as `searchCategoryId` when filtering contacts)
|
||||||
|
- `categoryName` — display name
|
||||||
|
- `icon` — font-awesome icon code (used in web UI, ignore in native app)
|
||||||
|
- `numBenef` — number of beneficiaries in this category (string)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Get Contacts (Paginated)
|
||||||
|
|
||||||
|
**POST** `https://faisamobilex-wv.mib.com.mv/ajaxBeneficiary/main`
|
||||||
|
|
||||||
|
**Request body (form-urlencoded):**
|
||||||
|
|
||||||
|
| Field | Example | Description |
|
||||||
|
|--------------------|----------|-----------------------------------------------------------|
|
||||||
|
| `page` | `1` | Page number (1-based) |
|
||||||
|
| `search` | `` | Search query (empty = all) |
|
||||||
|
| `searchCategoryId` | `0` | Category filter (`0` = all categories) |
|
||||||
|
| `benefType` | `A` | Beneficiary type: `A`=All, `L`=Local, `I`=Internal, `S`=Swift |
|
||||||
|
| `sortBenef` | `name` | Sort field |
|
||||||
|
| `sortDir` | `asc` | Sort direction |
|
||||||
|
| `start` | `1` | Record start index (1-based) |
|
||||||
|
| `end` | `100` | Record end index |
|
||||||
|
| `includeCount` | `1` | Include `total_count` in response |
|
||||||
|
|
||||||
|
**Beneficiary types:**
|
||||||
|
- `L` — Local (other Maldivian banks, e.g. BML)
|
||||||
|
- `I` — Internal (MIB to MIB transfers)
|
||||||
|
- `S` — Swift (international transfers)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"responseCode": "1",
|
||||||
|
"reasonText": "beneficiary retrieval success",
|
||||||
|
"reasonCode": "103",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"benefNo": "100001",
|
||||||
|
"benefName": "Person Name",
|
||||||
|
"benefNickName": "Nickname",
|
||||||
|
"benefAccount": "7700000000001",
|
||||||
|
"benefType": "L",
|
||||||
|
"bankColor": "#AC0000",
|
||||||
|
"benefBankName": "Bank of Maldives PLC",
|
||||||
|
"bankCode": "BML",
|
||||||
|
"benefBankBranch": null,
|
||||||
|
"benefAddress1": null,
|
||||||
|
"benefAddress2": null,
|
||||||
|
"benefAddress3": null,
|
||||||
|
"benefCity": null,
|
||||||
|
"benefRegion": null,
|
||||||
|
"benefCountry": null,
|
||||||
|
"benefStatus": "A",
|
||||||
|
"benefBankId": "3",
|
||||||
|
"benefSwiftCode": "MALBMVMV",
|
||||||
|
"transferCy": "462",
|
||||||
|
"transferCyDesc": "MVR",
|
||||||
|
"bicCode": null,
|
||||||
|
"intermBankCode": "0",
|
||||||
|
"customerImgHash": "abcd1234hash...",
|
||||||
|
"benefImgHash": "abcd1234hash...",
|
||||||
|
"benefCategoryID": "100002",
|
||||||
|
"BENEF_CIF_NO": null,
|
||||||
|
"rnum": "1",
|
||||||
|
"last": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"benefNo": "100002",
|
||||||
|
"benefName": "Another Person",
|
||||||
|
"benefNickName": "MIB Contact",
|
||||||
|
"benefAccount": "90103100000001000",
|
||||||
|
"benefType": "I",
|
||||||
|
"bankColor": "#FE860E",
|
||||||
|
"benefBankName": "MIB",
|
||||||
|
"bankCode": "MIB",
|
||||||
|
"benefBankBranch": null,
|
||||||
|
"benefStatus": "A",
|
||||||
|
"benefBankId": "2",
|
||||||
|
"benefSwiftCode": "SWIFTCODE",
|
||||||
|
"transferCy": "462",
|
||||||
|
"transferCyDesc": "MVR",
|
||||||
|
"customerImgHash": null,
|
||||||
|
"benefImgHash": null,
|
||||||
|
"benefCategoryID": "0",
|
||||||
|
"rnum": "2",
|
||||||
|
"last": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_count": "48",
|
||||||
|
"pos": "1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key fields:
|
||||||
|
- `benefNo` — unique beneficiary ID
|
||||||
|
- `benefNickName` — user-assigned nickname (prefer over `benefName` for display)
|
||||||
|
- `benefType` — `L`, `I`, or `S`
|
||||||
|
- `bankColor` — hex color representing the bank (use for placeholder avatar background)
|
||||||
|
- `customerImgHash` — hash used to fetch profile photo (null if no photo set)
|
||||||
|
- `benefCategoryID` — category ID, `"0"` means uncategorized
|
||||||
|
- `transferCyDesc` — currency (MVR, USD)
|
||||||
|
- `rnum` — row number (1-based position in full sorted list)
|
||||||
|
- `last` — `"1"` if this is the last record on the page
|
||||||
|
|
||||||
|
Pagination: use `start`/`end` to page through results. `total_count` gives the total number of records.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Get Profile Image
|
||||||
|
|
||||||
|
**POST** `https://faisamobilex-wv.mib.com.mv/ajaxBeneficiary/getProfileImage`
|
||||||
|
|
||||||
|
**Request body (form-urlencoded):**
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------------|------------------------------------|
|
||||||
|
| `imageHash` | The `customerImgHash` from contact |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"responseCode": "1",
|
||||||
|
"reasonCode": "1",
|
||||||
|
"reasonText": "image found",
|
||||||
|
"profileImage": "<base64-encoded JPEG>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `profileImage` — raw base64-encoded JPEG (no data URI prefix)
|
||||||
|
- Decode with `Base64.decode(value, Base64.DEFAULT)` then `BitmapFactory.decodeByteArray(...)`
|
||||||
|
- The same hash may be reused across multiple contacts (deduplication recommended)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Get Stats (optional)
|
||||||
|
|
||||||
|
**POST** `https://faisamobilex-wv.mib.com.mv/ajaxBeneficiary/getStats`
|
||||||
|
|
||||||
|
No request body required.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"responseCode": "1",
|
||||||
|
"reasonText": "Beneficiary Stats Retrieved",
|
||||||
|
"reasonCode": "109",
|
||||||
|
"data": [
|
||||||
|
{ "type": "L", "count": "30" },
|
||||||
|
{ "type": "I", "count": "10" },
|
||||||
|
{ "type": "S", "count": "2" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Gives counts per beneficiary type. Useful for showing tab badges.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Profile images are fetched on-demand per contact. Cache decoded bitmaps in memory to avoid re-fetching.
|
||||||
|
- Contacts with `customerImgHash == null` have no profile photo; show initials + bank color as placeholder.
|
||||||
|
- The `benefCategoryID` of `"0"` means uncategorized (not in any category group).
|
||||||
|
- Pagination: use `start=1&end=100` for the first 100 records. Increment accordingly using `total_count`.
|
||||||
Reference in New Issue
Block a user