diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt index d0db647..64cda54 100644 --- a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayLoginFlow.kt @@ -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 { + 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() + 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() + 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> = arrayOf( "device[available]" to "true", "device[platform]" to "Android", diff --git a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt index 1471d83..322f646 100644 --- a/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/fahipay/FahipayModels.kt @@ -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 +) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt index 7d3edca..39c8e57 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt @@ -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) { diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt index 8e06a37..e8b45ad 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt @@ -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) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt index bb9b163..3d5b796 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt @@ -115,6 +115,8 @@ class ContactsFragment : Fragment() { AddContactSheetFragment().show(childFragmentManager, "add_contact") } + (activity as? HomeActivity)?.loadAllContacts() + viewModel.contactCategories.observe(viewLifecycleOwner) { cats -> rebuildPager(cats) } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index 038020e..78f5dd8 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -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, bml: List diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt index 8a186ac..aeec856 100644 --- a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt +++ b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt @@ -131,6 +131,66 @@ object ContactsCache { } catch (e: Exception) { emptyList() } } + fun saveFahipay(context: Context, contacts: List, categories: List) { + 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 { + 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 { + 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 { val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(KEY_CATEGORIES, null) ?: return emptyList()