add support for fahipay contacts
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -115,6 +115,8 @@ class ContactsFragment : Fragment() {
|
||||
AddContactSheetFragment().show(childFragmentManager, "add_contact")
|
||||
}
|
||||
|
||||
(activity as? HomeActivity)?.loadAllContacts()
|
||||
|
||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||
rebuildPager(cats)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user