add support for fahipay contacts

This commit is contained in:
2026-05-16 22:26:16 +05:00
parent fd531066cd
commit 93405aade2
7 changed files with 177 additions and 19 deletions

View File

@@ -11,7 +11,9 @@ import okhttp3.RequestBody
import okio.Buffer
import org.json.JSONObject
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibBeneficiary
import sh.sar.basedbank.api.mib.Transaction
import sh.sar.basedbank.util.AccountInputParser
import java.security.SecureRandom
import java.util.concurrent.TimeUnit
@@ -245,6 +247,61 @@ class FahipayLoginFlow {
} catch (_: Exception) { Pair(emptyList(), 0) }
}
/**
* Fetches Fahipay saved favourites for the 4 service groups.
* Only includes entries whose number is a valid 7-digit Maldivian phone number (starts with 7 or 9).
* Groups with no valid entries are omitted.
*/
fun fetchContacts(session: FahipaySession): List<FahipayContactGroup> {
val endpoints = listOf(
Triple("FAHIPAY_RAASTAS", "Raastas", "ooredooraastas"),
Triple("FAHIPAY_RELOAD", "Reload", "dhiraagureload"),
Triple("FAHIPAY_OOREDOO_BILL", "Ooredoo Bill", "ooredoobillpay"),
Triple("FAHIPAY_DHIRAAGU_BILL", "Dhiraagu Bill", "dhiraagubillpay")
)
val result = mutableListOf<FahipayContactGroup>()
for ((catId, label, page) in endpoints) {
try {
val resp = client.newCall(
Request.Builder()
.url("$BASE_URL/api/app/favs/?page=$page&lang=en")
.header("authid", session.authId)
.header("User-Agent", UA_OKHTTP)
.build()
).execute()
val json = resp.body?.string() ?: continue
resp.close()
val obj = JSONObject(json)
// Empty group comes back as a JSON array [], not an object — optJSONObject returns null
val groupObj = obj.optJSONObject(page) ?: continue
val contacts = mutableListOf<MibBeneficiary>()
for (key in groupObj.keys()) {
val entry = groupObj.getJSONObject(key)
val number = entry.optString("number")
val name = entry.optString("name").trim().ifBlank { number }
if (AccountInputParser.detect(number) != AccountInputParser.InputType.PHONE) continue
contacts.add(MibBeneficiary(
benefNo = "fp_${page}_$number",
benefName = "",
benefNickName = name,
benefAccount = number,
benefType = "FAHIPAY",
bankColor = "#FF6B00",
benefBankName = label,
bankCode = "",
benefStatus = "",
transferCyDesc = "",
customerImgHash = null,
benefCategoryId = catId,
profileId = ""
))
}
if (contacts.isNotEmpty()) result.add(FahipayContactGroup(catId, label, contacts))
} catch (_: Exception) {}
}
return result
}
private fun deviceParts(deviceUuid: String): Array<Pair<String, String>> = arrayOf(
"device[available]" to "true",
"device[platform]" to "Android",

View File

@@ -19,3 +19,9 @@ data class FahipayLoginStep(
val twoFactorRequired: Boolean,
val authId: String? = null // non-null when 2FA not required
)
data class FahipayContactGroup(
val categoryId: String,
val label: String,
val contacts: List<sh.sar.basedbank.api.mib.MibBeneficiary>
)

View File

@@ -137,6 +137,8 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
viewModel.contacts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
(activity as? HomeActivity)?.loadAllContacts()
}
private fun attachMediator(pages: List<TabDef>) {

View File

@@ -100,9 +100,14 @@ class ContactsAdapter(
RecyclerView.ViewHolder(binding.root) {
fun bind(contact: MibBeneficiary, photo: Bitmap?) {
val isFahipay = contact.benefType == "FAHIPAY"
binding.tvContactName.text = contact.benefNickName
binding.tvContactAccount.text = contact.benefAccount
binding.tvRealName.text = "${contact.benefName} · ${contact.transferCyDesc} · ${contact.benefBankName}"
binding.tvRealName.text = if (isFahipay) "" else "${contact.benefName} · ${contact.transferCyDesc} · ${contact.benefBankName}"
binding.tvRealName.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
binding.btnTransferContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
binding.btnEditContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
binding.btnDeleteContact.visibility = if (isFahipay) android.view.View.GONE else android.view.View.VISIBLE
if (photo != null) {
binding.ivContactPhoto.setImageBitmap(photo)

View File

@@ -115,6 +115,8 @@ class ContactsFragment : Fragment() {
AddContactSheetFragment().show(childFragmentManager, "add_contact")
}
(activity as? HomeActivity)?.loadAllContacts()
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
rebuildPager(cats)
}

View File

@@ -117,19 +117,11 @@ class HomeActivity : AppCompatActivity() {
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedContacts = mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this))
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
val cachedCats = ContactsCache.loadCategories(this)
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
val cachedLimits = ForeignLimitsCache.load(this)
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles)
if (app.bmlSession != null) {
refreshBmlContacts(app)
refreshBmlLimits(app.bmlSession!!)
}
if (app.bmlSession != null) refreshBmlLimits(app.bmlSession!!)
} else {
// Came from lock screen — show caches immediately, refresh everything in background
val cachedMib = AccountCache.load(this)
@@ -139,10 +131,6 @@ class HomeActivity : AppCompatActivity() {
if (merged.isNotEmpty()) viewModel.accounts.value = merged
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val cachedContacts = mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this))
if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts
val cachedCats = ContactsCache.loadCategories(this)
if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats
val cachedLimits = ForeignLimitsCache.load(this)
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
@@ -432,12 +420,8 @@ class HomeActivity : AppCompatActivity() {
binding.refreshIndicator.visibility = View.GONE
val app = application as BasedBankApp
if (bmlSession != null) {
refreshBmlContacts(app)
refreshBmlLimits(bmlSession)
}
if (bmlSession != null) refreshBmlLimits(bmlSession)
refreshFinancing(app.mibSession, app.mibProfiles)
refreshContacts(app.mibSession, app.mibProfiles)
}
}
@@ -473,6 +457,48 @@ class HomeActivity : AppCompatActivity() {
}
}
fun loadAllContacts() {
val app = application as BasedBankApp
// Populate ViewModel from cache immediately if empty
if (viewModel.contacts.value.isNullOrEmpty()) {
val cached = mergeContacts(
mergeContacts(ContactsCache.loadContacts(this), ContactsCache.loadBml(this)),
ContactsCache.loadFahipay(this)
)
if (cached.isNotEmpty()) viewModel.contacts.value = cached
}
if (viewModel.contactCategories.value.isNullOrEmpty()) {
val cats = ContactsCache.loadCategories(this) + ContactsCache.loadFahipayCategories(this)
if (cats.isNotEmpty()) viewModel.contactCategories.value = cats
}
// Refresh all banks in background
refreshContacts(app.mibSession, app.mibProfiles)
refreshBmlContacts(app)
if (app.fahipaySession != null) refreshFahipayContacts(app.fahipaySession!!)
}
private fun refreshFahipayContacts(session: FahipaySession) {
lifecycleScope.launch {
try {
val groups = withContext(Dispatchers.IO) {
val flow = FahipayLoginFlow()
flow.setSessionCookie(session.sessionCookie)
flow.fetchContacts(session)
}
if (groups.isEmpty()) return@launch
val contacts = groups.flatMap { it.contacts }
val categories = groups.map { MibBeneficiaryCategory(it.categoryId, it.label, it.contacts.size) }
ContactsCache.saveFahipay(this@HomeActivity, contacts, categories)
val existing = viewModel.contacts.value ?: emptyList()
val nonFahipay = existing.filter { it.benefType != "FAHIPAY" }
viewModel.contacts.postValue(mergeContacts(nonFahipay, contacts))
val existingCats = viewModel.contactCategories.value ?: emptyList()
val nonFahipayCats = existingCats.filter { !it.id.startsWith("FAHIPAY_") }
viewModel.contactCategories.postValue(nonFahipayCats + categories)
} catch (_: Exception) {}
}
}
private fun mergeContacts(
mib: List<MibBeneficiary>,
bml: List<MibBeneficiary>

View File

@@ -131,6 +131,66 @@ object ContactsCache {
} catch (e: Exception) { emptyList() }
}
fun saveFahipay(context: Context, contacts: List<MibBeneficiary>, categories: List<MibBeneficiaryCategory>) {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
val arr = JSONArray()
for (c in contacts) arr.put(JSONObject().apply {
put("benefNo", c.benefNo)
put("benefNickName", c.benefNickName)
put("benefAccount", c.benefAccount)
put("bankColor", c.bankColor)
put("benefBankName", c.benefBankName)
put("benefCategoryId", c.benefCategoryId)
})
prefs.putString("fahipay_contacts", CacheEncryption.encrypt(arr.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("fahipay_categories", CacheEncryption.encrypt(catArr.toString()))
prefs.apply()
}
fun loadFahipay(context: Context): List<MibBeneficiary> {
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString("fahipay_contacts", null) ?: return emptyList()
return try {
val arr = JSONArray(CacheEncryption.decrypt(raw))
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
MibBeneficiary(
benefNo = o.optString("benefNo"),
benefName = "",
benefNickName = o.optString("benefNickName"),
benefAccount = o.optString("benefAccount"),
benefType = "FAHIPAY",
bankColor = o.optString("bankColor", "#FF6B00"),
benefBankName = o.optString("benefBankName"),
bankCode = "",
benefStatus = "",
transferCyDesc = "",
customerImgHash = null,
benefCategoryId = o.optString("benefCategoryId"),
profileId = ""
)
}
} catch (_: Exception) { emptyList() }
}
fun loadFahipayCategories(context: Context): List<MibBeneficiaryCategory> {
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString("fahipay_categories", null) ?: return emptyList()
return try {
val arr = JSONArray(CacheEncryption.decrypt(raw))
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
MibBeneficiaryCategory(o.optString("id"), o.optString("categoryName"), o.optInt("numBenef"))
}
} catch (_: Exception) { emptyList() }
}
fun loadCategories(context: Context): List<MibBeneficiaryCategory> {
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_CATEGORIES, null) ?: return emptyList()