forked from shihaam/thijooree
add support for fahipay contacts
This commit is contained in:
@@ -11,7 +11,9 @@ import okhttp3.RequestBody
|
|||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
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.Transaction
|
import sh.sar.basedbank.api.mib.Transaction
|
||||||
|
import sh.sar.basedbank.util.AccountInputParser
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -245,6 +247,61 @@ class FahipayLoginFlow {
|
|||||||
} catch (_: Exception) { Pair(emptyList(), 0) }
|
} 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(
|
private fun deviceParts(deviceUuid: String): Array<Pair<String, String>> = arrayOf(
|
||||||
"device[available]" to "true",
|
"device[available]" to "true",
|
||||||
"device[platform]" to "Android",
|
"device[platform]" to "Android",
|
||||||
|
|||||||
@@ -19,3 +19,9 @@ data class FahipayLoginStep(
|
|||||||
val twoFactorRequired: Boolean,
|
val twoFactorRequired: Boolean,
|
||||||
val authId: String? = null // non-null when 2FA not required
|
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.contacts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||||
|
|
||||||
|
(activity as? HomeActivity)?.loadAllContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attachMediator(pages: List<TabDef>) {
|
private fun attachMediator(pages: List<TabDef>) {
|
||||||
|
|||||||
@@ -100,9 +100,14 @@ class ContactsAdapter(
|
|||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun bind(contact: MibBeneficiary, photo: Bitmap?) {
|
fun bind(contact: MibBeneficiary, photo: Bitmap?) {
|
||||||
|
val isFahipay = contact.benefType == "FAHIPAY"
|
||||||
binding.tvContactName.text = contact.benefNickName
|
binding.tvContactName.text = contact.benefNickName
|
||||||
binding.tvContactAccount.text = contact.benefAccount
|
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) {
|
if (photo != null) {
|
||||||
binding.ivContactPhoto.setImageBitmap(photo)
|
binding.ivContactPhoto.setImageBitmap(photo)
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ class ContactsFragment : Fragment() {
|
|||||||
AddContactSheetFragment().show(childFragmentManager, "add_contact")
|
AddContactSheetFragment().show(childFragmentManager, "add_contact")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(activity as? HomeActivity)?.loadAllContacts()
|
||||||
|
|
||||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||||
rebuildPager(cats)
|
rebuildPager(cats)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,19 +117,11 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
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 = 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)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
refreshFinancing(app.mibSession, app.mibProfiles)
|
||||||
refreshContacts(app.mibSession, app.mibProfiles)
|
if (app.bmlSession != null) refreshBmlLimits(app.bmlSession!!)
|
||||||
if (app.bmlSession != null) {
|
|
||||||
refreshBmlContacts(app)
|
|
||||||
refreshBmlLimits(app.bmlSession!!)
|
|
||||||
}
|
|
||||||
} 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 cachedMib = AccountCache.load(this)
|
val cachedMib = AccountCache.load(this)
|
||||||
@@ -139,10 +131,6 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
||||||
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 = 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)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
@@ -432,12 +420,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
binding.refreshIndicator.visibility = View.GONE
|
binding.refreshIndicator.visibility = View.GONE
|
||||||
|
|
||||||
val app = application as BasedBankApp
|
val app = application as BasedBankApp
|
||||||
if (bmlSession != null) {
|
if (bmlSession != null) refreshBmlLimits(bmlSession)
|
||||||
refreshBmlContacts(app)
|
|
||||||
refreshBmlLimits(bmlSession)
|
|
||||||
}
|
|
||||||
refreshFinancing(app.mibSession, app.mibProfiles)
|
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(
|
private fun mergeContacts(
|
||||||
mib: List<MibBeneficiary>,
|
mib: List<MibBeneficiary>,
|
||||||
bml: List<MibBeneficiary>
|
bml: List<MibBeneficiary>
|
||||||
|
|||||||
@@ -131,6 +131,66 @@ object ContactsCache {
|
|||||||
} catch (e: Exception) { emptyList() }
|
} 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> {
|
fun loadCategories(context: Context): List<MibBeneficiaryCategory> {
|
||||||
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_CATEGORIES, null) ?: return emptyList()
|
.getString(KEY_CATEGORIES, null) ?: return emptyList()
|
||||||
|
|||||||
Reference in New Issue
Block a user