add support to detect dhiraagu and ooredoo packages
Auto Tag on Version Change / check-version (push) Successful in 2s
Auto Tag on Version Change / check-version (push) Successful in 2s
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package sh.sar.basedbank.api.dhiraagu
|
||||
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DhiraaguClient {
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
enum class CustType { RELOAD, BILL_PAY, UNSUPPORTED }
|
||||
|
||||
companion object {
|
||||
private const val UA = "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"
|
||||
}
|
||||
|
||||
data class Result(val type: CustType, val ownerName: String = "")
|
||||
|
||||
fun validateNumber(number: String): Result {
|
||||
// Step 1: fetch Easy Pay page to extract the nonce
|
||||
val pageResp = client.newCall(
|
||||
Request.Builder()
|
||||
.url("https://www.dhiraagu.com.mv/services/easy-pay")
|
||||
.header("User-Agent", UA)
|
||||
.get()
|
||||
.build()
|
||||
).execute()
|
||||
val html = pageResp.body?.string() ?: return Result(CustType.UNSUPPORTED)
|
||||
pageResp.close()
|
||||
|
||||
val nonce = Regex("""var nonce = "([^"]+)"""").find(html)
|
||||
?.groupValues?.getOrNull(1) ?: return Result(CustType.UNSUPPORTED)
|
||||
|
||||
// Step 2: look up number
|
||||
val body = """{"number":"$number"}"""
|
||||
.toRequestBody("application/json".toMediaType())
|
||||
val resp = client.newCall(
|
||||
Request.Builder()
|
||||
.url("https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-3A22-485B-A518-DA6B6DE653A5&sub=dhiraaguIO&act=infoUnlisted")
|
||||
.post(body)
|
||||
.header("User-Agent", UA)
|
||||
.header("nonce", nonce)
|
||||
.header("Origin", "https://dhiraagu.com.mv")
|
||||
.build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: return Result(CustType.UNSUPPORTED)
|
||||
resp.close()
|
||||
|
||||
return try {
|
||||
val obj = JSONObject(json)
|
||||
if (obj.optString("respStatus") != "OK") return Result(CustType.UNSUPPORTED)
|
||||
val prepaid = obj.optJSONArray("serviceDetails")
|
||||
?.optJSONObject(0)
|
||||
?.optString("prepaidIndicator")
|
||||
val ownerName = obj.optJSONObject("accountOwnerInfo")
|
||||
?.optString("name") ?: ""
|
||||
val type = when (prepaid) {
|
||||
"Y" -> CustType.RELOAD
|
||||
"N" -> CustType.BILL_PAY
|
||||
else -> CustType.UNSUPPORTED
|
||||
}
|
||||
Result(type, ownerName)
|
||||
} catch (_: Exception) { Result(CustType.UNSUPPORTED) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sh.sar.basedbank.api.fahipay
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OoredooClient {
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
enum class CustType { PRE, POST, HYBRID, UNSUPPORTED }
|
||||
|
||||
/**
|
||||
* Validates an Ooredoo number and returns which services are supported.
|
||||
* @param number 7-digit Maldivian phone number (without country code)
|
||||
*/
|
||||
fun validateNumber(number: String): CustType {
|
||||
val resp = client.newCall(
|
||||
Request.Builder()
|
||||
.url("https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?action=cust_details&msisdn=960$number")
|
||||
.get()
|
||||
.build()
|
||||
).execute()
|
||||
val json = resp.body?.string() ?: return CustType.UNSUPPORTED
|
||||
resp.close()
|
||||
return try {
|
||||
val custType = JSONObject(json)
|
||||
.optJSONObject("data")
|
||||
?.optString("custType")
|
||||
?.takeIf { it.isNotBlank() && it != "null" }
|
||||
when (custType) {
|
||||
"PRE" -> CustType.PRE
|
||||
"POST" -> CustType.POST
|
||||
"HYBRID" -> CustType.HYBRID
|
||||
else -> CustType.UNSUPPORTED
|
||||
}
|
||||
} catch (_: Exception) { CustType.UNSUPPORTED }
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
||||
import sh.sar.basedbank.api.fahipay.OoredooClient
|
||||
import sh.sar.basedbank.api.bml.BmlTransferResult
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||
@@ -65,6 +67,10 @@ class TransferFragment : Fragment() {
|
||||
private var resolvedRecipientName = ""
|
||||
private var resolvedBankName = ""
|
||||
|
||||
// Selected Fahipay service when source is Fahipay and destination is a phone number
|
||||
// Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL"
|
||||
private var selectedFahipayService: String? = null
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
@@ -220,7 +226,9 @@ class TransferFragment : Fragment() {
|
||||
binding.btnClearToInfo.setOnClickListener {
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
selectedFahipayService = null
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.layoutServiceSelector.visibility = View.INVISIBLE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
@@ -265,6 +273,16 @@ class TransferFragment : Fragment() {
|
||||
return
|
||||
}
|
||||
|
||||
// Fahipay source: only phone numbers are supported
|
||||
if (selectedAccount?.profileType == "FAHIPAY") {
|
||||
if (AccountInputParser.detect(accountNumber) == AccountInputParser.InputType.PHONE) {
|
||||
lookupFahipayTarget(accountNumber)
|
||||
} else {
|
||||
binding.tilTo.error = getString(R.string.transfer_fahipay_phone_only)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val mibSess = session
|
||||
val bmlSess = bmlSession
|
||||
if (mibSess == null && bmlSess == null) {
|
||||
@@ -352,6 +370,99 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun lookupFahipayTarget(number: String) {
|
||||
binding.tilTo.isEnabled = false
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
data class LookupResult(
|
||||
val dhiraagu: DhiraaguClient.Result,
|
||||
val ooredoo: OoredooClient.CustType
|
||||
)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
if (number.startsWith("7")) {
|
||||
// Dhiraagu first, fall back to Ooredoo
|
||||
val d = try { DhiraaguClient().validateNumber(number) }
|
||||
catch (_: Exception) { DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED) }
|
||||
val o = if (d.type == DhiraaguClient.CustType.UNSUPPORTED)
|
||||
try { OoredooClient().validateNumber(number) }
|
||||
catch (_: Exception) { OoredooClient.CustType.UNSUPPORTED }
|
||||
else OoredooClient.CustType.UNSUPPORTED
|
||||
LookupResult(d, o)
|
||||
} else {
|
||||
// Ooredoo first, fall back to Dhiraagu
|
||||
val o = try { OoredooClient().validateNumber(number) }
|
||||
catch (_: Exception) { OoredooClient.CustType.UNSUPPORTED }
|
||||
val d = if (o == OoredooClient.CustType.UNSUPPORTED)
|
||||
try { DhiraaguClient().validateNumber(number) }
|
||||
catch (_: Exception) { DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED) }
|
||||
else DhiraaguClient.Result(DhiraaguClient.CustType.UNSUPPORTED)
|
||||
LookupResult(d, o)
|
||||
}
|
||||
}
|
||||
binding.tilTo.isEnabled = true
|
||||
|
||||
val dhiraaguName = result.dhiraagu.ownerName.takeIf { it.isNotBlank() }
|
||||
|
||||
// Collect all applicable services
|
||||
val services = buildList {
|
||||
if (result.dhiraagu.type == DhiraaguClient.CustType.RELOAD) add("DHIRAAGU_RELOAD")
|
||||
if (result.dhiraagu.type == DhiraaguClient.CustType.BILL_PAY) add("DHIRAAGU_BILL")
|
||||
if (result.ooredoo == OoredooClient.CustType.PRE || result.ooredoo == OoredooClient.CustType.HYBRID) add("RAASTAS")
|
||||
if (result.ooredoo == OoredooClient.CustType.POST || result.ooredoo == OoredooClient.CustType.HYBRID) add("OOREDOO_BILL")
|
||||
}
|
||||
|
||||
if (services.isEmpty()) return@launch
|
||||
|
||||
// Only one option — auto-select, no chip UI needed
|
||||
if (services.size == 1) {
|
||||
selectFahipayService(services[0], number, dhiraaguName)
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Multiple options (Ooredoo HYBRID) — show chips
|
||||
binding.chipDhiraaguReload.visibility = if ("DHIRAAGU_RELOAD" in services) View.VISIBLE else View.GONE
|
||||
binding.chipDhiraaguBill.visibility = if ("DHIRAAGU_BILL" in services) View.VISIBLE else View.GONE
|
||||
binding.chipRaastas.visibility = if ("RAASTAS" in services) View.VISIBLE else View.GONE
|
||||
binding.chipOoredooBill.visibility = if ("OOREDOO_BILL" in services) View.VISIBLE else View.GONE
|
||||
binding.layoutServiceSelector.visibility = View.VISIBLE
|
||||
binding.chipGroupService.clearCheck()
|
||||
|
||||
binding.chipDhiraaguReload.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("DHIRAAGU_RELOAD", number, dhiraaguName); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
binding.chipDhiraaguBill.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("DHIRAAGU_BILL", number, dhiraaguName); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
binding.chipRaastas.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("RAASTAS", number); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
binding.chipOoredooBill.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) { selectFahipayService("OOREDOO_BILL", number); binding.layoutServiceSelector.visibility = View.INVISIBLE }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectFahipayService(service: String, number: String, ownerName: String? = null) {
|
||||
selectedFahipayService = service
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val displayName = ownerName
|
||||
?: contacts.firstOrNull { it.benefAccount == number }?.benefNickName
|
||||
?: number
|
||||
val serviceLabel = when (service) {
|
||||
"RAASTAS" -> "Raastas"
|
||||
"OOREDOO_BILL" -> "Ooredoo Bill Pay"
|
||||
"DHIRAAGU_RELOAD" -> "Dhiraagu Reload"
|
||||
"DHIRAAGU_BILL" -> "Dhiraagu Bill Pay"
|
||||
else -> service
|
||||
}
|
||||
prefillToDirectly(
|
||||
accountNumber = number,
|
||||
displayName = displayName,
|
||||
subtitle = "$serviceLabel · $number",
|
||||
colorHex = "#FF6B00",
|
||||
imageHash = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun prefillToDirectly(
|
||||
accountNumber: String,
|
||||
displayName: String,
|
||||
@@ -661,7 +772,9 @@ class TransferFragment : Fragment() {
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
resolvedBankName = ""
|
||||
selectedFahipayService = null
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.chipGroupService.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
|
||||
Reference in New Issue
Block a user