add support for fetching mib cards
Auto Tag on Version Change / check-version (push) Successful in 7s
Auto Tag on Version Change / check-version (push) Successful in 7s
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package sh.sar.basedbank.api.mib
|
||||
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MibCardsClient {
|
||||
|
||||
private val BASE_WV_URL = "https://faisamobilex-wv.mib.com.mv"
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(20, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private fun cookieHeader(session: MibSession) =
|
||||
"mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " +
|
||||
"mbnonce=${session.nonceGenerator}; time-tracker=597"
|
||||
|
||||
fun fetchCards(session: MibSession, loginTag: String): List<MibCard> {
|
||||
val body = FormBody.Builder()
|
||||
.add("name", "")
|
||||
.add("start", "1")
|
||||
.add("end", "50")
|
||||
.add("includeCount", "1")
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_WV_URL/ajaxDebitCard/fetchCardInfos")
|
||||
.post(body)
|
||||
.header("Cookie", cookieHeader(session))
|
||||
.header("User-Agent", "Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36")
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.header("Accept", "*/*")
|
||||
.header("Origin", BASE_WV_URL)
|
||||
.header("Referer", "$BASE_WV_URL//debitCards?dashurl=1")
|
||||
.build()
|
||||
|
||||
return client.newCall(request).execute().use { response ->
|
||||
val bodyStr = response.body?.string() ?: return emptyList()
|
||||
val json = try { JSONObject(bodyStr) } catch (_: Exception) { return emptyList() }
|
||||
if (!json.optBoolean("success")) return emptyList()
|
||||
val data = json.optJSONArray("data") ?: return emptyList()
|
||||
(0 until data.length()).map { i ->
|
||||
val item = data.getJSONObject(i)
|
||||
MibCard(
|
||||
cardId = item.optString("cardId"),
|
||||
maskedCardNumber = item.optString("maskedCardNumber"),
|
||||
cardStatus = item.optString("cardStatus"),
|
||||
cardType = item.optString("cardType"),
|
||||
cardTypeDesc = item.optString("cardTypeDesc"),
|
||||
customerId = item.optString("customerId"),
|
||||
phoneNumber = item.optString("phoneNumber"),
|
||||
cardHolderName = item.optString("cardHolderName"),
|
||||
loginTag = loginTag
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,18 @@ data class MibIpsAccountInfo(
|
||||
)
|
||||
|
||||
|
||||
data class MibCard(
|
||||
val cardId: String,
|
||||
val maskedCardNumber: String,
|
||||
val cardStatus: String,
|
||||
val cardType: String,
|
||||
val cardTypeDesc: String,
|
||||
val customerId: String,
|
||||
val phoneNumber: String,
|
||||
val cardHolderName: String,
|
||||
val loginTag: String
|
||||
)
|
||||
|
||||
data class MibFinanceDeal(
|
||||
val dealNo: String,
|
||||
val productDesc: String,
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.databinding.FragmentCardSettingsBinding
|
||||
|
||||
class CardSettingsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentCardSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentCardSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = CardSettingsAdapter(emptyList(), requireContext())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||
insets
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards == null) return@observe
|
||||
adapter.update(cards)
|
||||
binding.loadingView.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
if (viewModel.mibCards.value == null) {
|
||||
binding.loadingView.visibility = View.VISIBLE
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = getString(R.string.nav_card_settings)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class CardSettingsAdapter(
|
||||
private var cards: List<MibCard>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(context).inflate(R.layout.item_card_settings_entry, parent, false))
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
|
||||
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
|
||||
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
|
||||
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
|
||||
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
|
||||
private val btnChangePin: View = view.findViewById(R.id.btnChangePin)
|
||||
private val btnFreeze: View = view.findViewById(R.id.btnFreeze)
|
||||
private val btnBlock: View = view.findViewById(R.id.btnBlock)
|
||||
|
||||
fun bind(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(card.maskedCardNumber)
|
||||
tvCardType.text = card.cardTypeDesc
|
||||
PayWithCardFragment.loadCardImage(ivCardImage, PayWithCardFragment.cardImageAsset(card))
|
||||
val wip = View.OnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
btnChangePin.setOnClickListener(wip)
|
||||
btnFreeze.setOnClickListener(wip)
|
||||
btnBlock.setOnClickListener(wip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ import sh.sar.basedbank.ui.login.LoginActivity
|
||||
import sh.sar.basedbank.api.models.BankContact
|
||||
import sh.sar.basedbank.api.models.BankContactCategory
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.api.mib.MibCardsClient
|
||||
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||
import sh.sar.basedbank.api.mib.MibProfile
|
||||
import sh.sar.basedbank.api.mib.MibSession
|
||||
@@ -138,6 +139,8 @@ class HomeActivity : AppCompatActivity() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
else -> null
|
||||
}
|
||||
if (frag != null) show(frag)
|
||||
@@ -307,6 +310,8 @@ fun applyNavLabelVisibility() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
|
||||
}
|
||||
show(dest)
|
||||
@@ -932,6 +937,43 @@ fun applyNavLabelVisibility() {
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerRefreshCards() {
|
||||
val app = application as BasedBankApp
|
||||
for ((loginId, session) in app.mibSessions) {
|
||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||
refreshMibCards(loginId, session, profiles)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshMibCards(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||
if (profiles.isEmpty()) return
|
||||
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||
val client = MibCardsClient()
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val cards = withContext(Dispatchers.IO) {
|
||||
val result = mutableListOf<sh.sar.basedbank.api.mib.MibCard>()
|
||||
val seen = mutableSetOf<String>()
|
||||
for (profile in profiles) {
|
||||
try {
|
||||
flow.switchProfile(session, profile)
|
||||
for (card in client.fetchCards(session, "mib_$loginId")) {
|
||||
if (seen.add(card.cardId)) result += card
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
result
|
||||
}
|
||||
if (cards.isNotEmpty()) {
|
||||
val existing = viewModel.mibCards.value?.toMutableList() ?: mutableListOf()
|
||||
existing.removeAll { it.loginTag == "mib_$loginId" }
|
||||
existing += cards
|
||||
viewModel.mibCards.postValue(existing)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||
if (profiles.isEmpty()) return
|
||||
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||
|
||||
@@ -7,6 +7,7 @@ import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.api.models.BankContact
|
||||
import sh.sar.basedbank.api.models.BankContactCategory
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
@@ -20,5 +21,7 @@ class HomeViewModel : ViewModel() {
|
||||
data class BmlLimitsData(val userName: String, val limits: List<BmlForeignLimit>)
|
||||
val bmlLimits = MutableLiveData<List<BmlLimitsData>>(emptyList())
|
||||
|
||||
val mibCards = MutableLiveData<List<MibCard>?>(null)
|
||||
|
||||
val hideAmounts = MutableLiveData<Boolean>(false)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ object NavCustomization {
|
||||
NavItemDef(R.id.nav_activities, R.drawable.ic_nav_activities, R.string.nav_activities),
|
||||
NavItemDef(R.id.nav_transfer_history, R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history),
|
||||
NavItemDef(R.id.nav_finances, R.drawable.ic_nav_finances, R.string.nav_finances),
|
||||
NavItemDef(R.id.nav_pay_with_card, R.drawable.ic_nav_card, R.string.nav_pay_with_card),
|
||||
NavItemDef(R.id.nav_card_settings, R.drawable.ic_nav_card, R.string.nav_card_settings),
|
||||
NavItemDef(R.id.nav_otp, R.drawable.ic_nav_otp, R.string.nav_otp),
|
||||
NavItemDef(R.id.nav_settings, R.drawable.ic_nav_settings, R.string.nav_settings),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding
|
||||
|
||||
class PayWithCardFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentPayWithCardBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentPayWithCardBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = CardWalletAdapter(emptyList(), requireContext())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||
insets
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { cards ->
|
||||
if (cards == null) return@observe
|
||||
adapter.update(cards)
|
||||
binding.loadingView.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.emptyView.visibility = if (cards.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (cards.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
if (viewModel.mibCards.value == null) {
|
||||
binding.loadingView.visibility = View.VISIBLE
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = getString(R.string.nav_pay_with_card)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class CardWalletAdapter(
|
||||
private var cards: List<MibCard>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
|
||||
|
||||
fun update(newCards: List<MibCard>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(context).inflate(R.layout.item_card_wallet, parent, false))
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
|
||||
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
|
||||
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
|
||||
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
|
||||
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
|
||||
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
|
||||
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
|
||||
|
||||
fun bind(card: MibCard) {
|
||||
tvCardOwner.text = card.cardHolderName
|
||||
tvCardNumber.text = formatMasked(card.maskedCardNumber)
|
||||
tvCardType.text = card.cardTypeDesc
|
||||
loadCardImage(ivCardImage, cardImageAsset(card))
|
||||
btnPayQr.setOnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
btnPayNfc.setOnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun cardImageAsset(card: MibCard): String {
|
||||
val desc = card.cardTypeDesc.lowercase()
|
||||
return when {
|
||||
"corporate" in desc || "business" in desc -> "cards/mib/visa_business.jpg"
|
||||
"platinum" in desc -> "cards/mib/cards-platinum-bg-w-logo-1.jpg"
|
||||
else -> "cards/mib/cards-blue-bg-w-logo-1.jpg"
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCardImage(imageView: ImageView, assetPath: String) {
|
||||
try {
|
||||
val bitmap = imageView.context.assets.open(assetPath).use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
}
|
||||
imageView.setImageBitmap(bitmap)
|
||||
} catch (_: Exception) {
|
||||
imageView.setImageResource(android.R.color.darker_gray)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatMasked(masked: String): String {
|
||||
if (masked.length < 4) return masked
|
||||
return "\u2022\u2022\u2022\u2022 ${masked.takeLast(4)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user